diff --git a/api/apps.py b/api/apps.py index 66656fd2..878e7d54 100644 --- a/api/apps.py +++ b/api/apps.py @@ -2,5 +2,5 @@ from django.apps import AppConfig class ApiConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'api' + default_auto_field = "django.db.models.BigAutoField" + name = "api" diff --git a/api/consumers.py b/api/consumers.py index 8b8ac4ae..d32da48e 100644 --- a/api/consumers.py +++ b/api/consumers.py @@ -32,4 +32,4 @@ # await self.send(text_data=json.dumps({ # 'message': 'VIN received', # 'vin': vin -# })) \ No newline at end of file +# })) diff --git a/api/migrations/0001_initial.py b/api/migrations/0001_initial.py index 7441cd65..fa6dd7f1 100644 --- a/api/migrations/0001_initial.py +++ b/api/migrations/0001_initial.py @@ -4,19 +4,28 @@ from django.db import migrations, models class Migration(migrations.Migration): - initial = True - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='CarVIN', + name="CarVIN", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('vin', models.CharField(max_length=17, verbose_name='VIN')), - ('created', models.DateTimeField(auto_now_add=True, verbose_name='created')), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("vin", models.CharField(max_length=17, verbose_name="VIN")), + ( + "created", + models.DateTimeField(auto_now_add=True, verbose_name="created"), + ), ], ), ] diff --git a/api/models.py b/api/models.py index 810bb254..1c91d741 100644 --- a/api/models.py +++ b/api/models.py @@ -8,4 +8,3 @@ class CarVIN(models.Model): def __str__(self): return self.vin - diff --git a/api/serializers.py b/api/serializers.py index 80eba627..6bddc1cd 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -6,78 +6,93 @@ from inventory import models as inventory_models class CarVINSerializer(serializers.ModelSerializer): class Meta: model = models.CarVIN - fields = ['vin'] + fields = ["vin"] def create(self, validated_data): - vin = validated_data.pop('vin') + vin = validated_data.pop("vin") return models.CarVIN.objects.create(vin=vin, **validated_data) class CarMakeSerializer(serializers.ModelSerializer): - car_models = serializers.PrimaryKeyRelatedField(many=True, read_only=True, source='carmodel_set') + car_models = serializers.PrimaryKeyRelatedField( + many=True, read_only=True, source="carmodel_set" + ) class Meta: model = inventory_models.CarMake - fields = '__all__' + fields = "__all__" class CarModelSerializer(serializers.ModelSerializer): - car_series = serializers.PrimaryKeyRelatedField(many=True, read_only=True, source='carserie_set') + car_series = serializers.PrimaryKeyRelatedField( + many=True, read_only=True, source="carserie_set" + ) class Meta: model = inventory_models.CarModel - fields = '__all__' + fields = "__all__" class CarSerieSerializer(serializers.ModelSerializer): - car_trims = serializers.PrimaryKeyRelatedField(many=True, read_only=True, source='cartrim_set') + car_trims = serializers.PrimaryKeyRelatedField( + many=True, read_only=True, source="cartrim_set" + ) class Meta: model = inventory_models.CarSerie - fields = '__all__' + fields = "__all__" class CarTrimSerializer(serializers.ModelSerializer): - car_equipments = serializers.PrimaryKeyRelatedField(many=True, read_only=True, source='carequipment_set') - car_specification_values = serializers.PrimaryKeyRelatedField(many=True, read_only=True, - source='carspecificationvalue_set') + car_equipments = serializers.PrimaryKeyRelatedField( + many=True, read_only=True, source="carequipment_set" + ) + car_specification_values = serializers.PrimaryKeyRelatedField( + many=True, read_only=True, source="carspecificationvalue_set" + ) class Meta: model = inventory_models.CarTrim - fields = '__all__' + fields = "__all__" class CarEquipmentSerializer(serializers.ModelSerializer): - car_option_values = serializers.PrimaryKeyRelatedField(many=True, read_only=True, source='caroptionvalue_set') + car_option_values = serializers.PrimaryKeyRelatedField( + many=True, read_only=True, source="caroptionvalue_set" + ) class Meta: model = inventory_models.CarEquipment - fields = '__all__' + fields = "__all__" class CarSpecificationSerializer(serializers.ModelSerializer): - child_specifications = serializers.PrimaryKeyRelatedField(many=True, read_only=True, source='carspecification_set') + child_specifications = serializers.PrimaryKeyRelatedField( + many=True, read_only=True, source="carspecification_set" + ) class Meta: model = inventory_models.CarSpecification - fields = '__all__' + fields = "__all__" class CarSpecificationValueSerializer(serializers.ModelSerializer): class Meta: model = inventory_models.CarSpecificationValue - fields = '__all__' + fields = "__all__" class CarOptionSerializer(serializers.ModelSerializer): - child_options = serializers.PrimaryKeyRelatedField(many=True, read_only=True, source='caroption_set') + child_options = serializers.PrimaryKeyRelatedField( + many=True, read_only=True, source="caroption_set" + ) class Meta: model = inventory_models.CarOption - fields = '__all__' + fields = "__all__" class CarOptionValueSerializer(serializers.ModelSerializer): class Meta: model = inventory_models.CarOptionValue - fields = '__all__' \ No newline at end of file + fields = "__all__" diff --git a/api/services.py b/api/services.py index 7284a869..55be18c9 100644 --- a/api/services.py +++ b/api/services.py @@ -2,19 +2,12 @@ import requests def get_bearer(): - api_token = "f5204a00-6f31-4de2-96d8-ed998e0d230c" api_secret = "8c11320781a5b8f4f327b6937e6f8241" url = "https://carapi.app/api/auth/login" - headers = { - "accept": "text/plain", - "Content-Type": "application/json" - } - data = { - "api_token": api_token, - "api_secret": api_secret - } + headers = {"accept": "text/plain", "Content-Type": "application/json"} + data = {"api_token": api_token, "api_secret": api_secret} response = requests.post(url, headers=headers, json=data) @@ -26,11 +19,8 @@ def get_bearer(): def get_car_data(vin): - url = f"https://carapi.app/api/vin/{vin}?verbose=no&all_trims=no" - headers = { - "Authorization": f"Bearer {get_bearer()}" - } + headers = {"Authorization": f"Bearer {get_bearer()}"} try: response = requests.get(url, headers=headers) @@ -52,10 +42,8 @@ def get_from_cardatabase(vin): url = "https://api.vehicledatabases.com/premium/vin-decode/{vin}" payload = {} - headers = { - 'x-AuthKey': '3cefdfd4272445f1929b5801c55d8fa5' - } + headers = {"x-AuthKey": "3cefdfd4272445f1929b5801c55d8fa5"} response = requests.request("GET", url, headers=headers, data=payload) - print(response.text) \ No newline at end of file + print(response.text) diff --git a/api/tests.py b/api/tests.py index 42ddf071..a39b155a 100644 --- a/api/tests.py +++ b/api/tests.py @@ -1,2 +1 @@ - -# Create your tests here. \ No newline at end of file +# Create your tests here. diff --git a/api/urls.py b/api/urls.py index d48e0173..c94c955b 100644 --- a/api/urls.py +++ b/api/urls.py @@ -5,20 +5,20 @@ from api import views router = routers.DefaultRouter() -router.register(r'car-makes', views.CarMakeViewSet) -router.register(r'car-models', views.CarModelViewSet) -router.register(r'car-series', views.CarSerieViewSet) -router.register(r'car-trims', views.CarTrimViewSet) -router.register(r'car-equipments', views.CarEquipmentViewSet) -router.register(r'car-specifications', views.CarSpecificationViewSet) -router.register(r'car-specification-values', views.CarSpecificationValueViewSet) -router.register(r'car-options', views.CarOptionViewSet) -router.register(r'car-option-values', views.CarOptionValueViewSet) +router.register(r"car-makes", views.CarMakeViewSet) +router.register(r"car-models", views.CarModelViewSet) +router.register(r"car-series", views.CarSerieViewSet) +router.register(r"car-trims", views.CarTrimViewSet) +router.register(r"car-equipments", views.CarEquipmentViewSet) +router.register(r"car-specifications", views.CarSpecificationViewSet) +router.register(r"car-specification-values", views.CarSpecificationValueViewSet) +router.register(r"car-options", views.CarOptionViewSet) +router.register(r"car-option-values", views.CarOptionValueViewSet) urlpatterns = [ - path('', include(router.urls)), - path('cars/vin/', views.CarVINViewSet.as_view(), name='car_vin'), + path("", include(router.urls)), + path("cars/vin/", views.CarVINViewSet.as_view(), name="car_vin"), path("cars/", views.car_list, name="car-list"), - path('login/', views.LoginView.as_view(), name='login'), + path("login/", views.LoginView.as_view(), name="login"), path("decode-vin/", views.VinDecodeAPIView.as_view(), name="api-decode-vin"), ] diff --git a/api/views.py b/api/views.py index 3891d188..0c4bd145 100644 --- a/api/views.py +++ b/api/views.py @@ -16,7 +16,9 @@ logger = logging.getLogger(__name__) class LoginView(APIView): - permission_classes = [permissions.AllowAny,] + permission_classes = [ + permissions.AllowAny, + ] # def post(self, request, *args, **kwargs): # username = request.data.get('username') @@ -35,7 +37,7 @@ class LoginView(APIView): class CarVINViewSet(APIView): - queryset = models.CarVIN.objects.all().order_by('-created') + queryset = models.CarVIN.objects.all().order_by("-created") serializer_class = serializers.CarVINSerializer def get(self, request): @@ -100,30 +102,27 @@ class CarOptionValueViewSet(viewsets.ModelViewSet): serializer_class = serializers.CarOptionValueSerializer - def car_list(request): dealer = get_user_type(request) page = request.GET.get("page", 1) per_page = 10 cars = inventory_models.Car.objects.all().values( - "vin", - "year", - "id_car_make__name", - "id_car_model__name", - "status" + "vin", "year", "id_car_make__name", "id_car_model__name", "status" ) paginator = Paginator(cars, per_page) page_obj = paginator.get_page(page) - return JsonResponse({ - "data": list(page_obj), # Convert QuerySet to list - "page": page_obj.number, # Current page number - "per_page": per_page, - "total_pages": paginator.num_pages, # Total pages - "total_items": paginator.count # Total records - }) + return JsonResponse( + { + "data": list(page_obj), # Convert QuerySet to list + "page": page_obj.number, # Current page number + "per_page": per_page, + "total_pages": paginator.num_pages, # Total pages + "total_items": paginator.count, # Total records + } + ) class VinDecodeAPIView(APIView): @@ -162,4 +161,4 @@ class VinDecodeAPIView(APIView): "modelYear": result.get("year_model"), } - return Response({"success": True, "data": vin_data}, status=status.HTTP_200_OK) \ No newline at end of file + return Response({"success": True, "data": vin_data}, status=status.HTTP_200_OK) diff --git a/car_inventory/asgi.py b/car_inventory/asgi.py index e7563904..206c8884 100644 --- a/car_inventory/asgi.py +++ b/car_inventory/asgi.py @@ -15,13 +15,11 @@ from channels.routing import ProtocolTypeRouter, URLRouter from channels.auth import AuthMiddlewareStack from api import routing -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'car_inventory.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "car_inventory.settings") -application = ProtocolTypeRouter({ - "http": get_asgi_application(), - "websocket": AuthMiddlewareStack( - URLRouter( - routing.websocket_urlpatterns - ) - ), -}) +application = ProtocolTypeRouter( + { + "http": get_asgi_application(), + "websocket": AuthMiddlewareStack(URLRouter(routing.websocket_urlpatterns)), + } +) diff --git a/car_inventory/urls.py b/car_inventory/urls.py index 7fa793f5..681dfa60 100644 --- a/car_inventory/urls.py +++ b/car_inventory/urls.py @@ -4,6 +4,7 @@ from django.conf.urls.static import static from django.conf import settings from django.conf.urls.i18n import i18n_patterns from inventory import views + # import debug_toolbar from schema_graph.views import Schema # from two_factor.urls import urlpatterns as tf_urls @@ -11,24 +12,22 @@ from schema_graph.views import Schema urlpatterns = [ # path('__debug__/', include(debug_toolbar.urls)), # path('silk/', include('silk.urls', namespace='silk')), - path('api-auth/', include('rest_framework.urls')), - path('api/', include('api.urls')), + path("api-auth/", include("rest_framework.urls")), + path("api/", include("api.urls")), # path('dj-rest-auth/', include('dj_rest_auth.urls')), - - ] urlpatterns += i18n_patterns( - path('admin/', admin.site.urls), - path('switch_language/', views.switch_language, name='switch_language'), - path('accounts/', include('allauth.urls')), + path("admin/", admin.site.urls), + path("switch_language/", views.switch_language, name="switch_language"), + path("accounts/", include("allauth.urls")), # path('prometheus/', include('django_prometheus.urls')), - path('', include('inventory.urls')), - path('ledger/', include('django_ledger.urls', namespace='django_ledger')), + path("", include("inventory.urls")), + path("ledger/", include("django_ledger.urls", namespace="django_ledger")), path("haikalbot/", include("haikalbot.urls")), - path('appointment/', include('appointment.urls')), - path('plans/', include('plans.urls')), + path("appointment/", include("appointment.urls")), + path("plans/", include("plans.urls")), path("schema/", Schema.as_view()), - path('tours/', include('tours.urls')), + path("tours/", include("tours.urls")), # path('', include(tf_urls)), ) diff --git a/car_inventory/wsgi.py b/car_inventory/wsgi.py index 1e65e538..7c3b10f3 100644 --- a/car_inventory/wsgi.py +++ b/car_inventory/wsgi.py @@ -11,6 +11,6 @@ import os from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'car_inventory.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "car_inventory.settings") application = get_wsgi_application() diff --git a/docs/source/conf.py b/docs/source/conf.py index 2efff906..a0ecbe19 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -6,23 +6,23 @@ # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information -project = 'Haikal' -copyright = '2024, Marwan Alwali' -author = 'Marwan Alwali' -release = '01/11/2024' +project = "Haikal" +copyright = "2024, Marwan Alwali" +author = "Marwan Alwali" +release = "01/11/2024" # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration extensions = [] -templates_path = ['_templates'] +templates_path = ["_templates"] exclude_patterns = [] -language = '[en,ar]' +language = "[en,ar]" # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -html_theme = 'alabaster' -html_static_path = ['_static'] +html_theme = "alabaster" +html_static_path = ["_static"] diff --git a/generate.py b/generate.py index b06c6c2d..81bd1ef4 100644 --- a/generate.py +++ b/generate.py @@ -5,6 +5,7 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "car_inventory.settings") django.setup() from inventory.models import * + # from rich import print import random import datetime @@ -31,7 +32,6 @@ def run(): "LGJE1EE0XSM333551", "LGJE1EE02SM333561", "LGJE1EE0XSM333565", - ] for vin in vin_list: try: @@ -69,5 +69,6 @@ def run(): except Exception as e: print(e) + if __name__ == "__main__": - run() \ No newline at end of file + run() diff --git a/haikalbot/admin.py b/haikalbot/admin.py index 5acba54b..e9e7e1b1 100644 --- a/haikalbot/admin.py +++ b/haikalbot/admin.py @@ -4,10 +4,16 @@ from .models import AnalysisCache @admin.register(AnalysisCache) class AnalysisCacheAdmin(admin.ModelAdmin): - list_display = ('prompt_hash', 'dealer_id', 'created_at', 'expires_at', 'is_expired') - list_filter = ('dealer_id', 'created_at') - search_fields = ('prompt_hash',) - readonly_fields = ('prompt_hash', 'created_at', 'updated_at') + list_display = ( + "prompt_hash", + "dealer_id", + "created_at", + "expires_at", + "is_expired", + ) + list_filter = ("dealer_id", "created_at") + search_fields = ("prompt_hash",) + readonly_fields = ("prompt_hash", "created_at", "updated_at") def is_expired(self, obj): return obj.is_expired() diff --git a/haikalbot/ai_agent.py b/haikalbot/ai_agent.py index 083adb7a..1ed46368 100644 --- a/haikalbot/ai_agent.py +++ b/haikalbot/ai_agent.py @@ -20,10 +20,10 @@ from sqlalchemy.orm import relationship logger = logging.getLogger(__name__) # Configuration settings -LLM_MODEL = getattr(settings, 'MODEL_ANALYZER_LLM_MODEL', 'qwen3:8b') -LLM_TEMPERATURE = getattr(settings, 'MODEL_ANALYZER_LLM_TEMPERATURE', 0.3) -LLM_MAX_TOKENS = getattr(settings, 'MODEL_ANALYZER_LLM_MAX_TOKENS', 2048) -CACHE_TIMEOUT = getattr(settings, 'MODEL_ANALYZER_CACHE_TIMEOUT', 3600) +LLM_MODEL = getattr(settings, "MODEL_ANALYZER_LLM_MODEL", "qwen3:8b") +LLM_TEMPERATURE = getattr(settings, "MODEL_ANALYZER_LLM_TEMPERATURE", 0.3) +LLM_MAX_TOKENS = getattr(settings, "MODEL_ANALYZER_LLM_MAX_TOKENS", 2048) +CACHE_TIMEOUT = getattr(settings, "MODEL_ANALYZER_CACHE_TIMEOUT", 3600) system_instruction = """ You are a specialized AI agent designed to analyze Django models and extract relevant information based on user input in Arabic or English. You must: @@ -91,52 +91,55 @@ class ModelAnalysis: class DjangoModelAnalyzer: def __init__(self): self.analysis_patterns = { - 'count': { - 'patterns': [r'\b(count|number|how many)\b'], - 'fields': ['id'], - 'weight': 1.0 + "count": { + "patterns": [r"\b(count|number|how many)\b"], + "fields": ["id"], + "weight": 1.0, }, - 'aggregate': { - 'patterns': [r'\b(average|avg|mean|sum|total)\b'], - 'fields': ['price', 'amount', 'value', 'cost', 'quantity'], - 'weight': 0.8 + "aggregate": { + "patterns": [r"\b(average|avg|mean|sum|total)\b"], + "fields": ["price", "amount", "value", "cost", "quantity"], + "weight": 0.8, + }, + "temporal": { + "patterns": [r"\b(date|time|when|period)\b"], + "fields": ["created_at", "updated_at", "date", "timestamp"], + "weight": 0.7, }, - 'temporal': { - 'patterns': [r'\b(date|time|when|period)\b'], - 'fields': ['created_at', 'updated_at', 'date', 'timestamp'], - 'weight': 0.7 - } } def analyze_prompt(self, prompt: str, model_structure: List) -> ModelAnalysis: # Initialize LLM - llm = ChatOllama( - model=LLM_MODEL, - temperature=LLM_TEMPERATURE - ) + llm = ChatOllama(model=LLM_MODEL, temperature=LLM_TEMPERATURE) # Get model analysis from LLM messages = [ SystemMessage(content=system_instruction), - HumanMessage(content=prompt) + HumanMessage(content=prompt), ] try: response = llm.invoke(messages) - if not response or not hasattr(response, 'content') or response.content is None: + if ( + not response + or not hasattr(response, "content") + or response.content is None + ): raise ValueError("Empty response from LLM") analysis_requirements = self._parse_llm_response(response.content) except Exception as e: logger.error(f"Error in LLM analysis: {e}") - analysis_requirements = self._pattern_based_analysis(prompt, model_structure) + analysis_requirements = self._pattern_based_analysis( + prompt, model_structure + ) return self._enhance_analysis(analysis_requirements, model_structure) def _parse_llm_response(self, response: str) -> Dict: try: - json_match = re.search(r'({.*})', response.replace('\n', ' '), re.DOTALL) + json_match = re.search(r"({.*})", response.replace("\n", " "), re.DOTALL) if json_match: return json.loads(json_match.group(1)) return {} @@ -149,20 +152,22 @@ class DjangoModelAnalyzer: relevant_fields = [] for analysis_name, config in self.analysis_patterns.items(): - for pattern in config['patterns']: + for pattern in config["patterns"]: if re.search(pattern, prompt.lower()): - relevant_fields.extend(config['fields']) + relevant_fields.extend(config["fields"]) analysis_type = analysis_name break if analysis_type: break return { - 'analysis_type': analysis_type or 'basic', - 'fields': list(set(relevant_fields)) or ['id', 'name'] + "analysis_type": analysis_type or "basic", + "fields": list(set(relevant_fields)) or ["id", "name"], } - def _enhance_analysis(self, requirements: Dict, model_structure: List) -> ModelAnalysis: + def _enhance_analysis( + self, requirements: Dict, model_structure: List + ) -> ModelAnalysis: app_label = requirements.get("analysis_requirements", {}).get("app_label") model_name = requirements.get("analysis_requirements", {}).get("model_name") fields = requirements.get("analysis_requirements", {}).get("fields") or [] @@ -185,26 +190,31 @@ class DjangoModelAnalyzer: field_analysis = FieldAnalysis( name=field_name, field_type=field.get_internal_type(), - is_required=not field.null if hasattr(field, 'null') else True, + is_required=not field.null if hasattr(field, "null") else True, is_relation=field.is_relation, - related_model=field.related_model.__name__ if field.is_relation and hasattr(field, - 'related_model') and field.related_model else None + related_model=field.related_model.__name__ + if field.is_relation + and hasattr(field, "related_model") + and field.related_model + else None, ) field_analysis.analysis_relevance = self._calculate_field_relevance( - field_analysis, - requirements.get('analysis_type', 'basic') + field_analysis, requirements.get("analysis_type", "basic") ) relevant_fields.append(field_analysis) if field.is_relation: - relationships.append({ - 'field': field_name, - 'type': field.get_internal_type(), - 'to': field.related_model.__name__ if hasattr(field, - 'related_model') and field.related_model else '' - }) + relationships.append( + { + "field": field_name, + "type": field.get_internal_type(), + "to": field.related_model.__name__ + if hasattr(field, "related_model") and field.related_model + else "", + } + ) except FieldDoesNotExist: logger.warning(f"Field {field_name} not found in {model_name}") @@ -212,16 +222,20 @@ class DjangoModelAnalyzer: return ModelAnalysis( app_label=app_label, model_name=model_name, - relevant_fields=sorted(relevant_fields, key=lambda x: x.analysis_relevance, reverse=True), + relevant_fields=sorted( + relevant_fields, key=lambda x: x.analysis_relevance, reverse=True + ), relationships=relationships, - confidence_score=self._calculate_confidence_score(relevant_fields) + confidence_score=self._calculate_confidence_score(relevant_fields), ) - def _calculate_field_relevance(self, field: FieldAnalysis, analysis_type: str) -> float: + def _calculate_field_relevance( + self, field: FieldAnalysis, analysis_type: str + ) -> float: base_score = 0.5 if analysis_type in self.analysis_patterns: - if field.name in self.analysis_patterns[analysis_type]['fields']: - base_score += self.analysis_patterns[analysis_type]['weight'] + if field.name in self.analysis_patterns[analysis_type]["fields"]: + base_score += self.analysis_patterns[analysis_type]["weight"] if field.is_required: base_score += 0.2 if field.is_relation: @@ -257,26 +271,32 @@ def get_all_model_structures(filtered_apps: Optional[List[str]] = None) -> List[ if field.is_relation: # Get related model name safely related_model_name = None - if hasattr(field, 'related_model') and field.related_model: + if hasattr(field, "related_model") and field.related_model: related_model_name = field.related_model.__name__ - elif hasattr(field, 'model') and field.model: + elif hasattr(field, "model") and field.model: related_model_name = field.model.__name__ - if related_model_name: # Only add relationship if we have a valid related model - relationships.append({ - "field": field.name, - "type": field.get_internal_type(), - "to": related_model_name - }) + if ( + related_model_name + ): # Only add relationship if we have a valid related model + relationships.append( + { + "field": field.name, + "type": field.get_internal_type(), + "to": related_model_name, + } + ) else: fields[field.name] = field.get_internal_type() - structures.append({ - "app_label": app_label, - "model_name": model.__name__, - "fields": fields, - "relationships": relationships - }) + structures.append( + { + "app_label": app_label, + "model_name": model.__name__, + "fields": fields, + "relationships": relationships, + } + ) return structures @@ -332,25 +352,42 @@ def apply_filters(queryset: QuerySet, filters: Dict[str, Any]) -> QuerySet: for key, value in filters.items(): if isinstance(value, dict): # Handle complex filters - operation = value.get('operation', 'exact') - filter_value = value.get('value') + operation = value.get("operation", "exact") + filter_value = value.get("value") - if not filter_value and operation != 'isnull': + if not filter_value and operation != "isnull": continue - if operation == 'contains': + if operation == "contains": q_objects.append(Q(**{f"{key}__icontains": filter_value})) - elif operation == 'in': + elif operation == "in": if isinstance(filter_value, list) and filter_value: q_objects.append(Q(**{f"{key}__in": filter_value})) - elif operation in ['gt', 'gte', 'lt', 'lte', 'exact', 'iexact', 'startswith', 'endswith']: + elif operation in [ + "gt", + "gte", + "lt", + "lte", + "exact", + "iexact", + "startswith", + "endswith", + ]: q_objects.append(Q(**{f"{key}__{operation}": filter_value})) - elif operation == 'between' and isinstance(filter_value, list) and len(filter_value) >= 2: - q_objects.append(Q(**{ - f"{key}__gte": filter_value[0], - f"{key}__lte": filter_value[1] - })) - elif operation == 'isnull': + elif ( + operation == "between" + and isinstance(filter_value, list) + and len(filter_value) >= 2 + ): + q_objects.append( + Q( + **{ + f"{key}__gte": filter_value[0], + f"{key}__lte": filter_value[1], + } + ) + ) + elif operation == "isnull": q_objects.append(Q(**{f"{key}__isnull": bool(filter_value)})) else: # Simple exact match @@ -363,10 +400,10 @@ def apply_filters(queryset: QuerySet, filters: Dict[str, Any]) -> QuerySet: def process_aggregation( - queryset: QuerySet, - aggregation: str, - fields: List[str], - group_by: Optional[List[str]] = None + queryset: QuerySet, + aggregation: str, + fields: List[str], + group_by: Optional[List[str]] = None, ) -> Union[Dict[str, Any], List[Dict[str, Any]]]: """ Process aggregation queries with support for grouping. @@ -383,13 +420,7 @@ def process_aggregation( if not fields: return {"error": "No fields specified for aggregation"} - agg_func_map = { - "sum": Sum, - "avg": Avg, - "count": Count, - "max": Max, - "min": Min - } + agg_func_map = {"sum": Sum, "avg": Avg, "count": Count, "max": Max, "min": Min} agg_func = agg_func_map.get(aggregation.lower()) if not agg_func: @@ -404,22 +435,25 @@ def process_aggregation( agg_dict[f"{aggregation}_{field}"] = agg_func(field) if not agg_dict: - return {"error": "No valid fields for aggregation after excluding group_by fields"} + return { + "error": "No valid fields for aggregation after excluding group_by fields" + } # Apply group_by and aggregation return list(queryset.values(*group_by).annotate(**agg_dict)) else: # Simple aggregation without grouping - return queryset.aggregate(**{ - f"{aggregation}_{field}": agg_func(field) - for field in fields - }) + return queryset.aggregate( + **{f"{aggregation}_{field}": agg_func(field) for field in fields} + ) except Exception as e: logger.error(f"Aggregation error: {e}") return {"error": f"Aggregation failed: {str(e)}"} -def prepare_chart_data(data: List[Dict], fields: List[str], chart_type: str) -> Optional[Dict[str, Any]]: +def prepare_chart_data( + data: List[Dict], fields: List[str], chart_type: str +) -> Optional[Dict[str, Any]]: """ Prepare data for chart visualization. @@ -449,15 +483,18 @@ def prepare_chart_data(data: List[Dict], fields: List[str], chart_type: str) -> return { "type": chart_type, "labels": [str(label).replace(f"{fields[0]}_", "") for label in labels], - "data": [float(value) if isinstance(value, (int, float)) else 0 for value in values], + "data": [ + float(value) if isinstance(value, (int, float)) else 0 + for value in values + ], "backgroundColor": [ "rgba(54, 162, 235, 0.6)", "rgba(255, 99, 132, 0.6)", "rgba(255, 206, 86, 0.6)", "rgba(75, 192, 192, 0.6)", "rgba(153, 102, 255, 0.6)", - "rgba(255, 159, 64, 0.6)" - ] + "rgba(255, 159, 64, 0.6)", + ], } # For regular query results as list of dictionaries @@ -483,13 +520,14 @@ def prepare_chart_data(data: List[Dict], fields: List[str], chart_type: str) -> "labels": labels, "data": data_values, "backgroundColor": [ - "rgba(54, 162, 235, 0.6)", - "rgba(255, 99, 132, 0.6)", - "rgba(255, 206, 86, 0.6)", - "rgba(75, 192, 192, 0.6)", - "rgba(153, 102, 255, 0.6)", - "rgba(255, 159, 64, 0.6)" - ] * (len(data_values) // 6 + 1) # Repeat colors as needed + "rgba(54, 162, 235, 0.6)", + "rgba(255, 99, 132, 0.6)", + "rgba(255, 206, 86, 0.6)", + "rgba(75, 192, 192, 0.6)", + "rgba(153, 102, 255, 0.6)", + "rgba(255, 159, 64, 0.6)", + ] + * (len(data_values) // 6 + 1), # Repeat colors as needed } else: # For other charts, create dataset for each field after the first @@ -501,17 +539,13 @@ def prepare_chart_data(data: List[Dict], fields: List[str], chart_type: str) -> "data": [float(item.get(field, 0) or 0) for item in data], "backgroundColor": f"rgba({50 + i * 50}, {100 + i * 40}, 235, 0.6)", "borderColor": f"rgba({50 + i * 50}, {100 + i * 40}, 235, 1.0)", - "borderWidth": 1 + "borderWidth": 1, } datasets.append(dataset) except (ValueError, TypeError) as e: logger.warning(f"Error processing field {field} for chart: {e}") - return { - "type": chart_type, - "labels": labels, - "datasets": datasets - } + return {"type": chart_type, "labels": labels, "datasets": datasets} except Exception as e: logger.error(f"Error preparing chart data: {e}") return None @@ -556,7 +590,7 @@ def query_django_model(parsed: Dict[str, Any]) -> Dict[str, Any]: return { "status": "error", "error": "app_label and model are required", - "language": language + "language": language, } # Get model class @@ -566,7 +600,7 @@ def query_django_model(parsed: Dict[str, Any]) -> Dict[str, Any]: return { "status": "error", "error": f"Model '{model_name}' not found in app '{app_label}'", - "language": language + "language": language, } # Validate fields against model @@ -592,7 +626,7 @@ def query_django_model(parsed: Dict[str, Any]) -> Dict[str, Any]: return { "status": "error", "error": f"Invalid filters: {str(e)}", - "language": language + "language": language, } # Handle aggregations @@ -603,7 +637,7 @@ def query_django_model(parsed: Dict[str, Any]) -> Dict[str, Any]: return { "status": "error", "error": result["error"], - "language": language + "language": language, } chart_data = None @@ -614,7 +648,7 @@ def query_django_model(parsed: Dict[str, Any]) -> Dict[str, Any]: "status": "success", "data": result, "chart": chart_data, - "language": language + "language": language, } # Handle regular queries @@ -649,9 +683,9 @@ def query_django_model(parsed: Dict[str, Any]) -> Dict[str, Any]: "total_count": len(data), "fields": fields, "model": model_name, - "app": app_label + "app": app_label, }, - "language": language + "language": language, } except Exception as e: @@ -659,7 +693,7 @@ def query_django_model(parsed: Dict[str, Any]) -> Dict[str, Any]: return { "status": "error", "error": f"Query execution failed: {str(e)}", - "language": language + "language": language, } except Exception as e: @@ -667,11 +701,13 @@ def query_django_model(parsed: Dict[str, Any]) -> Dict[str, Any]: return { "status": "error", "error": f"Unexpected error: {str(e)}", - "language": parsed.get("language", "en") + "language": parsed.get("language", "en"), } -def determine_aggregation_type(prompt: str, fields: List[FieldAnalysis]) -> Optional[str]: +def determine_aggregation_type( + prompt: str, fields: List[FieldAnalysis] +) -> Optional[str]: """ Determine the appropriate aggregation type based on the prompt and fields. @@ -682,21 +718,39 @@ def determine_aggregation_type(prompt: str, fields: List[FieldAnalysis]) -> Opti Returns: Aggregation type or None """ - if any(pattern in prompt.lower() for pattern in ['average', 'avg', 'mean', 'معدل', 'متوسط']): - return 'avg' - elif any(pattern in prompt.lower() for pattern in ['sum', 'total', 'مجموع', 'إجمالي']): - return 'sum' - elif any(pattern in prompt.lower() for pattern in ['count', 'number', 'how many', 'عدد', 'كم']): - return 'count' - elif any(pattern in prompt.lower() for pattern in ['maximum', 'max', 'highest', 'أقصى', 'أعلى']): - return 'max' - elif any(pattern in prompt.lower() for pattern in ['minimum', 'min', 'lowest', 'أدنى', 'أقل']): - return 'min' + if any( + pattern in prompt.lower() + for pattern in ["average", "avg", "mean", "معدل", "متوسط"] + ): + return "avg" + elif any( + pattern in prompt.lower() for pattern in ["sum", "total", "مجموع", "إجمالي"] + ): + return "sum" + elif any( + pattern in prompt.lower() + for pattern in ["count", "number", "how many", "عدد", "كم"] + ): + return "count" + elif any( + pattern in prompt.lower() + for pattern in ["maximum", "max", "highest", "أقصى", "أعلى"] + ): + return "max" + elif any( + pattern in prompt.lower() + for pattern in ["minimum", "min", "lowest", "أدنى", "أقل"] + ): + return "min" # Check field types for numeric fields to determine default aggregation - numeric_fields = [field for field in fields if field.field_type in ['DecimalField', 'FloatField', 'IntegerField']] + numeric_fields = [ + field + for field in fields + if field.field_type in ["DecimalField", "FloatField", "IntegerField"] + ] if numeric_fields: - return 'sum' # Default to sum for numeric fields + return "sum" # Default to sum for numeric fields return None @@ -713,32 +767,47 @@ def determine_chart_type(prompt: str, fields: List[FieldAnalysis]) -> Optional[s Chart type or None """ # Check for explicit chart type mentions in prompt - if any(term in prompt.lower() for term in ['line chart', 'time series', 'trend', 'رسم خطي', 'اتجاه']): - return 'line' - elif any(term in prompt.lower() for term in ['bar chart', 'histogram', 'column', 'رسم شريطي', 'أعمدة']): - return 'bar' - elif any(term in prompt.lower() for term in ['pie chart', 'circle chart', 'رسم دائري', 'فطيرة']): - return 'pie' - elif any(term in prompt.lower() for term in ['doughnut', 'دونات']): - return 'doughnut' - elif any(term in prompt.lower() for term in ['radar', 'spider', 'رادار']): - return 'radar' + if any( + term in prompt.lower() + for term in ["line chart", "time series", "trend", "رسم خطي", "اتجاه"] + ): + return "line" + elif any( + term in prompt.lower() + for term in ["bar chart", "histogram", "column", "رسم شريطي", "أعمدة"] + ): + return "bar" + elif any( + term in prompt.lower() + for term in ["pie chart", "circle chart", "رسم دائري", "فطيرة"] + ): + return "pie" + elif any(term in prompt.lower() for term in ["doughnut", "دونات"]): + return "doughnut" + elif any(term in prompt.lower() for term in ["radar", "spider", "رادار"]): + return "radar" # Determine chart type based on field types and count - date_fields = [field for field in fields if field.field_type in ['DateField', 'DateTimeField']] - numeric_fields = [field for field in fields if field.field_type in ['DecimalField', 'FloatField', 'IntegerField']] + date_fields = [ + field for field in fields if field.field_type in ["DateField", "DateTimeField"] + ] + numeric_fields = [ + field + for field in fields + if field.field_type in ["DecimalField", "FloatField", "IntegerField"] + ] if date_fields and numeric_fields: - return 'line' # Time series data + return "line" # Time series data elif len(fields) == 2 and len(numeric_fields) >= 1: - return 'bar' # Category and value + return "bar" # Category and value elif len(fields) == 1 or (len(fields) == 2 and len(numeric_fields) == 1): - return 'pie' # Single dimension data + return "pie" # Single dimension data elif len(fields) > 2: - return 'bar' # Multi-dimensional data + return "bar" # Multi-dimensional data # Default - return 'bar' + return "bar" def analyze_prompt(prompt: str) -> Dict[str, Any]: @@ -752,8 +821,8 @@ def analyze_prompt(prompt: str) -> Dict[str, Any]: Dictionary with query results """ # Detect language - language = "ar" if bool(re.search(r'[\u0600-\u06FF]', prompt)) else "en" - filtered_apps = ['inventory'] + language = "ar" if bool(re.search(r"[\u0600-\u06FF]", prompt)) else "en" + filtered_apps = ["inventory"] try: analyzer = DjangoModelAnalyzer() model_structure = get_all_model_structures(filtered_apps=filtered_apps) @@ -761,23 +830,27 @@ def analyze_prompt(prompt: str) -> Dict[str, Any]: analysis = analyzer.analyze_prompt(prompt, model_structure) print(analysis) - if not analysis or not analysis.app_label or not analysis.model_name: return { "status": "error", - "message": "تعذر العثور على النموذج المطلوب" if language == "ar" else "Missing model information", - "language": language + "message": "تعذر العثور على النموذج المطلوب" + if language == "ar" + else "Missing model information", + "language": language, } query_params = { "app_label": analysis.app_label, "model_name": analysis.model_name, "fields": [field.name for field in analysis.relevant_fields], - "joins": [{"path": rel["field"], "type": rel["type"]} for rel in analysis.relationships], + "joins": [ + {"path": rel["field"], "type": rel["type"]} + for rel in analysis.relationships + ], "filters": {}, "aggregation": determine_aggregation_type(prompt, analysis.relevant_fields), "chart": determine_chart_type(prompt, analysis.relevant_fields), - "language": language + "language": language, } return query_django_model(query_params) @@ -785,6 +858,8 @@ def analyze_prompt(prompt: str) -> Dict[str, Any]: logger.error(f"Error analyzing prompt: {e}") return { "status": "error", - "error": "حدث خطأ أثناء تحليل الاستعلام" if language == "ar" else f"Error analyzing prompt: {str(e)}", - "language": language - } \ No newline at end of file + "error": "حدث خطأ أثناء تحليل الاستعلام" + if language == "ar" + else f"Error analyzing prompt: {str(e)}", + "language": language, + } diff --git a/haikalbot/apps.py b/haikalbot/apps.py index 1034b56b..c0c9b3c4 100644 --- a/haikalbot/apps.py +++ b/haikalbot/apps.py @@ -2,5 +2,5 @@ from django.apps import AppConfig class HaikalbotConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'haikalbot' + default_auto_field = "django.db.models.BigAutoField" + name = "haikalbot" diff --git a/haikalbot/haikal_agent.py b/haikalbot/haikal_agent.py index f09c9e52..b2ccffd2 100644 --- a/haikalbot/haikal_agent.py +++ b/haikalbot/haikal_agent.py @@ -63,7 +63,6 @@ class DatabaseType(Enum): MYSQL = "mysql" - @dataclass class DatabaseConnection: db_type: DatabaseType @@ -95,18 +94,23 @@ class DatabaseSchema(BaseModel): description="Database schema with table names as keys and column info as values" ) relationships: Optional[List[Dict[str, Any]]] = Field( - default=None, - description="Foreign key relationships between tables" + default=None, description="Foreign key relationships between tables" ) class InsightRequest(BaseModel): prompt: str = Field(description="Natural language query from user") - database_path: Optional[str] = Field(default=None, description="Path to database file (for SQLite)") + database_path: Optional[str] = Field( + default=None, description="Path to database file (for SQLite)" + ) chart_type: Optional[str] = Field(default=None, description="Preferred chart type") limit: Optional[int] = Field(default=1000, description="Maximum number of results") - language: Optional[str] = Field(default="auto", description="Response language preference") - use_django: Optional[bool] = Field(default=True, description="Use Django database if available") + language: Optional[str] = Field( + default="auto", description="Response language preference" + ) + use_django: Optional[bool] = Field( + default=True, description="Use Django database if available" + ) class DatabaseInsightSystem: @@ -114,7 +118,7 @@ class DatabaseInsightSystem: self.config = config or DatabaseConfig() self.model = OpenAIModel( model_name=self.config.LLM_MODEL, - provider=OpenAIProvider(base_url=self.config.LLM_BASE_URL) + provider=OpenAIProvider(base_url=self.config.LLM_BASE_URL), ) self.db_connection = None self._setup_agents() @@ -148,7 +152,7 @@ class DatabaseInsightSystem: } Handle both English and Arabic prompts. For Arabic text, respond in Arabic. - Focus on providing actionable insights, not just raw data.""" + Focus on providing actionable insights, not just raw data.""", ) def _get_django_database_config(self) -> Optional[DatabaseConnection]: @@ -158,27 +162,31 @@ class DatabaseInsightSystem: try: # Get default database configuration - db_config = settings.DATABASES.get('default', {}) + db_config = settings.DATABASES.get("default", {}) if not db_config: - logger.warning("No default database configuration found in Django settings") + logger.warning( + "No default database configuration found in Django settings" + ) return None - engine = db_config.get('ENGINE', '') - db_name = db_config.get('NAME', '') - host = db_config.get('HOST', 'localhost') - port = db_config.get('PORT', None) - user = db_config.get('USER', '') - password = db_config.get('PASSWORD', '') + engine = db_config.get("ENGINE", "") + db_name = db_config.get("NAME", "") + host = db_config.get("HOST", "localhost") + port = db_config.get("PORT", None) + user = db_config.get("USER", "") + password = db_config.get("PASSWORD", "") # Determine database type from engine - if 'sqlite' in engine.lower(): + if "sqlite" in engine.lower(): db_type = DatabaseType.SQLITE connection_string = db_name # For SQLite, NAME is the file path - elif 'postgresql' in engine.lower(): + elif "postgresql" in engine.lower(): db_type = DatabaseType.POSTGRESQL port = port or 5432 - connection_string = f"postgresql://{user}:{password}@{host}:{port}/{db_name}" - elif 'mysql' in engine.lower(): + connection_string = ( + f"postgresql://{user}:{password}@{host}:{port}/{db_name}" + ) + elif "mysql" in engine.lower(): db_type = DatabaseType.MYSQL port = port or 3306 connection_string = f"mysql://{user}:{password}@{host}:{port}/{db_name}" @@ -193,7 +201,7 @@ class DatabaseInsightSystem: host=host, port=port, user=user, - password=password + password=password, ) except Exception as e: @@ -218,8 +226,7 @@ class DatabaseInsightSystem: if request.database_path: # Assume SQLite for direct file path self.db_connection = DatabaseConnection( - db_type=DatabaseType.SQLITE, - connection_string=request.database_path + db_type=DatabaseType.SQLITE, connection_string=request.database_path ) return await self._analyze_sqlite_schema(request.database_path) @@ -247,24 +254,28 @@ class DatabaseInsightSystem: cursor.execute(f"PRAGMA table_info({table})") columns = [] for col in cursor.fetchall(): - columns.append({ - "name": col[1], - "type": col[2], - "notnull": bool(col[3]), - "default_value": col[4], - "primary_key": bool(col[5]) - }) + columns.append( + { + "name": col[1], + "type": col[2], + "notnull": bool(col[3]), + "default_value": col[4], + "primary_key": bool(col[5]), + } + ) schema_data[table] = columns # Get foreign key relationships cursor.execute(f"PRAGMA foreign_key_list({table})") for fk in cursor.fetchall(): - relationships.append({ - "from_table": table, - "from_column": fk[3], - "to_table": fk[2], - "to_column": fk[4] - }) + relationships.append( + { + "from_table": table, + "from_column": fk[3], + "to_table": fk[2], + "to_column": fk[4], + } + ) conn.close() return DatabaseSchema(tables=schema_data, relationships=relationships) @@ -287,27 +298,33 @@ class DatabaseInsightSystem: for field in model._meta.get_fields(): if not field.is_relation: - columns.append({ - "name": field.name, - "type": field.get_internal_type(), - "notnull": not getattr(field, 'null', True), - "primary_key": getattr(field, 'primary_key', False) - }) + columns.append( + { + "name": field.name, + "type": field.get_internal_type(), + "notnull": not getattr(field, "null", True), + "primary_key": getattr(field, "primary_key", False), + } + ) else: # Handle relationships - if hasattr(field, 'related_model') and field.related_model: - relationships.append({ - "from_table": table_name, - "from_column": field.name, - "to_table": field.related_model._meta.db_table, - "relationship_type": field.get_internal_type() - }) + if hasattr(field, "related_model") and field.related_model: + relationships.append( + { + "from_table": table_name, + "from_column": field.name, + "to_table": field.related_model._meta.db_table, + "relationship_type": field.get_internal_type(), + } + ) schema_data[table_name] = columns return DatabaseSchema(tables=schema_data, relationships=relationships) - async def _analyze_postgresql_schema(self, connection_string: str) -> DatabaseSchema: + async def _analyze_postgresql_schema( + self, connection_string: str + ) -> DatabaseSchema: """Analyze PostgreSQL database schema.""" if not POSTGRESQL_AVAILABLE: raise ImportError("psycopg2 is not available") @@ -325,47 +342,56 @@ class DatabaseInsightSystem: FROM information_schema.tables WHERE table_schema = 'public' """) - tables = [row['table_name'] for row in cursor.fetchall()] + tables = [row["table_name"] for row in cursor.fetchall()] schema_data = {} relationships = [] for table in tables: # Get column information - cursor.execute(""" + cursor.execute( + """ SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_name = %s ORDER BY ordinal_position - """, (table,)) + """, + (table,), + ) columns = [] for col in cursor.fetchall(): - columns.append({ - "name": col['column_name'], - "type": col['data_type'], - "notnull": col['is_nullable'] == 'NO', - "default_value": col['column_default'], - "primary_key": False # Will be updated below - }) + columns.append( + { + "name": col["column_name"], + "type": col["data_type"], + "notnull": col["is_nullable"] == "NO", + "default_value": col["column_default"], + "primary_key": False, # Will be updated below + } + ) # Get primary key information - cursor.execute(""" + cursor.execute( + """ SELECT column_name FROM information_schema.key_column_usage WHERE table_name = %s AND constraint_name LIKE '%_pkey' - """, (table,)) + """, + (table,), + ) - pk_columns = [row['column_name'] for row in cursor.fetchall()] + pk_columns = [row["column_name"] for row in cursor.fetchall()] for col in columns: - if col['name'] in pk_columns: - col['primary_key'] = True + if col["name"] in pk_columns: + col["primary_key"] = True schema_data[table] = columns # Get foreign key relationships - cursor.execute(""" + cursor.execute( + """ SELECT kcu.column_name, ccu.table_name AS foreign_table_name, ccu.column_name AS foreign_column_name @@ -376,15 +402,19 @@ class DatabaseInsightSystem: ON ccu.constraint_name = tc.constraint_name WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_name = %s - """, (table,)) + """, + (table,), + ) for fk in cursor.fetchall(): - relationships.append({ - "from_table": table, - "from_column": fk['column_name'], - "to_table": fk['foreign_table_name'], - "to_column": fk['foreign_column_name'] - }) + relationships.append( + { + "from_table": table, + "from_column": fk["column_name"], + "to_table": fk["foreign_table_name"], + "to_column": fk["foreign_column_name"], + } + ) conn.close() return DatabaseSchema(tables=schema_data, relationships=relationships) @@ -404,6 +434,7 @@ class DatabaseInsightSystem: # Parse connection string to get connection parameters # Format: mysql://user:password@host:port/database import urllib.parse + parsed = urllib.parse.urlparse(connection_string) conn = pymysql.connect( @@ -412,7 +443,7 @@ class DatabaseInsightSystem: user=parsed.username, password=parsed.password, database=parsed.path[1:], # Remove leading slash - cursorclass=pymysql.cursors.DictCursor + cursorclass=pymysql.cursors.DictCursor, ) cursor = conn.cursor() @@ -429,13 +460,15 @@ class DatabaseInsightSystem: cursor.execute(f"DESCRIBE {table}") columns = [] for col in cursor.fetchall(): - columns.append({ - "name": col['Field'], - "type": col['Type'], - "notnull": col['Null'] == 'NO', - "default_value": col['Default'], - "primary_key": col['Key'] == 'PRI' - }) + columns.append( + { + "name": col["Field"], + "type": col["Type"], + "notnull": col["Null"] == "NO", + "default_value": col["Default"], + "primary_key": col["Key"] == "PRI", + } + ) schema_data[table] = columns @@ -451,12 +484,14 @@ class DatabaseInsightSystem: """) for fk in cursor.fetchall(): - relationships.append({ - "from_table": table, - "from_column": fk['COLUMN_NAME'], - "to_table": fk['REFERENCED_TABLE_NAME'], - "to_column": fk['REFERENCED_COLUMN_NAME'] - }) + relationships.append( + { + "from_table": table, + "from_column": fk["COLUMN_NAME"], + "to_table": fk["REFERENCED_TABLE_NAME"], + "to_column": fk["REFERENCED_COLUMN_NAME"], + } + ) conn.close() return DatabaseSchema(tables=schema_data, relationships=relationships) @@ -467,7 +502,7 @@ class DatabaseInsightSystem: def _detect_language(self, text: str) -> str: """Detect if text is Arabic or English.""" - arabic_chars = re.findall(r'[\u0600-\u06FF]', text) + arabic_chars = re.findall(r"[\u0600-\u06FF]", text) return "ar" if len(arabic_chars) > len(text) * 0.3 else "en" def _execute_query_sync(self, query: str) -> List[Dict]: @@ -480,13 +515,19 @@ class DatabaseInsightSystem: raise ValueError("No database connection established") if self.db_connection.db_type == DatabaseType.SQLITE: - return await self._execute_sqlite_query(self.db_connection.connection_string, query) + return await self._execute_sqlite_query( + self.db_connection.connection_string, query + ) # elif self.db_connection.db_type == DatabaseType.DJANGO and DJANGO_AVAILABLE: # return await self._execute_django_query(query) elif self.db_connection.db_type == DatabaseType.POSTGRESQL: - return await self._execute_postgresql_query(self.db_connection.connection_string, query) + return await self._execute_postgresql_query( + self.db_connection.connection_string, query + ) elif self.db_connection.db_type == DatabaseType.MYSQL: - return await self._execute_mysql_query(self.db_connection.connection_string, query) + return await self._execute_mysql_query( + self.db_connection.connection_string, query + ) else: raise ValueError(f"Unsupported database type: {self.db_connection.db_type}") @@ -528,7 +569,9 @@ class DatabaseInsightSystem: logger.error(f"Django query execution failed: {e}") raise - async def _execute_postgresql_query(self, connection_string: str, query: str) -> List[Dict]: + async def _execute_postgresql_query( + self, connection_string: str, query: str + ) -> List[Dict]: """Execute SQL query on PostgreSQL database.""" try: import psycopg2 @@ -548,7 +591,9 @@ class DatabaseInsightSystem: logger.error(f"PostgreSQL query execution failed: {e}") raise - async def _execute_mysql_query(self, connection_string: str, query: str) -> List[Dict]: + async def _execute_mysql_query( + self, connection_string: str, query: str + ) -> List[Dict]: """Execute SQL query on MySQL database.""" try: import pymysql @@ -562,7 +607,7 @@ class DatabaseInsightSystem: user=parsed.username, password=parsed.password, database=parsed.path[1:], - cursorclass=pymysql.cursors.DictCursor + cursorclass=pymysql.cursors.DictCursor, ) cursor = conn.cursor() @@ -576,7 +621,9 @@ class DatabaseInsightSystem: logger.error(f"MySQL query execution failed: {e}") raise - def _prepare_chart_data(self, data: List[Dict], chart_type: str, fields: List[str]) -> Optional[Dict]: + def _prepare_chart_data( + self, data: List[Dict], chart_type: str, fields: List[str] + ) -> Optional[Dict]: """Prepare data for chart visualization.""" if not data or not fields: return None @@ -613,7 +660,7 @@ class DatabaseInsightSystem: "backgroundColor": [ f"rgba({50 + i * 30}, {100 + i * 25}, {200 + i * 20}, 0.7)" for i in range(len(values)) - ] + ], } else: # Multiple datasets for other chart types @@ -627,21 +674,19 @@ class DatabaseInsightSystem: value = 0 dataset_values.append(value) - datasets.append({ - "label": field, - "data": dataset_values, - "backgroundColor": f"rgba({50 + i * 40}, {100 + i * 30}, 235, 0.6)", - "borderColor": f"rgba({50 + i * 40}, {100 + i * 30}, 235, 1.0)", - "borderWidth": 2 - }) + datasets.append( + { + "label": field, + "data": dataset_values, + "backgroundColor": f"rgba({50 + i * 40}, {100 + i * 30}, 235, 0.6)", + "borderColor": f"rgba({50 + i * 40}, {100 + i * 30}, 235, 1.0)", + "borderWidth": 2, + } + ) except Exception as e: logger.warning(f"Error processing field {field}: {e}") - return { - "type": chart_type, - "labels": labels, - "datasets": datasets - } + return {"type": chart_type, "labels": labels, "datasets": datasets} except Exception as e: logger.error(f"Chart preparation failed: {e}") @@ -659,14 +704,18 @@ class DatabaseInsightSystem: "data": [], "metadata": {}, "error": str(e), - "language": "en" + "language": "en", } async def get_insights(self, request: InsightRequest) -> QueryResult: """Main method to get database insights from natural language prompt.""" try: # Detect language - language = self._detect_language(request.prompt) if request.language == "auto" else request.language + language = ( + self._detect_language(request.prompt) + if request.language == "auto" + else request.language + ) # Analyze database schema schema = await self.analyze_database_schema(request) @@ -674,7 +723,7 @@ class DatabaseInsightSystem: # Generate query plan using AI query_response = await self.query_agent.run( f"User prompt: {request.prompt}\nLanguage: {language}", - database_schema=schema + database_schema=schema, ) # Parse AI response @@ -682,13 +731,15 @@ class DatabaseInsightSystem: query_plan = json.loads(query_response.output) except json.JSONDecodeError: # Fallback: extract SQL from response - sql_match = re.search(r'SELECT.*?;', query_response.output, re.IGNORECASE | re.DOTALL) + sql_match = re.search( + r"SELECT.*?;", query_response.output, re.IGNORECASE | re.DOTALL + ) if sql_match: query_plan = { "sql_query": sql_match.group(0), "chart_suggestion": "bar", "expected_fields": [], - "language": language + "language": language, } else: raise ValueError("Could not parse AI response") @@ -710,21 +761,25 @@ class DatabaseInsightSystem: elif data: # Use first few fields if no specific fields suggested available_fields = list(data[0].keys()) if data else [] - chart_data = self._prepare_chart_data(data, chart_type, available_fields[:3]) + chart_data = self._prepare_chart_data( + data, chart_type, available_fields[:3] + ) # Prepare result return QueryResult( status="success", - data=data[:request.limit] if data else [], + data=data[: request.limit] if data else [], metadata={ "total_count": len(data) if data else 0, "query": sql_query, "analysis": query_plan.get("analysis", ""), "fields": expected_fields or (list(data[0].keys()) if data else []), - "database_type": self.db_connection.db_type.value if self.db_connection else "unknown" + "database_type": self.db_connection.db_type.value + if self.db_connection + else "unknown", }, chart_data=chart_data, - language=language + language=language, ) except Exception as e: @@ -734,7 +789,7 @@ class DatabaseInsightSystem: data=[], metadata={}, error=str(e), - language=language if 'language' in locals() else "en" + language=language if "language" in locals() else "en", ) # # Static method for Django view compatibility @@ -798,4 +853,4 @@ def analyze_prompt_sync(prompt: str, **kwargs) -> Dict[str, Any]: """ system = DatabaseInsightSystem() request = InsightRequest(prompt=prompt, **kwargs) - return system.get_insights_sync(request) \ No newline at end of file + return system.get_insights_sync(request) diff --git a/haikalbot/management/commands/generate_support_yaml.py b/haikalbot/management/commands/generate_support_yaml.py index cfdee27e..a844fe4b 100644 --- a/haikalbot/management/commands/generate_support_yaml.py +++ b/haikalbot/management/commands/generate_support_yaml.py @@ -9,7 +9,9 @@ from django.template.loaders.app_directories import get_app_template_dirs class Command(BaseCommand): - help = "Generate YAML support knowledge base from Django views, models, and templates" + help = ( + "Generate YAML support knowledge base from Django views, models, and templates" + ) def handle(self, *args, **kwargs): output_file = "haikal_kb.yaml" @@ -22,7 +24,7 @@ class Command(BaseCommand): "features": {}, "user_workflows": {}, # New section for step-by-step instructions "templates": {}, - "glossary": {} + "glossary": {}, } def extract_doc(item): @@ -42,31 +44,48 @@ class Command(BaseCommand): def get_all_model_classes(): all_models = [] for model in apps.get_models(): - all_models.append((model._meta.app_label, model.__name__, extract_doc(model))) + all_models.append( + (model._meta.app_label, model.__name__, extract_doc(model)) + ) return all_models def get_all_templates(): - template_dirs = get_app_template_dirs('templates') + template_dirs = get_app_template_dirs("templates") templates = [] for template_dir in template_dirs: - app_name = os.path.basename(os.path.dirname(os.path.dirname(template_dir))) + app_name = os.path.basename( + os.path.dirname(os.path.dirname(template_dir)) + ) for root, dirs, files in os.walk(template_dir): for file in files: - if file.endswith(('.html', '.htm', '.txt')): - rel_path = os.path.relpath(os.path.join(root, file), template_dir) - with open(os.path.join(root, file), 'r', encoding='utf-8', errors='ignore') as f: + if file.endswith((".html", ".htm", ".txt")): + rel_path = os.path.relpath( + os.path.join(root, file), template_dir + ) + with open( + os.path.join(root, file), + "r", + encoding="utf-8", + errors="ignore", + ) as f: try: content = f.read() # Extract template comment documentation if it exists doc = "" - if '{# DOC:' in content and '#}' in content: - doc_parts = content.split('{# DOC:') + if "{# DOC:" in content and "#}" in content: + doc_parts = content.split("{# DOC:") for part in doc_parts[1:]: - if '#}' in part: - doc += part.split('#}')[0].strip() + "\n" + if "#}" in part: + doc += ( + part.split("#}")[0].strip() + "\n" + ) except Exception as e: - self.stdout.write(self.style.WARNING(f"Error reading {rel_path}: {e}")) + self.stdout.write( + self.style.WARNING( + f"Error reading {rel_path}: {e}" + ) + ) continue templates.append((app_name, rel_path, doc.strip())) @@ -75,17 +94,24 @@ class Command(BaseCommand): # Look for workflow documentation files def get_workflow_docs(): workflows = {} - workflow_dir = os.path.join(settings.BASE_DIR, 'docs', 'workflows') + workflow_dir = os.path.join(settings.BASE_DIR, "docs", "workflows") if os.path.exists(workflow_dir): for file in os.listdir(workflow_dir): - if file.endswith('.yaml') or file.endswith('.yml'): + if file.endswith(".yaml") or file.endswith(".yml"): try: - with open(os.path.join(workflow_dir, file), 'r') as f: + with open(os.path.join(workflow_dir, file), "r") as f: workflow_data = yaml.safe_load(f) - for workflow_name, workflow_info in workflow_data.items(): + for ( + workflow_name, + workflow_info, + ) in workflow_data.items(): workflows[workflow_name] = workflow_info except Exception as e: - self.stdout.write(self.style.WARNING(f"Error reading workflow file {file}: {e}")) + self.stdout.write( + self.style.WARNING( + f"Error reading workflow file {file}: {e}" + ) + ) return workflows # Extract views @@ -96,26 +122,28 @@ class Command(BaseCommand): kb["features"][name] = { "description": doc, "source": f"{app}.views.{name}", - "type": "view_function" + "type": "view_function", } # Look for @workflow decorator or WORKFLOW tag in docstring - if hasattr(obj, 'workflow_steps') or 'WORKFLOW:' in doc: - workflow_name = name.replace('_', ' ').title() + if hasattr(obj, "workflow_steps") or "WORKFLOW:" in doc: + workflow_name = name.replace("_", " ").title() steps = [] - if hasattr(obj, 'workflow_steps'): + if hasattr(obj, "workflow_steps"): steps = obj.workflow_steps - elif 'WORKFLOW:' in doc: - workflow_section = doc.split('WORKFLOW:')[1].strip() - steps_text = workflow_section.split('\n') - steps = [step.strip() for step in steps_text if step.strip()] + elif "WORKFLOW:" in doc: + workflow_section = doc.split("WORKFLOW:")[1].strip() + steps_text = workflow_section.split("\n") + steps = [ + step.strip() for step in steps_text if step.strip() + ] if steps: kb["user_workflows"][workflow_name] = { "description": f"How to {name.replace('_', ' ')}", "steps": steps, - "source": f"{app}.views.{name}" + "source": f"{app}.views.{name}", } # Extract models @@ -124,7 +152,7 @@ class Command(BaseCommand): kb["features"][name] = { "description": doc, "source": f"{app}.models.{name}", - "type": "model_class" + "type": "model_class", } # Extract templates @@ -134,7 +162,7 @@ class Command(BaseCommand): kb["templates"][template_id] = { "description": doc, "path": template_path, - "app": app + "app": app, } # Add workflow documentation @@ -153,9 +181,9 @@ class Command(BaseCommand): "Select the car series from the available options", "Select the trim level for the car", "Fill in additional details like color, mileage, and price", - "Click 'Save' to add the car to inventory, or 'Save & Add Another' to continue adding cars" + "Click 'Save' to add the car to inventory, or 'Save & Add Another' to continue adding cars", ], - "source": "manual_documentation" + "source": "manual_documentation", }, "Create New Invoice": { "description": "How to create a new invoice", @@ -167,13 +195,15 @@ class Command(BaseCommand): "Select the car(s) to include in the invoice", "Add any additional services or parts by clicking 'Add Item'", "Set the payment terms and due date", - "Click 'Save Draft' to save without finalizing, or 'Finalize Invoice' to complete" + "Click 'Save Draft' to save without finalizing, or 'Finalize Invoice' to complete", ], - "source": "manual_documentation" - } + "source": "manual_documentation", + }, } with open(output_file, "w", encoding="utf-8") as f: yaml.dump(kb, f, allow_unicode=True, sort_keys=False) - self.stdout.write(self.style.SUCCESS(f"✅ YAML knowledge base saved to {output_file}")) + self.stdout.write( + self.style.SUCCESS(f"✅ YAML knowledge base saved to {output_file}") + ) diff --git a/haikalbot/migrations/0001_initial.py b/haikalbot/migrations/0001_initial.py index 6a1928ce..9f5ebf66 100644 --- a/haikalbot/migrations/0001_initial.py +++ b/haikalbot/migrations/0001_initial.py @@ -7,44 +7,90 @@ from django.db import migrations, models class Migration(migrations.Migration): - initial = True dependencies = [ - ('inventory', '__first__'), + ("inventory", "__first__"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( - name='AnalysisCache', + name="AnalysisCache", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('prompt_hash', models.CharField(db_index=True, max_length=64)), - ('dealer_id', models.IntegerField(blank=True, db_index=True, null=True)), - ('created_at', models.DateTimeField(default=django.utils.timezone.now)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('expires_at', models.DateTimeField(db_index=True)), - ('result', models.JSONField()), - ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("prompt_hash", models.CharField(db_index=True, max_length=64)), + ( + "dealer_id", + models.IntegerField(blank=True, db_index=True, null=True), + ), + ("created_at", models.DateTimeField(default=django.utils.timezone.now)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("expires_at", models.DateTimeField(db_index=True)), + ("result", models.JSONField()), + ( + "user", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'verbose_name_plural': 'Analysis caches', - 'indexes': [models.Index(fields=['prompt_hash', 'dealer_id'], name='haikalbot_a_prompt__b98e1e_idx'), models.Index(fields=['expires_at'], name='haikalbot_a_expires_e790cd_idx')], + "verbose_name_plural": "Analysis caches", + "indexes": [ + models.Index( + fields=["prompt_hash", "dealer_id"], + name="haikalbot_a_prompt__b98e1e_idx", + ), + models.Index( + fields=["expires_at"], name="haikalbot_a_expires_e790cd_idx" + ), + ], }, ), migrations.CreateModel( - name='ChatLog', + name="ChatLog", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('user_message', models.TextField()), - ('chatbot_response', models.TextField()), - ('timestamp', models.DateTimeField(auto_now_add=True, db_index=True)), - ('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='chatlogs', to='inventory.dealer')), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("user_message", models.TextField()), + ("chatbot_response", models.TextField()), + ("timestamp", models.DateTimeField(auto_now_add=True, db_index=True)), + ( + "dealer", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="chatlogs", + to="inventory.dealer", + ), + ), ], options={ - 'ordering': ['-timestamp'], - 'indexes': [models.Index(fields=['dealer', 'timestamp'], name='haikalbot_c_dealer__6f8d63_idx')], + "ordering": ["-timestamp"], + "indexes": [ + models.Index( + fields=["dealer", "timestamp"], + name="haikalbot_c_dealer__6f8d63_idx", + ) + ], }, ), ] diff --git a/haikalbot/models.py b/haikalbot/models.py index fed58f8e..6637aafd 100644 --- a/haikalbot/models.py +++ b/haikalbot/models.py @@ -24,15 +24,18 @@ class ChatLog(models.Model): :ivar timestamp: The date and time when the chat log entry was created. :type timestamp: datetime """ - dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name='chatlogs', db_index=True) + + dealer = models.ForeignKey( + Dealer, on_delete=models.CASCADE, related_name="chatlogs", db_index=True + ) user_message = models.TextField() chatbot_response = models.TextField() timestamp = models.DateTimeField(auto_now_add=True, db_index=True) class Meta: - ordering = ['-timestamp'] + ordering = ["-timestamp"] indexes = [ - models.Index(fields=['dealer', 'timestamp']), + models.Index(fields=["dealer", "timestamp"]), ] def __str__(self): @@ -62,8 +65,11 @@ class AnalysisCache(models.Model): :ivar result: The cached analysis result :type result: dict """ + prompt_hash = models.CharField(max_length=64, db_index=True) - user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True, blank=True) + user = models.ForeignKey( + settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True, blank=True + ) dealer_id = models.IntegerField(null=True, blank=True, db_index=True) created_at = models.DateTimeField(default=timezone.now) updated_at = models.DateTimeField(auto_now=True) @@ -72,8 +78,8 @@ class AnalysisCache(models.Model): class Meta: indexes = [ - models.Index(fields=['prompt_hash', 'dealer_id']), - models.Index(fields=['expires_at']), + models.Index(fields=["prompt_hash", "dealer_id"]), + models.Index(fields=["expires_at"]), ] verbose_name_plural = "Analysis caches" diff --git a/haikalbot/tests.py b/haikalbot/tests.py index 49290204..a39b155a 100644 --- a/haikalbot/tests.py +++ b/haikalbot/tests.py @@ -1,2 +1 @@ - # Create your tests here. diff --git a/haikalbot/utils/ask_haikalbot.py b/haikalbot/utils/ask_haikalbot.py index bcf11aea..cb8ebf12 100644 --- a/haikalbot/utils/ask_haikalbot.py +++ b/haikalbot/utils/ask_haikalbot.py @@ -43,10 +43,7 @@ Provide a clear step-by-step guide with numbered instructions. Include: Helpful Step-by-Step Instructions:""" -PROMPT = PromptTemplate( - template=template, - input_variables=["context", "question"] -) +PROMPT = PromptTemplate(template=template, input_variables=["context", "question"]) # Setup QA chain qa = RetrievalQA.from_chain_type( @@ -54,23 +51,25 @@ qa = RetrievalQA.from_chain_type( chain_type="stuff", retriever=index.vectorstore.as_retriever(), return_source_documents=True, - chain_type_kwargs={"prompt": PROMPT} + chain_type_kwargs={"prompt": PROMPT}, ) + # Function to run a query def ask_haikal(query): response = qa.invoke({"query": query}) - print("\n" + "="*50) + print("\n" + "=" * 50) print(f"Question: {query}") - print("="*50) + print("=" * 50) print("\nAnswer:") print(response["result"]) print("\nSources:") for doc in response["source_documents"]: print(f"- {doc.metadata.get('source', 'Unknown source')}") - print("="*50) + print("=" * 50) return response["result"] + # # Example query # if __name__ == "__main__": # query = "How do I add a new car to the inventory? answer in Arabic" diff --git a/haikalbot/utils/export.py b/haikalbot/utils/export.py index f2701671..41cb1341 100644 --- a/haikalbot/utils/export.py +++ b/haikalbot/utils/export.py @@ -15,17 +15,16 @@ def export_to_excel(self, data, filename): HttpResponse: Response with Excel file """ - # Convert data to DataFrame df = pd.DataFrame(data) # Create Excel file in memory excel_file = BytesIO() - with pd.ExcelWriter(excel_file, engine='xlsxwriter') as writer: - df.to_excel(writer, sheet_name='Model Analysis', index=False) + with pd.ExcelWriter(excel_file, engine="xlsxwriter") as writer: + df.to_excel(writer, sheet_name="Model Analysis", index=False) # Auto-adjust columns width - worksheet = writer.sheets['Model Analysis'] + worksheet = writer.sheets["Model Analysis"] for i, col in enumerate(df.columns): max_width = max(df[col].astype(str).map(len).max(), len(col)) + 2 worksheet.set_column(i, i, max_width) @@ -34,9 +33,9 @@ def export_to_excel(self, data, filename): excel_file.seek(0) response = HttpResponse( excel_file.read(), - content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ) - response['Content-Disposition'] = f'attachment; filename="{filename}.xlsx"' + response["Content-Disposition"] = f'attachment; filename="{filename}.xlsx"' return response @@ -52,7 +51,6 @@ def export_to_csv(self, data, filename): HttpResponse: Response with CSV file """ - # Convert data to DataFrame df = pd.DataFrame(data) @@ -61,6 +59,6 @@ def export_to_csv(self, data, filename): df.to_csv(csv_file, index=False) # Set up response - response = HttpResponse(csv_file.getvalue(), content_type='text/csv') - response['Content-Disposition'] = f'attachment; filename="{filename}.csv"' + response = HttpResponse(csv_file.getvalue(), content_type="text/csv") + response["Content-Disposition"] = f'attachment; filename="{filename}.csv"' return response diff --git a/haikalbot/utils/response_formatter.py b/haikalbot/utils/response_formatter.py index ea9e6ea1..438485a3 100644 --- a/haikalbot/utils/response_formatter.py +++ b/haikalbot/utils/response_formatter.py @@ -1,10 +1,10 @@ def format_response(prompt, language, request_id, timestamp): """ Format a standardized response structure based on language. - + This utility creates a consistent response structure with the appropriate keys based on the specified language. - + :param prompt: The original user prompt :type prompt: str :param language: Language code ('en' or 'ar') @@ -16,28 +16,28 @@ def format_response(prompt, language, request_id, timestamp): :return: Formatted response structure :rtype: dict """ - if language == 'ar': + if language == "ar": return { - 'حالة': "نجاح", - 'معرف_الطلب': request_id, - 'الطابع_الزمني': timestamp, - 'الاستعلام': prompt, - 'التحليلات': [] + "حالة": "نجاح", + "معرف_الطلب": request_id, + "الطابع_الزمني": timestamp, + "الاستعلام": prompt, + "التحليلات": [], } else: return { - 'status': "success", - 'request_id': request_id, - 'timestamp': timestamp, - 'prompt': prompt, - 'insights': [] + "status": "success", + "request_id": request_id, + "timestamp": timestamp, + "prompt": prompt, + "insights": [], } -def format_error_response(message, status_code, language='en'): +def format_error_response(message, status_code, language="en"): """ Format a standardized error response. - + :param message: Error message :type message: str :param status_code: HTTP status code @@ -47,24 +47,16 @@ def format_error_response(message, status_code, language='en'): :return: Formatted error response :rtype: dict """ - if language == 'ar': - return { - 'حالة': "خطأ", - 'رسالة': message, - 'رمز_الحالة': status_code - } + if language == "ar": + return {"حالة": "خطأ", "رسالة": message, "رمز_الحالة": status_code} else: - return { - 'status': "error", - 'message': message, - 'status_code': status_code - } + return {"status": "error", "message": message, "status_code": status_code} -def format_insights_for_display(insights, language='en'): +def format_insights_for_display(insights, language="en"): """ Format insights for human-readable display. - + :param insights: Raw insights data :type insights: dict :param language: Language code ('en' or 'ar') @@ -73,47 +65,55 @@ def format_insights_for_display(insights, language='en'): :rtype: str """ formatted_text = "" - + # Determine keys based on language - insights_key = 'التحليلات' if language == 'ar' else 'insights' - recs_key = 'التوصيات' if language == 'ar' else 'recommendations' - + insights_key = "التحليلات" if language == "ar" else "insights" + recs_key = "التوصيات" if language == "ar" else "recommendations" + # Format insights if insights_key in insights and insights[insights_key]: - header = "## نتائج التحليل\n\n" if language == 'ar' else "## Analysis Results\n\n" + header = ( + "## نتائج التحليل\n\n" if language == "ar" else "## Analysis Results\n\n" + ) formatted_text += header - + for insight in insights[insights_key]: if isinstance(insight, dict): # Add insight type as header - if 'type' in insight or 'نوع' in insight: - type_key = 'نوع' if language == 'ar' else 'type' - insight_type = insight.get(type_key, insight.get('type', insight.get('نوع', ''))) + if "type" in insight or "نوع" in insight: + type_key = "نوع" if language == "ar" else "type" + insight_type = insight.get( + type_key, insight.get("type", insight.get("نوع", "")) + ) formatted_text += f"### {insight_type}\n\n" - + # Format results if present - results_key = 'النتائج' if language == 'ar' else 'results' + results_key = "النتائج" if language == "ar" else "results" if results_key in insight: for result in insight[results_key]: - model_key = 'النموذج' if language == 'ar' else 'model' - error_key = 'خطأ' if language == 'ar' else 'error' - count_key = 'العدد' if language == 'ar' else 'count' - - model_name = result.get(model_key, result.get('model', '')) - + model_key = "النموذج" if language == "ar" else "model" + error_key = "خطأ" if language == "ar" else "error" + count_key = "العدد" if language == "ar" else "count" + + model_name = result.get(model_key, result.get("model", "")) + if error_key in result: - formatted_text += f"- **{model_name}**: {result[error_key]}\n" + formatted_text += ( + f"- **{model_name}**: {result[error_key]}\n" + ) elif count_key in result: - formatted_text += f"- **{model_name}**: {result[count_key]}\n" - + formatted_text += ( + f"- **{model_name}**: {result[count_key]}\n" + ) + formatted_text += "\n" - + # Format recommendations if recs_key in insights and insights[recs_key]: - header = "## التوصيات\n\n" if language == 'ar' else "## Recommendations\n\n" + header = "## التوصيات\n\n" if language == "ar" else "## Recommendations\n\n" formatted_text += header - + for rec in insights[recs_key]: formatted_text += f"- {rec}\n" - + return formatted_text diff --git a/haikalbot/views.py b/haikalbot/views.py index 9467391d..9451560d 100644 --- a/haikalbot/views.py +++ b/haikalbot/views.py @@ -5,20 +5,22 @@ from django.utils.translation import gettext as _ from django.views import View import logging from .ai_agent import analyze_prompt + # from .haikal_agent import DatabaseInsightSystem, analyze_prompt_sync from .utils.export import export_to_excel, export_to_csv logger = logging.getLogger(__name__) # analyze_prompt_ai = DatabaseInsightSystem + class HaikalBot(LoginRequiredMixin, View): def get(self, request, *args, **kwargs): """ Render the chat interface. """ context = { - 'dark_mode': request.session.get('dark_mode', False), - 'page_title': _('AI Assistant') + "dark_mode": request.session.get("dark_mode", False), + "page_title": _("AI Assistant"), } return render(request, "haikalbot/chat.html", context) @@ -31,7 +33,9 @@ class HaikalBot(LoginRequiredMixin, View): language = request.POST.get("language", request.LANGUAGE_CODE) if not prompt: - error_msg = _("Prompt is required.") if language != "ar" else "الاستعلام مطلوب." + error_msg = ( + _("Prompt is required.") if language != "ar" else "الاستعلام مطلوب." + ) return JsonResponse({"status": "error", "error": error_msg}, status=400) try: result = analyze_prompt(prompt) @@ -54,8 +58,11 @@ class HaikalBot(LoginRequiredMixin, View): if language == "ar": error_msg = "حدث خطأ أثناء معالجة طلبك." - return JsonResponse({ - "status": "error", - "error": error_msg, - "details": str(e) if request.user.is_staff else None - }, status=500) + return JsonResponse( + { + "status": "error", + "error": error_msg, + "details": str(e) if request.user.is_staff else None, + }, + status=500, + ) diff --git a/inventory/admin.py b/inventory/admin.py index 6983bb84..306c3a98 100644 --- a/inventory/admin.py +++ b/inventory/admin.py @@ -2,6 +2,7 @@ from django.contrib import admin from . import models from django_ledger import models as ledger_models + # from django_pdf_actions.actions import export_to_pdf_landscape, export_to_pdf_portrait # from appointment import models as appointment_models from import_export.admin import ExportMixin @@ -18,10 +19,12 @@ from import_export.resources import ModelResource # class CarSeriesAdmin(ExportMixin, admin.ModelAdmin): # resource_class = CarSerieResource + class CarTrimResource(ModelResource): class Meta: model = models.CarTrim + @admin.register(models.CarTrim) class CarTrimAdmin(ExportMixin, admin.ModelAdmin): resource_class = CarTrimResource @@ -71,44 +74,55 @@ admin.site.register(models.DealersMake) @admin.register(models.Car) class CarAdmin(admin.ModelAdmin): - search_fields = ('vin',) + search_fields = ("vin",) # actions = [export_to_pdf_landscape, export_to_pdf_portrait] + @admin.register(models.CarMake) class CarMakeAdmin(admin.ModelAdmin): - list_display = ('name', 'arabic_name', 'is_sa_import') - search_fields = ('name', 'arabic_name') - list_filter = ('is_sa_import', 'name',) + list_display = ("name", "arabic_name", "is_sa_import") + search_fields = ("name", "arabic_name") + list_filter = ( + "is_sa_import", + "name", + ) # actions = [export_to_pdf_landscape, export_to_pdf_portrait] class Meta: verbose_name = "Car Make" - ordering = ('name',) + ordering = ("name",) @admin.register(models.CarModel) class CarModelAdmin(admin.ModelAdmin): - list_display = ('name', 'arabic_name', 'id_car_make', 'get_is_sa_import') - search_fields = ('id_car_model', 'name', 'arabic_name') - list_filter = ('id_car_make__is_sa_import', 'id_car_make') - sortable_by = ['name', 'arabic_name', 'id_car_make'] + list_display = ("name", "arabic_name", "id_car_make", "get_is_sa_import") + search_fields = ("id_car_model", "name", "arabic_name") + list_filter = ("id_car_make__is_sa_import", "id_car_make") + sortable_by = ["name", "arabic_name", "id_car_make"] def get_is_sa_import(self, obj): return obj.id_car_make.is_sa_import + get_is_sa_import.boolean = True - get_is_sa_import.short_description = 'Is SA Import' + get_is_sa_import.short_description = "Is SA Import" class Meta: verbose_name = "Car Model" - ordering = ('name',) + ordering = ("name",) @admin.register(models.CarSerie) class CarSeriesAdmin(admin.ModelAdmin): - list_display = ('name', 'arabic_name', 'id_car_model', ) - search_fields = ('name',) - list_filter = ('id_car_model__id_car_make__is_sa_import', - 'id_car_model__id_car_make__name',) + list_display = ( + "name", + "arabic_name", + "id_car_model", + ) + search_fields = ("name",) + list_filter = ( + "id_car_model__id_car_make__is_sa_import", + "id_car_model__id_car_make__name", + ) class Meta: verbose_name = "Car Series" @@ -130,9 +144,9 @@ class CarSeriesAdmin(admin.ModelAdmin): @admin.register(models.CarSpecification) class CarSpecificationAdmin(admin.ModelAdmin): - list_display = ('name', 'arabic_name', 'id_parent') - search_fields = ('name', 'id_parent') - list_filter = ('id_parent',) + list_display = ("name", "arabic_name", "id_parent") + search_fields = ("name", "id_parent") + list_filter = ("id_parent",) class Meta: verbose_name = "Car Specification" @@ -140,12 +154,14 @@ class CarSpecificationAdmin(admin.ModelAdmin): @admin.register(models.CarOption) class CarOptionAdmin(admin.ModelAdmin): - list_display = ('name', 'arabic_name', 'id_parent') - search_fields = ('name', 'id_parent') + list_display = ("name", "arabic_name", "id_parent") + search_fields = ("name", "id_parent") + # list_filter = ('id_parent',) class Meta: verbose_name = "Car Option" + # @admin.register(models.UserActivityLog) # class UserActivityLogAdmin(admin.ModelAdmin): # list_display = ('user', 'action', 'timestamp') @@ -156,5 +172,3 @@ class CarOptionAdmin(admin.ModelAdmin): # class ItemTransactionModelAdmin(admin.ModelAdmin): # actions = [export_to_pdf_landscape, export_to_pdf_portrait] - - diff --git a/inventory/apps.py b/inventory/apps.py index ad2bcf1a..b13bce27 100644 --- a/inventory/apps.py +++ b/inventory/apps.py @@ -1,8 +1,9 @@ from django.apps import AppConfig + class InventoryConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'inventory' + default_auto_field = "django.db.models.BigAutoField" + name = "inventory" def ready(self): import inventory.signals diff --git a/inventory/context_processors.py b/inventory/context_processors.py index 55cff5ef..7f749d6a 100644 --- a/inventory/context_processors.py +++ b/inventory/context_processors.py @@ -1,5 +1,6 @@ from django.conf import settings + def currency_context(request): """ Provides a context dictionary containing the currency setting. This is typically @@ -14,9 +15,7 @@ def currency_context(request): project's CURRENCY setting. :rtype: dict """ - return { - 'CURRENCY': settings.CURRENCY - } + return {"CURRENCY": settings.CURRENCY} def breadcrumbs(request): @@ -37,8 +36,8 @@ def breadcrumbs(request): :rtype: dict """ breadcrumbs = [] - path = request.path.strip('/').split('/') + path = request.path.strip("/").split("/") for i in range(len(path)): - url = '/' + '/'.join(path[:i+1]) + '/' - breadcrumbs.append({'name': path[i].capitalize(), 'url': url}) - return {'breadcrumbs': breadcrumbs} \ No newline at end of file + url = "/" + "/".join(path[: i + 1]) + "/" + breadcrumbs.append({"name": path[i].capitalize(), "url": url}) + return {"breadcrumbs": breadcrumbs} diff --git a/inventory/filters.py b/inventory/filters.py index 8fdb0206..cced1175 100644 --- a/inventory/filters.py +++ b/inventory/filters.py @@ -1,6 +1,7 @@ from django_ledger.models import AccountModel import django_filters + class AccountModelFilter(django_filters.FilterSet): """ Handles filtering functionality for the AccountModel. @@ -17,6 +18,7 @@ class AccountModelFilter(django_filters.FilterSet): :ivar fields: List of fields defined in the model that can be filtered. :type fields: list(str) """ + class Meta: model = AccountModel - fields = ['code', 'name','role'] \ No newline at end of file + fields = ["code", "name", "role"] diff --git a/inventory/forms.py b/inventory/forms.py index 7ace0c01..af6d0a24 100644 --- a/inventory/forms.py +++ b/inventory/forms.py @@ -1270,6 +1270,7 @@ class OpportunityForm(forms.ModelForm): widgets = { "expected_revenue": forms.NumberInput(attrs={"readonly": "readonly"}), } + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Add a visible number input to display the current value @@ -1354,19 +1355,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 \ No newline at end of file + return csv_file diff --git a/inventory/haikalna.py b/inventory/haikalna.py index 7d4e9297..a66d40d4 100644 --- a/inventory/haikalna.py +++ b/inventory/haikalna.py @@ -3,15 +3,57 @@ import re def vin_year(vin_char: str) -> int: YEAR_MAPPING = { - 'A': 1980, 'B': 1981, 'C': 1982, 'D': 1983, 'E': 1984, 'F': 1985, - 'G': 1986, 'H': 1987, 'J': 1988, 'K': 1989, 'L': 1990, 'M': 1991, - 'N': 1992, 'P': 1993, 'R': 1994, 'S': 1995, 'T': 1996, 'V': 1997, - 'W': 1998, 'X': 1999, 'Y': 2000, '1': 2001, '2': 2002, '3': 2003, - '4': 2004, '5': 2005, '6': 2006, '7': 2007, '8': 2008, '9': 2009, - 'A': 2010, 'B': 2011, 'C': 2012, 'D': 2013, 'E': 2014, 'F': 2015, - 'G': 2016, 'H': 2017, 'J': 2018, 'K': 2019, 'L': 2020, 'M': 2021, - 'N': 2022, 'P': 2023, 'R': 2024, 'S': 2025, 'T': 2026, 'V': 2027, - 'W': 2028, 'X': 2029, 'Y': 2030, + "A": 1980, + "B": 1981, + "C": 1982, + "D": 1983, + "E": 1984, + "F": 1985, + "G": 1986, + "H": 1987, + "J": 1988, + "K": 1989, + "L": 1990, + "M": 1991, + "N": 1992, + "P": 1993, + "R": 1994, + "S": 1995, + "T": 1996, + "V": 1997, + "W": 1998, + "X": 1999, + "Y": 2000, + "1": 2001, + "2": 2002, + "3": 2003, + "4": 2004, + "5": 2005, + "6": 2006, + "7": 2007, + "8": 2008, + "9": 2009, + "A": 2010, + "B": 2011, + "C": 2012, + "D": 2013, + "E": 2014, + "F": 2015, + "G": 2016, + "H": 2017, + "J": 2018, + "K": 2019, + "L": 2020, + "M": 2021, + "N": 2022, + "P": 2023, + "R": 2024, + "S": 2025, + "T": 2026, + "V": 2027, + "W": 2028, + "X": 2029, + "Y": 2030, } # Normalize the input character to uppercase @@ -19,7 +61,9 @@ def vin_year(vin_char: str) -> int: # Check if the character is valid if vin_char not in YEAR_MAPPING: - raise ValueError(f"Invalid year character: '{vin_char}'. Expected one of {list(YEAR_MAPPING.keys())}.") + raise ValueError( + f"Invalid year character: '{vin_char}'. Expected one of {list(YEAR_MAPPING.keys())}." + ) # Return the corresponding year return YEAR_MAPPING[vin_char] @@ -1355,452 +1399,425 @@ wmi_manufacturer_mapping = { "MM0": "Mazda", "MM6": "Mazda", "MM7": "Mazda", - "MM8": "Mazda" + "MM8": "Mazda", } def decode_vds(manufacturer, vds): # Mapping of manufacturers to their VDS positions and corresponding models vds_model_mapping = { - 'Honda': { + "Honda": { 1: { - 'F': 'Agila', - 'G': 'Insignia', - 'J': 'Mokka', - 'L': 'Antara', - 'M': 'Movano', - 'P': ['Astra J', 'Zafira C'], - 'R': 'Astra GTC J', - 'S': 'Meriva', - 'V': 'Combo II', - 'W': 'Cascada', + "F": "Agila", + "G": "Insignia", + "J": "Mokka", + "L": "Antara", + "M": "Movano", + "P": ["Astra J", "Zafira C"], + "R": "Astra GTC J", + "S": "Meriva", + "V": "Combo II", + "W": "Cascada", } }, - - 'Kia': { + "Kia": { 1: { - 'A': ['Rio', 'EV9'], - 'C': ['Niro', 'EV6'], - 'D': 'Rio', - 'E': ['Stinger', 'Seltos'], - 'F': ['Forte', 'K4'], - 'G': ['Optima', 'Magentis', 'K5'], - 'H': 'Rondo', - 'J': 'Soul', - 'K': ['Mohave', 'Sorento', 'Sportage'], - 'L': ['Cadenza', 'K9'], - 'M': 'Sedona', - 'N': 'Carnival', - 'P': ['Sportage', 'Sorento', 'Telluride'], - 'R': 'Sorento', - 'S': 'K9' + "A": ["Rio", "EV9"], + "C": ["Niro", "EV6"], + "D": "Rio", + "E": ["Stinger", "Seltos"], + "F": ["Forte", "K4"], + "G": ["Optima", "Magentis", "K5"], + "H": "Rondo", + "J": "Soul", + "K": ["Mohave", "Sorento", "Sportage"], + "L": ["Cadenza", "K9"], + "M": "Sedona", + "N": "Carnival", + "P": ["Sportage", "Sorento", "Telluride"], + "R": "Sorento", + "S": "K9", } }, - - 'Peugeot': { + "Peugeot": { 1: { - 'A': '604', - '2': '206', - '8': ['406', '508'], - '6': '407', - '9': '308', - "4": '308', - 'B': 'Expert', - 'C': ['208', '504'], - 'D': '301', - '3': '307', - '7': ['306', 'Partner'], - 'U': '2008', - 'M': '3008', + "A": "604", + "2": "206", + "8": ["406", "508"], + "6": "407", + "9": "308", + "4": "308", + "B": "Expert", + "C": ["208", "504"], + "D": "301", + "3": "307", + "7": ["306", "Partner"], + "U": "2008", + "M": "3008", } }, - - 'Toyota': { + "Toyota": { 1: { # 5th character in VDS - '0': 'MR2 Spyder', - '1': 'Tundra', - '3': ['Echo', 'Yaris'], - 'A': ['Highlander', 'Sequoia', 'Celica', 'Supra'], - 'B': 'Avalon', - 'C': ['Sienna', 'Previa'], - 'D': 'T100', - 'E': ['Corolla', 'Matrix'], - 'F': 'FJ Cruiser', - 'G': 'Hilux', - 'H': 'Highlander', - 'J': 'Land Cruiser', - 'K': 'Camry', - 'L': ['Tercel', 'Paseo'], - 'M': 'Previa', - 'N': 'Tacoma', - 'P': 'Camry', - 'R': ['4Runner', 'Corolla'], - 'T': 'Celica FWD', - 'U': 'Prius', - 'V': 'RAV4', - 'W': 'MR2 non Spyder', - 'X': 'Cressida', + "0": "MR2 Spyder", + "1": "Tundra", + "3": ["Echo", "Yaris"], + "A": ["Highlander", "Sequoia", "Celica", "Supra"], + "B": "Avalon", + "C": ["Sienna", "Previa"], + "D": "T100", + "E": ["Corolla", "Matrix"], + "F": "FJ Cruiser", + "G": "Hilux", + "H": "Highlander", + "J": "Land Cruiser", + "K": "Camry", + "L": ["Tercel", "Paseo"], + "M": "Previa", + "N": "Tacoma", + "P": "Camry", + "R": ["4Runner", "Corolla"], + "T": "Celica FWD", + "U": "Prius", + "V": "RAV4", + "W": "MR2 non Spyder", + "X": "Cressida", } }, - - 'Nissan': { + "Nissan": { 2: { - 'A': ['Armada', 'Titan', 'Maxima'], - 'B': 'Sentra', - 'C': 'Versa (07-11)', - 'D': ['Truck', 'Xterra (00-04)', 'Frontier'], - 'J': 'Maxima', - 'L': 'Altima', - 'N': 'Xterra (05-11)', - 'P': 'Kicks', - 'R': 'Pathfinder', - 'S': ['240SX', 'Rogue (08-11)'], - 'T': 'X Trail', - 'U': 'Altima', - 'Y': 'Patrol', - 'Z': ['300Z', '350Z', 'Murano'], + "A": ["Armada", "Titan", "Maxima"], + "B": "Sentra", + "C": "Versa (07-11)", + "D": ["Truck", "Xterra (00-04)", "Frontier"], + "J": "Maxima", + "L": "Altima", + "N": "Xterra (05-11)", + "P": "Kicks", + "R": "Pathfinder", + "S": ["240SX", "Rogue (08-11)"], + "T": "X Trail", + "U": "Altima", + "Y": "Patrol", + "Z": ["300Z", "350Z", "Murano"], } }, - - 'Renault': { + "Renault": { 2: { # 5th character in VDS - '0': 'Twingo', - '1': 'R4', - '2': 'R25', - '3': 'R4', - '4': ['R21', 'Express'], - '5': ['Clio I', 'Laguna', 'R19', 'Safrane'], - 'A': ['Megane I', 'Master'], - 'B': 'Clio II', - 'C': 'Kangoo', - 'D': 'Master', - 'E': ['Espace III', 'Avantime'], - 'G': 'Laguna II', - 'H': 'Master Propulsion', - 'J': ['Vel Satis', 'New Trafic'], - 'K': 'Espace IV', - 'L': 'Trafic', - 'M': 'Megan II', - 'P': 'Modus', - 'S': ['Logan', 'Sandero', 'Duster', 'Dokker', 'Lodgy'], - 'Y': 'Koleos', + "0": "Twingo", + "1": "R4", + "2": "R25", + "3": "R4", + "4": ["R21", "Express"], + "5": ["Clio I", "Laguna", "R19", "Safrane"], + "A": ["Megane I", "Master"], + "B": "Clio II", + "C": "Kangoo", + "D": "Master", + "E": ["Espace III", "Avantime"], + "G": "Laguna II", + "H": "Master Propulsion", + "J": ["Vel Satis", "New Trafic"], + "K": "Espace IV", + "L": "Trafic", + "M": "Megan II", + "P": "Modus", + "S": ["Logan", "Sandero", "Duster", "Dokker", "Lodgy"], + "Y": "Koleos", } }, - # New Manufacturers Added Below - 'Ford': { + "Ford": { 3: { - 'A': ['Fiesta', 'Focus'], - 'B': ['Mustang', 'Explorer'], - 'C': 'Ranger', - 'D': ['Escape', 'Edge'], - 'E': 'F-150', - 'F': 'Transit', - 'G': 'Bronco', - 'H': 'Expedition', + "A": ["Fiesta", "Focus"], + "B": ["Mustang", "Explorer"], + "C": "Ranger", + "D": ["Escape", "Edge"], + "E": "F-150", + "F": "Transit", + "G": "Bronco", + "H": "Expedition", } }, - - 'BMW': { + "BMW": { 4: { - '1': '1 Series', - '2': '2 Series', - '3': '3 Series', - '4': '4 Series', - '5': '5 Series', - '6': '6 Series', - '7': '7 Series', - '8': '8 Series', - 'X': 'X Series (SUV)', - 'Z': 'Z Series (Roadster)', + "1": "1 Series", + "2": "2 Series", + "3": "3 Series", + "4": "4 Series", + "5": "5 Series", + "6": "6 Series", + "7": "7 Series", + "8": "8 Series", + "X": "X Series (SUV)", + "Z": "Z Series (Roadster)", } }, - - 'Mercedes-Benz': { + "Mercedes-Benz": { 3: { - 'A': 'A-Class', - 'B': 'B-Class', - 'C': 'C-Class', - 'E': 'E-Class', - 'G': 'G-Class', - 'S': 'S-Class', - 'V': 'V-Class', - 'X': 'GL-Class', + "A": "A-Class", + "B": "B-Class", + "C": "C-Class", + "E": "E-Class", + "G": "G-Class", + "S": "S-Class", + "V": "V-Class", + "X": "GL-Class", } }, - - 'Volkswagen': { + "Volkswagen": { 2: { - '1': 'Golf', - '2': 'Jetta', - '3': 'Passat', - '4': 'Tiguan', - '5': 'Polo', - '6': 'Arteon', - '7': 'Atlas', - '8': 'Touareg', - '9': 'Beetle', + "1": "Golf", + "2": "Jetta", + "3": "Passat", + "4": "Tiguan", + "5": "Polo", + "6": "Arteon", + "7": "Atlas", + "8": "Touareg", + "9": "Beetle", } }, - - 'Hyundai': { + "Hyundai": { 1: { - 'A': 'Accent', - 'B': 'Elantra', - 'C': 'Sonata', - 'D': 'Tucson', - 'E': 'Santa Fe', - 'F': 'Kona', - 'G': 'Palisade', - 'H': 'Veloster', - 'J': 'Genesis', + "A": "Accent", + "B": "Elantra", + "C": "Sonata", + "D": "Tucson", + "E": "Santa Fe", + "F": "Kona", + "G": "Palisade", + "H": "Veloster", + "J": "Genesis", } }, - - 'Chevrolet': { + "Chevrolet": { 2: { - 'A': 'Camaro', - 'B': 'Corvette', - 'C': 'Cruze', - 'D': 'Malibu', - 'E': 'Equinox', - 'F': 'Traverse', - 'G': 'Silverado', - 'H': 'Tahoe', - 'J': 'Suburban', + "A": "Camaro", + "B": "Corvette", + "C": "Cruze", + "D": "Malibu", + "E": "Equinox", + "F": "Traverse", + "G": "Silverado", + "H": "Tahoe", + "J": "Suburban", } }, - - 'Audi': { + "Audi": { 3: { - '1': 'A1', - '2': 'A2', - '3': 'A3', - '4': 'A4', - '5': 'A5', - '6': 'A6', - '7': 'A7', - '8': 'A8', - 'Q': 'Q Series (SUV)', - 'T': 'TT', + "1": "A1", + "2": "A2", + "3": "A3", + "4": "A4", + "5": "A5", + "6": "A6", + "7": "A7", + "8": "A8", + "Q": "Q Series (SUV)", + "T": "TT", } }, - - 'Subaru': { + "Subaru": { 2: { - 'A': 'Impreza', - 'B': 'Legacy', - 'C': 'Outback', - 'D': 'Forester', - 'E': 'Crosstrek', - 'F': 'BRZ', - 'G': 'Ascent', + "A": "Impreza", + "B": "Legacy", + "C": "Outback", + "D": "Forester", + "E": "Crosstrek", + "F": "BRZ", + "G": "Ascent", } }, - - 'Mazda': { + "Mazda": { 1: { - 'A': 'Mazda3', - 'B': 'Mazda6', - 'C': 'CX-5', - 'D': 'CX-9', - 'E': 'MX-5 Miata', - 'F': 'CX-30', - 'G': 'RX-8', + "A": "Mazda3", + "B": "Mazda6", + "C": "CX-5", + "D": "CX-9", + "E": "MX-5 Miata", + "F": "CX-30", + "G": "RX-8", } }, - - 'Dongfeng': { + "Dongfeng": { 1: { - 'A': 'A-Series', - 'B': 'SHINE', - 'C': 'C-Series', - 'D': 'MAGE', - 'E': ['CAPTAIN E', 'E32'], - 'F': 'CAPTAIN C', - 'G': 'S50', - 'H': 'Dongfeng Fengshen AX3', - 'J': 'Dongfeng Joyear SUV', - 'K': 'Dongfeng Rich 6', - 'L': 'Dongfeng Sokon', - 'M': 'Dongfeng Glory 580', + "A": "A-Series", + "B": "SHINE", + "C": "C-Series", + "D": "MAGE", + "E": ["CAPTAIN E", "E32"], + "F": "CAPTAIN C", + "G": "S50", + "H": "Dongfeng Fengshen AX3", + "J": "Dongfeng Joyear SUV", + "K": "Dongfeng Rich 6", + "L": "Dongfeng Sokon", + "M": "Dongfeng Glory 580", }, - 2: { - '3': 'C35', # Specific models in C-Series - '1': 'C31', - '2': 'C32', - '7': 'C72', - '6': 'A60', # Specific models in A-Series - '3': 'A30', - 'X': ['AX7', 'AX4'], + "3": "C35", # Specific models in C-Series + "1": "C31", + "2": "C32", + "7": "C72", + "6": "A60", # Specific models in A-Series + "3": "A30", + "X": ["AX7", "AX4"], }, 3: { # Third character (for AX models) - '7': 'AX7', # Resolves AX7 - '4': 'AX4', # Resolves AX4 - } - + "7": "AX7", # Resolves AX7 + "4": "AX4", # Resolves AX4 + }, }, - - 'Changan': { + "Changan": { 1: { - 'A': 'Changan CS35', - 'B': 'Changan CS55', - 'C': 'Changan CS75', - 'D': 'Changan CS85', - 'E': 'Changan CS95', - 'F': 'Changan Eado', - 'G': 'Changan Raeton', - 'H': 'Changan Alsvin', - 'J': 'Changan UNI-T', - 'K': 'Changan UNI-K', - 'L': 'Changan Oushang', - 'M': 'Changan Benni', + "A": "Changan CS35", + "B": "Changan CS55", + "C": "Changan CS75", + "D": "Changan CS85", + "E": "Changan CS95", + "F": "Changan Eado", + "G": "Changan Raeton", + "H": "Changan Alsvin", + "J": "Changan UNI-T", + "K": "Changan UNI-K", + "L": "Changan Oushang", + "M": "Changan Benni", } }, - - 'Chery': { + "Chery": { 1: { - 'A': 'Chery Arrizo 5', - 'B': 'Chery Arrizo 7', - 'C': 'Chery Tiggo 3', - 'D': 'Chery Tiggo 5', - 'E': 'Chery Tiggo 7', - 'F': 'Chery Tiggo 8', - 'G': 'Chery QQ', - 'H': 'Chery Fulwin', - 'J': 'Chery Cowin', - 'K': 'Chery eQ1', - 'L': 'Chery Exeed TX', - 'M': 'Chery Exeed LX', + "A": "Chery Arrizo 5", + "B": "Chery Arrizo 7", + "C": "Chery Tiggo 3", + "D": "Chery Tiggo 5", + "E": "Chery Tiggo 7", + "F": "Chery Tiggo 8", + "G": "Chery QQ", + "H": "Chery Fulwin", + "J": "Chery Cowin", + "K": "Chery eQ1", + "L": "Chery Exeed TX", + "M": "Chery Exeed LX", } }, - - 'MG': { + "MG": { 1: { - 'A': 'MG 3', - 'B': 'MG 5', - 'C': 'MG 6', - 'D': 'MG ZS', - 'E': 'MG HS', - 'F': 'MG RX5', - 'G': 'MG Marvel R', - 'H': 'MG EZS', - 'J': 'MG GT', - 'K': 'MG TF', - 'L': 'MG Cyberster', + "A": "MG 3", + "B": "MG 5", + "C": "MG 6", + "D": "MG ZS", + "E": "MG HS", + "F": "MG RX5", + "G": "MG Marvel R", + "H": "MG EZS", + "J": "MG GT", + "K": "MG TF", + "L": "MG Cyberster", } }, - - 'JMC': { + "JMC": { 1: { - 'A': 'JMC Yusheng', - 'B': 'JMC Vigus', - 'C': 'JMC Baodian', - 'D': 'JMC Ford Transit', - 'E': 'JMC S350', - 'F': 'JMC Teshun', - 'G': 'JMC Realm', - 'H': 'JMC Yuhu', - 'J': 'JMC E200', - 'K': 'JMC E400', + "A": "JMC Yusheng", + "B": "JMC Vigus", + "C": "JMC Baodian", + "D": "JMC Ford Transit", + "E": "JMC S350", + "F": "JMC Teshun", + "G": "JMC Realm", + "H": "JMC Yuhu", + "J": "JMC E200", + "K": "JMC E400", } }, - - 'JAC': { + "JAC": { 1: { - 'A': 'JAC J3', - 'B': 'JAC J4', - 'C': 'JAC J5', - 'D': 'JAC J6', - 'E': 'JAC J7', - 'F': 'JAC S2', - 'G': 'JAC S3', - 'H': 'JAC S4', - 'J': 'JAC S5', - 'K': 'JAC S7', - 'L': 'JAC iEV7S', - 'M': 'JAC iEVS4', + "A": "JAC J3", + "B": "JAC J4", + "C": "JAC J5", + "D": "JAC J6", + "E": "JAC J7", + "F": "JAC S2", + "G": "JAC S3", + "H": "JAC S4", + "J": "JAC S5", + "K": "JAC S7", + "L": "JAC iEV7S", + "M": "JAC iEVS4", } }, - - 'BYD': { + "BYD": { 1: { - 'A': 'BYD F3', - 'B': 'BYD F6', - 'C': 'BYD S6', - 'D': 'BYD Tang', - 'E': 'BYD Song', - 'F': 'BYD Yuan', - 'G': 'BYD Qin', - 'H': 'BYD Han', - 'J': 'BYD e5', - 'K': 'BYD e6', - 'L': 'BYD Dolphin', - 'M': 'BYD Seal', + "A": "BYD F3", + "B": "BYD F6", + "C": "BYD S6", + "D": "BYD Tang", + "E": "BYD Song", + "F": "BYD Yuan", + "G": "BYD Qin", + "H": "BYD Han", + "J": "BYD e5", + "K": "BYD e6", + "L": "BYD Dolphin", + "M": "BYD Seal", } }, - - 'Geely': { + "Geely": { 1: { - 'A': 'Geely Emgrand EC7', - 'B': 'Geely Emgrand GS', - 'C': 'Geely Emgrand GL', - 'D': 'Geely Boyue', - 'E': 'Geely Xingyue', - 'F': 'Geely Binrui', - 'G': 'Geely Borui', - 'H': 'Geely Vision', - 'J': 'Geely Coolray', - 'K': 'Geely Monjaro', - 'L': 'Geely Geometry A', - 'M': 'Geely Geometry C', + "A": "Geely Emgrand EC7", + "B": "Geely Emgrand GS", + "C": "Geely Emgrand GL", + "D": "Geely Boyue", + "E": "Geely Xingyue", + "F": "Geely Binrui", + "G": "Geely Borui", + "H": "Geely Vision", + "J": "Geely Coolray", + "K": "Geely Monjaro", + "L": "Geely Geometry A", + "M": "Geely Geometry C", } }, - - 'Great Wall Motors (GWM)': { + "Great Wall Motors (GWM)": { 1: { - 'A': 'GWM Haval H6', - 'B': 'GWM Haval H9', - 'C': 'GWM Haval Jolion', - 'D': 'GWM WEY VV5', - 'E': 'GWM WEY VV6', - 'F': 'GWM WEY VV7', - 'G': 'GWM Ora R1', - 'H': 'GWM Ora Good Cat', - 'J': 'GWM Poer', - 'K': 'GWM Tank 300', - 'L': 'GWM Tank 500', + "A": "GWM Haval H6", + "B": "GWM Haval H9", + "C": "GWM Haval Jolion", + "D": "GWM WEY VV5", + "E": "GWM WEY VV6", + "F": "GWM WEY VV7", + "G": "GWM Ora R1", + "H": "GWM Ora Good Cat", + "J": "GWM Poer", + "K": "GWM Tank 300", + "L": "GWM Tank 500", } }, - - 'FAW': { + "FAW": { 1: { - 'A': 'FAW Besturn B50', - 'B': 'FAW Besturn X40', - 'C': 'FAW Besturn X80', - 'D': 'FAW Hongqi H5', - 'E': 'FAW Hongqi H7', - 'F': 'FAW Hongqi HS5', - 'G': 'FAW Hongqi HS7', - 'H': 'FAW Jiefang Truck', - 'J': 'FAW Oley', - 'K': 'FAW Vita', + "A": "FAW Besturn B50", + "B": "FAW Besturn X40", + "C": "FAW Besturn X80", + "D": "FAW Hongqi H5", + "E": "FAW Hongqi H7", + "F": "FAW Hongqi HS5", + "G": "FAW Hongqi HS7", + "H": "FAW Jiefang Truck", + "J": "FAW Oley", + "K": "FAW Vita", } }, - - 'SAIC Motor': { + "SAIC Motor": { 1: { - 'A': 'SAIC Maxus G10', - 'B': 'SAIC Maxus G50', - 'C': 'SAIC Maxus T60', - 'D': 'SAIC Roewe RX5', - 'E': 'SAIC Roewe i5', - 'F': 'SAIC Roewe i6', - 'G': 'SAIC MG ZS', - 'H': 'SAIC MG HS', - 'J': 'SAIC IM LS7', - 'K': 'SAIC IM Marvel R', + "A": "SAIC Maxus G10", + "B": "SAIC Maxus G50", + "C": "SAIC Maxus T60", + "D": "SAIC Roewe RX5", + "E": "SAIC Roewe i5", + "F": "SAIC Roewe i6", + "G": "SAIC MG ZS", + "H": "SAIC MG HS", + "J": "SAIC IM LS7", + "K": "SAIC IM Marvel R", } }, } @@ -1827,7 +1844,9 @@ def decode_vin_haikalna(vin): pattern = r"^[A-HJ-NPR-Z0-9]{17}$" if not re.match(pattern, vin): - raise Exception("VIN number must only contain alphanumeric symbols except 'I', 'O', and 'Q' ") + raise Exception( + "VIN number must only contain alphanumeric symbols except 'I', 'O', and 'Q' " + ) vin = vin.upper() @@ -1842,10 +1861,9 @@ def decode_vin_haikalna(vin): model = decode_vds(manufacturer, vds) data = { - 'maker': manufacturer, - 'model': model, - 'modelYear': year, - + "maker": manufacturer, + "model": model, + "modelYear": year, } print(data) - return data \ No newline at end of file + return data diff --git a/inventory/management/commands/add_id_car_model.py b/inventory/management/commands/add_id_car_model.py index 482fc876..bfc87cd1 100644 --- a/inventory/management/commands/add_id_car_model.py +++ b/inventory/management/commands/add_id_car_model.py @@ -6,51 +6,71 @@ from inventory.models import CarTrim class Command(BaseCommand): - help = 'Add id_car_model column to CarTrim model and populate it using a CSV file.' + help = "Add id_car_model column to CarTrim model and populate it using a CSV file." def handle(self, *args, **options): # Define the file path relative to the project directory - base_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - file_path = os.path.join(base_dir, 'data/mappings.csv') + base_dir = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + ) + file_path = os.path.join(base_dir, "data/mappings.csv") # Step 1: Add the new column if it does not exist with connection.cursor() as cursor: try: - cursor.execute("ALTER TABLE inventory_cartrim ADD COLUMN id_car_model INTEGER") - self.stdout.write(self.style.SUCCESS("Column 'id_car_model' added successfully.")) + cursor.execute( + "ALTER TABLE inventory_cartrim ADD COLUMN id_car_model INTEGER" + ) + self.stdout.write( + self.style.SUCCESS("Column 'id_car_model' added successfully.") + ) except Exception as e: - self.stdout.write(self.style.WARNING(f"Column 'id_car_model' might already exist: {e}")) + self.stdout.write( + self.style.WARNING( + f"Column 'id_car_model' might already exist: {e}" + ) + ) # Step 2: Read and process the CSV file try: - with open(file_path, mode='r', encoding='utf-8-sig') as csvfile: + with open(file_path, mode="r", encoding="utf-8-sig") as csvfile: reader = csv.DictReader(csvfile) for row in reader: # Extract id_car_serie and id_car_model from the current row - id_car_serie = row.get('id_car_serie') - id_car_model = row.get('id_car_model') + id_car_serie = row.get("id_car_serie") + id_car_model = row.get("id_car_model") if not id_car_serie or not id_car_model: - self.stdout.write(self.style.WARNING(f"Skipping row with missing data: {row}")) + self.stdout.write( + self.style.WARNING(f"Skipping row with missing data: {row}") + ) continue # Step 3: Update CarTrim rows based on the id_car_serie - updated_count = CarTrim.objects.filter(id_car_serie=id_car_serie).update(id_car_model=id_car_model) + updated_count = CarTrim.objects.filter( + id_car_serie=id_car_serie + ).update(id_car_model=id_car_model) # Output progress if updated_count > 0: - self.stdout.write(self.style.SUCCESS( - f"Updated {updated_count} rows for id_car_serie={id_car_serie} with id_car_model={id_car_model}." - )) + self.stdout.write( + self.style.SUCCESS( + f"Updated {updated_count} rows for id_car_serie={id_car_serie} with id_car_model={id_car_model}." + ) + ) else: - self.stdout.write(self.style.WARNING( - f"No rows found for id_car_serie={id_car_serie}." - )) + self.stdout.write( + self.style.WARNING( + f"No rows found for id_car_serie={id_car_serie}." + ) + ) - self.stdout.write(self.style.SUCCESS("All rows have been processed successfully!")) + self.stdout.write( + self.style.SUCCESS("All rows have been processed successfully!") + ) except FileNotFoundError: self.stdout.write(self.style.ERROR(f"File not found: {file_path}")) except Exception as e: - self.stdout.write(self.style.ERROR(f"An error occurred: {e}")) \ No newline at end of file + self.stdout.write(self.style.ERROR(f"An error occurred: {e}")) diff --git a/inventory/management/commands/analyze_car_hierarchy.py b/inventory/management/commands/analyze_car_hierarchy.py index bb7a35b0..405d8460 100644 --- a/inventory/management/commands/analyze_car_hierarchy.py +++ b/inventory/management/commands/analyze_car_hierarchy.py @@ -7,24 +7,24 @@ from django.conf import settings class Command(BaseCommand): - help = 'Analyzes the car hierarchy to identify makes without models, models without series, and series without trims' + help = "Analyzes the car hierarchy to identify makes without models, models without series, and series without trims" def add_arguments(self, parser): parser.add_argument( - '--export', - action='store_true', - help='Export results to CSV files', + "--export", + action="store_true", + help="Export results to CSV files", ) parser.add_argument( - '--export-path', + "--export-path", type=str, - default='exports', + default="exports", help='Directory to export CSV files (default: "exports")', ) def handle(self, *args, **options): - export = options['export'] - export_path = options['export_path'] + export = options["export"] + export_path = options["export_path"] # Create export directory if needed if export: @@ -35,14 +35,18 @@ class Command(BaseCommand): # Analyze makes without models all_makes = CarMake.objects.all() total_makes = all_makes.count() - makes_without_models = CarMake.objects.annotate(model_count=Count('carmodel')).filter(model_count=0) + makes_without_models = CarMake.objects.annotate( + model_count=Count("carmodel") + ).filter(model_count=0) makes_without_models_count = makes_without_models.count() self.stdout.write(self.style.SUCCESS(f"Total car makes: {total_makes}")) - self.stdout.write(self.style.SUCCESS( - f"Car makes without models: {makes_without_models_count} " - f"({makes_without_models_count/total_makes*100:.2f}% of all makes)" - )) + self.stdout.write( + self.style.SUCCESS( + f"Car makes without models: {makes_without_models_count} " + f"({makes_without_models_count / total_makes * 100:.2f}% of all makes)" + ) + ) if makes_without_models_count > 0: self.stdout.write("\nSample of car makes without models:") @@ -52,14 +56,18 @@ class Command(BaseCommand): # Analyze models without series all_models = CarModel.objects.all() total_models = all_models.count() - models_without_series = CarModel.objects.annotate(serie_count=Count('carserie')).filter(serie_count=0) + models_without_series = CarModel.objects.annotate( + serie_count=Count("carserie") + ).filter(serie_count=0) models_without_series_count = models_without_series.count() self.stdout.write(self.style.SUCCESS(f"\nTotal car models: {total_models}")) - self.stdout.write(self.style.SUCCESS( - f"Car models without series: {models_without_series_count} " - f"({models_without_series_count/total_models*100:.2f}% of all models)" - )) + self.stdout.write( + self.style.SUCCESS( + f"Car models without series: {models_without_series_count} " + f"({models_without_series_count / total_models * 100:.2f}% of all models)" + ) + ) if models_without_series_count > 0: self.stdout.write("\nSample of car models without series:") @@ -69,14 +77,18 @@ class Command(BaseCommand): # Analyze series without trims all_series = CarSerie.objects.all() total_series = all_series.count() - series_without_trims = CarSerie.objects.annotate(trim_count=Count('cartrim')).filter(trim_count=0) + series_without_trims = CarSerie.objects.annotate( + trim_count=Count("cartrim") + ).filter(trim_count=0) series_without_trims_count = series_without_trims.count() self.stdout.write(self.style.SUCCESS(f"\nTotal car series: {total_series}")) - self.stdout.write(self.style.SUCCESS( - f"Car series without trims: {series_without_trims_count} " - f"({series_without_trims_count/total_series*100:.2f}% of all series)" - )) + self.stdout.write( + self.style.SUCCESS( + f"Car series without trims: {series_without_trims_count} " + f"({series_without_trims_count / total_series * 100:.2f}% of all series)" + ) + ) if series_without_trims_count > 0: self.stdout.write("\nSample of car series without trims:") @@ -90,50 +102,77 @@ class Command(BaseCommand): if export: # Export makes without models if makes_without_models_count > 0: - filepath = os.path.join(export_dir, 'makes_without_models.csv') - with open(filepath, 'w', newline='') as csvfile: + filepath = os.path.join(export_dir, "makes_without_models.csv") + with open(filepath, "w", newline="") as csvfile: writer = csv.writer(csvfile) - writer.writerow(['make_id', 'make_name', 'is_sa_import']) + writer.writerow(["make_id", "make_name", "is_sa_import"]) for make in makes_without_models: - writer.writerow([make.id_car_make, make.name, make.is_sa_import]) - self.stdout.write(self.style.SUCCESS(f"Exported makes without models to {filepath}")) + writer.writerow( + [make.id_car_make, make.name, make.is_sa_import] + ) + self.stdout.write( + self.style.SUCCESS(f"Exported makes without models to {filepath}") + ) # Export models without series if models_without_series_count > 0: - filepath = os.path.join(export_dir, 'models_without_series.csv') - with open(filepath, 'w', newline='') as csvfile: + filepath = os.path.join(export_dir, "models_without_series.csv") + with open(filepath, "w", newline="") as csvfile: writer = csv.writer(csvfile) - writer.writerow(['model_id', 'model_name', 'make_id', 'make_name']) + writer.writerow(["model_id", "model_name", "make_id", "make_name"]) for model in models_without_series: - writer.writerow([ - model.id_car_model, - model.name, - model.id_car_make.id_car_make, - model.id_car_make.name - ]) - self.stdout.write(self.style.SUCCESS(f"Exported models without series to {filepath}")) + writer.writerow( + [ + model.id_car_model, + model.name, + model.id_car_make.id_car_make, + model.id_car_make.name, + ] + ) + self.stdout.write( + self.style.SUCCESS(f"Exported models without series to {filepath}") + ) # Export series without trims if series_without_trims_count > 0: - filepath = os.path.join(export_dir, 'series_without_trims.csv') - with open(filepath, 'w', newline='') as csvfile: + filepath = os.path.join(export_dir, "series_without_trims.csv") + with open(filepath, "w", newline="") as csvfile: writer = csv.writer(csvfile) - writer.writerow(['serie_id', 'serie_name', 'model_id', 'model_name', 'make_id', 'make_name']) + writer.writerow( + [ + "serie_id", + "serie_name", + "model_id", + "model_name", + "make_id", + "make_name", + ] + ) for serie in series_without_trims: - writer.writerow([ - serie.id_car_serie, - serie.name, - serie.id_car_model.id_car_model, - serie.id_car_model.name, - serie.id_car_model.id_car_make.id_car_make, - serie.id_car_model.id_car_make.name - ]) - self.stdout.write(self.style.SUCCESS(f"Exported series without trims to {filepath}")) + writer.writerow( + [ + serie.id_car_serie, + serie.name, + serie.id_car_model.id_car_model, + serie.id_car_model.name, + serie.id_car_model.id_car_make.id_car_make, + serie.id_car_model.id_car_make.name, + ] + ) + self.stdout.write( + self.style.SUCCESS(f"Exported series without trims to {filepath}") + ) # Summary - self.stdout.write("\n" + "="*50) + self.stdout.write("\n" + "=" * 50) self.stdout.write(self.style.SUCCESS("SUMMARY")) - self.stdout.write("="*50) - self.stdout.write(f"Total makes: {total_makes} | Without models: {makes_without_models_count} ({makes_without_models_count/total_makes*100:.2f}%)") - self.stdout.write(f"Total models: {total_models} | Without series: {models_without_series_count} ({models_without_series_count/total_models*100:.2f}%)") - self.stdout.write(f"Total series: {total_series} | Without trims: {series_without_trims_count} ({series_without_trims_count/total_series*100:.2f}%)") + self.stdout.write("=" * 50) + self.stdout.write( + f"Total makes: {total_makes} | Without models: {makes_without_models_count} ({makes_without_models_count / total_makes * 100:.2f}%)" + ) + self.stdout.write( + f"Total models: {total_models} | Without series: {models_without_series_count} ({models_without_series_count / total_models * 100:.2f}%)" + ) + self.stdout.write( + f"Total series: {total_series} | Without trims: {series_without_trims_count} ({series_without_trims_count / total_series * 100:.2f}%)" + ) diff --git a/inventory/management/commands/check_vin.py b/inventory/management/commands/check_vin.py index 43d44d93..98c43a26 100644 --- a/inventory/management/commands/check_vin.py +++ b/inventory/management/commands/check_vin.py @@ -1,26 +1,56 @@ from django.core.management.base import BaseCommand -from inventory.services import get_model,get_make,decodevin +from inventory.services import get_model, get_make, decodevin + class Command(BaseCommand): - help = 'Seed the Customer model with 20 records' + help = "Seed the Customer model with 20 records" def handle(self, *args, **kwargs): # vin,description = self.generate_vin() result = decodevin("1HGCM82633A123456") - self.stdout.write(self.style.SUCCESS('####################################################################################################')) - self.stdout.write(self.style.SUCCESS('####################################################################################################')) + self.stdout.write( + self.style.SUCCESS( + "####################################################################################################" + ) + ) + self.stdout.write( + self.style.SUCCESS( + "####################################################################################################" + ) + ) # self.stdout.write(self.style.SUCCESS(f'Generated VIN: {vin}')) # self.stdout.write(self.style.SUCCESS(f'Description: {description}')) - self.stdout.write(self.style.SUCCESS('####################################################################################################')) - self.stdout.write(self.style.SUCCESS('####################################################################################################')) - self.stdout.write(self.style.SUCCESS(f'Decoded VIN: {result}')) - make,model,year_model = result.values() - self.stdout.write(self.style.SUCCESS(f'VIN:"1HGCM82633A123456" - Make {make} - Model {model} - Model Year {year_model}')) + self.stdout.write( + self.style.SUCCESS( + "####################################################################################################" + ) + ) + self.stdout.write( + self.style.SUCCESS( + "####################################################################################################" + ) + ) + self.stdout.write(self.style.SUCCESS(f"Decoded VIN: {result}")) + make, model, year_model = result.values() + self.stdout.write( + self.style.SUCCESS( + f'VIN:"1HGCM82633A123456" - Make {make} - Model {model} - Model Year {year_model}' + ) + ) make = get_make(make) - model = get_model(model,make) - - self.stdout.write(self.style.SUCCESS(f'Make: {make} - Model: {model} - Year: {year_model}')) - self.stdout.write(self.style.SUCCESS('####################################################################################################')) - self.stdout.write(self.style.SUCCESS('####################################################################################################')) + model = get_model(model, make) + self.stdout.write( + self.style.SUCCESS(f"Make: {make} - Model: {model} - Year: {year_model}") + ) + self.stdout.write( + self.style.SUCCESS( + "####################################################################################################" + ) + ) + self.stdout.write( + self.style.SUCCESS( + "####################################################################################################" + ) + ) diff --git a/inventory/management/commands/claude.py b/inventory/management/commands/claude.py index bfb14a05..d4491bad 100644 --- a/inventory/management/commands/claude.py +++ b/inventory/management/commands/claude.py @@ -1,11 +1,11 @@ from django_ledger.io import roles from django.core.management.base import BaseCommand from django.utils.translation import gettext_lazy as _ -from django_ledger.models import ChartOfAccountModel, AccountModel,EntityModel +from django_ledger.models import ChartOfAccountModel, AccountModel, EntityModel class Command(BaseCommand): - help = 'Creates Chart of Accounts for Deepseek entity' + help = "Creates Chart of Accounts for Deepseek entity" def handle(self, *args, **options): """ @@ -13,9 +13,7 @@ class Command(BaseCommand): """ # Create Chart of Accounts - entity_model = EntityModel.objects.get( - name="Claude" - ) + entity_model = EntityModel.objects.get(name="Claude") coa_model = entity_model.get_default_coa() # entity_model.get_all_accounts().delete() # coa_model, created = ChartOfAccountModel.objects.get_or_create( @@ -1130,389 +1128,382 @@ class Command(BaseCommand): # } # ] - accounts_data = [ # Current Assets (must start with 1) { - 'code': '1010', - 'name': 'Cash on Hand', - 'role': roles.ASSET_CA_CASH, - 'balance_type': roles.DEBIT, - 'locked': True, - 'default': True # Default for ASSET_CA_CASH + "code": "1010", + "name": "Cash on Hand", + "role": roles.ASSET_CA_CASH, + "balance_type": roles.DEBIT, + "locked": True, + "default": True, # Default for ASSET_CA_CASH }, { - 'code': '1020', - 'name': 'Bank', - 'role': roles.ASSET_CA_CASH, - 'balance_type': roles.DEBIT, - 'locked': True, - 'default': False + "code": "1020", + "name": "Bank", + "role": roles.ASSET_CA_CASH, + "balance_type": roles.DEBIT, + "locked": True, + "default": False, }, { - 'code': '1030', - 'name': 'Accounts Receivable', - 'role': roles.ASSET_CA_RECEIVABLES, - 'balance_type': roles.DEBIT, - 'locked': True, - 'default': True # Default for ASSET_CA_RECEIVABLES + "code": "1030", + "name": "Accounts Receivable", + "role": roles.ASSET_CA_RECEIVABLES, + "balance_type": roles.DEBIT, + "locked": True, + "default": True, # Default for ASSET_CA_RECEIVABLES }, { - 'code': '1040', - 'name': 'Inventory (Cars)', - 'role': roles.ASSET_CA_INVENTORY, - 'balance_type': roles.DEBIT, - 'locked': True, - 'default': True # Default for ASSET_CA_INVENTORY + "code": "1040", + "name": "Inventory (Cars)", + "role": roles.ASSET_CA_INVENTORY, + "balance_type": roles.DEBIT, + "locked": True, + "default": True, # Default for ASSET_CA_INVENTORY }, { - 'code': '1045', - 'name': 'Spare Parts Inventory', - 'role': roles.ASSET_CA_INVENTORY, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': False + "code": "1045", + "name": "Spare Parts Inventory", + "role": roles.ASSET_CA_INVENTORY, + "balance_type": roles.DEBIT, + "locked": False, + "default": False, }, { - 'code': '1050', - 'name': 'Employee Advances', - 'role': roles.ASSET_CA_RECEIVABLES, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': False + "code": "1050", + "name": "Employee Advances", + "role": roles.ASSET_CA_RECEIVABLES, + "balance_type": roles.DEBIT, + "locked": False, + "default": False, }, { - 'code': '1060', - 'name': 'Prepaid Expenses', - 'role': roles.ASSET_CA_PREPAID, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': True # Default for ASSET_CA_PREPAID + "code": "1060", + "name": "Prepaid Expenses", + "role": roles.ASSET_CA_PREPAID, + "balance_type": roles.DEBIT, + "locked": False, + "default": True, # Default for ASSET_CA_PREPAID }, { - 'code': '1070', - 'name': 'Notes Receivable', - 'role': roles.ASSET_LTI_NOTES_RECEIVABLE, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': True # Default for ASSET_LTI_NOTES_RECEIVABLE + "code": "1070", + "name": "Notes Receivable", + "role": roles.ASSET_LTI_NOTES_RECEIVABLE, + "balance_type": roles.DEBIT, + "locked": False, + "default": True, # Default for ASSET_LTI_NOTES_RECEIVABLE }, - # Fixed Assets (must also start with 1) { - 'code': '1110', - 'name': 'Lands', - 'role': roles.ASSET_LTI_LAND, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': True # Default for ASSET_LTI_LAND + "code": "1110", + "name": "Lands", + "role": roles.ASSET_LTI_LAND, + "balance_type": roles.DEBIT, + "locked": False, + "default": True, # Default for ASSET_LTI_LAND }, { - 'code': '1111', - 'name': 'Buildings', - 'role': roles.ASSET_PPE_BUILDINGS, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': True # Default for ASSET_PPE_BUILDINGS + "code": "1111", + "name": "Buildings", + "role": roles.ASSET_PPE_BUILDINGS, + "balance_type": roles.DEBIT, + "locked": False, + "default": True, # Default for ASSET_PPE_BUILDINGS }, { - 'code': '1112', - 'name': 'Company Vehicles', - 'role': roles.ASSET_PPE_EQUIPMENT, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': True # Default for ASSET_PPE_EQUIPMENT + "code": "1112", + "name": "Company Vehicles", + "role": roles.ASSET_PPE_EQUIPMENT, + "balance_type": roles.DEBIT, + "locked": False, + "default": True, # Default for ASSET_PPE_EQUIPMENT }, { - 'code': '1113', - 'name': 'Equipment & Tools', - 'role': roles.ASSET_PPE_EQUIPMENT, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': False + "code": "1113", + "name": "Equipment & Tools", + "role": roles.ASSET_PPE_EQUIPMENT, + "balance_type": roles.DEBIT, + "locked": False, + "default": False, }, { - 'code': '1114', - 'name': 'Furniture & Fixtures', - 'role': roles.ASSET_PPE_EQUIPMENT, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': False + "code": "1114", + "name": "Furniture & Fixtures", + "role": roles.ASSET_PPE_EQUIPMENT, + "balance_type": roles.DEBIT, + "locked": False, + "default": False, }, { - 'code': '1115', - 'name': 'Other Fixed Assets', - 'role': roles.ASSET_PPE_EQUIPMENT, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': False + "code": "1115", + "name": "Other Fixed Assets", + "role": roles.ASSET_PPE_EQUIPMENT, + "balance_type": roles.DEBIT, + "locked": False, + "default": False, }, { - 'code': '1120', - 'name': 'Long-term Investments', - 'role': roles.ASSET_LTI_SECURITIES, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': True # Default for ASSET_LTI_SECURITIES + "code": "1120", + "name": "Long-term Investments", + "role": roles.ASSET_LTI_SECURITIES, + "balance_type": roles.DEBIT, + "locked": False, + "default": True, # Default for ASSET_LTI_SECURITIES }, { - 'code': '1130', - 'name': 'Intangible Assets', - 'role': roles.ASSET_INTANGIBLE_ASSETS, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': True # Default for ASSET_INTANGIBLE_ASSETS + "code": "1130", + "name": "Intangible Assets", + "role": roles.ASSET_INTANGIBLE_ASSETS, + "balance_type": roles.DEBIT, + "locked": False, + "default": True, # Default for ASSET_INTANGIBLE_ASSETS }, - # Current Liabilities (must start with 2) { - 'code': '2010', - 'name': 'Accounts Payable', - 'role': roles.LIABILITY_CL_ACC_PAYABLE, - 'balance_type': roles.CREDIT, - 'locked': True, - 'default': True # Default for LIABILITY_CL_ACC_PAYABLE + "code": "2010", + "name": "Accounts Payable", + "role": roles.LIABILITY_CL_ACC_PAYABLE, + "balance_type": roles.CREDIT, + "locked": True, + "default": True, # Default for LIABILITY_CL_ACC_PAYABLE }, { - 'code': '2020', - 'name': 'Notes Payable', - 'role': roles.LIABILITY_CL_ST_NOTES_PAYABLE, - 'balance_type': roles.CREDIT, - 'locked': False, - 'default': True # Default for LIABILITY_CL_ST_NOTES_PAYABLE + "code": "2020", + "name": "Notes Payable", + "role": roles.LIABILITY_CL_ST_NOTES_PAYABLE, + "balance_type": roles.CREDIT, + "locked": False, + "default": True, # Default for LIABILITY_CL_ST_NOTES_PAYABLE }, { - 'code': '2030', - 'name': 'Short-term Loans', - 'role': roles.LIABILITY_CL_ST_NOTES_PAYABLE, - 'balance_type': roles.CREDIT, - 'locked': False, - 'default': False + "code": "2030", + "name": "Short-term Loans", + "role": roles.LIABILITY_CL_ST_NOTES_PAYABLE, + "balance_type": roles.CREDIT, + "locked": False, + "default": False, }, { - 'code': '2040', - 'name': 'Employee Payables', - 'role': roles.LIABILITY_CL_WAGES_PAYABLE, - 'balance_type': roles.CREDIT, - 'locked': False, - 'default': True # Default for LIABILITY_CL_WAGES_PAYABLE + "code": "2040", + "name": "Employee Payables", + "role": roles.LIABILITY_CL_WAGES_PAYABLE, + "balance_type": roles.CREDIT, + "locked": False, + "default": True, # Default for LIABILITY_CL_WAGES_PAYABLE }, { - 'code': '2050', - 'name': 'Accrued Expenses', - 'role': roles.LIABILITY_CL_OTHER, - 'balance_type': roles.CREDIT, - 'locked': False, - 'default': True # Default for LIABILITY_CL_OTHER + "code": "2050", + "name": "Accrued Expenses", + "role": roles.LIABILITY_CL_OTHER, + "balance_type": roles.CREDIT, + "locked": False, + "default": True, # Default for LIABILITY_CL_OTHER }, { - 'code': '2060', - 'name': 'Accrued Taxes', - 'role': roles.LIABILITY_CL_TAXES_PAYABLE, - 'balance_type': roles.CREDIT, - 'locked': False, - 'default': True # Default for LIABILITY_CL_TAXES_PAYABLE + "code": "2060", + "name": "Accrued Taxes", + "role": roles.LIABILITY_CL_TAXES_PAYABLE, + "balance_type": roles.CREDIT, + "locked": False, + "default": True, # Default for LIABILITY_CL_TAXES_PAYABLE }, { - 'code': '2070', - 'name': 'Provisions', - 'role': roles.LIABILITY_CL_OTHER, - 'balance_type': roles.CREDIT, - 'locked': False, - 'default': False + "code": "2070", + "name": "Provisions", + "role": roles.LIABILITY_CL_OTHER, + "balance_type": roles.CREDIT, + "locked": False, + "default": False, }, - # Long-term Liabilities (must also start with 2) { - 'code': '2210', - 'name': 'Long-term Bank Loans', - 'role': roles.LIABILITY_LTL_NOTES_PAYABLE, - 'balance_type': roles.CREDIT, - 'locked': False, - 'default': True # Default for LIABILITY_LTL_NOTES_PAYABLE + "code": "2210", + "name": "Long-term Bank Loans", + "role": roles.LIABILITY_LTL_NOTES_PAYABLE, + "balance_type": roles.CREDIT, + "locked": False, + "default": True, # Default for LIABILITY_LTL_NOTES_PAYABLE }, { - 'code': '2220', - 'name': 'Lease Liabilities', - 'role': roles.LIABILITY_LTL_NOTES_PAYABLE, - 'balance_type': roles.CREDIT, - 'locked': False, - 'default': False + "code": "2220", + "name": "Lease Liabilities", + "role": roles.LIABILITY_LTL_NOTES_PAYABLE, + "balance_type": roles.CREDIT, + "locked": False, + "default": False, }, { - 'code': '2230', - 'name': 'Other Long-term Liabilities', - 'role': roles.LIABILITY_LTL_NOTES_PAYABLE, - 'balance_type': roles.CREDIT, - 'locked': False, - 'default': False + "code": "2230", + "name": "Other Long-term Liabilities", + "role": roles.LIABILITY_LTL_NOTES_PAYABLE, + "balance_type": roles.CREDIT, + "locked": False, + "default": False, }, - # Equity (must start with 3) { - 'code': '3010', - 'name': 'Capital', - 'role': roles.EQUITY_CAPITAL, - 'balance_type': roles.CREDIT, - 'locked': True, - 'default': True # Default for EQUITY_CAPITAL + "code": "3010", + "name": "Capital", + "role": roles.EQUITY_CAPITAL, + "balance_type": roles.CREDIT, + "locked": True, + "default": True, # Default for EQUITY_CAPITAL }, { - 'code': '3020', - 'name': 'Statutory Reserve', - 'role': roles.EQUITY_ADJUSTMENT, - 'balance_type': roles.CREDIT, - 'locked': False, - 'default': True # Default for EQUITY_ADJUSTMENT + "code": "3020", + "name": "Statutory Reserve", + "role": roles.EQUITY_ADJUSTMENT, + "balance_type": roles.CREDIT, + "locked": False, + "default": True, # Default for EQUITY_ADJUSTMENT }, { - 'code': '3030', - 'name': 'Retained Earnings', - 'role': roles.EQUITY_ADJUSTMENT, - 'balance_type': roles.CREDIT, - 'locked': False, - 'default': False + "code": "3030", + "name": "Retained Earnings", + "role": roles.EQUITY_ADJUSTMENT, + "balance_type": roles.CREDIT, + "locked": False, + "default": False, }, { - 'code': '3040', - 'name': 'Profit & Loss for the Period', - 'role': roles.EQUITY_ADJUSTMENT, - 'balance_type': roles.CREDIT, - 'locked': False, - 'default': False + "code": "3040", + "name": "Profit & Loss for the Period", + "role": roles.EQUITY_ADJUSTMENT, + "balance_type": roles.CREDIT, + "locked": False, + "default": False, }, - # Revenue (must start with 4) { - 'code': '4010', - 'name': 'Car Sales', - 'role': roles.INCOME_OPERATIONAL, - 'balance_type': roles.CREDIT, - 'locked': True, - 'default': True # Default for INCOME_OPERATIONAL + "code": "4010", + "name": "Car Sales", + "role": roles.INCOME_OPERATIONAL, + "balance_type": roles.CREDIT, + "locked": True, + "default": True, # Default for INCOME_OPERATIONAL }, { - 'code': '4020', - 'name': 'After-Sales Services', - 'role': roles.INCOME_OPERATIONAL, - 'balance_type': roles.CREDIT, - 'locked': False, - 'default': False + "code": "4020", + "name": "After-Sales Services", + "role": roles.INCOME_OPERATIONAL, + "balance_type": roles.CREDIT, + "locked": False, + "default": False, }, { - 'code': '4030', - 'name': 'Car Rental Income', - 'role': roles.INCOME_PASSIVE, - 'balance_type': roles.CREDIT, - 'locked': False, - 'default': True # Default for INCOME_PASSIVE + "code": "4030", + "name": "Car Rental Income", + "role": roles.INCOME_PASSIVE, + "balance_type": roles.CREDIT, + "locked": False, + "default": True, # Default for INCOME_PASSIVE }, { - 'code': '4040', - 'name': 'Other Income', - 'role': roles.INCOME_OTHER, - 'balance_type': roles.CREDIT, - 'locked': False, - 'default': True # Default for INCOME_OTHER + "code": "4040", + "name": "Other Income", + "role": roles.INCOME_OTHER, + "balance_type": roles.CREDIT, + "locked": False, + "default": True, # Default for INCOME_OTHER }, - # Expenses (must start with 5 for COGS, 6 for others) { - 'code': '5010', - 'name': 'Cost of Goods Sold', - 'role': roles.COGS, - 'balance_type': roles.DEBIT, - 'locked': True, - 'default': True # Default for COGS + "code": "5010", + "name": "Cost of Goods Sold", + "role": roles.COGS, + "balance_type": roles.DEBIT, + "locked": True, + "default": True, # Default for COGS }, { - 'code': '5015', - 'name': 'Spare Parts Cost Consumed', - 'role': roles.COGS, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': False + "code": "5015", + "name": "Spare Parts Cost Consumed", + "role": roles.COGS, + "balance_type": roles.DEBIT, + "locked": False, + "default": False, }, { - 'code': '6010', - 'name': 'Salaries & Wages', - 'role': roles.EXPENSE_OPERATIONAL, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': True # Default for EXPENSE_OPERATIONAL + "code": "6010", + "name": "Salaries & Wages", + "role": roles.EXPENSE_OPERATIONAL, + "balance_type": roles.DEBIT, + "locked": False, + "default": True, # Default for EXPENSE_OPERATIONAL }, { - 'code': '6020', - 'name': 'Rent', - 'role': roles.EXPENSE_OPERATIONAL, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': False + "code": "6020", + "name": "Rent", + "role": roles.EXPENSE_OPERATIONAL, + "balance_type": roles.DEBIT, + "locked": False, + "default": False, }, { - 'code': '6030', - 'name': 'Utilities', - 'role': roles.EXPENSE_OPERATIONAL, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': False + "code": "6030", + "name": "Utilities", + "role": roles.EXPENSE_OPERATIONAL, + "balance_type": roles.DEBIT, + "locked": False, + "default": False, }, { - 'code': '6040', - 'name': 'Advertising & Marketing', - 'role': roles.EXPENSE_OPERATIONAL, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': False + "code": "6040", + "name": "Advertising & Marketing", + "role": roles.EXPENSE_OPERATIONAL, + "balance_type": roles.DEBIT, + "locked": False, + "default": False, }, { - 'code': '6050', - 'name': 'Maintenance', - 'role': roles.EXPENSE_OPERATIONAL, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': False + "code": "6050", + "name": "Maintenance", + "role": roles.EXPENSE_OPERATIONAL, + "balance_type": roles.DEBIT, + "locked": False, + "default": False, }, { - 'code': '6060', - 'name': 'Operating Expenses', - 'role': roles.EXPENSE_OPERATIONAL, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': False + "code": "6060", + "name": "Operating Expenses", + "role": roles.EXPENSE_OPERATIONAL, + "balance_type": roles.DEBIT, + "locked": False, + "default": False, }, { - 'code': '6070', - 'name': 'Depreciation', - 'role': roles.EXPENSE_DEPRECIATION, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': True # Default for EXPENSE_DEPRECIATION + "code": "6070", + "name": "Depreciation", + "role": roles.EXPENSE_DEPRECIATION, + "balance_type": roles.DEBIT, + "locked": False, + "default": True, # Default for EXPENSE_DEPRECIATION }, { - 'code': '6080', - 'name': 'Fees & Taxes', - 'role': roles.EXPENSE_OPERATIONAL, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': False + "code": "6080", + "name": "Fees & Taxes", + "role": roles.EXPENSE_OPERATIONAL, + "balance_type": roles.DEBIT, + "locked": False, + "default": False, }, { - 'code': '6090', - 'name': 'Bank Charges', - 'role': roles.EXPENSE_OPERATIONAL, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': False + "code": "6090", + "name": "Bank Charges", + "role": roles.EXPENSE_OPERATIONAL, + "balance_type": roles.DEBIT, + "locked": False, + "default": False, }, { - 'code': '6100', - 'name': 'Other Expenses', - 'role': roles.EXPENSE_OTHER, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': True # Default for EXPENSE_OTHER - } + "code": "6100", + "name": "Other Expenses", + "role": roles.EXPENSE_OTHER, + "balance_type": roles.DEBIT, + "locked": False, + "default": True, # Default for EXPENSE_OTHER + }, ] created_accounts = [] @@ -1521,23 +1512,29 @@ class Command(BaseCommand): try: account = entity_model.create_account( coa_model=coa_model, - code=account_data['code'], - name=_(account_data['name']), - role=_(account_data['role']), - balance_type=_(account_data['balance_type']), - active=True + code=account_data["code"], + name=_(account_data["name"]), + role=_(account_data["role"]), + balance_type=_(account_data["balance_type"]), + active=True, ) - account.role_default = account_data['default'] + account.role_default = account_data["default"] account.save() created_accounts.append(account) - self.stdout.write(self.style.SUCCESS( - f"Created account: {account.code} - {account.name}" - )) + self.stdout.write( + self.style.SUCCESS( + f"Created account: {account.code} - {account.name}" + ) + ) except Exception as e: - self.stdout.write(self.style.ERROR( - f"Error creating account {account_data['code']}: {str(e)}" - )) + self.stdout.write( + self.style.ERROR( + f"Error creating account {account_data['code']}: {str(e)}" + ) + ) - self.stdout.write(self.style.SUCCESS( - f"\nSuccessfully created {len(created_accounts)} accounts in Chart of Accounts" - )) + self.stdout.write( + self.style.SUCCESS( + f"\nSuccessfully created {len(created_accounts)} accounts in Chart of Accounts" + ) + ) diff --git a/inventory/management/commands/db2json.py b/inventory/management/commands/db2json.py index 11dfd41a..cfb514c6 100644 --- a/inventory/management/commands/db2json.py +++ b/inventory/management/commands/db2json.py @@ -7,14 +7,15 @@ from tqdm import tqdm # Progress bar support # Database connection details db_config = { - 'host': 'localhost', - 'user': 'root', - 'password': "Kfsh&rc9788", - 'database': 'car2db_june' + "host": "localhost", + "user": "root", + "password": "Kfsh&rc9788", + "database": "car2db_june", } EXCLUDED_TABLES = {"car_serie", "car_generation"} # Tables to exclude from direct dump + class Command(BaseCommand): help = "Merge car_serie with car_generation, include in final JSON dump with a progress bar." @@ -22,7 +23,9 @@ class Command(BaseCommand): try: self.stdout.write(self.style.SUCCESS("Connecting to database...")) # Create SQLAlchemy engine - engine = create_engine(f"mysql+pymysql://{db_config['user']}:{db_config['password']}@{db_config['host']}/{db_config['database']}") + engine = create_engine( + f"mysql+pymysql://{db_config['user']}:{db_config['password']}@{db_config['host']}/{db_config['database']}" + ) # Load car_generation table self.stdout.write(self.style.SUCCESS("Loading car_generation data...")) @@ -35,22 +38,39 @@ class Command(BaseCommand): car_serie_df = pd.read_sql(car_serie_query, engine) # Perform a LEFT JOIN to keep all car series and merge with car generations - self.stdout.write(self.style.SUCCESS("Merging car_serie with car_generation...")) - merged_df = pd.merge(car_serie_df, car_generation_df, on="id_car_generation", how="left") + self.stdout.write( + self.style.SUCCESS("Merging car_serie with car_generation...") + ) + merged_df = pd.merge( + car_serie_df, car_generation_df, on="id_car_generation", how="left" + ) # Select and rename the relevant columns - final_df = merged_df.rename(columns={ - "id_car_serie": "id_car_serie", - "id_car_model_x": "id_car_model", - "name_y": "generation_name", - "name_x": "serie_name", - "year_begin": "year_begin", - "year_end": "year_end" - })[["id_car_serie", "id_car_model", "generation_name", "serie_name", "year_begin", "year_end"]] + final_df = merged_df.rename( + columns={ + "id_car_serie": "id_car_serie", + "id_car_model_x": "id_car_model", + "name_y": "generation_name", + "name_x": "serie_name", + "year_begin": "year_begin", + "year_end": "year_end", + } + )[ + [ + "id_car_serie", + "id_car_model", + "generation_name", + "serie_name", + "year_begin", + "year_end", + ] + ] # Convert merged data to a JSON-ready format self.stdout.write(self.style.SUCCESS("Processing merged data...")) - car_serie_json = list(tqdm(final_df.to_dict(orient="records"), desc="Processing car_serie")) + car_serie_json = list( + tqdm(final_df.to_dict(orient="records"), desc="Processing car_serie") + ) # Export the full database including merged car_serie self.export_database_to_json(car_serie_json) @@ -59,7 +79,7 @@ class Command(BaseCommand): self.stdout.write(self.style.ERROR(f"Error: {e}")) def export_database_to_json(self, car_serie_data): - """ Export the entire MariaDB database to JSON, replacing car_serie with merged data """ + """Export the entire MariaDB database to JSON, replacing car_serie with merged data""" try: self.stdout.write(self.style.SUCCESS("Exporting database to JSON...")) # Connect to the database using pymysql @@ -90,13 +110,17 @@ class Command(BaseCommand): # Save the JSON to a file self.stdout.write(self.style.SUCCESS("Saving database_export.json...")) - with open('database_export.json', 'w', encoding='utf-8') as json_file: + with open("database_export.json", "w", encoding="utf-8") as json_file: json.dump(database_json, json_file, indent=4, ensure_ascii=False) - self.stdout.write(self.style.SUCCESS("✅ Database exported to JSON successfully! (Including merged car_serie)")) + self.stdout.write( + self.style.SUCCESS( + "✅ Database exported to JSON successfully! (Including merged car_serie)" + ) + ) except Exception as e: self.stdout.write(self.style.ERROR(f"Error exporting database: {e}")) finally: if connection: - connection.close() \ No newline at end of file + connection.close() diff --git a/inventory/management/commands/deactivate_expired_plans.py b/inventory/management/commands/deactivate_expired_plans.py index 4087e158..6e420a48 100644 --- a/inventory/management/commands/deactivate_expired_plans.py +++ b/inventory/management/commands/deactivate_expired_plans.py @@ -2,17 +2,17 @@ from django.core.management.base import BaseCommand from django.utils import timezone from plans.models import UserPlan + class Command(BaseCommand): - help = 'Deactivates expired user plans' + help = "Deactivates expired user plans" def handle(self, *args, **options): - expired_plans = UserPlan.objects.filter( - active=True, - expire__lt=timezone.now() - ) + expired_plans = UserPlan.objects.filter(active=True, expire__lt=timezone.now()) count = expired_plans.count() for plan in expired_plans: plan.expire_account() - self.stdout.write(self.style.SUCCESS(f'Successfully deactivated {count} expired plans')) \ No newline at end of file + self.stdout.write( + self.style.SUCCESS(f"Successfully deactivated {count} expired plans") + ) diff --git a/inventory/management/commands/generate_slugs.py b/inventory/management/commands/generate_slugs.py index 6c9dfdfa..f043c1fc 100644 --- a/inventory/management/commands/generate_slugs.py +++ b/inventory/management/commands/generate_slugs.py @@ -4,69 +4,72 @@ from django.db import transaction, models from django.utils.text import slugify from django.db.models import Case, When, Value + class Command(BaseCommand): - help = 'Generate slugs for model instances with proper empty value handling' + help = "Generate slugs for model instances with proper empty value handling" def add_arguments(self, parser): parser.add_argument( - '--model', + "--model", type=str, required=True, - help='Model name (format: "app_label.ModelName")' + help='Model name (format: "app_label.ModelName")', ) parser.add_argument( - '--field', + "--field", type=str, - default='name', - help='Field to use as slug source (default: "name")' + default="name", + help='Field to use as slug source (default: "name")', ) parser.add_argument( - '--batch-size', + "--batch-size", type=int, default=1000, - help='Number of records to process at once (default: 1000)' + help="Number of records to process at once (default: 1000)", ) parser.add_argument( - '--dry-run', - action='store_true', - help='Test without actually saving changes' + "--dry-run", + action="store_true", + help="Test without actually saving changes", ) parser.add_argument( - '--fill-empty', - action='store_true', - help='Fill empty slugs with model-ID when source field is empty' + "--fill-empty", + action="store_true", + help="Fill empty slugs with model-ID when source field is empty", ) def handle(self, *args, **options): - model = self.get_model(options['model']) - source_field = options['field'] - batch_size = options['batch_size'] - dry_run = options['dry_run'] - fill_empty = options['fill_empty'] + model = self.get_model(options["model"]) + source_field = options["field"] + batch_size = options["batch_size"] + dry_run = options["dry_run"] + fill_empty = options["fill_empty"] - queryset = model.objects.filter(models.Q(slug__isnull=True) | models.Q(slug='')) + queryset = model.objects.filter(models.Q(slug__isnull=True) | models.Q(slug="")) total_count = queryset.count() processed = 0 empty_source = 0 self.stdout.write( self.style.SUCCESS( - f'Generating slugs for {total_count} {model._meta.model_name} records ' + f"Generating slugs for {total_count} {model._meta.model_name} records " f'using field "{source_field}" (batch size: {batch_size})' ) ) with transaction.atomic(): if dry_run: - self.stdout.write(self.style.WARNING('DRY RUN - No changes will be saved')) + self.stdout.write( + self.style.WARNING("DRY RUN - No changes will be saved") + ) transaction.set_rollback(True) for offset in range(0, total_count, batch_size): - batch = queryset[offset:offset + batch_size] + batch = queryset[offset : offset + batch_size] updates = [] for obj in batch: - source_value = getattr(obj, source_field, '') + source_value = getattr(obj, source_field, "") if not source_value: if fill_empty: @@ -76,12 +79,14 @@ class Command(BaseCommand): else: self.stdout.write( self.style.WARNING( - f'Skipping {obj} (empty {source_field})' + f"Skipping {obj} (empty {source_field})" ) ) continue else: - slug_base = slugify(str(source_value))[:50] # Ensure string and truncate + slug_base = slugify(str(source_value))[ + :50 + ] # Ensure string and truncate new_slug = f"{slug_base}-{obj.pk}" # Guaranteed unique updates.append((obj.pk, new_slug)) @@ -94,27 +99,26 @@ class Command(BaseCommand): ) self.stdout.write( - f'Processed batch {offset//batch_size + 1}: ' - f'{min(offset + batch_size, total_count)}/{total_count}' + f"Processed batch {offset // batch_size + 1}: " + f"{min(offset + batch_size, total_count)}/{total_count}" ) stats = [ f"Total processed: {processed}", f"Records with empty source field: {empty_source}", - f"Skipped records: {total_count - processed - empty_source}" + f"Skipped records: {total_count - processed - empty_source}", ] - self.stdout.write( - self.style.SUCCESS('\n'.join(stats)) - ) + self.stdout.write(self.style.SUCCESS("\n".join(stats))) def get_model(self, model_path): """Get model class from 'app_label.ModelName' string""" from django.apps import apps + try: - app_label, model_name = model_path.split('.') + app_label, model_name = model_path.split(".") return apps.get_model(app_label, model_name) except ValueError: raise self.style.ERROR('Model must be specified as "app_label.ModelName"') except LookupError as e: - raise self.style.ERROR(f'Model not found: {e}') \ No newline at end of file + raise self.style.ERROR(f"Model not found: {e}") diff --git a/inventory/management/commands/generate_vin.py b/inventory/management/commands/generate_vin.py index bb1a4186..36dc6132 100644 --- a/inventory/management/commands/generate_vin.py +++ b/inventory/management/commands/generate_vin.py @@ -1,32 +1,59 @@ from django.core.management.base import BaseCommand -from inventory.services import get_model,decodevin +from inventory.services import get_model, decodevin from bs4 import BeautifulSoup import requests + class Command(BaseCommand): - help = 'Seed the Customer model with 20 records' + help = "Seed the Customer model with 20 records" def handle(self, *args, **kwargs): - vin,description = self.generate_vin() + vin, description = self.generate_vin() result = decodevin(vin) - self.stdout.write(self.style.SUCCESS('####################################################################################################')) - self.stdout.write(self.style.SUCCESS('####################################################################################################')) - self.stdout.write(self.style.SUCCESS(f'Generated VIN: {vin}')) - self.stdout.write(self.style.SUCCESS(f'Description: {description}')) - self.stdout.write(self.style.SUCCESS('####################################################################################################')) - self.stdout.write(self.style.SUCCESS('####################################################################################################')) - self.stdout.write(self.style.SUCCESS(f'Decoded VIN: {result}')) - make,model,year_model = result.values() - self.stdout.write(self.style.SUCCESS(f'VIN: {vin} - Make {make} - Model {model} - Model Year {year_model}')) + self.stdout.write( + self.style.SUCCESS( + "####################################################################################################" + ) + ) + self.stdout.write( + self.style.SUCCESS( + "####################################################################################################" + ) + ) + self.stdout.write(self.style.SUCCESS(f"Generated VIN: {vin}")) + self.stdout.write(self.style.SUCCESS(f"Description: {description}")) + self.stdout.write( + self.style.SUCCESS( + "####################################################################################################" + ) + ) + self.stdout.write( + self.style.SUCCESS( + "####################################################################################################" + ) + ) + self.stdout.write(self.style.SUCCESS(f"Decoded VIN: {result}")) + make, model, year_model = result.values() + self.stdout.write( + self.style.SUCCESS( + f"VIN: {vin} - Make {make} - Model {model} - Model Year {year_model}" + ) + ) m = get_model(model) - self.stdout.write(self.style.SUCCESS(f'Make: {m.id_car_make} - Model: {m}')) - self.stdout.write(self.style.SUCCESS('####################################################################################################')) - self.stdout.write(self.style.SUCCESS('####################################################################################################')) + self.stdout.write(self.style.SUCCESS(f"Make: {m.id_car_make} - Model: {m}")) + self.stdout.write( + self.style.SUCCESS( + "####################################################################################################" + ) + ) + self.stdout.write( + self.style.SUCCESS( + "####################################################################################################" + ) + ) - - def generate_vin(self): # url = "https://www.vindecoder.org/vin-decoder" url = "https://vingenerator.org/" @@ -34,5 +61,5 @@ class Command(BaseCommand): soup = BeautifulSoup(response.content, "html.parser") vin = soup.find("input", {"name": "vin"})["value"] description = soup.find("div", {"class": "description"}).text - - return vin,description + + return vin, description diff --git a/inventory/management/commands/import_data.py b/inventory/management/commands/import_data.py index 7e27b24c..3c33cef9 100644 --- a/inventory/management/commands/import_data.py +++ b/inventory/management/commands/import_data.py @@ -8,8 +8,15 @@ django.setup() import json from tqdm import tqdm from inventory.models import ( - CarMake, CarModel, CarSerie, CarTrim, CarEquipment, - CarSpecification, CarSpecificationValue, CarOption, CarOptionValue + CarMake, + CarModel, + CarSerie, + CarTrim, + CarEquipment, + CarSpecification, + CarSpecificationValue, + CarOption, + CarOptionValue, ) # Load the cleaned JSON data @@ -26,7 +33,7 @@ for item in tqdm(data["car_make"], desc="Inserting CarMake"): # "arabic_name": item.get("arabic_name", ""), # "logo": item.get("Logo", ""), # "is_sa_import": item.get("is_sa_import", False), - } + }, ) # Step 2: Insert CarModel @@ -38,7 +45,7 @@ for item in tqdm(data["car_model"], desc="Inserting CarModel"): "id_car_make_id": item["id_car_make"], "name": item["name"], # "arabic_name": item.get("arabic_name", ""), - } + }, ) # Step 3: Insert CarSerie @@ -53,7 +60,7 @@ for item in tqdm(data["car_serie"], desc="Inserting CarSerie"): "year_begin": item.get("year_begin"), "year_end": item.get("year_end"), "generation_name": item.get("generation_name", ""), - } + }, ) # Step 4: Insert CarTrim @@ -67,7 +74,7 @@ for item in tqdm(data["car_trim"], desc="Inserting CarTrim"): # "arabic_name": item.get("arabic_name", ""), "start_production_year": item["start_production_year"], "end_production_year": item["end_production_year"], - } + }, ) # Step 5: Insert CarEquipment @@ -79,12 +86,14 @@ for item in tqdm(data["car_equipment"], desc="Inserting CarEquipment"): "id_car_trim_id": item["id_car_trim"], "name": item["name"], "year_begin": item.get("year"), - } + }, ) # Step 6: Insert CarSpecification (Parent specifications first) parent_specs = [item for item in data["car_specification"] if item["id_parent"] is None] -child_specs = [item for item in data["car_specification"] if item["id_parent"] is not None] +child_specs = [ + item for item in data["car_specification"] if item["id_parent"] is not None +] for item in tqdm(parent_specs, desc="Inserting Parent CarSpecifications"): CarSpecification.objects.update_or_create( @@ -92,8 +101,8 @@ for item in tqdm(parent_specs, desc="Inserting Parent CarSpecifications"): defaults={ "name": item["name"], # "arabic_name": item.get("arabic_name", ""), - "id_parent_id": None - } + "id_parent_id": None, + }, ) for item in tqdm(child_specs, desc="Inserting Child CarSpecifications"): @@ -103,12 +112,14 @@ for item in tqdm(child_specs, desc="Inserting Child CarSpecifications"): defaults={ "name": item["name"], # "arabic_name": item.get("arabic_name", ""), - "id_parent_id": item["id_parent"] - } + "id_parent_id": item["id_parent"], + }, ) # Step 7: Insert CarSpecificationValue -for item in tqdm(data["car_specification_value"], desc="Inserting CarSpecificationValue"): +for item in tqdm( + data["car_specification_value"], desc="Inserting CarSpecificationValue" +): CarTrim.objects.get(id_car_trim=item["id_car_trim"]) CarSpecification.objects.get(id_car_specification=item["id_car_specification"]) CarSpecificationValue.objects.update_or_create( @@ -118,7 +129,7 @@ for item in tqdm(data["car_specification_value"], desc="Inserting CarSpecificati "id_car_specification_id": item["id_car_specification"], "value": item["value"], "unit": item.get("unit", ""), - } + }, ) # Step 8: Insert CarOption (Parent options first) @@ -131,8 +142,8 @@ for item in tqdm(parent_options, desc="Inserting Parent CarOptions"): defaults={ "name": item["name"], # "arabic_name": item.get("arabic_name", ""), - "id_parent_id": None - } + "id_parent_id": None, + }, ) for item in tqdm(child_options, desc="Inserting Child CarOptions"): @@ -142,8 +153,8 @@ for item in tqdm(child_options, desc="Inserting Child CarOptions"): defaults={ "name": item["name"], # "arabic_name": item.get("arabic_name", ""), - "id_parent_id": item["id_parent"] - } + "id_parent_id": item["id_parent"], + }, ) # Step 9: Insert CarOptionValue @@ -156,7 +167,7 @@ for item in tqdm(data["car_option_value"], desc="Inserting CarOptionValue"): "id_car_option_id": item["id_car_option"], "id_car_equipment_id": item["id_car_equipment"], "is_base": item["is_base"], - } + }, ) -print("Data population completed successfully.") \ No newline at end of file +print("Data population completed successfully.") diff --git a/inventory/management/commands/initial_services_offered.py b/inventory/management/commands/initial_services_offered.py index 1838b082..1624712b 100644 --- a/inventory/management/commands/initial_services_offered.py +++ b/inventory/management/commands/initial_services_offered.py @@ -2,11 +2,31 @@ from django.core.management.base import BaseCommand from appointment.models import Service import datetime + + class Command(BaseCommand): - help = 'create initial services offered' + help = "create initial services offered" def handle(self, *args, **options): Service.objects.all().delete() - Service.objects.create(name='call', price=0,duration=datetime.timedelta(minutes=10),currency='SAR',description='15 min call') - Service.objects.create(name='meeting', price=0,duration=datetime.timedelta(minutes=30),currency='SAR',description='30 min meeting') - Service.objects.create(name='email', price=0,duration=datetime.timedelta(minutes=30),currency='SAR',description='30 min visit') \ No newline at end of file + Service.objects.create( + name="call", + price=0, + duration=datetime.timedelta(minutes=10), + currency="SAR", + description="15 min call", + ) + Service.objects.create( + name="meeting", + price=0, + duration=datetime.timedelta(minutes=30), + currency="SAR", + description="30 min meeting", + ) + Service.objects.create( + name="email", + price=0, + duration=datetime.timedelta(minutes=30), + currency="SAR", + description="30 min visit", + ) diff --git a/inventory/management/commands/populate_colors.py b/inventory/management/commands/populate_colors.py index f03a0f0f..70e25662 100644 --- a/inventory/management/commands/populate_colors.py +++ b/inventory/management/commands/populate_colors.py @@ -48,7 +48,9 @@ class Command(BaseCommand): if created: self.stdout.write(f"Added Exterior Color: {obj.name} ({obj.rgb})") else: - self.stdout.write(f"Exterior Color already exists: {obj.name} ({obj.rgb})") + self.stdout.write( + f"Exterior Color already exists: {obj.name} ({obj.rgb})" + ) self.stdout.write("Populating Interior Colors...") for color in self.interior_colors: @@ -58,6 +60,8 @@ class Command(BaseCommand): if created: self.stdout.write(f"Added Interior Color: {obj.name} ({obj.rgb})") else: - self.stdout.write(f"Interior Color already exists: {obj.name} ({obj.rgb})") + self.stdout.write( + f"Interior Color already exists: {obj.name} ({obj.rgb})" + ) - self.stdout.write("Finished populating colors.") \ No newline at end of file + self.stdout.write("Finished populating colors.") diff --git a/inventory/management/commands/seed_customer.py b/inventory/management/commands/seed_customer.py index 4ef2518b..71c43f26 100644 --- a/inventory/management/commands/seed_customer.py +++ b/inventory/management/commands/seed_customer.py @@ -2,24 +2,27 @@ from django.core.management.base import BaseCommand from faker import Faker from inventory.models import Customer, Dealer + class Command(BaseCommand): - help = 'Seed the Customer model with 20 records' + help = "Seed the Customer model with 20 records" def handle(self, *args, **kwargs): fake = Faker() dealers = Dealer.objects.all() if not dealers.exists(): - self.stdout.write(self.style.ERROR('No dealers found. Please create dealers first.')) + self.stdout.write( + self.style.ERROR("No dealers found. Please create dealers first.") + ) return for _ in range(20): dealer = fake.random_element(elements=dealers) first_name = fake.first_name() - middle_name = fake.first_name() if fake.boolean() else '' + middle_name = fake.first_name() if fake.boolean() else "" last_name = fake.last_name() email = fake.unique.email() - national_id = fake.unique.bothify(text='##########') + national_id = fake.unique.bothify(text="##########") phone_number = fake.unique.phone_number() address = fake.address() @@ -31,7 +34,7 @@ class Command(BaseCommand): email=email, national_id=national_id, phone_number=phone_number, - address=address + address=address, ) - self.stdout.write(self.style.SUCCESS('Successfully seeded 20 customers.')) \ No newline at end of file + self.stdout.write(self.style.SUCCESS("Successfully seeded 20 customers.")) diff --git a/inventory/management/commands/serie_translate.py b/inventory/management/commands/serie_translate.py index 6660ccaf..3af1afc3 100644 --- a/inventory/management/commands/serie_translate.py +++ b/inventory/management/commands/serie_translate.py @@ -3,7 +3,6 @@ from inventory.models import CarSerie TRANSLATIONS = { "Liftback 5-doors": "ليفت باك - خمسة أبواب", - } @@ -18,9 +17,15 @@ class Command(BaseCommand): car_serie.arabic_name = arabic_translation car_serie.save() updated_count += 1 - self.stdout.write(self.style.SUCCESS(f"Updated: {car_serie.name} -> {arabic_translation}")) + self.stdout.write( + self.style.SUCCESS( + f"Updated: {car_serie.name} -> {arabic_translation}" + ) + ) if updated_count: - self.stdout.write(self.style.SUCCESS(f"Successfully updated {updated_count} entries.")) + self.stdout.write( + self.style.SUCCESS(f"Successfully updated {updated_count} entries.") + ) else: self.stdout.write(self.style.WARNING("No updates were made.")) diff --git a/inventory/management/commands/serie_up.py b/inventory/management/commands/serie_up.py index 4f2582d1..298ca372 100644 --- a/inventory/management/commands/serie_up.py +++ b/inventory/management/commands/serie_up.py @@ -3,38 +3,53 @@ import csv from django.core.management.base import BaseCommand from inventory.models import CarSerie, CarModel + class Command(BaseCommand): help = "Update or add CarSerie entries from a merged CSV file" def handle(self, *args, **kwargs): # Path to the merged CSV file base_dir = os.path.dirname(os.path.abspath(__file__)) - file_path = os.path.join(base_dir, "../../data/Updated_Merged_Car_Generation_and_Serie_Data.csv") # Adjust the path if needed + file_path = os.path.join( + base_dir, "../../data/Updated_Merged_Car_Generation_and_Serie_Data.csv" + ) # Adjust the path if needed if not os.path.exists(file_path): self.stdout.write(self.style.ERROR(f"File not found: {file_path}")) return - with open(file_path, newline='', encoding='utf-8') as csvfile: + with open(file_path, newline="", encoding="utf-8") as csvfile: reader = csv.DictReader(csvfile) for row in reader: try: - car_model = CarModel.objects.get(pk=row['id_car_model']) + car_model = CarModel.objects.get(pk=row["id_car_model"]) except CarModel.DoesNotExist: - self.stdout.write(self.style.WARNING(f"CarModel with ID {row['id_car_model']} not found")) + self.stdout.write( + self.style.WARNING( + f"CarModel with ID {row['id_car_model']} not found" + ) + ) continue car_serie, created = CarSerie.objects.update_or_create( - id_car_serie=row['id_car_serie'], + id_car_serie=row["id_car_serie"], defaults={ - 'id_car_model': car_model, - 'name': row['name'], - 'arabic_name': "-", - 'year_begin': int(float(row['year_begin'])) if row['year_begin'] else None, - 'year_end': int(float(row['year_end'])) if row['year_end'] else None, - 'generation_name': row['generation_name'], + "id_car_model": car_model, + "name": row["name"], + "arabic_name": "-", + "year_begin": int(float(row["year_begin"])) + if row["year_begin"] + else None, + "year_end": int(float(row["year_end"])) + if row["year_end"] + else None, + "generation_name": row["generation_name"], }, ) action = "Created" if created else "Updated" - self.stdout.write(self.style.SUCCESS(f"{action} CarSerie with ID {car_serie.id_car_serie}")) \ No newline at end of file + self.stdout.write( + self.style.SUCCESS( + f"{action} CarSerie with ID {car_serie.id_car_serie}" + ) + ) diff --git a/inventory/management/commands/set_vat.py b/inventory/management/commands/set_vat.py index 5460cc1d..d80c9293 100644 --- a/inventory/management/commands/set_vat.py +++ b/inventory/management/commands/set_vat.py @@ -1,6 +1,8 @@ from django.core.management.base import BaseCommand from inventory.models import VatRate from decimal import Decimal + + class Command(BaseCommand): def handle(self, *args, **kwargs): - VatRate.objects.get_or_create(rate=Decimal('0.15'), is_active=True) \ No newline at end of file + VatRate.objects.get_or_create(rate=Decimal("0.15"), is_active=True) diff --git a/inventory/management/commands/setplan.py b/inventory/management/commands/setplan.py index e168d5ef..9bbfa2a0 100644 --- a/inventory/management/commands/setplan.py +++ b/inventory/management/commands/setplan.py @@ -4,24 +4,23 @@ from plans.models import Plan, Quota, Pricing, PlanPricing from decimal import Decimal from django.db.models import Q + class Command(BaseCommand): - help = 'Create basic subscription plans structure' + help = "Create basic subscription plans structure" def add_arguments(self, parser): parser.add_argument( - '--reset', - action='store_true', - help='Delete existing plans and quotas before creating new ones' + "--reset", + action="store_true", + help="Delete existing plans and quotas before creating new ones", ) def handle(self, *args, **options): - if options['reset']: - self.stdout.write(self.style.WARNING('Resetting existing plans data...')) + if options["reset"]: + self.stdout.write(self.style.WARNING("Resetting existing plans data...")) Plan.objects.all().delete() Quota.objects.filter( - Q(codename='basic') | - Q(codename='pro') | - Q(codename='premium') + Q(codename="basic") | Q(codename="pro") | Q(codename="premium") ).delete() # Ensure no existing plans are marked as default @@ -32,93 +31,89 @@ class Command(BaseCommand): # Create core quotas basic_quota, _ = Quota.objects.update_or_create( - codename='basic', + codename="basic", defaults={ - 'name': 'Basic Features', - 'description': 'Essential platform access', - 'is_boolean': True - } + "name": "Basic Features", + "description": "Essential platform access", + "is_boolean": True, + }, ) pro_quota, _ = Quota.objects.update_or_create( - codename='pro', + codename="pro", defaults={ - 'name': 'Pro Features', - 'description': 'Advanced functionality', - 'is_boolean': True - } + "name": "Pro Features", + "description": "Advanced functionality", + "is_boolean": True, + }, ) premium_quota, _ = Quota.objects.update_or_create( - codename='premium', + codename="premium", defaults={ - 'name': 'Premium Features', - 'description': 'Full platform access', - 'is_boolean': True - } + "name": "Premium Features", + "description": "Full platform access", + "is_boolean": True, + }, ) # Create pricing period monthly_pricing, _ = Pricing.objects.update_or_create( - name='Monthly', - defaults={'period': 30} + name="Monthly", defaults={"period": 30} ) # Define plan structure plans = [ { - 'name': 'Basic', - 'description': 'Entry-level plan', - 'price': Decimal('0.00'), - 'period': None, - 'quotas': [basic_quota], - 'default': True + "name": "Basic", + "description": "Entry-level plan", + "price": Decimal("0.00"), + "period": None, + "quotas": [basic_quota], + "default": True, }, { - 'name': 'Pro', - 'description': 'Professional plan', - 'price': Decimal('29.00'), - 'period': 30, - 'quotas': [basic_quota, pro_quota], - 'default': False + "name": "Pro", + "description": "Professional plan", + "price": Decimal("29.00"), + "period": 30, + "quotas": [basic_quota, pro_quota], + "default": False, }, { - 'name': 'Premium', - 'description': 'Full access plan', - 'price': Decimal('99.00'), - 'period': 30, - 'quotas': [basic_quota, pro_quota, premium_quota], - 'default': None - } + "name": "Premium", + "description": "Full access plan", + "price": Decimal("99.00"), + "period": 30, + "quotas": [basic_quota, pro_quota, premium_quota], + "default": None, + }, ] # Create plans and associations for plan_data in plans: plan, created = Plan.objects.update_or_create( - name=plan_data['name'], + name=plan_data["name"], defaults={ - 'description': plan_data['description'], - 'default': plan_data.get('default', False), - 'available': True, - 'visible': True - } + "description": plan_data["description"], + "default": plan_data.get("default", False), + "available": True, + "visible": True, + }, ) # Set quotas - plan.quotas.set(plan_data['quotas']) + plan.quotas.set(plan_data["quotas"]) # Create pricing if applicable - if plan_data['price'] > 0: + if plan_data["price"] > 0: PlanPricing.objects.update_or_create( plan=plan, pricing=monthly_pricing, - defaults={ - 'price': plan_data['price'], - 'visible': True - } + defaults={"price": plan_data["price"], "visible": True}, ) - status = 'Created' if created else 'Updated' - self.stdout.write(self.style.SUCCESS(f'{status} {plan.name} plan')) + status = "Created" if created else "Updated" + self.stdout.write(self.style.SUCCESS(f"{status} {plan.name} plan")) - self.stdout.write(self.style.SUCCESS('Successfully created plans structure')) \ No newline at end of file + self.stdout.write(self.style.SUCCESS("Successfully created plans structure")) diff --git a/inventory/management/commands/tenhal_plan.py b/inventory/management/commands/tenhal_plan.py index acd3d39b..a301a6ad 100644 --- a/inventory/management/commands/tenhal_plan.py +++ b/inventory/management/commands/tenhal_plan.py @@ -2,14 +2,16 @@ from decimal import Decimal from django.core.management.base import BaseCommand from plans.models import Plan, Quota, PlanQuota, Pricing, PlanPricing + + class Command(BaseCommand): - help = 'Create basic subscription plans structure' + help = "Create basic subscription plans structure" def add_arguments(self, parser): parser.add_argument( - '--reset', - action='store_true', - help='Delete existing plans and quotas before creating new ones' + "--reset", + action="store_true", + help="Delete existing plans and quotas before creating new ones", ) def handle(self, *args, **options): @@ -22,13 +24,23 @@ class Command(BaseCommand): # Order.objects.all().delete() # BillingInfo.objects.all().delete() - - users_quota = Quota.objects.create(name='Users', codename='Users', unit='number') - cars_quota = Quota.objects.create(name='Cars', codename='Cars', unit='number') + users_quota = Quota.objects.create( + name="Users", codename="Users", unit="number" + ) + cars_quota = Quota.objects.create(name="Cars", codename="Cars", unit="number") # Create plans - basic_plan = Plan.objects.create(name='Basic', description='basic plan', available=True, visible=True) - pro_plan = Plan.objects.create(name='Pro', description='Pro plan', available=True, visible=True) - enterprise_plan = Plan.objects.create(name='Enterprise', description='Enterprise plan', available=True, visible=True) + basic_plan = Plan.objects.create( + name="Basic", description="basic plan", available=True, visible=True + ) + pro_plan = Plan.objects.create( + name="Pro", description="Pro plan", available=True, visible=True + ) + enterprise_plan = Plan.objects.create( + name="Enterprise", + description="Enterprise plan", + available=True, + visible=True, + ) # Assign quotas to plans PlanQuota.objects.create(plan=basic_plan, quota=users_quota, value=3) @@ -44,13 +56,19 @@ class Command(BaseCommand): # PlanQuota.objects.create(plan=pro_plan, quota=storage_quota, value=100) # Define pricing - basic_pricing = Pricing.objects.create(name='Monthly', period=30) - pro_pricing = Pricing.objects.create(name='Monthly', period=30) - enterprise_pricing = Pricing.objects.create(name='Monthly', period=30) + basic_pricing = Pricing.objects.create(name="Monthly", period=30) + pro_pricing = Pricing.objects.create(name="Monthly", period=30) + enterprise_pricing = Pricing.objects.create(name="Monthly", period=30) - PlanPricing.objects.create(plan=basic_plan, pricing=basic_pricing, price=Decimal('9.99')) - PlanPricing.objects.create(plan=pro_plan, pricing=pro_pricing, price=Decimal('19.99')) - PlanPricing.objects.create(plan=enterprise_plan, pricing=enterprise_pricing, price=Decimal('29.99')) + PlanPricing.objects.create( + plan=basic_plan, pricing=basic_pricing, price=Decimal("9.99") + ) + PlanPricing.objects.create( + plan=pro_plan, pricing=pro_pricing, price=Decimal("19.99") + ) + PlanPricing.objects.create( + plan=enterprise_plan, pricing=enterprise_pricing, price=Decimal("29.99") + ) # # Create quotas # project_quota = Quota.objects.create(name='projects', codename='projects', unit='projects') @@ -72,35 +90,34 @@ class Command(BaseCommand): # basic = Pricing.objects.create(name='Monthly', period=30) # pro = Pricing.objects.create(name='Monthly', period=30) - # basic_pricing = PlanPricing.objects.create(plan=basic_plan, pricing=basic, price=Decimal('19.99')) # pro_pricing = PlanPricing.objects.create(plan=pro_plan, pricing=pro, price=Decimal('29.99')) # Create users # user = User.objects.first() - # # Create user plans - # billing_info = BillingInfo.objects.create( - # user=user, - # tax_number='123456789', - # name='John Doe', - # street='123 Main St', - # zipcode='12345', - # city='Anytown', - # country='US', - # ) + # # Create user plans + # billing_info = BillingInfo.objects.create( + # user=user, + # tax_number='123456789', + # name='John Doe', + # street='123 Main St', + # zipcode='12345', + # city='Anytown', + # country='US', + # ) - # order = Order.objects.create( - # user=user, - # plan=pro_plan, - # pricing=pro_pricing, - # amount=pro_pricing.price, - # currency="SAR", - # ) + # order = Order.objects.create( + # user=user, + # plan=pro_plan, + # pricing=pro_pricing, + # amount=pro_pricing.price, + # currency="SAR", + # ) - # UserPlan.objects.create( - # user=user, - # plan=pro_plan, - # expire=timezone.now() + timedelta(days=2), - # active=True, - # ) + # UserPlan.objects.create( + # user=user, + # plan=pro_plan, + # expire=timezone.now() + timedelta(days=2), + # active=True, + # ) diff --git a/inventory/management/commands/test.py b/inventory/management/commands/test.py index 2f2b7326..e16ba9a2 100644 --- a/inventory/management/commands/test.py +++ b/inventory/management/commands/test.py @@ -4,8 +4,9 @@ from inventory.tasks import create_coa_accounts from inventory.models import Dealer User = get_user_model() -class Command(BaseCommand): + +class Command(BaseCommand): def handle(self, *args, **kwargs): # user = User.objects.last() # print(user.email) @@ -17,4 +18,4 @@ class Command(BaseCommand): # result = re.match(r'^05\d{8}$', '0625252522') # print(result) dealer = Dealer.objects.last() - create_coa_accounts(dealer.pk) \ No newline at end of file + create_coa_accounts(dealer.pk) diff --git a/inventory/management/commands/test_task_process_running.py b/inventory/management/commands/test_task_process_running.py index 8603c5b3..3eed3551 100644 --- a/inventory/management/commands/test_task_process_running.py +++ b/inventory/management/commands/test_task_process_running.py @@ -1,4 +1,5 @@ from django.core.management.base import BaseCommand + # from background_task.models import Task # from background_task import background # from django_q.tasks import async_task @@ -6,7 +7,5 @@ from inventory.tasks import send_email class Command(BaseCommand): - def handle(self, *args, **kwargs): - send_email('ismail.mosa@gmail.com', 'teset1@gmail.com', 'test', 'test') - + send_email("ismail.mosa@gmail.com", "teset1@gmail.com", "test", "test") diff --git a/inventory/management/commands/translate.py b/inventory/management/commands/translate.py index fb1a0a22..5d593e47 100644 --- a/inventory/management/commands/translate.py +++ b/inventory/management/commands/translate.py @@ -5,17 +5,19 @@ from django.conf import settings class Command(BaseCommand): - help = 'Translates car model names to Arabic and saves them in the arabic_name field.' + help = ( + "Translates car model names to Arabic and saves them in the arabic_name field." + ) def handle(self, *args, **kwargs): client = OpenAI(api_key=settings.OPENAI_API_KEY) car_models = CarModel.objects.all() total = car_models.count() - print(f'Translating {total} names...') + print(f"Translating {total} names...") for index, car_model in enumerate(car_models, start=1): - if not car_model.arabic_name or car_model.arabic_name == '-': + if not car_model.arabic_name or car_model.arabic_name == "-": if isinstance(car_model.name, int): car_model.arabic_name = car_model.name car_model.save() @@ -31,12 +33,9 @@ class Command(BaseCommand): "You are an assistant that translates English car names to Arabic." "If the name is purely numeric, keep it as is." "For mixed names like 'D9', translate them as 'دي 9'." - ) + ), }, - { - "role": "user", - "content": car_model.name - } + {"role": "user", "content": car_model.name}, ], temperature=0.2, ) @@ -45,4 +44,4 @@ class Command(BaseCommand): car_model.save() print(f"[{index}/{total}] .. Done") except Exception as e: - print(f"Error translating '{car_model.name}': {e}") \ No newline at end of file + print(f"Error translating '{car_model.name}': {e}") diff --git a/inventory/management/commands/update_car_make.py b/inventory/management/commands/update_car_make.py index 6da22408..6f8ede65 100644 --- a/inventory/management/commands/update_car_make.py +++ b/inventory/management/commands/update_car_make.py @@ -2,32 +2,37 @@ from django.core.management.base import BaseCommand from inventory.models import CarMake import json + class Command(BaseCommand): - help = 'Update CarMake model with data from a JSON file' + help = "Update CarMake model with data from a JSON file" def handle(self, *args, **kwargs): # Load the JSON data from the file - with open('carmake_updated_backup.json', 'r', encoding='utf-8') as file: + with open("carmake_updated_backup.json", "r", encoding="utf-8") as file: car_makes_data = json.load(file) # Iterate over the data and update the CarMake model for car_make_data in car_makes_data: - pk = car_make_data['pk'] - fields = car_make_data['fields'] + pk = car_make_data["pk"] + fields = car_make_data["fields"] # Get or create the CarMake instance car_make, created = CarMake.objects.get_or_create(pk=pk) # Update the fields - car_make.name = fields['name'] - car_make.arabic_name = fields['arabic_name'] - car_make.logo = fields['logo'] - car_make.is_sa_import = fields['is_sa_import'] + car_make.name = fields["name"] + car_make.arabic_name = fields["arabic_name"] + car_make.logo = fields["logo"] + car_make.is_sa_import = fields["is_sa_import"] # Save the updated instance car_make.save() if created: - self.stdout.write(self.style.SUCCESS(f'Created CarMake: {car_make.name}')) + self.stdout.write( + self.style.SUCCESS(f"Created CarMake: {car_make.name}") + ) else: - self.stdout.write(self.style.SUCCESS(f'Updated CarMake: {car_make.name}')) \ No newline at end of file + self.stdout.write( + self.style.SUCCESS(f"Updated CarMake: {car_make.name}") + ) diff --git a/inventory/management/commands/update_car_model.py b/inventory/management/commands/update_car_model.py index 84b18c86..723a3157 100644 --- a/inventory/management/commands/update_car_model.py +++ b/inventory/management/commands/update_car_model.py @@ -3,35 +3,46 @@ import csv from django.core.management.base import BaseCommand from inventory.models import CarModel, CarMake + class Command(BaseCommand): help = "Update or add CarModel entries from a CSV file" def handle(self, *args, **kwargs): # Path to the car_model CSV file base_dir = os.path.dirname(os.path.abspath(__file__)) - file_path = os.path.join(base_dir, "../../data/car_model.csv") # Adjust path if needed + file_path = os.path.join( + base_dir, "../../data/car_model.csv" + ) # Adjust path if needed if not os.path.exists(file_path): self.stdout.write(self.style.ERROR(f"File not found: {file_path}")) return - with open(file_path, newline='', encoding='utf-8') as csvfile: + with open(file_path, newline="", encoding="utf-8") as csvfile: reader = csv.DictReader(csvfile) for row in reader: try: - car_make = CarMake.objects.get(pk=row['id_car_make']) + car_make = CarMake.objects.get(pk=row["id_car_make"]) except CarMake.DoesNotExist: - self.stdout.write(self.style.WARNING(f"CarMake with ID {row['id_car_make']} not found")) + self.stdout.write( + self.style.WARNING( + f"CarMake with ID {row['id_car_make']} not found" + ) + ) continue car_model, created = CarModel.objects.update_or_create( - id_car_model=row['id_car_model'], + id_car_model=row["id_car_model"], defaults={ - 'id_car_make': car_make, - 'name': row['name'], - 'arabic_name': row.get('arabic_name', ''), + "id_car_make": car_make, + "name": row["name"], + "arabic_name": row.get("arabic_name", ""), }, ) action = "Created" if created else "Updated" - self.stdout.write(self.style.SUCCESS(f"{action} CarModel with ID {car_model.id_car_model}")) \ No newline at end of file + self.stdout.write( + self.style.SUCCESS( + f"{action} CarModel with ID {car_model.id_car_model}" + ) + ) diff --git a/inventory/management/commands/update_car_specification_value.py b/inventory/management/commands/update_car_specification_value.py index 6d321f7c..4cc61907 100644 --- a/inventory/management/commands/update_car_specification_value.py +++ b/inventory/management/commands/update_car_specification_value.py @@ -3,42 +3,61 @@ import csv from django.core.management.base import BaseCommand from inventory.models import CarSpecificationValue, CarSpecification, CarTrim + class Command(BaseCommand): help = "Update or add CarSpecificationValue entries from a CSV file" def handle(self, *args, **kwargs): # Path to the car_specification_value CSV file base_dir = os.path.dirname(os.path.abspath(__file__)) - file_path = os.path.join(base_dir, "../../data/car_specification_value.csv") # Adjust this path if needed + file_path = os.path.join( + base_dir, "../../data/car_specification_value.csv" + ) # Adjust this path if needed if not os.path.exists(file_path): self.stdout.write(self.style.ERROR(f"File not found: {file_path}")) return - with open(file_path, newline='', encoding='utf-8') as csvfile: + with open(file_path, newline="", encoding="utf-8") as csvfile: reader = csv.DictReader(csvfile) for row in reader: try: - car_trim = CarTrim.objects.get(pk=row['id_car_trim']) + car_trim = CarTrim.objects.get(pk=row["id_car_trim"]) except CarTrim.DoesNotExist: - self.stdout.write(self.style.WARNING(f"CarTrim with ID {row['id_car_trim']} not found")) + self.stdout.write( + self.style.WARNING( + f"CarTrim with ID {row['id_car_trim']} not found" + ) + ) continue try: - car_specification = CarSpecification.objects.get(pk=row['id_car_specification']) + car_specification = CarSpecification.objects.get( + pk=row["id_car_specification"] + ) except CarSpecification.DoesNotExist: - self.stdout.write(self.style.WARNING(f"CarSpecification with ID {row['id_car_specification']} not found")) + self.stdout.write( + self.style.WARNING( + f"CarSpecification with ID {row['id_car_specification']} not found" + ) + ) continue - car_specification_value, created = CarSpecificationValue.objects.update_or_create( - id_car_specification_value=row['id_car_specification_value'], - defaults={ - 'id_car_trim': car_trim, - 'id_car_specification': car_specification, - 'value': row['value'], - 'unit': row.get('unit', ''), - }, + car_specification_value, created = ( + CarSpecificationValue.objects.update_or_create( + id_car_specification_value=row["id_car_specification_value"], + defaults={ + "id_car_trim": car_trim, + "id_car_specification": car_specification, + "value": row["value"], + "unit": row.get("unit", ""), + }, + ) ) action = "Created" if created else "Updated" - self.stdout.write(self.style.SUCCESS(f"{action} CarSpecificationValue with ID {car_specification_value.id_car_specification_value}")) \ No newline at end of file + self.stdout.write( + self.style.SUCCESS( + f"{action} CarSpecificationValue with ID {car_specification_value.id_car_specification_value}" + ) + ) diff --git a/inventory/management/commands/update_car_trim.py b/inventory/management/commands/update_car_trim.py index 8a50d86e..f76e3766 100644 --- a/inventory/management/commands/update_car_trim.py +++ b/inventory/management/commands/update_car_trim.py @@ -3,45 +3,66 @@ import csv from django.core.management.base import BaseCommand from inventory.models import CarTrim, CarSerie, CarModel + class Command(BaseCommand): help = "Update or add CarTrim entries from a CSV file" def handle(self, *args, **kwargs): # Path to the car_trim CSV file base_dir = os.path.dirname(os.path.abspath(__file__)) - file_path = os.path.join(base_dir, "../../data/car_trim.csv") # Adjust path if needed + file_path = os.path.join( + base_dir, "../../data/car_trim.csv" + ) # Adjust path if needed if not os.path.exists(file_path): self.stdout.write(self.style.ERROR(f"File not found: {file_path}")) return - with open(file_path, newline='', encoding='utf-8') as csvfile: + with open(file_path, newline="", encoding="utf-8") as csvfile: reader = csv.DictReader(csvfile) for row in reader: try: - car_serie = CarSerie.objects.get(pk=row['id_car_serie']) + car_serie = CarSerie.objects.get(pk=row["id_car_serie"]) except CarSerie.DoesNotExist: - self.stdout.write(self.style.WARNING(f"CarSerie with ID {row['id_car_serie']} not found")) + self.stdout.write( + self.style.WARNING( + f"CarSerie with ID {row['id_car_serie']} not found" + ) + ) continue car_model = None - if row['id_car_model']: + if row["id_car_model"]: try: - car_model = CarModel.objects.get(pk=row['id_car_model']) + car_model = CarModel.objects.get(pk=row["id_car_model"]) except CarModel.DoesNotExist: - self.stdout.write(self.style.WARNING(f"CarModel with ID {row['id_car_model']} not found")) + self.stdout.write( + self.style.WARNING( + f"CarModel with ID {row['id_car_model']} not found" + ) + ) car_trim, created = CarTrim.objects.update_or_create( - id_car_trim=row['id_car_trim'], + id_car_trim=row["id_car_trim"], defaults={ - 'id_car_serie': car_serie, - 'id_car_model': car_model, - 'name': row['name'], - 'arabic_name': row.get('arabic_name', ''), - 'start_production_year': int(float(row['start_production_year'])) if row['start_production_year'] else None, - 'end_production_year': int(float(row['end_production_year'])) if row['end_production_year'] else None, + "id_car_serie": car_serie, + "id_car_model": car_model, + "name": row["name"], + "arabic_name": row.get("arabic_name", ""), + "start_production_year": int( + float(row["start_production_year"]) + ) + if row["start_production_year"] + else None, + "end_production_year": int(float(row["end_production_year"])) + if row["end_production_year"] + else None, }, ) action = "Created" if created else "Updated" - self.stdout.write(self.style.SUCCESS(f"{action} CarTrim with ID {car_trim.id_car_trim}")) \ No newline at end of file + self.stdout.write( + self.style.SUCCESS( + f"{action} CarTrim with ID {car_trim.id_car_trim}" + ) + ) diff --git a/inventory/middleware.py b/inventory/middleware.py index 8a65685b..3ee697b8 100644 --- a/inventory/middleware.py +++ b/inventory/middleware.py @@ -4,7 +4,7 @@ from django.utils import timezone from inventory.utils import get_user_type -logger = logging.getLogger('user_activity') +logger = logging.getLogger("user_activity") class LogUserActivityMiddleware: @@ -20,6 +20,7 @@ class LogUserActivityMiddleware: chain. :type get_response: Callable """ + def __init__(self, get_response): self.get_response = get_response @@ -29,18 +30,16 @@ class LogUserActivityMiddleware: if request.user.is_authenticated: action = f"{request.method} {request.path}" models.UserActivityLog.objects.create( - user=request.user, - action=action, - timestamp=timezone.now() + user=request.user, action=action, timestamp=timezone.now() ) return response def get_client_ip(self, request): - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR") if x_forwarded_for: - return x_forwarded_for.split(',')[0] - return request.META.get('REMOTE_ADDR') - + return x_forwarded_for.split(",")[0] + return request.META.get("REMOTE_ADDR") + class InjectParamsMiddleware: """ @@ -54,15 +53,16 @@ class InjectParamsMiddleware: :ivar get_response: The callable to get the next middleware or view response. :type get_response: Callable """ + def __init__(self, get_response): self.get_response = get_response - def __call__(self, request): - try: + def __call__(self, request): + try: # request.entity = request.user.dealer.entity request.dealer = get_user_type(request) except Exception: - pass + pass response = self.get_response(request) return response @@ -81,22 +81,24 @@ class InjectDealerMiddleware: to process the next middleware or the view in the request-response cycle. :type get_response: Callable """ + def __init__(self, get_response): self.get_response = get_response - def __call__(self, request): - try: - request.is_dealer = False - request.is_staff = False + def __call__(self, request): + try: + request.is_dealer = False + request.is_staff = False if hasattr(request.user, "dealer"): - request.is_dealer = True - if hasattr(request.user, "staffmember"): + request.is_dealer = True + if hasattr(request.user, "staffmember"): request.is_staff = True except Exception: - pass + pass response = self.get_response(request) return response + # class OTPVerificationMiddleware: # def __init__(self, get_response): # self.get_response = get_response @@ -105,4 +107,3 @@ class InjectDealerMiddleware: # if request.user.is_authenticated and not request.session.get('otp_verified', False): # return redirect(reverse('verify_otp')) # return self.get_response(request) - diff --git a/inventory/migrations/0001_initial.py b/inventory/migrations/0001_initial.py new file mode 100644 index 00000000..a629e2f3 --- /dev/null +++ b/inventory/migrations/0001_initial.py @@ -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')], + }, + ), + ] diff --git a/inventory/migrations/0002_remove_saleorder_journal_entry.py b/inventory/migrations/0002_remove_saleorder_journal_entry.py new file mode 100644 index 00000000..aee1533b --- /dev/null +++ b/inventory/migrations/0002_remove_saleorder_journal_entry.py @@ -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', + ), + ] diff --git a/inventory/migrations/0003_saleorder_journal_entry.py b/inventory/migrations/0003_saleorder_journal_entry.py new file mode 100644 index 00000000..5cee8325 --- /dev/null +++ b/inventory/migrations/0003_saleorder_journal_entry.py @@ -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'), + ), + ] diff --git a/inventory/migrations/0004_remove_saleorder_agreed_price_and_more.py b/inventory/migrations/0004_remove_saleorder_agreed_price_and_more.py new file mode 100644 index 00000000..5d924dd6 --- /dev/null +++ b/inventory/migrations/0004_remove_saleorder_agreed_price_and_more.py @@ -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', + ), + ] diff --git a/inventory/migrations/0005_remove_saleorder_car.py b/inventory/migrations/0005_remove_saleorder_car.py new file mode 100644 index 00000000..113040fc --- /dev/null +++ b/inventory/migrations/0005_remove_saleorder_car.py @@ -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', + ), + ] diff --git a/inventory/migrations/0017_intendedvehicle_purchase_order.py b/inventory/migrations/0006_saleorder_dealer.py similarity index 53% rename from inventory/migrations/0017_intendedvehicle_purchase_order.py rename to inventory/migrations/0006_saleorder_dealer.py index feed14dc..16181308 100644 --- a/inventory/migrations/0017_intendedvehicle_purchase_order.py +++ b/inventory/migrations/0006_saleorder_dealer.py @@ -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, ), ] diff --git a/inventory/migrations/0007_saleorder_estimate.py b/inventory/migrations/0007_saleorder_estimate.py new file mode 100644 index 00000000..7d5254db --- /dev/null +++ b/inventory/migrations/0007_saleorder_estimate.py @@ -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'), + ), + ] diff --git a/inventory/migrations/0008_saleorder_opportunity.py b/inventory/migrations/0008_saleorder_opportunity.py new file mode 100644 index 00000000..41fecf33 --- /dev/null +++ b/inventory/migrations/0008_saleorder_opportunity.py @@ -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'), + ), + ] diff --git a/inventory/migrations/0009_saleorder_customer.py b/inventory/migrations/0009_saleorder_customer.py new file mode 100644 index 00000000..0fc198f5 --- /dev/null +++ b/inventory/migrations/0009_saleorder_customer.py @@ -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'), + ), + ] diff --git a/inventory/migrations/0010_alter_saleorder_created_by_and_more.py b/inventory/migrations/0010_alter_saleorder_created_by_and_more.py new file mode 100644 index 00000000..72054443 --- /dev/null +++ b/inventory/migrations/0010_alter_saleorder_created_by_and_more.py @@ -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), + ), + ] diff --git a/inventory/migrations/0015_intendedvehicle_purchaseorder_deliveryreceipt.py b/inventory/migrations/0015_intendedvehicle_purchaseorder_deliveryreceipt.py deleted file mode 100644 index 26a7c129..00000000 --- a/inventory/migrations/0015_intendedvehicle_purchaseorder_deliveryreceipt.py +++ /dev/null @@ -1,63 +0,0 @@ -# Generated by Django 5.2.1 on 2025-06-03 11:03 - -import django.db.models.deletion -import django.utils.timezone -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('inventory', '0014_alter_opportunity_amount'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='IntendedVehicle', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('year', models.PositiveIntegerField()), - ('color', models.CharField(max_length=30)), - ('engine', models.CharField(blank=True, max_length=50, null=True)), - ('condition', models.CharField(choices=[('new', 'New'), ('used', 'Used'), ('certified', 'Certified Pre-Owned')], max_length=20)), - ('expected_cost', models.DecimalField(decimal_places=2, max_digits=12)), - ('make', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.carmake')), - ('model', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.carmodel')), - ('serie', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.carserie')), - ('trim', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.cartrim')), - ], - ), - migrations.CreateModel( - name='PurchaseOrder', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('po_number', models.CharField(editable=False, max_length=50, unique=True)), - ('status', models.CharField(choices=[('pending', 'Pending'), ('approved', 'Approved'), ('rejected', 'Rejected'), ('completed', 'Completed'), ('canceled', 'Canceled')], default='pending', max_length=20)), - ('quantity', models.PositiveIntegerField(default=1)), - ('total_cost', models.DecimalField(decimal_places=2, max_digits=12)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('expected_delivery_date', models.DateField(blank=True, null=True)), - ('notes', models.TextField(blank=True, null=True)), - ('approved_at', models.DateTimeField(blank=True, null=True)), - ('approved_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='approved_orders', to=settings.AUTH_USER_MODEL)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_orders', to=settings.AUTH_USER_MODEL)), - ('intended_vehicle', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.intendedvehicle')), - ('supplier', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.vendor')), - ], - ), - migrations.CreateModel( - name='DeliveryReceipt', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('receipt_number', models.CharField(editable=False, max_length=50, unique=True)), - ('received_at', models.DateTimeField(default=django.utils.timezone.now)), - ('notes', models.TextField(blank=True, null=True)), - ('car', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='inventory.car')), - ('received_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), - ('purchase_order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.purchaseorder')), - ], - ), - ] diff --git a/inventory/migrations/0016_purchaseorderitem_sale.py b/inventory/migrations/0016_purchaseorderitem_sale.py deleted file mode 100644 index 295a2e43..00000000 --- a/inventory/migrations/0016_purchaseorderitem_sale.py +++ /dev/null @@ -1,35 +0,0 @@ -# Generated by Django 5.2.1 on 2025-06-03 11:21 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('inventory', '0015_intendedvehicle_purchaseorder_deliveryreceipt'), - ] - - operations = [ - migrations.CreateModel( - name='PurchaseOrderItem', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('quantity', models.PositiveIntegerField(default=1)), - ('price', models.DecimalField(decimal_places=2, max_digits=10)), - ('purchase_order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.purchaseorder')), - ('vehicle', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.intendedvehicle')), - ], - ), - migrations.CreateModel( - name='Sale', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('sale_date', models.DateTimeField(auto_now_add=True)), - ('selling_price', models.DecimalField(decimal_places=2, max_digits=12)), - ('car', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.car')), - ('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.customer')), - ('salesperson', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='inventory.staff')), - ], - ), - ] diff --git a/inventory/migrations/0018_intendedvehicle_quantity.py b/inventory/migrations/0018_intendedvehicle_quantity.py deleted file mode 100644 index d8336b71..00000000 --- a/inventory/migrations/0018_intendedvehicle_quantity.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.1 on 2025-06-03 13:56 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('inventory', '0017_intendedvehicle_purchase_order'), - ] - - operations = [ - migrations.AddField( - model_name='intendedvehicle', - name='quantity', - field=models.PositiveIntegerField(default=1), - ), - ] diff --git a/inventory/migrations/0019_alter_intendedvehicle_make_and_more.py b/inventory/migrations/0019_alter_intendedvehicle_make_and_more.py deleted file mode 100644 index 81564bb1..00000000 --- a/inventory/migrations/0019_alter_intendedvehicle_make_and_more.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 5.2.1 on 2025-06-03 17:42 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('inventory', '0018_intendedvehicle_quantity'), - ] - - operations = [ - migrations.AlterField( - model_name='intendedvehicle', - name='make', - field=models.CharField(max_length=30), - ), - migrations.AlterField( - model_name='intendedvehicle', - name='model', - field=models.CharField(max_length=30), - ), - migrations.AlterField( - model_name='intendedvehicle', - name='serie', - field=models.CharField(blank=True, max_length=30, null=True), - ), - migrations.AlterField( - model_name='intendedvehicle', - name='trim', - field=models.CharField(blank=True, max_length=30, null=True), - ), - ] diff --git a/inventory/migrations/0020_remove_intendedvehicle_purchase_order_and_more.py b/inventory/migrations/0020_remove_intendedvehicle_purchase_order_and_more.py deleted file mode 100644 index bb76f0d8..00000000 --- a/inventory/migrations/0020_remove_intendedvehicle_purchase_order_and_more.py +++ /dev/null @@ -1,68 +0,0 @@ -# Generated by Django 5.2.1 on 2025-06-04 13:33 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('inventory', '0019_alter_intendedvehicle_make_and_more'), - ] - - operations = [ - migrations.RemoveField( - model_name='intendedvehicle', - name='purchase_order', - ), - migrations.RemoveField( - model_name='purchaseorder', - name='intended_vehicle', - ), - migrations.RemoveField( - model_name='purchaseorderitem', - name='vehicle', - ), - migrations.RemoveField( - model_name='purchaseorder', - name='approved_by', - ), - migrations.RemoveField( - model_name='purchaseorder', - name='created_by', - ), - migrations.RemoveField( - model_name='purchaseorder', - name='supplier', - ), - migrations.RemoveField( - model_name='purchaseorderitem', - name='purchase_order', - ), - migrations.RemoveField( - model_name='sale', - name='car', - ), - migrations.RemoveField( - model_name='sale', - name='customer', - ), - migrations.RemoveField( - model_name='sale', - name='salesperson', - ), - migrations.DeleteModel( - name='DeliveryReceipt', - ), - migrations.DeleteModel( - name='IntendedVehicle', - ), - migrations.DeleteModel( - name='PurchaseOrder', - ), - migrations.DeleteModel( - name='PurchaseOrderItem', - ), - migrations.DeleteModel( - name='Sale', - ), - ] diff --git a/inventory/migrations/__init__.py b/inventory/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/inventory/mixins.py b/inventory/mixins.py index 9a6f9c77..ae55f329 100644 --- a/inventory/mixins.py +++ b/inventory/mixins.py @@ -1,5 +1,6 @@ from django.utils.translation import get_language + class AddClassMixin: """ Provides functionality for automatically adding CSS classes to form field widgets. @@ -10,6 +11,7 @@ class AddClassMixin: apply different CSS classes. """ + def add_class_to_fields(self): """ Adds the class to the fields of the model. @@ -20,8 +22,10 @@ class AddClassMixin: # existing_classes = field.widget.attrs.get('class', '') # field.widget.attrs['class'] = f"{existing_classes} form-select form-select-sm".strip() # else: - existing_classes = field.widget.attrs.get('class', '') - field.widget.attrs['class'] = f"{existing_classes} form-control form-control-sm".strip() + existing_classes = field.widget.attrs.get("class", "") + field.widget.attrs["class"] = ( + f"{existing_classes} form-control form-control-sm".strip() + ) class LocalizedNameMixin: @@ -38,13 +42,14 @@ class LocalizedNameMixin: :ivar name: Default name used for non-Arabic languages. :type name: Optional[str] """ + def get_local_name(self): """ Returns the localized name based on the current language. """ - if get_language() == 'ar': - return getattr(self, 'arabic_name', None) - return getattr(self, 'name', None) + if get_language() == "ar": + return getattr(self, "arabic_name", None) + return getattr(self, "name", None) # class AddDealerInstanceMixin: diff --git a/inventory/models.py b/inventory/models.py index 8fab2192..5dcb548e 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -16,6 +16,7 @@ from django_ledger.models import ( EntityModel, ItemModel, CustomerModel, + JournalEntryModel, ) from django_ledger.io.io_core import get_localdate from django.core.exceptions import ValidationError @@ -29,7 +30,7 @@ from django_ledger.models import ( EstimateModel, InvoiceModel, AccountModel, - EntityManagementModel + EntityManagementModel, ) from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType @@ -630,6 +631,7 @@ class Car(Base): [ self.colors, self.finances, + self.finances.selling_price > 0, ] ) except Exception: @@ -712,10 +714,14 @@ class Car(Base): .filter(name=f"Cogs:{self.id_car_make.name}") .first() ) - def add_colors(self,exterior,interior): - self.colors = CarColors.objects.create(car=self,exterior=exterior,interior=interior) + + def add_colors(self, exterior, interior): + self.colors = CarColors.objects.create( + car=self, exterior=exterior, interior=interior + ) self.save() + class CarTransfer(models.Model): car = models.ForeignKey( "Car", @@ -1368,7 +1374,7 @@ class Customer(models.Model): def full_name(self): return f"{self.first_name} {self.last_name}" - def create_customer_model(self,for_lead=False): + def create_customer_model(self, for_lead=False): customer_dict = to_dict(self) customer = self.dealer.entity.create_customer( commit=False, @@ -1411,7 +1417,7 @@ class Customer(models.Model): customer.save() return customer - def create_user_model(self,for_lead=False): + def create_user_model(self, for_lead=False): user = User.objects.create_user( username=self.email, email=self.email, @@ -1501,7 +1507,7 @@ class Organization(models.Model, LocalizedNameMixin): def __str__(self): return self.name - def create_customer_model(self,for_lead=False): + def create_customer_model(self, for_lead=False): customer_dict = to_dict(self) customer = self.dealer.entity.create_customer( commit=False, @@ -1543,7 +1549,7 @@ class Organization(models.Model, LocalizedNameMixin): customer.save() return customer - def create_user_model(self,for_lead=False): + def create_user_model(self, for_lead=False): user = User.objects.create_user( username=self.email, email=self.email, @@ -1777,6 +1783,7 @@ class Lead(models.Model): def get_opportunities(self): return Opportunity.objects.filter(lead=self) + @property def get_current_action(self): return ( @@ -1912,7 +1919,10 @@ class Opportunity(models.Model): max_digits=10, decimal_places=2, verbose_name=_("Salary"), blank=True, null=True ) priority = models.CharField( - max_length=20, choices=[("high", "High"), ("medium", "Medium"), ("low", "Low")], verbose_name=_("Priority"),default="medium" + max_length=20, + choices=[("high", "High"), ("medium", "Medium"), ("low", "Low")], + verbose_name=_("Priority"), + default="medium", ) stage = models.CharField( max_length=20, choices=Stage.choices, verbose_name=_("Stage") @@ -1933,10 +1943,16 @@ class Opportunity(models.Model): ) probability = models.PositiveIntegerField(validators=[validate_probability]) amount = models.DecimalField( - max_digits=10, decimal_places=2, verbose_name=_("Amount"), + max_digits=10, + decimal_places=2, + verbose_name=_("Amount"), ) expected_revenue = models.DecimalField( - max_digits=10, decimal_places=2, verbose_name=_("Expected Revenue"), blank=True, null=True + max_digits=10, + decimal_places=2, + verbose_name=_("Expected Revenue"), + blank=True, + null=True, ) vehicle_of_interest_make = models.CharField(max_length=50, blank=True, null=True) vehicle_of_interest_model = models.CharField(max_length=100, blank=True, null=True) @@ -1961,6 +1977,7 @@ class Opportunity(models.Model): def get_notes(self): return self._get_filter(Notes).order_by("-created") + def get_activities(self): return self._get_filter(Activity) @@ -1969,17 +1986,21 @@ class Opportunity(models.Model): def get_meetings(self): return self.lead.get_meetings() + def get_calls(self): return self.lead.get_calls() + def get_schedules(self): - return self.lead.get_all_schedules().filter( - scheduled_at__gt=timezone.now() - ).order_by("scheduled_at") + return ( + self.lead.get_all_schedules() + .filter(scheduled_at__gt=timezone.now()) + .order_by("scheduled_at") + ) def get_emails(self): return self._get_filter(Email) - def _get_filter(self,Model): + def _get_filter(self, Model): objects = Model.objects.filter( content_type__model="opportunity", object_id=self.id ) @@ -2001,9 +2022,7 @@ class Opportunity(models.Model): opportinity_for = self.organization.name if not self.slug: - self.slug = slugify( - f"opportunity {opportinity_for}" - ) + self.slug = slugify(f"opportunity {opportinity_for}") super().save(*args, **kwargs) class Meta: @@ -2012,7 +2031,9 @@ class Opportunity(models.Model): def __str__(self): if self.customer: - return f"Opportunity for {self.customer.first_name} {self.customer.last_name}" + return ( + f"Opportunity for {self.customer.first_name} {self.customer.last_name}" + ) return f"Opportunity for {self.organization.name}" @@ -2340,12 +2361,16 @@ class SaleOrder(models.Model): ("DELIVERED", "Delivered"), ("CANCELLED", "Cancelled"), ] - + dealer = models.ForeignKey( + Dealer, on_delete=models.CASCADE, related_name="sale_orders" + ) estimate = models.ForeignKey( EstimateModel, on_delete=models.CASCADE, related_name="sale_orders", verbose_name=_("Estimate"), + null=True, + blank=True, ) invoice = models.ForeignKey( InvoiceModel, @@ -2355,93 +2380,25 @@ class SaleOrder(models.Model): null=True, blank=True, ) - payment_method = models.CharField( - max_length=20, - choices=[ - ("cash", _("Cash")), - ("finance", _("Finance")), - ("lease", _("Lease")), - ("credit_card", _("Credit Card")), - ("bank_transfer", _("Bank Transfer")), - ("sadad", _("SADAD")), - ], + customer = models.ForeignKey( + Customer, + on_delete=models.CASCADE, + related_name="sale_orders", + verbose_name=_("Customer"), + null=True, + blank=True, + ) + opportunity = models.ForeignKey( + Opportunity, + on_delete=models.CASCADE, + related_name="sale_orders", + verbose_name=_("Opportunity"), + null=True, + blank=True, ) comments = models.TextField(blank=True, null=True) formatted_order_id = models.CharField(max_length=10, unique=True, editable=False) - # Link to the specific opportunity this sales order is fulfilling - opportunity = models.OneToOneField( - "Opportunity", # Use string reference if Opportunity is defined later or in another app - on_delete=models.CASCADE, - related_name="sales_order", - help_text="The associated sales opportunity for this order.", - ) - - # Link to the customer who is purchasing the vehicle - customer = models.ForeignKey( - "Customer", # Use string reference - on_delete=models.PROTECT, # Protect customer data if order exists - related_name="sales_orders", - help_text="The customer making the purchase.", - ) - - # Link to the specific vehicle being sold - # This assumes a Vehicle model exists which represents an actual car in inventory - car = models.ForeignKey( - "Car", # Use string reference to your Vehicle/Inventory model - on_delete=models.PROTECT, # Don't delete vehicle if it's part of an order - related_name="sales_orders", - help_text="The specific vehicle (VIN) being sold.", - null=True, - blank=True, - ) - - # Financial Details - agreed_price = models.DecimalField( - max_digits=12, - decimal_places=2, - help_text="The final agreed-upon selling price of the vehicle.", - ) - down_payment_amount = models.DecimalField( - max_digits=12, - decimal_places=2, - default=0.00, - help_text="The initial payment made by the customer.", - ) - trade_in_value = models.DecimalField( - max_digits=12, - decimal_places=2, - default=0.00, - help_text="The value of any vehicle traded in by the customer.", - ) - # Reference to the trade-in vehicle if it also exists in inventory - trade_in_vehicle = models.ForeignKey( - "Car", # Assuming Vehicle model can also represent trade-ins - on_delete=models.SET_NULL, - blank=True, - null=True, - related_name="traded_in_on_orders", - help_text="The vehicle traded in by the customer, if any.", - ) - loan_amount = models.DecimalField( - max_digits=12, - decimal_places=2, - default=0.00, - help_text="The amount financed by a bank or third-party lender.", - ) - total_paid_amount = models.DecimalField( - max_digits=12, - decimal_places=2, - default=0.00, - help_text="Sum of down payment, trade-in value, and loan amount received so far.", - ) - remaining_balance = models.DecimalField( - max_digits=12, - decimal_places=2, - default=0.00, - help_text="The remaining amount due from the customer or financing.", - ) - # Status and Dates status = models.CharField( max_length=20, @@ -2469,13 +2426,13 @@ class SaleOrder(models.Model): blank=True, null=True, help_text="Reason for cancellation, if applicable." ) - # Audit fields created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, + blank=True, related_name="created_sales_orders", help_text="The user who created this sales order.", ) @@ -2483,6 +2440,7 @@ class SaleOrder(models.Model): settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, + blank=True, related_name="modified_sales_orders", help_text="The user who last modified this sales order.", ) @@ -2501,14 +2459,7 @@ class SaleOrder(models.Model): next_id = 1 year = get_localdate().year self.formatted_order_id = f"O-{year}-{next_id:09d}" - self.total_paid_amount = ( - Decimal(self.down_payment_amount) - + Decimal(self.trade_in_value) - + Decimal(self.loan_amount) - ) - self.remaining_balance = Decimal(self.agreed_price) - Decimal( - self.total_paid_amount - ) + super().save(*args, **kwargs) def __str__(self): @@ -2524,13 +2475,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 - ###################################################################################################### ###################################################################################################### ###################################################################################################### diff --git a/inventory/services.py b/inventory/services.py index e22883d2..ccc26e2b 100644 --- a/inventory/services.py +++ b/inventory/services.py @@ -27,11 +27,12 @@ def get_make(item): if not data: r = item.split(" ") for i in r: - if data:= CarMake.objects.filter(name__iexact=i).first(): + if data := CarMake.objects.filter(name__iexact=i).first(): break return data -def get_model(item,make): + +def get_model(item, make): """ Searches for a car model associated with a specific make by performing an exact case-insensitive match. If no match is found, it attempts to match @@ -47,10 +48,11 @@ def get_model(item,make): if not data: r = item.split(" ") for i in r: - if data:=make.carmodel_set.filter(name__iexact=i).first(): + if data := make.carmodel_set.filter(name__iexact=i).first(): break return data + def normalize_name(name): """ Normalizes a given name by removing spaces and hyphens and converting it to lowercase. @@ -68,7 +70,6 @@ def normalize_name(name): def decodevin(vin): - """ Decodes a Vehicle Identification Number (VIN) using multiple decoding functions and returns the decoded result. This function attempts to decode the VIN using @@ -81,9 +82,9 @@ def decodevin(vin): all decoding attempts fail. :rtype: dict | None """ - if result:=decode_vin(vin): + if result := decode_vin(vin): return result - elif result:=elm(vin): + elif result := elm(vin): return result # elif result:=decode_vin_haikalna(vin): # return result @@ -158,6 +159,3 @@ def elm(vin): } print([x for x in data.values()]) return data if all([x for x in data.values()]) else None - - - diff --git a/inventory/signals.py b/inventory/signals.py index 20306bf9..f91edee5 100644 --- a/inventory/signals.py +++ b/inventory/signals.py @@ -12,7 +12,7 @@ from django_ledger.models import ( JournalEntryModel, TransactionModel, LedgerModel, - AccountModel + AccountModel, ) from . import models from django.utils.timezone import now @@ -59,7 +59,10 @@ User = get_user_model() @receiver(post_save, sender=models.Car) def create_dealers_make(sender, instance, created, **kwargs): if created: - models.DealersMake.objects.get_or_create(dealer=instance.dealer, car_make=instance.id_car_make) + models.DealersMake.objects.get_or_create( + dealer=instance.dealer, car_make=instance.id_car_make + ) + @receiver(post_save, sender=models.Car) def create_car_location(sender, instance, created, **kwargs): @@ -94,6 +97,7 @@ def create_car_location(sender, instance, created, **kwargs): except Exception as e: print(f"Failed to create CarLocation for car {instance.vin}: {e}") + # Create Entity @receiver(post_save, sender=models.Dealer) def create_ledger_entity(sender, instance, created, **kwargs): @@ -143,6 +147,7 @@ def create_ledger_entity(sender, instance, created, **kwargs): # create_settings(instance.pk) # create_accounts_for_make(instance.pk) + @receiver(post_save, sender=models.Dealer) def create_dealer_groups(sender, instance, created, **kwargs): """ @@ -162,11 +167,18 @@ def create_dealer_groups(sender, instance, created, **kwargs): def create_groups(): for group_name in group_names: - group, created = Group.objects.get_or_create(name=f"{instance.pk}_{group_name}") - group_manager,created = models.CustomGroup.objects.get_or_create(name=group_name, dealer=instance, group=group) + group, created = Group.objects.get_or_create( + name=f"{instance.pk}_{group_name}" + ) + group_manager, created = models.CustomGroup.objects.get_or_create( + name=group_name, dealer=instance, group=group + ) group_manager.set_default_permissions() instance.user.groups.add(group) + transaction.on_commit(create_groups) + + # Create Vendor @receiver(post_save, sender=models.Vendor) def create_ledger_vendor(sender, instance, created, **kwargs): @@ -189,6 +201,7 @@ def create_ledger_vendor(sender, instance, created, **kwargs): else: instance.update_vendor_model() + # Create Item @receiver(post_save, sender=models.Car) def create_item_model(sender, instance, created, **kwargs): @@ -228,12 +241,13 @@ def create_item_model(sender, instance, created, **kwargs): # ) instance.item_model = inventory inventory.additional_info = {} - inventory.additional_info.update({'car_info': instance.to_dict()}) + inventory.additional_info.update({"car_info": instance.to_dict()}) inventory.save() else: - instance.item_model.additional_info.update({'car_info': instance.to_dict()}) + instance.item_model.additional_info.update({"car_info": instance.to_dict()}) instance.item_model.save() + # # update price - CarFinance @receiver(post_save, sender=models.CarFinance) def update_item_model_cost(sender, instance, created, **kwargs): @@ -252,20 +266,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"): diff --git a/inventory/tables.py b/inventory/tables.py index 84881c70..570c6d64 100644 --- a/inventory/tables.py +++ b/inventory/tables.py @@ -47,17 +47,29 @@ class CarTable(tables.Table): with badge-style formatting based on the status value. :type status: tables.Column """ + stock_type = tables.Column(verbose_name=_("Stock Type")) - vin = tables.LinkColumn("car_detail", args=[tables.A("pk")], verbose_name=_("VIN"), attrs={"td": {"class": "fw-bold"}}) + vin = tables.LinkColumn( + "car_detail", + args=[tables.A("pk")], + verbose_name=_("VIN"), + attrs={"td": {"class": "fw-bold"}}, + ) id_car_make = tables.Column(verbose_name=_("Make")) id_car_model = tables.Column(verbose_name=_("Model")) year = tables.Column(verbose_name=_("Year")) id_car_serie = tables.Column(verbose_name=_("Series")) id_car_trim = tables.Column(verbose_name=_("Trim")) mileage = tables.Column(verbose_name=_("Mileage")) - selling_price = tables.Column(accessor="finances.selling_price", verbose_name=_("Price")) - exterior_color = tables.Column(accessor="colors.exterior.name", verbose_name=_("Exterior Color")) - interior_color = tables.Column(accessor="colors.interior.name", verbose_name=_("Interior Color")) + selling_price = tables.Column( + accessor="finances.selling_price", verbose_name=_("Price") + ) + exterior_color = tables.Column( + accessor="colors.exterior.name", verbose_name=_("Exterior Color") + ) + interior_color = tables.Column( + accessor="colors.interior.name", verbose_name=_("Interior Color") + ) receiving_date = tables.Column(verbose_name=_("Age")) status = tables.Column(verbose_name=_("Status")) @@ -111,7 +123,9 @@ class CarTable(tables.Table): "transfer": "badge-phoenix-warning", } badge_class = status_badges.get(value.lower(), "badge-secondary") - return format_html('{}', badge_class, value) + return format_html( + '{}', badge_class, value + ) def render_stock_type(self, value): type_badges = { @@ -119,4 +133,6 @@ class CarTable(tables.Table): "used": "badge-phoenix-warning", } badge_class = type_badges.get(value.lower(), "badge-secondary") - return format_html('{}', badge_class, value) \ No newline at end of file + return format_html( + '{}', badge_class, value + ) diff --git a/inventory/tasks.py b/inventory/tasks.py index 821f3e44..c04577ec 100644 --- a/inventory/tasks.py +++ b/inventory/tasks.py @@ -4,8 +4,7 @@ from django_ledger.io import roles from django.core.mail import send_mail from background_task import background from django.utils.translation import gettext_lazy as _ -from inventory.models import DealerSettings,Dealer - +from inventory.models import DealerSettings, Dealer # @background @@ -13,13 +12,25 @@ def create_settings(pk): instance = Dealer.objects.get(pk=pk) DealerSettings.objects.create( - dealer=instance, - invoice_cash_account=instance.entity.get_all_accounts().filter(role=roles.ASSET_CA_CASH).first(), - invoice_prepaid_account=instance.entity.get_all_accounts().filter(role=roles.ASSET_CA_RECEIVABLES).first(), - invoice_unearned_account=instance.entity.get_all_accounts().filter(role=roles.LIABILITY_CL_DEFERRED_REVENUE).first(), - bill_cash_account=instance.entity.get_all_accounts().filter(role=roles.ASSET_CA_CASH).first(), - bill_prepaid_account=instance.entity.get_all_accounts().filter(role=roles.ASSET_CA_PREPAID).first(), - bill_unearned_account=instance.entity.get_all_accounts().filter(role=roles.LIABILITY_CL_ACC_PAYABLE).first() + dealer=instance, + invoice_cash_account=instance.entity.get_all_accounts() + .filter(role=roles.ASSET_CA_CASH) + .first(), + invoice_prepaid_account=instance.entity.get_all_accounts() + .filter(role=roles.ASSET_CA_RECEIVABLES) + .first(), + invoice_unearned_account=instance.entity.get_all_accounts() + .filter(role=roles.LIABILITY_CL_DEFERRED_REVENUE) + .first(), + bill_cash_account=instance.entity.get_all_accounts() + .filter(role=roles.ASSET_CA_CASH) + .first(), + bill_prepaid_account=instance.entity.get_all_accounts() + .filter(role=roles.ASSET_CA_PREPAID) + .first(), + bill_unearned_account=instance.entity.get_all_accounts() + .filter(role=roles.LIABILITY_CL_ACC_PAYABLE) + .first(), ) @@ -371,402 +382,397 @@ def create_coa_accounts(pk): accounts_data = [ # Current Assets (must start with 1) { - 'code': '1010', - 'name': 'Cash on Hand', - 'role': roles.ASSET_CA_CASH, - 'balance_type': roles.DEBIT, - 'locked': True, - 'default': True # Default for ASSET_CA_CASH + "code": "1010", + "name": "Cash on Hand", + "role": roles.ASSET_CA_CASH, + "balance_type": roles.DEBIT, + "locked": True, + "default": True, # Default for ASSET_CA_CASH }, { - 'code': '1020', - 'name': 'Bank', - 'role': roles.ASSET_CA_CASH, - 'balance_type': roles.DEBIT, - 'locked': True, - 'default': False + "code": "1020", + "name": "Bank", + "role": roles.ASSET_CA_CASH, + "balance_type": roles.DEBIT, + "locked": True, + "default": False, }, { - 'code': '1030', - 'name': 'Accounts Receivable', - 'role': roles.ASSET_CA_RECEIVABLES, - 'balance_type': roles.DEBIT, - 'locked': True, - 'default': True # Default for ASSET_CA_RECEIVABLES + "code": "1030", + "name": "Accounts Receivable", + "role": roles.ASSET_CA_RECEIVABLES, + "balance_type": roles.DEBIT, + "locked": True, + "default": True, # Default for ASSET_CA_RECEIVABLES }, { - 'code': '1040', - 'name': 'Inventory (Cars)', - 'role': roles.ASSET_CA_INVENTORY, - 'balance_type': roles.DEBIT, - 'locked': True, - 'default': True # Default for ASSET_CA_INVENTORY + "code": "1040", + "name": "Inventory (Cars)", + "role": roles.ASSET_CA_INVENTORY, + "balance_type": roles.DEBIT, + "locked": True, + "default": True, # Default for ASSET_CA_INVENTORY }, { - 'code': '1045', - 'name': 'Spare Parts Inventory', - 'role': roles.ASSET_CA_INVENTORY, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': False + "code": "1045", + "name": "Spare Parts Inventory", + "role": roles.ASSET_CA_INVENTORY, + "balance_type": roles.DEBIT, + "locked": False, + "default": False, }, { - 'code': '1050', - 'name': 'Employee Advances', - 'role': roles.ASSET_CA_RECEIVABLES, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': False + "code": "1050", + "name": "Employee Advances", + "role": roles.ASSET_CA_RECEIVABLES, + "balance_type": roles.DEBIT, + "locked": False, + "default": False, }, { - 'code': '1060', - 'name': 'Prepaid Expenses', - 'role': roles.ASSET_CA_PREPAID, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': True # Default for ASSET_CA_PREPAID + "code": "1060", + "name": "Prepaid Expenses", + "role": roles.ASSET_CA_PREPAID, + "balance_type": roles.DEBIT, + "locked": False, + "default": True, # Default for ASSET_CA_PREPAID }, { - 'code': '1070', - 'name': 'Notes Receivable', - 'role': roles.ASSET_LTI_NOTES_RECEIVABLE, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': True # Default for ASSET_LTI_NOTES_RECEIVABLE + "code": "1070", + "name": "Notes Receivable", + "role": roles.ASSET_LTI_NOTES_RECEIVABLE, + "balance_type": roles.DEBIT, + "locked": False, + "default": True, # Default for ASSET_LTI_NOTES_RECEIVABLE }, - # Fixed Assets (must also start with 1) { - 'code': '1110', - 'name': 'Lands', - 'role': roles.ASSET_LTI_LAND, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': True # Default for ASSET_LTI_LAND + "code": "1110", + "name": "Lands", + "role": roles.ASSET_LTI_LAND, + "balance_type": roles.DEBIT, + "locked": False, + "default": True, # Default for ASSET_LTI_LAND }, { - 'code': '1111', - 'name': 'Buildings', - 'role': roles.ASSET_PPE_BUILDINGS, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': True # Default for ASSET_PPE_BUILDINGS + "code": "1111", + "name": "Buildings", + "role": roles.ASSET_PPE_BUILDINGS, + "balance_type": roles.DEBIT, + "locked": False, + "default": True, # Default for ASSET_PPE_BUILDINGS }, { - 'code': '1112', - 'name': 'Company Vehicles', - 'role': roles.ASSET_PPE_EQUIPMENT, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': True # Default for ASSET_PPE_EQUIPMENT + "code": "1112", + "name": "Company Vehicles", + "role": roles.ASSET_PPE_EQUIPMENT, + "balance_type": roles.DEBIT, + "locked": False, + "default": True, # Default for ASSET_PPE_EQUIPMENT }, { - 'code': '1113', - 'name': 'Equipment & Tools', - 'role': roles.ASSET_PPE_EQUIPMENT, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': False + "code": "1113", + "name": "Equipment & Tools", + "role": roles.ASSET_PPE_EQUIPMENT, + "balance_type": roles.DEBIT, + "locked": False, + "default": False, }, { - 'code': '1114', - 'name': 'Furniture & Fixtures', - 'role': roles.ASSET_PPE_EQUIPMENT, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': False + "code": "1114", + "name": "Furniture & Fixtures", + "role": roles.ASSET_PPE_EQUIPMENT, + "balance_type": roles.DEBIT, + "locked": False, + "default": False, }, { - 'code': '1115', - 'name': 'Other Fixed Assets', - 'role': roles.ASSET_PPE_EQUIPMENT, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': False + "code": "1115", + "name": "Other Fixed Assets", + "role": roles.ASSET_PPE_EQUIPMENT, + "balance_type": roles.DEBIT, + "locked": False, + "default": False, }, { - 'code': '1120', - 'name': 'Long-term Investments', - 'role': roles.ASSET_LTI_SECURITIES, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': True # Default for ASSET_LTI_SECURITIES + "code": "1120", + "name": "Long-term Investments", + "role": roles.ASSET_LTI_SECURITIES, + "balance_type": roles.DEBIT, + "locked": False, + "default": True, # Default for ASSET_LTI_SECURITIES }, { - 'code': '1130', - 'name': 'Intangible Assets', - 'role': roles.ASSET_INTANGIBLE_ASSETS, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': True # Default for ASSET_INTANGIBLE_ASSETS + "code": "1130", + "name": "Intangible Assets", + "role": roles.ASSET_INTANGIBLE_ASSETS, + "balance_type": roles.DEBIT, + "locked": False, + "default": True, # Default for ASSET_INTANGIBLE_ASSETS }, - # Current Liabilities (must start with 2) { - 'code': '2010', - 'name': 'Accounts Payable', - 'role': roles.LIABILITY_CL_ACC_PAYABLE, - 'balance_type': roles.CREDIT, - 'locked': True, - 'default': True # Default for LIABILITY_CL_ACC_PAYABLE + "code": "2010", + "name": "Accounts Payable", + "role": roles.LIABILITY_CL_ACC_PAYABLE, + "balance_type": roles.CREDIT, + "locked": True, + "default": True, # Default for LIABILITY_CL_ACC_PAYABLE }, { - 'code': '2020', - 'name': 'Notes Payable', - 'role': roles.LIABILITY_CL_ST_NOTES_PAYABLE, - 'balance_type': roles.CREDIT, - 'locked': False, - 'default': True # Default for LIABILITY_CL_ST_NOTES_PAYABLE + "code": "2020", + "name": "Notes Payable", + "role": roles.LIABILITY_CL_ST_NOTES_PAYABLE, + "balance_type": roles.CREDIT, + "locked": False, + "default": True, # Default for LIABILITY_CL_ST_NOTES_PAYABLE }, { - 'code': '2030', - 'name': 'Short-term Loans', - 'role': roles.LIABILITY_CL_ST_NOTES_PAYABLE, - 'balance_type': roles.CREDIT, - 'locked': False, - 'default': False + "code": "2030", + "name": "Short-term Loans", + "role": roles.LIABILITY_CL_ST_NOTES_PAYABLE, + "balance_type": roles.CREDIT, + "locked": False, + "default": False, }, { - 'code': '2040', - 'name': 'Employee Payables', - 'role': roles.LIABILITY_CL_WAGES_PAYABLE, - 'balance_type': roles.CREDIT, - 'locked': False, - 'default': True # Default for LIABILITY_CL_WAGES_PAYABLE + "code": "2040", + "name": "Employee Payables", + "role": roles.LIABILITY_CL_WAGES_PAYABLE, + "balance_type": roles.CREDIT, + "locked": False, + "default": True, # Default for LIABILITY_CL_WAGES_PAYABLE }, { - 'code': '2050', - 'name': 'Accrued Expenses', - 'role': roles.LIABILITY_CL_OTHER, - 'balance_type': roles.CREDIT, - 'locked': False, - 'default': True # Default for LIABILITY_CL_OTHER + "code": "2050", + "name": "Accrued Expenses", + "role": roles.LIABILITY_CL_OTHER, + "balance_type": roles.CREDIT, + "locked": False, + "default": True, # Default for LIABILITY_CL_OTHER }, { - 'code': '2060', - 'name': 'Accrued Taxes', - 'role': roles.LIABILITY_CL_TAXES_PAYABLE, - 'balance_type': roles.CREDIT, - 'locked': False, - 'default': True # Default for LIABILITY_CL_TAXES_PAYABLE + "code": "2060", + "name": "Accrued Taxes", + "role": roles.LIABILITY_CL_TAXES_PAYABLE, + "balance_type": roles.CREDIT, + "locked": False, + "default": True, # Default for LIABILITY_CL_TAXES_PAYABLE }, { - 'code': '2070', - 'name': 'Provisions', - 'role': roles.LIABILITY_CL_OTHER, - 'balance_type': roles.CREDIT, - 'locked': False, - 'default': False + "code": "2070", + "name": "Provisions", + "role": roles.LIABILITY_CL_OTHER, + "balance_type": roles.CREDIT, + "locked": False, + "default": False, }, - # Long-term Liabilities (must also start with 2) { - 'code': '2210', - 'name': 'Long-term Bank Loans', - 'role': roles.LIABILITY_LTL_NOTES_PAYABLE, - 'balance_type': roles.CREDIT, - 'locked': False, - 'default': True # Default for LIABILITY_LTL_NOTES_PAYABLE + "code": "2210", + "name": "Long-term Bank Loans", + "role": roles.LIABILITY_LTL_NOTES_PAYABLE, + "balance_type": roles.CREDIT, + "locked": False, + "default": True, # Default for LIABILITY_LTL_NOTES_PAYABLE }, { - 'code': '2220', - 'name': 'Lease Liabilities', - 'role': roles.LIABILITY_LTL_NOTES_PAYABLE, - 'balance_type': roles.CREDIT, - 'locked': False, - 'default': False + "code": "2220", + "name": "Lease Liabilities", + "role": roles.LIABILITY_LTL_NOTES_PAYABLE, + "balance_type": roles.CREDIT, + "locked": False, + "default": False, }, { - 'code': '2230', - 'name': 'Other Long-term Liabilities', - 'role': roles.LIABILITY_LTL_NOTES_PAYABLE, - 'balance_type': roles.CREDIT, - 'locked': False, - 'default': False + "code": "2230", + "name": "Other Long-term Liabilities", + "role": roles.LIABILITY_LTL_NOTES_PAYABLE, + "balance_type": roles.CREDIT, + "locked": False, + "default": False, }, - # Equity (must start with 3) { - 'code': '3010', - 'name': 'Capital', - 'role': roles.EQUITY_CAPITAL, - 'balance_type': roles.CREDIT, - 'locked': True, - 'default': True # Default for EQUITY_CAPITAL + "code": "3010", + "name": "Capital", + "role": roles.EQUITY_CAPITAL, + "balance_type": roles.CREDIT, + "locked": True, + "default": True, # Default for EQUITY_CAPITAL }, { - 'code': '3020', - 'name': 'Statutory Reserve', - 'role': roles.EQUITY_ADJUSTMENT, - 'balance_type': roles.CREDIT, - 'locked': False, - 'default': True # Default for EQUITY_ADJUSTMENT + "code": "3020", + "name": "Statutory Reserve", + "role": roles.EQUITY_ADJUSTMENT, + "balance_type": roles.CREDIT, + "locked": False, + "default": True, # Default for EQUITY_ADJUSTMENT }, { - 'code': '3030', - 'name': 'Retained Earnings', - 'role': roles.EQUITY_ADJUSTMENT, - 'balance_type': roles.CREDIT, - 'locked': False, - 'default': False + "code": "3030", + "name": "Retained Earnings", + "role": roles.EQUITY_ADJUSTMENT, + "balance_type": roles.CREDIT, + "locked": False, + "default": False, }, { - 'code': '3040', - 'name': 'Profit & Loss for the Period', - 'role': roles.EQUITY_ADJUSTMENT, - 'balance_type': roles.CREDIT, - 'locked': False, - 'default': False + "code": "3040", + "name": "Profit & Loss for the Period", + "role": roles.EQUITY_ADJUSTMENT, + "balance_type": roles.CREDIT, + "locked": False, + "default": False, }, - # Revenue (must start with 4) { - 'code': '4010', - 'name': 'Car Sales', - 'role': roles.INCOME_OPERATIONAL, - 'balance_type': roles.CREDIT, - 'locked': True, - 'default': True # Default for INCOME_OPERATIONAL + "code": "4010", + "name": "Car Sales", + "role": roles.INCOME_OPERATIONAL, + "balance_type": roles.CREDIT, + "locked": True, + "default": True, # Default for INCOME_OPERATIONAL }, { - 'code': '4020', - 'name': 'After-Sales Services', - 'role': roles.INCOME_OPERATIONAL, - 'balance_type': roles.CREDIT, - 'locked': False, - 'default': False + "code": "4020", + "name": "After-Sales Services", + "role": roles.INCOME_OPERATIONAL, + "balance_type": roles.CREDIT, + "locked": False, + "default": False, }, { - 'code': '4030', - 'name': 'Car Rental Income', - 'role': roles.INCOME_PASSIVE, - 'balance_type': roles.CREDIT, - 'locked': False, - 'default': True # Default for INCOME_PASSIVE + "code": "4030", + "name": "Car Rental Income", + "role": roles.INCOME_PASSIVE, + "balance_type": roles.CREDIT, + "locked": False, + "default": True, # Default for INCOME_PASSIVE }, { - 'code': '4040', - 'name': 'Other Income', - 'role': roles.INCOME_OTHER, - 'balance_type': roles.CREDIT, - 'locked': False, - 'default': True # Default for INCOME_OTHER + "code": "4040", + "name": "Other Income", + "role": roles.INCOME_OTHER, + "balance_type": roles.CREDIT, + "locked": False, + "default": True, # Default for INCOME_OTHER }, - # Expenses (must start with 5 for COGS, 6 for others) { - 'code': '5010', - 'name': 'Cost of Goods Sold', - 'role': roles.COGS, - 'balance_type': roles.DEBIT, - 'locked': True, - 'default': True # Default for COGS + "code": "5010", + "name": "Cost of Goods Sold", + "role": roles.COGS, + "balance_type": roles.DEBIT, + "locked": True, + "default": True, # Default for COGS }, { - 'code': '5015', - 'name': 'Spare Parts Cost Consumed', - 'role': roles.COGS, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': False + "code": "5015", + "name": "Spare Parts Cost Consumed", + "role": roles.COGS, + "balance_type": roles.DEBIT, + "locked": False, + "default": False, }, { - 'code': '6010', - 'name': 'Salaries & Wages', - 'role': roles.EXPENSE_OPERATIONAL, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': True # Default for EXPENSE_OPERATIONAL + "code": "6010", + "name": "Salaries & Wages", + "role": roles.EXPENSE_OPERATIONAL, + "balance_type": roles.DEBIT, + "locked": False, + "default": True, # Default for EXPENSE_OPERATIONAL }, { - 'code': '6020', - 'name': 'Rent', - 'role': roles.EXPENSE_OPERATIONAL, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': False + "code": "6020", + "name": "Rent", + "role": roles.EXPENSE_OPERATIONAL, + "balance_type": roles.DEBIT, + "locked": False, + "default": False, }, { - 'code': '6030', - 'name': 'Utilities', - 'role': roles.EXPENSE_OPERATIONAL, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': False + "code": "6030", + "name": "Utilities", + "role": roles.EXPENSE_OPERATIONAL, + "balance_type": roles.DEBIT, + "locked": False, + "default": False, }, { - 'code': '6040', - 'name': 'Advertising & Marketing', - 'role': roles.EXPENSE_OPERATIONAL, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': False + "code": "6040", + "name": "Advertising & Marketing", + "role": roles.EXPENSE_OPERATIONAL, + "balance_type": roles.DEBIT, + "locked": False, + "default": False, }, { - 'code': '6050', - 'name': 'Maintenance', - 'role': roles.EXPENSE_OPERATIONAL, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': False + "code": "6050", + "name": "Maintenance", + "role": roles.EXPENSE_OPERATIONAL, + "balance_type": roles.DEBIT, + "locked": False, + "default": False, }, { - 'code': '6060', - 'name': 'Operating Expenses', - 'role': roles.EXPENSE_OPERATIONAL, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': False + "code": "6060", + "name": "Operating Expenses", + "role": roles.EXPENSE_OPERATIONAL, + "balance_type": roles.DEBIT, + "locked": False, + "default": False, }, { - 'code': '6070', - 'name': 'Depreciation', - 'role': roles.EXPENSE_DEPRECIATION, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': True # Default for EXPENSE_DEPRECIATION + "code": "6070", + "name": "Depreciation", + "role": roles.EXPENSE_DEPRECIATION, + "balance_type": roles.DEBIT, + "locked": False, + "default": True, # Default for EXPENSE_DEPRECIATION }, { - 'code': '6080', - 'name': 'Fees & Taxes', - 'role': roles.EXPENSE_OPERATIONAL, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': False + "code": "6080", + "name": "Fees & Taxes", + "role": roles.EXPENSE_OPERATIONAL, + "balance_type": roles.DEBIT, + "locked": False, + "default": False, }, { - 'code': '6090', - 'name': 'Bank Charges', - 'role': roles.EXPENSE_OPERATIONAL, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': False + "code": "6090", + "name": "Bank Charges", + "role": roles.EXPENSE_OPERATIONAL, + "balance_type": roles.DEBIT, + "locked": False, + "default": False, }, { - 'code': '6100', - 'name': 'Other Expenses', - 'role': roles.EXPENSE_OTHER, - 'balance_type': roles.DEBIT, - 'locked': False, - 'default': True # Default for EXPENSE_OTHER - } + "code": "6100", + "name": "Other Expenses", + "role": roles.EXPENSE_OTHER, + "balance_type": roles.DEBIT, + "locked": False, + "default": True, # Default for EXPENSE_OTHER + }, ] for account_data in accounts_data: try: account = entity.create_account( - coa_model=coa, - code=account_data['code'], - name=_(account_data['name']), - role=_(account_data['role']), - balance_type=_(account_data['balance_type']), - active=True + coa_model=coa, + code=account_data["code"], + name=_(account_data["name"]), + role=_(account_data["role"]), + balance_type=_(account_data["balance_type"]), + active=True, ) - account.role_default = account_data['default'] + account.role_default = account_data["default"] account.save() except Exception as e: print(e) + @background def create_coa_accounts1(pk): with transaction.atomic(): @@ -810,7 +816,6 @@ def create_coa_accounts1(pk): asset_ca_inventory.role_default = True asset_ca_inventory.save() - # Prepaid Expenses Account asset_ca_prepaid = entity.create_account( coa_model=coa, @@ -833,8 +838,7 @@ def create_coa_accounts1(pk): active=True, ) - - # VAT Payable Account + # VAT Payable Account liability_ltl_vat_receivable = entity.create_account( coa_model=coa, code="1106", @@ -904,7 +908,6 @@ def create_coa_accounts1(pk): # asset_lti_land.role_default = True # asset_lti_land.save() - # Buildings Account asset_ppe_buildings = entity.create_account( coa_model=coa, @@ -917,8 +920,6 @@ def create_coa_accounts1(pk): asset_ppe_buildings.role_default = True asset_ppe_buildings.save() - - # Accounts Payable Account liability_cl_acc_payable = entity.create_account( coa_model=coa, @@ -998,7 +999,14 @@ def create_coa_accounts1(pk): ) # End of Service Benefits - entity.create_account(coa_model=coa, code="2202", role=roles.LIABILITY_LTL_NOTES_PAYABLE, name=_("End of Service Benefits"), balance_type="credit", active=True) + entity.create_account( + coa_model=coa, + code="2202", + role=roles.LIABILITY_LTL_NOTES_PAYABLE, + name=_("End of Service Benefits"), + balance_type="credit", + active=True, + ) # Mortgage Payable Account liability_ltl_mortgage_payable = entity.create_account( @@ -1013,21 +1021,56 @@ def create_coa_accounts1(pk): liability_ltl_mortgage_payable.save() # Capital - equity_capital = entity.create_account(coa_model=coa, code="3101", role=roles.EQUITY_CAPITAL, name=_("Registered Capital"), balance_type="credit", active=True) + equity_capital = entity.create_account( + coa_model=coa, + code="3101", + role=roles.EQUITY_CAPITAL, + name=_("Registered Capital"), + balance_type="credit", + active=True, + ) equity_capital.role_default = True equity_capital.save() - entity.create_account(coa_model=coa, code="3102", role=roles.EQUITY_CAPITAL, name=_("Additional Paid-In Capital"), balance_type="credit", active=True) + entity.create_account( + coa_model=coa, + code="3102", + role=roles.EQUITY_CAPITAL, + name=_("Additional Paid-In Capital"), + balance_type="credit", + active=True, + ) # Other Equity - other_equity = entity.create_account(coa_model=coa, code="3201", role=roles.EQUITY_COMMON_STOCK, name=_("Opening Balances"), balance_type="credit", active=True) + other_equity = entity.create_account( + coa_model=coa, + code="3201", + role=roles.EQUITY_COMMON_STOCK, + name=_("Opening Balances"), + balance_type="credit", + active=True, + ) other_equity.role_default = True other_equity.save() # Reserves - reserve = entity.create_account(coa_model=coa, code="3301", role=roles.EQUITY_ADJUSTMENT, name=_("Statutory Reserve"), balance_type="credit", active=True) + reserve = entity.create_account( + coa_model=coa, + code="3301", + role=roles.EQUITY_ADJUSTMENT, + name=_("Statutory Reserve"), + balance_type="credit", + active=True, + ) reserve.role_default = True reserve.save() - entity.create_account(coa_model=coa, code="3302", role=roles.EQUITY_ADJUSTMENT, name=_("Foreign Currency Translation Reserve"), balance_type="credit", active=True) + entity.create_account( + coa_model=coa, + code="3302", + role=roles.EQUITY_ADJUSTMENT, + name=_("Foreign Currency Translation Reserve"), + balance_type="credit", + active=True, + ) # Retained Earnings Account equity_retained_earnings = entity.create_account( @@ -1085,11 +1128,24 @@ def create_coa_accounts1(pk): ) # Operating Revenues - entity.create_account(coa_model=coa, code="4104", role=roles.INCOME_OPERATIONAL, name=_("Sales/Service Revenue"), balance_type="credit", active=True) - - #Non-Operating Revenues - entity.create_account(coa_model=coa, code="4201", role=roles.INCOME_OTHER, name=_("Non-Operating Revenues"), balance_type="credit", active=True) + entity.create_account( + coa_model=coa, + code="4104", + role=roles.INCOME_OPERATIONAL, + name=_("Sales/Service Revenue"), + balance_type="credit", + active=True, + ) + # Non-Operating Revenues + entity.create_account( + coa_model=coa, + code="4201", + role=roles.INCOME_OTHER, + name=_("Non-Operating Revenues"), + balance_type="credit", + active=True, + ) # Cost of Goods Sold (COGS) Account expense_cogs = entity.create_account( @@ -1103,7 +1159,6 @@ def create_coa_accounts1(pk): expense_cogs.role_default = True expense_cogs.save() - # accrued Expenses Account expense_cogs = entity.create_account( coa_model=coa, @@ -1277,19 +1332,76 @@ def create_coa_accounts1(pk): ) # 5.1 Direct Costs - entity.create_account(coa_model=coa, code="6201", role=roles.EXPENSE_OPERATIONAL, name=_("Cost of Goods Sold"), balance_type="debit", active=True) - entity.create_account(coa_model=coa, code="6202", role=roles.EXPENSE_OPERATIONAL, name=_("Salaries and Wages"), balance_type="debit", active=True) - entity.create_account(coa_model=coa, code="6203", role=roles.EXPENSE_OPERATIONAL, name=_("Sales Commissions"), balance_type="debit", active=True) - entity.create_account(coa_model=coa, code="6204", role=roles.EXPENSE_OPERATIONAL, name=_("Shipping and Customs Clearance"), balance_type="debit", active=True) + entity.create_account( + coa_model=coa, + code="6201", + role=roles.EXPENSE_OPERATIONAL, + name=_("Cost of Goods Sold"), + balance_type="debit", + active=True, + ) + entity.create_account( + coa_model=coa, + code="6202", + role=roles.EXPENSE_OPERATIONAL, + name=_("Salaries and Wages"), + balance_type="debit", + active=True, + ) + entity.create_account( + coa_model=coa, + code="6203", + role=roles.EXPENSE_OPERATIONAL, + name=_("Sales Commissions"), + balance_type="debit", + active=True, + ) + entity.create_account( + coa_model=coa, + code="6204", + role=roles.EXPENSE_OPERATIONAL, + name=_("Shipping and Customs Clearance"), + balance_type="debit", + active=True, + ) # 5.3 Non-Operating Expenses - entity.create_account(coa_model=coa, code="6301", role=roles.EXPENSE_OTHER, name=_("Zakat"), balance_type="debit", active=True) - entity.create_account(coa_model=coa, code="6302", role=roles.EXPENSE_OTHER, name=_("Taxes"), balance_type="debit", active=True) - entity.create_account(coa_model=coa, code="6303", role=roles.EXPENSE_OTHER, name=_("Foreign Currency Translation"), balance_type="debit", active=True) - entity.create_account(coa_model=coa, code="6304", role=roles.EXPENSE_OTHER, name=_("Interest Expenses"), balance_type="debit", active=True) + entity.create_account( + coa_model=coa, + code="6301", + role=roles.EXPENSE_OTHER, + name=_("Zakat"), + balance_type="debit", + active=True, + ) + entity.create_account( + coa_model=coa, + code="6302", + role=roles.EXPENSE_OTHER, + name=_("Taxes"), + balance_type="debit", + active=True, + ) + entity.create_account( + coa_model=coa, + code="6303", + role=roles.EXPENSE_OTHER, + name=_("Foreign Currency Translation"), + balance_type="debit", + active=True, + ) + entity.create_account( + coa_model=coa, + code="6304", + role=roles.EXPENSE_OTHER, + name=_("Interest Expenses"), + balance_type="debit", + active=True, + ) # create_settings(instance.pk) + # @background # def create_groups(instance): # group_names = ["Inventory", "Accountant", "Sales"] @@ -1299,32 +1411,40 @@ def create_coa_accounts1(pk): # group_manager.set_default_permissions() # instance.user.groups.add(group) + # @background -def create_accounts_for_make(dealer,makes): +def create_accounts_for_make(dealer, makes): entity = dealer.entity coa = entity.get_default_coa() name = ["Inventory", "Revenue", "Cogs"] - role = [roles.ASSET_CA_INVENTORY,roles.ASSET_CA_RECEIVABLES, roles.COGS] - balance_type = ["debit","credit","debit"] + role = [roles.ASSET_CA_INVENTORY, roles.ASSET_CA_RECEIVABLES, roles.COGS] + balance_type = ["debit", "credit", "debit"] - for name,role,balance_type in zip(name,role,balance_type): - create_make_accounts(entity,coa,makes,name,role,balance_type) + for name, role, balance_type in zip(name, role, balance_type): + create_make_accounts(entity, coa, makes, name, role, balance_type) -def create_make_accounts(entity,coa,makes,name,role,balance_type): + +def create_make_accounts(entity, coa, makes, name, role, balance_type): for make in makes: - last_account = entity.get_all_accounts().filter(role=role).order_by('-created').first() + last_account = ( + entity.get_all_accounts().filter(role=role).order_by("-created").first() + ) if len(last_account.code) == 4: code = f"{int(last_account.code)}{1:03d}" elif len(last_account.code) > 4: - code = f"{int(last_account.code)+1}" - acc = entity.get_all_accounts().filter( - name=f"{name}:{make.name}", - role=role, - coa_model=coa, - balance_type=balance_type, - active=True - ).first() + code = f"{int(last_account.code) + 1}" + acc = ( + entity.get_all_accounts() + .filter( + name=f"{name}:{make.name}", + role=role, + coa_model=coa, + balance_type=balance_type, + active=True, + ) + .first() + ) if not acc: acc = entity.create_account( name=f"{name}:{make.name}", @@ -1332,12 +1452,11 @@ def create_make_accounts(entity,coa,makes,name,role,balance_type): role=role, coa_model=coa, balance_type=balance_type, - active=True + active=True, ) return acc - @background def send_email(from_, to_, subject, message): subject = subject @@ -1354,8 +1473,8 @@ def long_running_task(task_id, *args, **kwargs): # Simulate work for i in range(5): - print(f"Task {task_id} progress: {i+1}/5") + print(f"Task {task_id} progress: {i + 1}/5") result = f"Task {task_id} completed at {datetime.now()}" print(result) - return result \ No newline at end of file + return result diff --git a/inventory/templatetags/custom_filters.py b/inventory/templatetags/custom_filters.py index 965ac3ca..d3e97b55 100644 --- a/inventory/templatetags/custom_filters.py +++ b/inventory/templatetags/custom_filters.py @@ -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' - \ No newline at end of file + return "sedan" + + + +@register.filter +def status_badge_color(status): + color_map = { + 'PENDING_APPROVAL': 'warning', + 'APPROVED': 'info', + 'IN_FINANCING': 'primary', + 'PARTIALLY_PAID': 'success', + 'FULLY_PAID': 'success', + 'PENDING_DELIVERY': 'warning', + 'DELIVERED': 'success', + 'CANCELLED': 'danger', + } + return color_map.get(status, 'secondary') + + +@register.inclusion_tag('inventory/tags/inventory_table.html', takes_context=True) +def inventory_table(context, queryset): + ctx = { + 'entity_slug': context['view'].kwargs['entity_slug'], + 'inventory_list': queryset + } + ctx.update(queryset.aggregate(inventory_total_value=Sum('total_value'))) + return ctx \ No newline at end of file diff --git a/inventory/templatetags/num2words_tags.py b/inventory/templatetags/num2words_tags.py index 788d3080..6cd70f0b 100644 --- a/inventory/templatetags/num2words_tags.py +++ b/inventory/templatetags/num2words_tags.py @@ -3,9 +3,10 @@ from num2words import num2words register = template.Library() + @register.filter -def num_to_words(value, lang='ar'): +def num_to_words(value, lang="ar"): try: return num2words(value, lang=lang) except: - return value \ No newline at end of file + return value diff --git a/inventory/templatetags/tenhal_tag.py b/inventory/templatetags/tenhal_tag.py index ed862964..111007ee 100644 --- a/inventory/templatetags/tenhal_tag.py +++ b/inventory/templatetags/tenhal_tag.py @@ -25,9 +25,12 @@ from django_ledger.io.io_core import get_localdate register = template.Library() + @register.filter() def to_int(value): return Decimal(value).quantize(Decimal("0.01")) + + # @register.simple_tag(name='current_version') # def current_version(): # return __version__ @@ -535,89 +538,96 @@ def to_int(value): # } -@register.inclusion_tag('inventory/ledger/reports/components/period_navigator.html', takes_context=True) +@register.inclusion_tag( + "inventory/ledger/reports/components/period_navigator.html", takes_context=True +) def period_navigation(context, base_url: str): kwargs = dict() - entity_slug = context['view'].kwargs['entity_slug'] - kwargs['entity_slug'] = entity_slug + entity_slug = context["view"].kwargs["entity_slug"] + kwargs["entity_slug"] = entity_slug - if context['view'].kwargs.get('ledger_pk'): - kwargs['ledger_pk'] = context['view'].kwargs.get('ledger_pk') + if context["view"].kwargs.get("ledger_pk"): + kwargs["ledger_pk"] = context["view"].kwargs.get("ledger_pk") - if context['view'].kwargs.get('account_pk'): - kwargs['account_pk'] = context['view'].kwargs.get('account_pk') + if context["view"].kwargs.get("account_pk"): + kwargs["account_pk"] = context["view"].kwargs.get("account_pk") - if context['view'].kwargs.get('unit_slug'): - kwargs['unit_slug'] = context['view'].kwargs.get('unit_slug') + if context["view"].kwargs.get("unit_slug"): + kwargs["unit_slug"] = context["view"].kwargs.get("unit_slug") - if context['view'].kwargs.get('coa_slug'): - kwargs['coa_slug'] = context['view'].kwargs.get('coa_slug') + if context["view"].kwargs.get("coa_slug"): + kwargs["coa_slug"] = context["view"].kwargs.get("coa_slug") ctx = dict() - ctx['year'] = context['year'] - ctx['has_year'] = context.get('has_year') - ctx['has_quarter'] = context.get('has_quarter') - ctx['has_month'] = context.get('has_month') - ctx['has_date'] = context.get('has_date') - ctx['previous_year'] = context['previous_year'] + ctx["year"] = context["year"] + ctx["has_year"] = context.get("has_year") + ctx["has_quarter"] = context.get("has_quarter") + ctx["has_month"] = context.get("has_month") + ctx["has_date"] = context.get("has_date") + ctx["previous_year"] = context["previous_year"] - kwargs['year'] = context['previous_year'] - ctx['previous_year_url'] = reverse(f'django_ledger:{base_url}-year', kwargs=kwargs) - ctx['next_year'] = context['next_year'] + kwargs["year"] = context["previous_year"] + ctx["previous_year_url"] = reverse(f"django_ledger:{base_url}-year", kwargs=kwargs) + ctx["next_year"] = context["next_year"] - kwargs['year'] = context['next_year'] - ctx['next_year_url'] = reverse(f'django_ledger:{base_url}-year', kwargs=kwargs) + kwargs["year"] = context["next_year"] + ctx["next_year_url"] = reverse(f"django_ledger:{base_url}-year", kwargs=kwargs) - kwargs['year'] = context['year'] - ctx['current_year_url'] = reverse(f'django_ledger:{base_url}-year', kwargs=kwargs) + kwargs["year"] = context["year"] + ctx["current_year_url"] = reverse(f"django_ledger:{base_url}-year", kwargs=kwargs) dt = get_localdate() KWARGS_CURRENT_MONTH = { - 'entity_slug': context['view'].kwargs['entity_slug'], - 'year': dt.year, - 'month': dt.month + "entity_slug": context["view"].kwargs["entity_slug"], + "year": dt.year, + "month": dt.month, } - if 'unit_slug' in kwargs: - KWARGS_CURRENT_MONTH['unit_slug'] = kwargs['unit_slug'] - if 'account_pk' in kwargs: - KWARGS_CURRENT_MONTH['account_pk'] = kwargs['account_pk'] - if 'ledger_pk' in kwargs: - KWARGS_CURRENT_MONTH['ledger_pk'] = kwargs['ledger_pk'] - if 'coa_slug' in kwargs: - KWARGS_CURRENT_MONTH['coa_slug'] = kwargs['coa_slug'] + if "unit_slug" in kwargs: + KWARGS_CURRENT_MONTH["unit_slug"] = kwargs["unit_slug"] + if "account_pk" in kwargs: + KWARGS_CURRENT_MONTH["account_pk"] = kwargs["account_pk"] + if "ledger_pk" in kwargs: + KWARGS_CURRENT_MONTH["ledger_pk"] = kwargs["ledger_pk"] + if "coa_slug" in kwargs: + KWARGS_CURRENT_MONTH["coa_slug"] = kwargs["coa_slug"] - ctx['current_month_url'] = reverse(f'django_ledger:{base_url}-month', - kwargs=KWARGS_CURRENT_MONTH) + ctx["current_month_url"] = reverse( + f"django_ledger:{base_url}-month", kwargs=KWARGS_CURRENT_MONTH + ) quarter_urls = list() - ctx['quarter'] = context.get('quarter') + ctx["quarter"] = context.get("quarter") for Q in range(1, 5): - kwargs['quarter'] = Q - quarter_urls.append({ - 'url': reverse(f'django_ledger:{base_url}-quarter', kwargs=kwargs), - 'quarter': Q, - 'quarter_name': f'Q{Q}' - }) - del kwargs['quarter'] - ctx['quarter_urls'] = quarter_urls + kwargs["quarter"] = Q + quarter_urls.append( + { + "url": reverse(f"django_ledger:{base_url}-quarter", kwargs=kwargs), + "quarter": Q, + "quarter_name": f"Q{Q}", + } + ) + del kwargs["quarter"] + ctx["quarter_urls"] = quarter_urls month_urls = list() - ctx['month'] = context.get('month') + ctx["month"] = context.get("month") for M in range(1, 13): - kwargs['month'] = M - month_urls.append({ - 'url': reverse(f'django_ledger:{base_url}-month', kwargs=kwargs), - 'month': M, - 'month_abbr': month_abbr[M] - }) - ctx['month_urls'] = month_urls - ctx['from_date'] = context['from_date'] - ctx['to_date'] = context['to_date'] + kwargs["month"] = M + month_urls.append( + { + "url": reverse(f"django_ledger:{base_url}-month", kwargs=kwargs), + "month": M, + "month_abbr": month_abbr[M], + } + ) + ctx["month_urls"] = month_urls + ctx["from_date"] = context["from_date"] + ctx["to_date"] = context["to_date"] ctx.update(kwargs) - ctx['date_navigation_url'] = context.get('date_navigation_url') + ctx["date_navigation_url"] = context.get("date_navigation_url") return ctx diff --git a/inventory/tests.py b/inventory/tests.py index aa2da193..d1fdf426 100644 --- a/inventory/tests.py +++ b/inventory/tests.py @@ -8,7 +8,7 @@ from django_ledger.io.io_core import get_localdate from django.core.exceptions import ObjectDoesNotExist from decimal import Decimal from unittest.mock import MagicMock -from inventory.models import VatRate +from inventory.models import VatRate from inventory.utils import CarFinanceCalculator @@ -47,6 +47,7 @@ class ModelTest(TestCase): :ivar car_finances: Car finance object for the car under test. :type car_finances: CarFinance instance """ + def setUp(self): email = "RkzgO@example.com" name = "John Doe" @@ -178,46 +179,46 @@ class AuthenticationTest(TestCase): :ivar url: URL for account signup endpoint used in the test cases. :type url: str """ + def setUp(self): self.client = Client() self.url = reverse("account_signup") + def test_login(self): url = reverse("account_login") - response = self.client.post(url, {"email": "RkzgO@example.com", "password": "password"}) + response = self.client.post( + url, {"email": "RkzgO@example.com", "password": "password"} + ) self.assertEqual(response.status_code, 200) - def test_valid_data(self): # Create valid JSON data data = { "wizardValidationForm1": { "email": "test@example.com", "password": "password123", - "confirm_password": "password123" + "confirm_password": "password123", }, "wizardValidationForm2": { "name": "John Doe", "arabic_name": "جون دو", - "phone_number": "1234567890" + "phone_number": "1234567890", }, "wizardValidationForm3": { "crn": "123456", "vrn": "789012", - "address": "123 Main St" - } + "address": "123 Main St", + }, } # Send a POST request with the JSON data response = self.client.post( - self.url, - data=json.dumps(data), - content_type='application/json' + self.url, data=json.dumps(data), content_type="application/json" ) # Check the response self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), {'message': 'User created successfully.'}) - + self.assertEqual(response.json(), {"message": "User created successfully."}) def test_passwords_do_not_match(self): # Create JSON data with mismatched passwords @@ -225,25 +226,23 @@ class AuthenticationTest(TestCase): "wizardValidationForm1": { "email": "test@example.com", "password": "password123", - "confirm_password": "differentpassword" + "confirm_password": "differentpassword", }, "wizardValidationForm2": { "name": "John Doe", "arabic_name": "جون دو", - "phone_number": "1234567890" + "phone_number": "1234567890", }, "wizardValidationForm3": { "crn": "123456", "vrn": "789012", - "address": "123 Main St" - } + "address": "123 Main St", + }, } # Send a POST request with the JSON data response = self.client.post( - self.url, - data=json.dumps(data), - content_type='application/json' + self.url, data=json.dumps(data), content_type="application/json" ) # Check the response @@ -261,26 +260,26 @@ class AuthenticationTest(TestCase): "wizardValidationForm2": { "name": "John Doe", "arabic_name": "جون دو", - "phone_number": "1234567890" + "phone_number": "1234567890", }, "wizardValidationForm3": { "crn": "123456", "vrn": "789012", - "address": "123 Main St" - } + "address": "123 Main St", + }, } # Send a POST request with the JSON data response = self.client.post( - self.url, - data=json.dumps(data), - content_type='application/json' + self.url, data=json.dumps(data), content_type="application/json" ) # Check the response self.assertEqual(response.status_code, 400) - self.assertIn("error", response.json()) # Assuming the view returns an error for missing fields - + self.assertIn( + "error", response.json() + ) # Assuming the view returns an error for missing fields + class CarFinanceCalculatorTests(TestCase): """ @@ -297,10 +296,11 @@ class CarFinanceCalculatorTests(TestCase): :ivar vat_rate: Active VAT rate used for testing VAT rate retrieval. :type vat_rate: VatRate """ + def setUp(self): # Common setup for all tests self.mock_model = MagicMock() - self.vat_rate = VatRate.objects.create(rate=Decimal('0.20'), is_active=True) + self.vat_rate = VatRate.objects.create(rate=Decimal("0.20"), is_active=True) def test_no_active_vat_rate_raises_error(self): VatRate.objects.all().delete() # Ensure no active VAT @@ -309,11 +309,13 @@ class CarFinanceCalculatorTests(TestCase): def test_vat_rate_retrieval(self): calculator = CarFinanceCalculator(self.mock_model) - self.assertEqual(calculator.vat_rate, Decimal('0.20')) + self.assertEqual(calculator.vat_rate, Decimal("0.20")) def test_item_transactions_retrieval(self): mock_item = MagicMock() - self.mock_model.get_itemtxs_data.return_value = [MagicMock(all=lambda: [mock_item])] + self.mock_model.get_itemtxs_data.return_value = [ + MagicMock(all=lambda: [mock_item]) + ] calculator = CarFinanceCalculator(self.mock_model) self.assertEqual(calculator.item_transactions, [mock_item]) @@ -321,136 +323,152 @@ class CarFinanceCalculatorTests(TestCase): mock_item = MagicMock() mock_item.ce_quantity = 2 mock_item.item_model = MagicMock() - mock_item.item_model.item_number = '123' + mock_item.item_model.item_number = "123" mock_item.item_model.additional_info = { CarFinanceCalculator.CAR_FINANCE_KEY: { - 'selling_price': '10000', - 'cost_price': '8000', - 'discount_amount': '500', - 'total_vat': '2000' + "selling_price": "10000", + "cost_price": "8000", + "discount_amount": "500", + "total_vat": "2000", }, CarFinanceCalculator.CAR_INFO_KEY: { - 'vin': 'VIN123', - 'make': 'Toyota', - 'model': 'Camry', - 'year': 2020, - 'trim': 'LE', - 'mileage': 15000 + "vin": "VIN123", + "make": "Toyota", + "model": "Camry", + "year": 2020, + "trim": "LE", + "mileage": 15000, }, CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [ - {'name': 'Service 1', 'price': '200', 'taxable': True, 'price_': '240'} - ] + {"name": "Service 1", "price": "200", "taxable": True, "price_": "240"} + ], } calculator = CarFinanceCalculator(self.mock_model) car_data = calculator._get_car_data(mock_item) - self.assertEqual(car_data['item_number'], '123') - self.assertEqual(car_data['vin'], 'VIN123') - self.assertEqual(car_data['make'], 'Toyota') - self.assertEqual(car_data['selling_price'], '10000') - self.assertEqual(car_data['unit_price'], Decimal('10000')) - self.assertEqual(car_data['quantity'], 2) - self.assertEqual(car_data['total'], Decimal('20000')) - self.assertEqual(car_data['total_vat'], '2000') - self.assertEqual(car_data['additional_services'], [{'name': 'Service 1', 'price': '200', 'taxable': True, 'price_': '240'}]) + self.assertEqual(car_data["item_number"], "123") + self.assertEqual(car_data["vin"], "VIN123") + self.assertEqual(car_data["make"], "Toyota") + self.assertEqual(car_data["selling_price"], "10000") + self.assertEqual(car_data["unit_price"], Decimal("10000")) + self.assertEqual(car_data["quantity"], 2) + self.assertEqual(car_data["total"], Decimal("20000")) + self.assertEqual(car_data["total_vat"], "2000") + self.assertEqual( + car_data["additional_services"], + [{"name": "Service 1", "price": "200", "taxable": True, "price_": "240"}], + ) def test_get_additional_services(self): mock_item1 = MagicMock() mock_item1.item_model.additional_info = { CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [ - {'name': 'Service 1', 'price': '100', 'taxable': True, 'price_': '120'} + {"name": "Service 1", "price": "100", "taxable": True, "price_": "120"} ] } mock_item2 = MagicMock() mock_item2.item_model.additional_info = { CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [ - {'name': 'Service 2', 'price': '200', 'taxable': False, 'price_': '200'} + {"name": "Service 2", "price": "200", "taxable": False, "price_": "200"} ] } - self.mock_model.get_itemtxs_data.return_value = [MagicMock(all=lambda: [mock_item1, mock_item2])] + self.mock_model.get_itemtxs_data.return_value = [ + MagicMock(all=lambda: [mock_item1, mock_item2]) + ] calculator = CarFinanceCalculator(self.mock_model) services = calculator._get_additional_services() self.assertEqual(len(services), 2) - self.assertEqual(services[0]['name'], 'Service 1') - self.assertEqual(services[1]['name'], 'Service 2') - self.assertEqual(services[0]['price_'], '120') - self.assertEqual(services[1]['price_'], '200') + self.assertEqual(services[0]["name"], "Service 1") + self.assertEqual(services[1]["name"], "Service 2") + self.assertEqual(services[0]["price_"], "120") + self.assertEqual(services[1]["price_"], "200") def test_calculate_totals(self): mock_item1 = MagicMock() mock_item1.ce_quantity = 2 mock_item1.item_model.additional_info = { CarFinanceCalculator.CAR_FINANCE_KEY: { - 'selling_price': '10000', - 'discount_amount': '500' + "selling_price": "10000", + "discount_amount": "500", }, CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [ - {'price_': '100'}, - {'price_': '200'} - ] + {"price_": "100"}, + {"price_": "200"}, + ], } mock_item2 = MagicMock() mock_item2.quantity = 3 mock_item2.item_model.additional_info = { CarFinanceCalculator.CAR_FINANCE_KEY: { - 'selling_price': '20000', - 'discount_amount': '1000' + "selling_price": "20000", + "discount_amount": "1000", }, - CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [ - {'price_': '300'} - ] + CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [{"price_": "300"}], } - self.mock_model.get_itemtxs_data.return_value = [MagicMock(all=lambda: [mock_item1, mock_item2])] + self.mock_model.get_itemtxs_data.return_value = [ + MagicMock(all=lambda: [mock_item1, mock_item2]) + ] calculator = CarFinanceCalculator(self.mock_model) totals = calculator.calculate_totals() - expected_total_price = (Decimal('10000') * 2 + Decimal('20000') * 3) - (Decimal('500') + Decimal('1000')) - expected_vat = expected_total_price * Decimal('0.15') - expected_additionals = Decimal('100') + Decimal('200') + Decimal('300') - expected_grand_total = (expected_total_price + expected_vat + expected_additionals).quantize(Decimal('0.00')) + expected_total_price = (Decimal("10000") * 2 + Decimal("20000") * 3) - ( + Decimal("500") + Decimal("1000") + ) + expected_vat = expected_total_price * Decimal("0.15") + expected_additionals = Decimal("100") + Decimal("200") + Decimal("300") + expected_grand_total = ( + expected_total_price + expected_vat + expected_additionals + ).quantize(Decimal("0.00")) - self.assertEqual(totals['total_price'], expected_total_price) - self.assertEqual(totals['total_discount'], Decimal('1500')) - self.assertEqual(totals['total_vat_amount'], expected_vat) - self.assertEqual(totals['total_additionals'], expected_additionals) - self.assertEqual(totals['grand_total'], expected_grand_total) + self.assertEqual(totals["total_price"], expected_total_price) + self.assertEqual(totals["total_discount"], Decimal("1500")) + self.assertEqual(totals["total_vat_amount"], expected_vat) + self.assertEqual(totals["total_additionals"], expected_additionals) + self.assertEqual(totals["grand_total"], expected_grand_total) def test_get_finance_data(self): mock_item = MagicMock() mock_item.ce_quantity = 1 mock_item.item_model = MagicMock() - mock_item.item_model.item_number = '456' + mock_item.item_model.item_number = "456" mock_item.item_model.additional_info = { CarFinanceCalculator.CAR_FINANCE_KEY: { - 'selling_price': '15000', - 'discount_amount': '1000', - 'total_vat': '2800' + "selling_price": "15000", + "discount_amount": "1000", + "total_vat": "2800", }, CarFinanceCalculator.CAR_INFO_KEY: { - 'vin': 'VIN456', - 'make': 'Honda', - 'model': 'Civic', - 'year': 2021 + "vin": "VIN456", + "make": "Honda", + "model": "Civic", + "year": 2021, }, CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [ - {'name': 'Service', 'price': '150', 'taxable': True, 'price_': '180'} - ] + {"name": "Service", "price": "150", "taxable": True, "price_": "180"} + ], } - self.mock_model.get_itemtxs_data.return_value = [MagicMock(all=lambda: [mock_item])] + self.mock_model.get_itemtxs_data.return_value = [ + MagicMock(all=lambda: [mock_item]) + ] calculator = CarFinanceCalculator(self.mock_model) finance_data = calculator.get_finance_data() - self.assertEqual(len(finance_data['cars']), 1) - self.assertEqual(finance_data['quantity'], 1) - self.assertEqual(finance_data['total_price'], Decimal('14000')) # 15000 - 1000 - self.assertEqual(finance_data['total_vat'], Decimal('14000') + (Decimal('14000') * Decimal('0.20'))) - self.assertEqual(finance_data['total_vat_amount'], Decimal('14000') * Decimal('0.20')) - self.assertEqual(finance_data['total_additionals'], Decimal('180')) - self.assertEqual(finance_data['additionals'][0]['name'], 'Service') - self.assertEqual(finance_data['vat'], Decimal('0.20')) \ No newline at end of file + self.assertEqual(len(finance_data["cars"]), 1) + self.assertEqual(finance_data["quantity"], 1) + self.assertEqual(finance_data["total_price"], Decimal("14000")) # 15000 - 1000 + self.assertEqual( + finance_data["total_vat"], + Decimal("14000") + (Decimal("14000") * Decimal("0.20")), + ) + self.assertEqual( + finance_data["total_vat_amount"], Decimal("14000") * Decimal("0.20") + ) + self.assertEqual(finance_data["total_additionals"], Decimal("180")) + self.assertEqual(finance_data["additionals"][0]["name"], "Service") + self.assertEqual(finance_data["vat"], Decimal("0.20")) diff --git a/inventory/urls.py b/inventory/urls.py index 12c83091..a763ac0b 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import handler403,handler400,handler404,handler500 +from django.conf.urls import handler403, handler400, handler404, handler500 from django.urls import path from django_tables2.export.export import TableExport @@ -43,30 +43,39 @@ urlpatterns = [ # ), # ), # Tasks - - path('tasks/', views.task_list, name='task_list'), - path('legal/', views.terms_and_privacy, name='terms_and_privacy'), + path("tasks/", views.task_list, name="task_list"), + path("legal/", views.terms_and_privacy, name="terms_and_privacy"), # path('tasks//detail/', views.task_detail, name='task_detail'), # Dashboards # path("user//settings/", views.UserSettingsView.as_view(), name="user_settings"), path("pricing/", views.pricing_page, name="pricing_page"), path("submit_plan/", views.submit_plan, name="submit_plan"), - path('payment-callback/', views.payment_callback, name='payment_callback'), + path("payment-callback/", views.payment_callback, name="payment_callback"), # path( "dealers/activity/", views.UserActivityLogListView.as_view(), name="dealer_activity", ), - path("dealers//settings/", views.DealerSettingsView, name="dealer_settings"), + path( + "dealers//settings/", + views.DealerSettingsView, + name="dealer_settings", + ), path("dealers/assign-car-makes/", views.assign_car_makes, name="assign_car_makes"), - path("dashboards/manager/", views.ManagerDashboard.as_view(), name="manager_dashboard"), + path( + "dashboards/manager/", + views.ManagerDashboard.as_view(), + name="manager_dashboard", + ), path("dashboards/sales/", views.SalesDashboard.as_view(), name="sales_dashboard"), path("test/", views.TestView.as_view(), name="test"), - path('cars/inventory/table/', views.CarListViewTable.as_view(), name="car_table"), + path("cars/inventory/table/", views.CarListViewTable.as_view(), name="car_table"), path("export/format/", TableExport, name="export"), # Dealer URLs - path("dealers//", views.DealerDetailView.as_view(), name="dealer_detail"), + path( + "dealers//", views.DealerDetailView.as_view(), name="dealer_detail" + ), path( "dealers//update/", views.DealerUpdateView.as_view(), @@ -93,7 +102,9 @@ urlpatterns = [ views.CustomerUpdateView.as_view(), name="customer_update", ), - path("customers//delete/", views.delete_customer, name="customer_delete"), + path( + "customers//delete/", views.delete_customer, name="customer_delete" + ), path( "customers//opportunities/create/", views.OpportunityCreateView.as_view(), @@ -101,19 +112,26 @@ urlpatterns = [ ), path("crm/leads/create/", views.lead_create, name="lead_create"), path( - "crm/leads//view/", views.LeadDetailView.as_view(), name="lead_detail" + "crm/leads//view/", + views.LeadDetailView.as_view(), + name="lead_detail", ), - path('update-lead-actions/', views.update_lead_actions, name='update_lead_actions'), - path('crm/leads/lead_tracking/', views.lead_tracking, name='lead_tracking'), - path('crm/leads/lead_view/', views.lead_view, name='lead_view'), - + path("update-lead-actions/", views.update_lead_actions, name="update_lead_actions"), + path("crm/leads/lead_tracking/", views.lead_tracking, name="lead_tracking"), + path("crm/leads/lead_view/", views.lead_view, name="lead_view"), path("crm/leads/", views.LeadListView.as_view(), name="lead_list"), path( - "crm/leads//update/", views.LeadUpdateView.as_view(), name="lead_update" + "crm/leads//update/", + views.LeadUpdateView.as_view(), + name="lead_update", ), path("crm/leads//delete/", views.LeadDeleteView, name="lead_delete"), - path("crm/leads//lead-convert/", views.lead_convert, name="lead_convert"), - path("crm/leads//delete-note/", views.delete_note, name="delete_note_to_lead"), + path( + "crm/leads//lead-convert/", views.lead_convert, name="lead_convert" + ), + path( + "crm/leads//delete-note/", views.delete_note, name="delete_note_to_lead" + ), path( "crm//update-note/", views.update_note, @@ -211,17 +229,22 @@ urlpatterns = [ ), # path('crm/opportunities//logs/', views.OpportunityLogsView.as_view(), name='opportunity_logs'), # ####################### - path('stream/', views.sse_stream, name='sse_stream'), - path('fetch/', views.fetch_notifications, name='fetch_notifications'), - + path("stream/", views.sse_stream, name="sse_stream"), + path("fetch/", views.fetch_notifications, name="fetch_notifications"), # Mark single notification as read - path('/mark-read/', views.mark_notification_as_read, name='mark_notification_as_read'), - + path( + "/mark-read/", + views.mark_notification_as_read, + name="mark_notification_as_read", + ), # Mark all notifications as read - path('mark-all-read/', views.mark_all_notifications_as_read, name='mark_all_notifications_as_read'), - + path( + "mark-all-read/", + views.mark_all_notifications_as_read, + name="mark_all_notifications_as_read", + ), # Notification history - path('history/', views.notifications_history, name='notifications_history'), + path("history/", views.notifications_history, name="notifications_history"), # ####################### path( "crm/notifications/", @@ -238,7 +261,7 @@ urlpatterns = [ views.mark_notification_as_read, name="mark_notification_as_read", ), - path('crm/calender/', views.EmployeeCalendarView.as_view(), name='calendar_list'), + path("crm/calender/", views.EmployeeCalendarView.as_view(), name="calendar_list"), # Vendor URLs path("vendors/create/", views.VendorCreateView.as_view(), name="vendor_create"), path("vendors", views.VendorListView.as_view(), name="vendor_list"), @@ -254,8 +277,8 @@ urlpatterns = [ name="vendor_delete", ), # Car URLs - path('cars/upload_cars/', views.upload_cars, name='upload_cars'), - path('cars//upload_cars/', views.upload_cars, name='upload_cars'), + path("cars/upload_cars/", views.upload_cars, name="upload_cars"), + path("cars//upload_cars/", views.upload_cars, name="upload_cars"), path("cars/add/", views.CarCreateView.as_view(), name="car_add"), path("cars/inventory/", views.CarInventory.as_view(), name="car_inventory_all"), path( @@ -288,13 +311,17 @@ urlpatterns = [ path( "cars//add-color/", views.CarColorCreate.as_view(), name="add_color" ), - path('car/colors//update/', views.CarColorsUpdateView.as_view(), name='car_colors_update'), + path( + "car/colors//update/", + views.CarColorsUpdateView.as_view(), + name="car_colors_update", + ), path( "cars//location/add/", views.CarLocationCreateView.as_view(), name="add_car_location", ), -path( + path( "cars//location//update", views.CarLocationUpdateView.as_view(), name="update_car_location", @@ -324,13 +351,9 @@ path( views.CarTransferPreviewView, name="transfer_preview", ), - path("cars/inventory/search/", - views.SearchCodeView.as_view(), - name="car_search"), + path("cars/inventory/search/", views.SearchCodeView.as_view(), name="car_search"), # path('cars//colors//update/',views.CarColorUpdateView.as_view(),name='color_update'), - path("cars/reserve//", - views.reserve_car_view, - name="reserve_car"), + path("cars/reserve//", views.reserve_car_view, name="reserve_car"), path( "reservations//", views.manage_reservation, @@ -341,16 +364,20 @@ path( views.CustomCardCreateView.as_view(), name="add_custom_card", ), - path('cars//add-registration/', - views.CarRegistrationCreateView.as_view(), - name='add_registration'), - -#sales list path( - 'sales/list/', - views.sales_list_view, - name='sales_list', + "cars//add-registration/", + views.CarRegistrationCreateView.as_view(), + name="add_registration", ), + # sales list + path( + "sales/list/", + views.sales_list_view, + name="sales_list", + ), + path('sale_orders//', views.SaleOrderDetailView.as_view(), name='order_detail'), + path('inventory//list/', views.InventoryListView.as_view(), name='inventort_list'), + # Sales URLs quotation_create # path( # "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//", views.UserDetailView.as_view(), name="user_detail"), path("user//groups/", views.UserGroupView, name="user_groups"), - path("user//update/", views.UserUpdateView.as_view(), name="user_update"), + path( + "user//update/", views.UserUpdateView.as_view(), name="user_update" + ), path("user//confirm/", views.UserDeleteview, name="user_delete"), # Group URLs path("group/create/", views.GroupCreateView.as_view(), name="group_create"), - path("group//update/", views.GroupUpdateView.as_view(), name="group_update"), + path( + "group//update/", views.GroupUpdateView.as_view(), name="group_update" + ), path("group//", views.GroupDetailView.as_view(), name="group_detail"), path("group/", views.GroupListView.as_view(), name="group_list"), path("group//confirm/", views.GroupDeleteview, name="group_delete"), - path("group//permission/", views.GroupPermissionView, name="group_permission"), + path( + "group//permission/", views.GroupPermissionView, name="group_permission" + ), # Organization URLs path( "organizations/create/", @@ -468,76 +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//detail//", views.LedgerModelDetailView.as_view(), name="ledger_detail" + "ledgers//detail//", + views.LedgerModelDetailView.as_view(), + name="ledger_detail", ), path( - "ledgers//lock_all_journals//", views.ledger_lock_all_journals, name="lock_all_journals" + "ledgers//lock_all_journals//", + views.ledger_lock_all_journals, + name="lock_all_journals", ), path( - "ledgers//unlock_all_journals//", views.ledger_unlock_all_journals, name="unlock_all_journals" + "ledgers//unlock_all_journals//", + views.ledger_unlock_all_journals, + name="unlock_all_journals", ), path( - "ledgers//post_all_journals//", views.ledger_post_all_journals, name="post_all_journals" + "ledgers//post_all_journals//", + views.ledger_post_all_journals, + name="post_all_journals", ), path( - "ledgers//unpost_all_journals//", views.ledger_unpost_all_journals, name="unpost_all_journals" + "ledgers//unpost_all_journals//", + views.ledger_unpost_all_journals, + name="unpost_all_journals", ), # path( # "ledgers/create/", views.LedgerModelCreateView.as_view(), name="ledger_create" # ), path( - "journalentries//list/", views.JournalEntryListView.as_view(), name="journalentry_list" + "journalentries//list/", + views.JournalEntryListView.as_view(), + name="journalentry_list", ), path( - "journalentries//create/", views.JournalEntryCreateView.as_view(), name="journalentry_create" + "journalentries//create/", + views.JournalEntryCreateView.as_view(), + name="journalentry_create", ), path( - "journalentries//delete/", views.JournalEntryDeleteView, name="journalentry_delete" + "journalentries//delete/", + views.JournalEntryDeleteView, + name="journalentry_delete", ), path( "journalentries//transactions/", views.JournalEntryTransactionsView, name="journalentry_transactions", ), - path('journalentries///detail//txs/', - views.JournalEntryModelTXSDetailView.as_view(), - name='journalentry_txs'), + path( + "journalentries///detail//txs/", + views.JournalEntryModelTXSDetailView.as_view(), + name="journalentry_txs", + ), # ledger actions - - path('ledgers//action//post/', - views.LedgerModelModelActionView.as_view(action_name='post'), - name='ledger-action-post'), - path('ledgers//action//post-journal-entries/', - views.LedgerModelModelActionView.as_view(action_name='post_journal_entries'), - name='ledger-action-post-journal-entries'), - path('ledgers//action//unpost/', - views.LedgerModelModelActionView.as_view(action_name='unpost'), - name='ledger-action-unpost'), - path('ledgers//action//lock/', - views.LedgerModelModelActionView.as_view(action_name='lock'), - name='ledger-action-lock'), - path('ledgers//action//lock-journal-entries/', - views.LedgerModelModelActionView.as_view(action_name='lock_journal_entries'), - name='ledger-action-lock-journal-entries'), - path('ledgers//action//unlock/', - views.LedgerModelModelActionView.as_view(action_name='unlock'), - name='ledger-action-unlock'), - path('ledgers//action//hide/', - views.LedgerModelModelActionView.as_view(action_name='hide'), - name='ledger-action-hide'), - path('ledgers//action//unhide/', - views.LedgerModelModelActionView.as_view(action_name='unhide'), - name='ledger-action-unhide'), - path('ledgers//delete//', - views.LedgerModelDeleteView.as_view(), - name='ledger-delete'), + path( + "ledgers//action//post/", + views.LedgerModelModelActionView.as_view(action_name="post"), + name="ledger-action-post", + ), + path( + "ledgers//action//post-journal-entries/", + views.LedgerModelModelActionView.as_view(action_name="post_journal_entries"), + name="ledger-action-post-journal-entries", + ), + path( + "ledgers//action//unpost/", + views.LedgerModelModelActionView.as_view(action_name="unpost"), + name="ledger-action-unpost", + ), + path( + "ledgers//action//lock/", + views.LedgerModelModelActionView.as_view(action_name="lock"), + name="ledger-action-lock", + ), + path( + "ledgers//action//lock-journal-entries/", + views.LedgerModelModelActionView.as_view(action_name="lock_journal_entries"), + name="ledger-action-lock-journal-entries", + ), + path( + "ledgers//action//unlock/", + views.LedgerModelModelActionView.as_view(action_name="unlock"), + name="ledger-action-unlock", + ), + path( + "ledgers//action//hide/", + views.LedgerModelModelActionView.as_view(action_name="hide"), + name="ledger-action-hide", + ), + path( + "ledgers//action//unhide/", + views.LedgerModelModelActionView.as_view(action_name="unhide"), + name="ledger-action-unhide", + ), + path( + "ledgers//delete//", + views.LedgerModelDeleteView.as_view(), + name="ledger-delete", + ), # Bank Account path( "bank_accounts/", views.BankAccountListView.as_view(), name="bank_account_list" @@ -586,7 +651,11 @@ path( name="estimate_detail", ), path("sales/estimates/create/", views.create_estimate, name="estimate_create"), - path("sales/estimates/create//", views.create_estimate, name="estimate_create_from_opportunity"), + path( + "sales/estimates/create//", + views.create_estimate, + name="estimate_create_from_opportunity", + ), path( "sales/estimates//estimate_mark_as/", views.estimate_mark_as, @@ -605,10 +674,21 @@ path( path( "sales/estimates//send_email", views.send_email_view, name="send_email" ), - path('sales/estimates//sale_order/', views.create_sale_order, name='create_sale_order'), - path('sales/estimates//sale_order//details/', views.SaleOrderDetail.as_view(), name='sale_order_details'), - path('sales/estimates//sale_order/preview/', views.preview_sale_order, name='preview_sale_order'), - + path( + "sales/estimates//sale_order/", + views.create_sale_order, + name="create_sale_order", + ), + path( + "sales/estimates//sale_order//details/", + views.SaleOrderDetail.as_view(), + name="sale_order_details", + ), + path( + "sales/estimates//sale_order/preview/", + views.preview_sale_order, + name="preview_sale_order", + ), # Invoice path("sales/invoices/", views.InvoiceListView.as_view(), name="invoice_list"), path( @@ -702,52 +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//create/', - views.BillModelCreateView.as_view(), - name='bill-create'), - path('items/bills//create/purchase-order//', - views.BillModelCreateView.as_view(for_purchase_order=True), - name='bill-create-po'), - path('items/bills//create/estimate//', - views.BillModelCreateView.as_view(for_estimate=True), - name='bill-create-estimate'), - path('items/bills//detail//', - views.BillModelDetailViewView.as_view(), - name='bill-detail'), - path('items/bills//update//', - views.BillModelUpdateViewView.as_view(), - name='bill-update'), - path('items/bills//update//items/', - views.BillModelUpdateViewView.as_view(action_update_items=True), - name='bill-update-items'), - ############################################################ - path('items/bills//actions//mark-as-draft/', - views.BillModelActionMarkAsDraftView.as_view(), - name='bill-action-mark-as-draft'), - path('items/bills//actions//mark-as-review/', - views.BillModelActionMarkAsInReviewView.as_view(), - name='bill-action-mark-as-review'), - path('items/bills//actions//mark-as-approved/', - views.BillModelActionMarkAsApprovedView.as_view(), - name='bill-action-mark-as-approved'), - path('items/bills//actions//mark-as-paid/', - views.BillModelActionMarkAsPaidView.as_view(), - name='bill-action-mark-as-paid'), - path('items/bills//actions//mark-as-void/', - views.BillModelActionVoidView.as_view(), - name='bill-action-mark-as-void'), - path('items/bills//actions//mark-as-canceled/', - views.BillModelActionCanceledView.as_view(), - name='bill-action-mark-as-canceled'), - path('items/bills//actions//lock-ledger/', - views.BillModelActionLockLedgerView.as_view(), - name='bill-action-lock-ledger'), - path('items/bills//actions//unlock-ledger/', - views.BillModelActionUnlockLedgerView.as_view(), - name='bill-action-unlock-ledger'), - path('items/bills//actions//force-migration/', - views.BillModelActionForceMigrateView.as_view(), - name='bill-action-force-migrate'), + path( + "items/bills//create/", + views.BillModelCreateView.as_view(), + name="bill-create", + ), + path( + "items/bills//create/purchase-order//", + views.BillModelCreateView.as_view(for_purchase_order=True), + name="bill-create-po", + ), + path( + "items/bills//create/estimate//", + views.BillModelCreateView.as_view(for_estimate=True), + name="bill-create-estimate", + ), + path( + "items/bills//detail//", + views.BillModelDetailViewView.as_view(), + name="bill-detail", + ), + path( + "items/bills//update//", + views.BillModelUpdateViewView.as_view(), + name="bill-update", + ), + path( + "items/bills//update//items/", + views.BillModelUpdateViewView.as_view(action_update_items=True), + name="bill-update-items", + ), + ############################################################ + path( + "items/bills//actions//mark-as-draft/", + views.BillModelActionMarkAsDraftView.as_view(), + name="bill-action-mark-as-draft", + ), + path( + "items/bills//actions//mark-as-review/", + views.BillModelActionMarkAsInReviewView.as_view(), + name="bill-action-mark-as-review", + ), + path( + "items/bills//actions//mark-as-approved/", + views.BillModelActionMarkAsApprovedView.as_view(), + name="bill-action-mark-as-approved", + ), + path( + "items/bills//actions//mark-as-paid/", + views.BillModelActionMarkAsPaidView.as_view(), + name="bill-action-mark-as-paid", + ), + path( + "items/bills//actions//mark-as-void/", + views.BillModelActionVoidView.as_view(), + name="bill-action-mark-as-void", + ), + path( + "items/bills//actions//mark-as-canceled/", + views.BillModelActionCanceledView.as_view(), + name="bill-action-mark-as-canceled", + ), + path( + "items/bills//actions//lock-ledger/", + views.BillModelActionLockLedgerView.as_view(), + name="bill-action-lock-ledger", + ), + path( + "items/bills//actions//unlock-ledger/", + views.BillModelActionUnlockLedgerView.as_view(), + name="bill-action-unlock-ledger", + ), + path( + "items/bills//actions//force-migration/", + views.BillModelActionForceMigrateView.as_view(), + name="bill-action-force-migrate", + ), # path("items/bills/create/", views.bill_create, name="bill_create"), path( "items/bills//bill_detail/", @@ -775,130 +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//balance-sheet/', - views.BaseBalanceSheetRedirectView.as_view(), - name='entity-bs'), - path('entity//balance-sheet/year//', - views.FiscalYearBalanceSheetViewBase.as_view(), - name='entity-bs-year'), - path('entity//balance-sheet/quarter///', - views.QuarterlyBalanceSheetView.as_view(), - name='entity-bs-quarter'), - path('entity//balance-sheet/month///', - views.MonthlyBalanceSheetView.as_view(), - name='entity-bs-month'), - path('entity//balance-sheet/date////', - views.DateBalanceSheetView.as_view(), - name='entity-bs-date'), + path( + "entity//balance-sheet/", + views.BaseBalanceSheetRedirectView.as_view(), + name="entity-bs", + ), + path( + "entity//balance-sheet/year//", + views.FiscalYearBalanceSheetViewBase.as_view(), + name="entity-bs-year", + ), + path( + "entity//balance-sheet/quarter///", + views.QuarterlyBalanceSheetView.as_view(), + name="entity-bs-quarter", + ), + path( + "entity//balance-sheet/month///", + views.MonthlyBalanceSheetView.as_view(), + name="entity-bs-month", + ), + path( + "entity//balance-sheet/date////", + views.DateBalanceSheetView.as_view(), + name="entity-bs-date", + ), # INCOME STATEMENT Reports ---- # Entity ..... - path('entity//income-statement/', - views.BaseIncomeStatementRedirectViewBase.as_view(), - name='entity-ic'), - path('entity//income-statement/year//', - views.FiscalYearIncomeStatementViewBase.as_view(), - name='entity-ic-year'), - path('entity//income-statement/quarter///', - views.QuarterlyIncomeStatementView.as_view(), - name='entity-ic-quarter'), - path('entity//income-statement/month///', - views.MonthlyIncomeStatementView.as_view(), - name='entity-ic-month'), - path('entity//income-statement/date////', - views.MonthlyIncomeStatementView.as_view(), - name='entity-ic-date'), - # CASH FLOW STATEMENTS... + path( + "entity//income-statement/", + views.BaseIncomeStatementRedirectViewBase.as_view(), + name="entity-ic", + ), + path( + "entity//income-statement/year//", + views.FiscalYearIncomeStatementViewBase.as_view(), + name="entity-ic-year", + ), + path( + "entity//income-statement/quarter///", + views.QuarterlyIncomeStatementView.as_view(), + name="entity-ic-quarter", + ), + path( + "entity//income-statement/month///", + views.MonthlyIncomeStatementView.as_view(), + name="entity-ic-month", + ), + path( + "entity//income-statement/date////", + views.MonthlyIncomeStatementView.as_view(), + name="entity-ic-date", + ), + # CASH FLOW STATEMENTS... # Entities... - path('entity//cash-flow-statement/', - views.BaseCashFlowStatementRedirectViewBase.as_view(), - name='entity-cf'), - path('entity//cash-flow-statement/year//', - views.FiscalYearCashFlowStatementViewBase.as_view(), - name='entity-cf-year'), - path('entity//cash-flow-statement/quarter///', - views.QuarterlyCashFlowStatementView.as_view(), - name='entity-cf-quarter'), - path('entity//cash-flow-statement/month///', - views.MonthlyCashFlowStatementView.as_view(), - name='entity-cf-month'), - path('entity//cash-flow-statement/date////', - views.DateCashFlowStatementView.as_view(), - name='entity-cf-date'), - #Dashboard + path( + "entity//cash-flow-statement/", + views.BaseCashFlowStatementRedirectViewBase.as_view(), + name="entity-cf", + ), + path( + "entity//cash-flow-statement/year//", + views.FiscalYearCashFlowStatementViewBase.as_view(), + name="entity-cf-year", + ), + path( + "entity//cash-flow-statement/quarter///", + views.QuarterlyCashFlowStatementView.as_view(), + name="entity-cf-quarter", + ), + path( + "entity//cash-flow-statement/month///", + views.MonthlyCashFlowStatementView.as_view(), + name="entity-cf-month", + ), + path( + "entity//cash-flow-statement/date////", + views.DateCashFlowStatementView.as_view(), + name="entity-cf-date", + ), + # Dashboard # DASHBOARD Views... - path('/dashboard/', - views.EntityModelDetailHandlerViewBase.as_view(), - name='entity-dashboard'), - path('/dashboard/year//', - views.FiscalYearEntityModelDashboardView.as_view(), - name='entity-dashboard-year'), - path('/dashboard/quarter///', - views.QuarterlyEntityDashboardView.as_view(), - name='entity-dashboard-quarter'), - path('/dashboard/month///', - views.MonthlyEntityDashboardView.as_view(), - name='entity-dashboard-month'), - path('/dashboard/date////', - views.DateEntityDashboardView.as_view(), - name='entity-dashboard-date'), - #dashboard api - path('entity//data/net-payables/', - views.PayableNetAPIView.as_view(), - name='entity-json-net-payables'), - path('entity//data/net-receivables/', - views.ReceivableNetAPIView.as_view(), - name='entity-json-net-receivables'), - path('entity//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///activate_account/', views.activate_account, name='activate_account'), - path('management///permenant_delete_account/', views.permenant_delete_account, name='permenant_delete_account'), - path('management/audit_log_dashboard/', views.AuditLogDashboardView, name='audit_log_dashboard'), - - + path( + "/dashboard/", + views.EntityModelDetailHandlerViewBase.as_view(), + name="entity-dashboard", + ), + path( + "/dashboard/year//", + views.FiscalYearEntityModelDashboardView.as_view(), + name="entity-dashboard-year", + ), + path( + "/dashboard/quarter///", + views.QuarterlyEntityDashboardView.as_view(), + name="entity-dashboard-quarter", + ), + path( + "/dashboard/month///", + views.MonthlyEntityDashboardView.as_view(), + name="entity-dashboard-month", + ), + path( + "/dashboard/date////", + views.DateEntityDashboardView.as_view(), + name="entity-dashboard-date", + ), + # dashboard api + path( + "entity//data/net-payables/", + views.PayableNetAPIView.as_view(), + name="entity-json-net-payables", + ), + path( + "entity//data/net-receivables/", + views.ReceivableNetAPIView.as_view(), + name="entity-json-net-receivables", + ), + path( + "entity//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///activate_account/", + views.activate_account, + name="activate_account", + ), + path( + "management///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//detail/', views.PurchaseOrderDetailView.as_view(), name='purchase_order_detail'), - path('purchase_orders///update/', views.PurchaseOrderUpdateView.as_view(), name='purchase_order_update'), - path('purchase_orders//update//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//delete//', - views.PurchaseOrderModelDeleteView.as_view(), - name='po-delete'), - path('purchase_orders///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//detail/", + views.PurchaseOrderDetailView.as_view(), + name="purchase_order_detail", + ), + path( + "purchase_orders///update/", + views.PurchaseOrderUpdateView.as_view(), + name="purchase_order_update", + ), + path( + "purchase_orders//update//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//delete//", + views.PurchaseOrderModelDeleteView.as_view(), + name="po-delete", + ), + path( + "purchase_orders///upload/", + view=views.view_items_inventory, + name="view_items_inventory", + ), # Actions.... - path('/action//mark-as-draft/', - views.PurchaseOrderMarkAsDraftView.as_view(), - name='po-action-mark-as-draft'), - path('/action//mark-as-review/', - views.PurchaseOrderMarkAsReviewView.as_view(), - name='po-action-mark-as-review'), - path('/action//mark-as-approved/', - views.PurchaseOrderMarkAsApprovedView.as_view(), - name='po-action-mark-as-approved'), - path('/action//mark-as-fulfilled/', - views.PurchaseOrderMarkAsFulfilledView.as_view(), - name='po-action-mark-as-fulfilled'), - path('/action//mark-as-canceled/', - views.PurchaseOrderMarkAsCanceledView.as_view(), - name='po-action-mark-as-canceled'), - path('/action//mark-as-void/', - views.PurchaseOrderMarkAsVoidView.as_view(), - name='po-action-mark-as-void'), + path( + "/action//mark-as-draft/", + views.PurchaseOrderMarkAsDraftView.as_view(), + name="po-action-mark-as-draft", + ), + path( + "/action//mark-as-review/", + views.PurchaseOrderMarkAsReviewView.as_view(), + name="po-action-mark-as-review", + ), + path( + "/action//mark-as-approved/", + views.PurchaseOrderMarkAsApprovedView.as_view(), + name="po-action-mark-as-approved", + ), + path( + "/action//mark-as-fulfilled/", + views.PurchaseOrderMarkAsFulfilledView.as_view(), + name="po-action-mark-as-fulfilled", + ), + path( + "/action//mark-as-canceled/", + views.PurchaseOrderMarkAsCanceledView.as_view(), + name="po-action-mark-as-canceled", + ), + path( + "/action//mark-as-void/", + views.PurchaseOrderMarkAsVoidView.as_view(), + name="po-action-mark-as-void", + ), ] handler404 = "inventory.views.custom_page_not_found_view" diff --git a/inventory/utilities/financials.py b/inventory/utilities/financials.py index fab7a159..b9b32270 100644 --- a/inventory/utilities/financials.py +++ b/inventory/utilities/financials.py @@ -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 \ No newline at end of file + total = get_total_financials(instance, vat=True) + return total diff --git a/inventory/utilities/sa.py b/inventory/utilities/sa.py index 195ec5d9..07852dff 100644 --- a/inventory/utilities/sa.py +++ b/inventory/utilities/sa.py @@ -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 \ No newline at end of file + return rate diff --git a/inventory/utils.py b/inventory/utils.py index 384946b8..07b85a90 100644 --- a/inventory/utils.py +++ b/inventory/utils.py @@ -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 - diff --git a/inventory/validators.py b/inventory/validators.py index e1d20bb3..68a95412 100644 --- a/inventory/validators.py +++ b/inventory/validators.py @@ -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)") - ) \ No newline at end of file + regex=r"^(\+9665|05)[0-9]{8}$", + message=_("Enter a valid Saudi phone number (05XXXXXXXX or +9665XXXXXXXX)"), + ) diff --git a/inventory/views.py b/inventory/views.py index 8927a6a8..9bdcdfcb 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -8,6 +8,7 @@ import logging import tempfile import numpy as np from time import sleep + # from rich import print from random import randint from decimal import Decimal @@ -33,12 +34,19 @@ from django.db.models import Q from django.conf import settings from django.db.models import Func from django.contrib import messages -from django.http import Http404, HttpResponseBadRequest, HttpResponseNotFound, HttpResponseRedirect, JsonResponse, HttpResponseForbidden +from django.http import ( + Http404, + HttpResponseBadRequest, + HttpResponseNotFound, + HttpResponseRedirect, + JsonResponse, + HttpResponseForbidden, +) from django.forms import HiddenInput, ValidationError from django.shortcuts import HttpResponse from django.db.models import Sum, F, Count -from django.core.paginator import Paginator,EmptyPage, PageNotAnInteger +from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.contrib.auth.models import User from django.contrib.auth.models import Group from django.db.models import Value @@ -79,6 +87,7 @@ from django_ledger.views import ( LedgerModelCreateView as LedgerModelCreateViewBase, ) from django_ledger.forms.account import AccountModelCreateForm, AccountModelUpdateForm +from django_ledger.views.inventory import InventoryListView as InventoryListViewBase from django_ledger.views.entity import ( EntityModelDetailBaseView, EntityModelDetailHandlerView, @@ -93,18 +102,17 @@ from django_ledger.forms.bank_account import ( BankAccountUpdateForm, ) from django_ledger.views.bill import ( - # BillModelCreateView, - BillModelDetailView, - BillModelUpdateView, - BaseBillActionView as BaseBillActionViewBase, - BillModelModelBaseView + # BillModelCreateView, + BillModelDetailView, + BillModelUpdateView, + BaseBillActionView as BaseBillActionViewBase, + BillModelModelBaseView, ) from django_ledger.forms.bill import ( ApprovedBillModelUpdateForm, InReviewBillModelUpdateForm, get_bill_itemtxs_formset_class, - BillModelCreateForm - + BillModelCreateForm, ) from django_ledger.forms.invoice import ( DraftInvoiceModelUpdateForm, @@ -114,15 +122,19 @@ from django_ledger.forms.invoice import ( from django_ledger.forms.item import ( InventoryItemCreateForm, ) -from django_ledger.forms.purchase_order import (PurchaseOrderModelCreateForm, BasePurchaseOrderModelUpdateForm, - DraftPurchaseOrderModelUpdateForm, ReviewPurchaseOrderModelUpdateForm, - ApprovedPurchaseOrderModelUpdateForm, - get_po_itemtxs_formset_class) +from django_ledger.forms.purchase_order import ( + PurchaseOrderModelCreateForm, + BasePurchaseOrderModelUpdateForm, + DraftPurchaseOrderModelUpdateForm, + ReviewPurchaseOrderModelUpdateForm, + ApprovedPurchaseOrderModelUpdateForm, + get_po_itemtxs_formset_class, +) from django_ledger.views.purchase_order import ( PurchaseOrderModelDetailView as PurchaseOrderModelDetailViewBase, PurchaseOrderModelUpdateView as PurchaseOrderModelUpdateViewBase, BasePurchaseOrderActionActionView as BasePurchaseOrderActionActionViewBase, - PurchaseOrderModelDeleteView as PurchaseOrderModelDeleteViewBase + PurchaseOrderModelDeleteView as PurchaseOrderModelDeleteViewBase, ) from django_ledger.models import ( ItemTransactionModel, @@ -137,7 +149,7 @@ from django_ledger.models import ( ItemModel, BillModel, LedgerModel, - PurchaseOrderModel + PurchaseOrderModel, ) from django_ledger.views.financial_statement import ( FiscalYearBalanceSheetView, @@ -183,11 +195,10 @@ from .utils import ( ) from .tasks import create_accounts_for_make, send_email -#djago easy audit log +# djago easy audit log from easyaudit.models import RequestEvent, CRUDEvent, LoginEvent - logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) @@ -548,7 +559,7 @@ class SalesDashboard(LoginRequiredMixin, TemplateView): def terms_and_privacy(request): - return render(request, 'terms_and_privacy.html') + return render(request, "terms_and_privacy.html") class WelcomeView(TemplateView): @@ -983,15 +994,15 @@ class CarColorCreate(LoginRequiredMixin, PermissionRequiredMixin, CreateView): return context - -class CarColorsUpdateView( LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView): +class CarColorsUpdateView( + LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView +): model = models.CarColors form_class = forms.CarColorsForm template_name = "inventory/add_colors.html" success_message = _("Car Colors details updated successfully") permission_required = ["inventory.change_car"] - def get_object(self, queryset=None): """ Retrieves the CarColors instance associated with the Car slug from the URL. @@ -999,14 +1010,13 @@ class CarColorsUpdateView( LoginRequiredMixin, PermissionRequiredMixin, SuccessM """ # Get the car_slug from the URL keywords arguments - slug = self.kwargs.get('slug') + slug = self.kwargs.get("slug") # If no car_slug is provided, it's an invalid request if not slug: # You might want to raise Http404 or a more specific error here raise ValueError("Car slug is required to identify the colors to update.") - return get_object_or_404(models.CarColors, car__slug=slug) def get_success_url(self): @@ -1023,10 +1033,13 @@ class CarColorsUpdateView( LoginRequiredMixin, PermissionRequiredMixin, SuccessM """ context = super().get_context_data(**kwargs) # self.object is already available here from get_object() - context['car'] = self.object.car - context['page_title'] = _("Update Colors for %(car_name)s") % {'car_name': context['car']} + context["car"] = self.object.car + context["page_title"] = _("Update Colors for %(car_name)s") % { + "car_name": context["car"] + } return context + class CarListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): """ Represents a view for listing and managing a collection of cars. @@ -2140,7 +2153,14 @@ class CustomerCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView return redirect("customer_create") dealer = get_user_type(self.request) form.instance.dealer = dealer - user = form.instance.create_user_model() + try: + user = form.instance.create_user_model() + except IntegrityError as e: + if "UNIQUE constraint" in str(e): + messages.error(self.request, _("Email already exists")) + else: + messages.error(self.request, str(e)) + return redirect("customer_create") customer = form.instance.create_customer_model() form.instance.user = user @@ -2750,7 +2770,9 @@ class UserCreateView( except IntegrityError as e: messages.error( self.request, - _("A user with this email already exists. Please use a different email."), + _( + "A user with this email already exists. Please use a different email." + ), ) return redirect("user_create") staff_member = StaffMember.objects.create(user=user) @@ -3595,16 +3617,35 @@ def sales_list_view(request): dealer = get_user_type(request) entity = dealer.entity - transactions = ItemTransactionModel.objects.for_entity( - entity_slug=entity.slug, user_model=dealer.user - ).order_by("created") - paginator = Paginator(transactions, 30) + sale_orders = models.SaleOrder.objects.filter( + dealer=dealer, + ) + paginator = Paginator(sale_orders, 30) page_number = request.GET.get("page") page_obj = paginator.get_page(page_number) - txs = get_item_transactions(page_obj) - context = {"txs": txs, "page_obj": page_obj} + + context = {"txs": page_obj, "page_obj": page_obj} return render(request, "sales/sales_list.html", context) +class SaleOrderDetailView(LoginRequiredMixin, DetailView): + model = models.SaleOrder + template_name = 'sales/saleorder_detail.html' + context_object_name = 'sale_order' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + sale_order = self.get_object() + + # Add additional context data + context['status_choices'] = dict(models.SaleOrder.STATUS_CHOICES) + context['page_title'] = _('Sales Order Details') + + # Calculate any additional properties you want to display + context['is_delivered'] = sale_order.status == 'DELIVERED' + context['is_cancelled'] = sale_order.status == 'CANCELLED' + context['is_pending_approval'] = sale_order.status == 'PENDING_APPROVAL' + + return context # Estimates class EstimateListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): @@ -3772,7 +3813,9 @@ def create_estimate(request, slug=None): # ), # } # ) - car_instance = models.Car.objects.filter(hash=item.get("item_id"),finances__is_sold=False).all() + car_instance = models.Car.objects.filter( + hash=item.get("item_id"), finances__is_sold=False + ).all() for i in car_instance[: int(quantities[0])]: items_txs.append( @@ -3785,7 +3828,6 @@ def create_estimate(request, slug=None): } ) - estimate_itemtxs = { item.get("item_number"): { "unit_cost": item.get("unit_cost"), @@ -3850,15 +3892,16 @@ def create_estimate(request, slug=None): ) form = forms.EstimateModelCreateForm() - form.fields['customer'].queryset = models.Customer.objects.filter( - dealer=dealer, - active=True + form.fields["customer"].queryset = models.Customer.objects.filter( + dealer=dealer, active=True ) if slug: opportunity = models.Opportunity.objects.get(slug=slug) customer = opportunity.customer - form.fields['customer'].queryset = models.Customer.objects.filter(pk=customer.pk) + form.fields["customer"].queryset = models.Customer.objects.filter( + pk=customer.pk + ) form.initial["customer"] = customer car_list = ( @@ -3866,6 +3909,7 @@ def create_estimate(request, slug=None): dealer=dealer, colors__isnull=False, finances__isnull=False, + finances__selling_price__gt=0, status="available", ) .annotate( @@ -3963,13 +4007,14 @@ def create_sale_order(request, pk): POST data, or redirects to the estimate detail view upon successful creation. :rtype: HttpResponse """ + dealer = get_user_type(request) estimate = get_object_or_404(EstimateModel, pk=pk) items = estimate.get_itemtxs_data()[0].all() - if request.method == "POST": form = forms.SaleOrderForm(request.POST) if form.is_valid(): instance = form.save(commit=False) + instance.dealer = dealer instance.estimate = estimate instance.customer = estimate.customer.customer_set.first() instance.created_by = request.user @@ -3985,21 +4030,27 @@ def create_sale_order(request, pk): item.item_model.save() except KeyError: pass - dealer = get_user_type(request) item.item_model.car.mark_as_sold() + messages.success(request, "Sale Order created successfully") - return redirect("estimate_detail", pk=estimate.pk) - # models.Activity.objects.create(dealer=dealer,content_object=item.item_model.car, notes="Car Sold",created_by=request.user,activity_type=models.ActionChoices.SALE_CAR) else: print(form.errors) - - messages.success(request, "Sale Order created successfully") - return redirect("estimate_detail", pk=estimate.pk) + messages.error(request, "Invalid form data") + return redirect("estimate_detail", pk=estimate.pk) form = forms.SaleOrderForm() + customer = estimate.customer.customer_set.first() form.fields["estimate"].queryset = EstimateModel.objects.filter(pk=pk) form.initial["estimate"] = estimate + form.fields["customer"].queryset = models.Customer.objects.filter( + pk=customer.pk) + form.initial["customer"] = customer + if hasattr(estimate, "opportunity"): + form.initial["opportunity"] = estimate.opportunity + else: + form.fields["opportunity"].widget = HiddenInput() + calculator = CarFinanceCalculator(estimate) finance_data = calculator.get_finance_data() return render( @@ -4008,16 +4059,17 @@ def create_sale_order(request, pk): {"form": form, "estimate": estimate, "items": items, "data": finance_data}, ) + class SaleOrderDetail(DetailView): model = models.SaleOrder template_name = "sales/orders/order_details.html" context_object_name = "saleorder" def get_object(self, queryset=None): - order_pk = self.kwargs.get('order_pk') - return models.SaleOrder.objects.get( - pk=order_pk, - ) + order_pk = self.kwargs.get("order_pk") + return models.SaleOrder.objects.get( + pk=order_pk, + ) def get_context_data(self, **kwargs): saleorder = kwargs.get("object") @@ -4517,7 +4569,7 @@ def invoice_create(request, pk): "unearned_account": dealer.settings.invoice_unearned_account, } ) - print(dir(form.fields["customer"])) + context = { "form": form, @@ -4667,9 +4719,7 @@ def PaymentListView(request): page_number = request.GET.get("page") page_obj = paginator.get_page(page_number) - return render( - request, "sales/payments/payment_list.html", {"page_obj": page_obj} - ) + return render(request, "sales/payments/payment_list.html", {"page_obj": page_obj}) @login_required @@ -4825,7 +4875,7 @@ class LeadListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): if self.request.is_dealer: return qs staffmember = getattr(self.request.user, "staffmember", None) - if staff:= getattr(staffmember, "staff", None): + if staff := getattr(staffmember, "staff", None): return qs.filter(staff=staff) return models.Lead.objects.none() @@ -4873,7 +4923,11 @@ class LeadDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): lead=self.object ) context["transfer_form"] = forms.LeadTransferForm() - context["transfer_form"].fields["transfer_to"].queryset = (models.Staff.objects.filter(dealer=self.object.dealer,staff_type="sales")) + context["transfer_form"].fields[ + "transfer_to" + ].queryset = models.Staff.objects.filter( + dealer=self.object.dealer, staff_type="sales" + ) context["activity_form"] = forms.ActivityForm() context["staff_task_form"] = forms.StaffTaskForm() context["note_form"] = forms.NoteForm() @@ -4915,7 +4969,9 @@ def lead_create(request): instance.staff = form.cleaned_data.get("staff") if instance.lead_type == "customer": - customer = models.Customer.objects.filter(email=instance.email).first() + customer = models.Customer.objects.filter( + email=instance.email + ).first() if not customer: customer = models.Customer( dealer=dealer, @@ -4975,11 +5031,17 @@ def lead_create(request): id_car_make=int(make) ) else: - dealer_make_list = models.DealersMake.objects.filter(dealer=dealer).values_list("car_make",flat=True) - qs = form.fields["id_car_make"].queryset.filter(is_sa_import=True,pk__in=dealer_make_list) - form.fields["staff"].queryset = form.fields["staff"].queryset.filter(dealer=dealer,staff_type="sales") + dealer_make_list = models.DealersMake.objects.filter(dealer=dealer).values_list( + "car_make", flat=True + ) + qs = form.fields["id_car_make"].queryset.filter( + is_sa_import=True, pk__in=dealer_make_list + ) + form.fields["staff"].queryset = form.fields["staff"].queryset.filter( + dealer=dealer, staff_type="sales" + ) print(form.fields["staff"].queryset) - if hasattr(request.user.staffmember,"staff"): + if hasattr(request.user.staffmember, "staff"): form.initial["staff"] = request.user.staffmember.staff form.fields["staff"].widget.attrs["disabled"] = True form.fields["id_car_make"].queryset = qs @@ -5176,7 +5238,9 @@ def add_note_to_opportunity(request, slug): else: models.Notes.objects.create( dealer=dealer, - content_object=opportunity, created_by=request.user, note=notes + content_object=opportunity, + created_by=request.user, + note=notes, ) messages.success(request, _("Note added successfully")) return redirect("opportunity_detail", slug=opportunity.slug) @@ -5301,9 +5365,7 @@ def schedule_lead(request, slug): ) instance.save() - messages.success( - request, _("Appointment Created Successfully") - ) + messages.success(request, _("Appointment Created Successfully")) try: if lead.opportunity: return redirect("opportunity_detail", slug=lead.opportunity.slug) @@ -5386,8 +5448,12 @@ def send_lead_email(request, slug, email_pk=None): messages.success(request, _("Email Draft successfully")) try: if lead.opportunity: - response = HttpResponse(redirect("opportunity_detail", slug=lead.opportunity.slug)) - response["HX-Redirect"] = reverse("opportunity_detail", args=[lead.opportunity.slug]) + response = HttpResponse( + redirect("opportunity_detail", slug=lead.opportunity.slug) + ) + response["HX-Redirect"] = reverse( + "opportunity_detail", args=[lead.opportunity.slug] + ) else: response = HttpResponse(redirect("lead_detail", slug=lead.slug)) response["HX-Redirect"] = reverse("lead_detail", args=[lead.slug]) @@ -5541,7 +5607,6 @@ class OpportunityCreateView(CreateView, SuccessMessageMixin, LoginRequiredMixin) instance.lead.save() return super().form_valid(form) - def get_success_url(self): return reverse_lazy("opportunity_detail", kwargs={"slug": self.object.slug}) @@ -5648,7 +5713,7 @@ class OpportunityDetailView(LoginRequiredMixin, DetailView): "schedules": self.object.lead.get_all_schedules().filter( scheduled_at__gt=timezone.now() ), - } + } return context @@ -6096,7 +6161,7 @@ class BillListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): def get_queryset(self): dealer = get_user_type(self.request) - qs = dealer.entity.get_bills() + qs = dealer.entity.get_bills() return qs def get_context_data(self, **kwargs): @@ -6104,6 +6169,7 @@ class BillListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): context["entity"] = get_user_type(self.request).entity return context + class BillDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): """ Handles the functionality for viewing detailed information about a single bill. @@ -6441,6 +6507,7 @@ def bill_mark_as_paid(request, pk): # ) # bill_model.save() + # # Redirect to our custom URL # return HttpResponseRedirect( # reverse('bill-detail', @@ -6450,12 +6517,12 @@ def bill_mark_as_paid(request, pk): # }) # ) class BillModelCreateView(CreateView): - template_name = 'bill/bill_create.html' - PAGE_TITLE = _('Create Bill') + template_name = "bill/bill_create.html" + PAGE_TITLE = _("Create Bill") extra_context = { - 'page_title': PAGE_TITLE, - 'header_title': PAGE_TITLE, - 'header_subtitle_icon': 'uil:bill' + "page_title": PAGE_TITLE, + "header_title": PAGE_TITLE, + "header_subtitle_icon": "uil:bill", } for_purchase_order = False for_estimate = False @@ -6464,77 +6531,81 @@ class BillModelCreateView(CreateView): if not request.user.is_authenticated: return HttpResponseForbidden() - if self.for_estimate and 'ce_pk' in self.kwargs: + if self.for_estimate and "ce_pk" in self.kwargs: estimate_qs = EstimateModel.objects.for_entity( - entity_slug=self.kwargs['entity_slug'], - user_model=self.request.user + entity_slug=self.kwargs["entity_slug"], user_model=self.request.user + ) + estimate_model: EstimateModel = get_object_or_404( + estimate_qs, uuid__exact=self.kwargs["ce_pk"] ) - estimate_model: EstimateModel = get_object_or_404(estimate_qs, uuid__exact=self.kwargs['ce_pk']) if not estimate_model.can_bind(): - return HttpResponseNotFound('404 Not Found') + return HttpResponseNotFound("404 Not Found") return super(BillModelCreateView, self).get(request, **kwargs) def get_context_data(self, **kwargs): context = super(BillModelCreateView, self).get_context_data(**kwargs) if self.for_purchase_order: - po_pk = self.kwargs['po_pk'] - po_item_uuids_qry_param = self.request.GET.get('item_uuids') + po_pk = self.kwargs["po_pk"] + po_item_uuids_qry_param = self.request.GET.get("item_uuids") if po_item_uuids_qry_param: try: - po_item_uuids = po_item_uuids_qry_param.split(',') + po_item_uuids = po_item_uuids_qry_param.split(",") except: return HttpResponseBadRequest() else: return HttpResponseBadRequest() po_qs = PurchaseOrderModel.objects.for_entity( - entity_slug=self.kwargs['entity_slug'], - user_model=self.request.user - ).prefetch_related('itemtransactionmodel_set') + entity_slug=self.kwargs["entity_slug"], user_model=self.request.user + ).prefetch_related("itemtransactionmodel_set") po_model: PurchaseOrderModel = get_object_or_404(po_qs, uuid__exact=po_pk) po_itemtxs_qs = po_model.itemtransactionmodel_set.filter( - bill_model__isnull=True, - uuid__in=po_item_uuids + bill_model__isnull=True, uuid__in=po_item_uuids + ) + context["po_model"] = po_model + context["po_itemtxs_qs"] = po_itemtxs_qs + form_action = ( + reverse( + "bill-create-po", + kwargs={ + "entity_slug": self.kwargs["entity_slug"], + "po_pk": po_model.uuid, + }, + ) + + f"?item_uuids={po_item_uuids_qry_param}" ) - context['po_model'] = po_model - context['po_itemtxs_qs'] = po_itemtxs_qs - form_action = reverse('bill-create-po', - kwargs={ - 'entity_slug': self.kwargs['entity_slug'], - 'po_pk': po_model.uuid - }) + f'?item_uuids={po_item_uuids_qry_param}' elif self.for_estimate: estimate_qs = EstimateModel.objects.for_entity( - entity_slug=self.kwargs['entity_slug'], - user_model=self.request.user + entity_slug=self.kwargs["entity_slug"], user_model=self.request.user + ) + estimate_uuid = self.kwargs["ce_pk"] + estimate_model: EstimateModel = get_object_or_404( + estimate_qs, uuid__exact=estimate_uuid + ) + form_action = reverse( + "bill-create-estimate", + kwargs={ + "entity_slug": self.kwargs["entity_slug"], + "ce_pk": estimate_model.uuid, + }, ) - estimate_uuid = self.kwargs['ce_pk'] - estimate_model: EstimateModel = get_object_or_404(estimate_qs, uuid__exact=estimate_uuid) - form_action = reverse('bill-create-estimate', - kwargs={ - 'entity_slug': self.kwargs['entity_slug'], - 'ce_pk': estimate_model.uuid - }) else: - form_action = reverse('bill-create', - kwargs={ - 'entity_slug': self.kwargs['entity_slug'], - }) - context['form_action_url'] = form_action + form_action = reverse( + "bill-create", + kwargs={ + "entity_slug": self.kwargs["entity_slug"], + }, + ) + context["form_action_url"] = form_action return context def get_initial(self): - return { - 'date_draft': get_localdate() - } + return {"date_draft": get_localdate()} def get_form(self, form_class=None): dealer = get_user_type(self.request) - return BillModelCreateForm( - entity_model=dealer.entity, - **self.get_form_kwargs() - ) + return BillModelCreateForm(entity_model=dealer.entity, **self.get_form_kwargs()) def form_valid(self, form): dealer = get_user_type(self.request) @@ -6542,40 +6613,43 @@ class BillModelCreateView(CreateView): ledger_model, bill_model = bill_model.configure( entity_slug=dealer.entity.slug, user_model=self.request.user, - commit_ledger=True + commit_ledger=True, ) if self.for_estimate: - ce_pk = self.kwargs['ce_pk'] + ce_pk = self.kwargs["ce_pk"] estimate_model_qs = EstimateModel.objects.for_entity( - entity_slug=self.kwargs['entity_slug'], - user_model=self.request.user) + entity_slug=self.kwargs["entity_slug"], user_model=self.request.user + ) estimate_model = get_object_or_404(estimate_model_qs, uuid__exact=ce_pk) bill_model.bind_estimate(estimate_model=estimate_model, commit=False) elif self.for_purchase_order: - po_pk = self.kwargs['po_pk'] - item_uuids = self.request.GET.get('item_uuids') + po_pk = self.kwargs["po_pk"] + item_uuids = self.request.GET.get("item_uuids") if not item_uuids: return HttpResponseBadRequest() - item_uuids = item_uuids.split(',') + item_uuids = item_uuids.split(",") po_qs = PurchaseOrderModel.objects.for_entity( - entity_slug=self.kwargs['entity_slug'], - user_model=self.request.user + entity_slug=self.kwargs["entity_slug"], user_model=self.request.user ) po_model: PurchaseOrderModel = get_object_or_404(po_qs, uuid__exact=po_pk) try: bill_model.can_bind_po(po_model, raise_exception=True) except ValidationError as e: - messages.add_message(self.request, - message=e.message, - level=messages.ERROR, - extra_tags='is-danger') + messages.add_message( + self.request, + message=e.message, + level=messages.ERROR, + extra_tags="is-danger", + ) return self.render_to_response(self.get_context_data(form=form)) - po_model_items_qs = po_model.itemtransactionmodel_set.filter(uuid__in=item_uuids) + po_model_items_qs = po_model.itemtransactionmodel_set.filter( + uuid__in=item_uuids + ) if po_model.is_contract_bound(): bill_model.ce_model_id = po_model.ce_model_id @@ -6590,35 +6664,34 @@ class BillModelCreateView(CreateView): return super(BillModelCreateView, self).form_valid(form) def get_success_url(self): - entity_slug = self.kwargs['entity_slug'] + entity_slug = self.kwargs["entity_slug"] if self.for_purchase_order: - po_pk = self.kwargs['po_pk'] - return reverse('purchase_order_update', - kwargs={ - 'entity_slug': entity_slug, - 'po_pk': po_pk - }) + po_pk = self.kwargs["po_pk"] + return reverse( + "purchase_order_update", + kwargs={"entity_slug": entity_slug, "po_pk": po_pk}, + ) elif self.for_estimate: - return reverse('customer-estimate-detail', - kwargs={ - 'entity_slug': entity_slug, - 'ce_pk': self.kwargs['ce_pk'] - }) + return reverse( + "customer-estimate-detail", + kwargs={"entity_slug": entity_slug, "ce_pk": self.kwargs["ce_pk"]}, + ) bill_model: BillModel = self.object - return reverse('bill-detail', - kwargs={ - 'entity_slug': entity_slug, - 'bill_pk': bill_model.uuid - }) + return reverse( + "bill-detail", + kwargs={"entity_slug": entity_slug, "bill_pk": bill_model.uuid}, + ) class BillModelDetailViewView(BillModelDetailView): - template_name = 'bill/bill_detail.html' + template_name = "bill/bill_detail.html" + + class BillModelUpdateViewView(BillModelUpdateView): - template_name = 'bill/bill_update.html' + template_name = "bill/bill_update.html" + def post(self, request, *args, **kwargs): if self.action_update_items: - if not request.user.is_authenticated: return HttpResponseForbidden() @@ -6631,9 +6704,7 @@ class BillModelUpdateViewView(BillModelUpdateView): bill_itemtxs_formset_class = get_bill_itemtxs_formset_class(bill_model) itemtxs_formset = bill_itemtxs_formset_class( - request.POST, - bill_model=bill_model, - entity_model=entity_model + request.POST, bill_model=bill_model, entity_model=entity_model ) if itemtxs_formset.has_changed(): @@ -6650,38 +6721,50 @@ class BillModelUpdateViewView(BillModelUpdateView): bill_model.clean() bill_model.save( update_fields=[ - 'amount_due', - 'amount_receivable', - 'amount_unearned', - 'amount_earned', - 'updated' - ]) - - bill_model.migrate_state( - entity_slug=self.kwargs['entity_slug'], - user_model=self.request.user, - itemtxs_qs=itemtxs_qs, - raise_exception=False + "amount_due", + "amount_receivable", + "amount_unearned", + "amount_earned", + "updated", + ] ) - messages.add_message(request, - message=f'Items for Invoice {bill_model.bill_number} saved.', - level=messages.SUCCESS,) + bill_model.migrate_state( + entity_slug=self.kwargs["entity_slug"], + user_model=self.request.user, + itemtxs_qs=itemtxs_qs, + raise_exception=False, + ) + + messages.add_message( + request, + message=f"Items for Invoice {bill_model.bill_number} saved.", + level=messages.SUCCESS, + ) # if valid get saved formset from DB return HttpResponseRedirect( - redirect_to=reverse('bill-update', - kwargs={ - 'entity_slug': entity_model.slug, - 'bill_pk': bill_pk - }) + redirect_to=reverse( + "bill-update", + kwargs={ + "entity_slug": entity_model.slug, + "bill_pk": bill_pk, + }, + ) ) context = self.get_context_data(itemtxs_formset=itemtxs_formset) return self.render_to_response(context=context) return super(BillModelUpdateViewView, self).post(request, **kwargs) def get_success_url(self): - return reverse("bill-update", kwargs={"entity_slug": self.kwargs["entity_slug"], "bill_pk": self.kwargs["bill_pk"]}) + return reverse( + "bill-update", + kwargs={ + "entity_slug": self.kwargs["entity_slug"], + "bill_pk": self.kwargs["bill_pk"], + }, + ) + # @login_required # @permission_required("django_ledger.add_billmodel", raise_exception=True) @@ -8664,25 +8747,26 @@ def user_management(request): return render(request, "admin_management/user_management.html", context) - def AuditLogDashboardView(request): """ Displays audit logs (User Actions, Login Events, Request Events) with pagination. Log type is determined by the 'q' query parameter (e.g., ?q=userActions). Pagination page number is passed as a query parameter (e.g., ?page=2). """ - q = request.GET.get('q') # Get the log type from the 'q' query parameter - current_pagination_page = request.GET.get('page', 1) + q = request.GET.get("q") # Get the log type from the 'q' query parameter + current_pagination_page = request.GET.get("page", 1) context = {} template_name = None - logs_per_page = 30 # Define logs per page once + logs_per_page = 30 # Define logs per page once # --- Determine Data Source and Template based on 'q' parameter --- - if q=='userRequests': # This block handles cases where 'q' is 'requestEvents', None, or any other invalid value. - # It defaults to Request Logs if 'q' is not 'userActions' or 'loginEvents'. - template_name = 'admin_management/request_logs.html' - context['title'] = 'Request Logs Dashboard' - request_events = RequestEvent.objects.all().order_by('-datetime') + if ( + q == "userRequests" + ): # This block handles cases where 'q' is 'requestEvents', None, or any other invalid value. + # It defaults to Request Logs if 'q' is not 'userActions' or 'loginEvents'. + template_name = "admin_management/request_logs.html" + context["title"] = "Request Logs Dashboard" + request_events = RequestEvent.objects.all().order_by("-datetime") paginator = Paginator(request_events, logs_per_page) try: page_obj = paginator.page(current_pagination_page) @@ -8691,11 +8775,10 @@ def AuditLogDashboardView(request): except EmptyPage: page_obj = paginator.page(paginator.num_pages) - - elif q == 'loginEvents': - template_name = 'admin_management/auth_logs.html' - context['title'] = 'Login Events Dashboard' - auth_events = LoginEvent.objects.all().order_by('-datetime') + elif q == "loginEvents": + template_name = "admin_management/auth_logs.html" + context["title"] = "Login Events Dashboard" + auth_events = LoginEvent.objects.all().order_by("-datetime") paginator = Paginator(auth_events, logs_per_page) try: page_obj = paginator.page(current_pagination_page) @@ -8705,11 +8788,11 @@ def AuditLogDashboardView(request): page_obj = paginator.page(paginator.num_pages) else: - template_name = 'admin_management/model_logs.html' - context['title'] = 'User Actions Dashboard' + template_name = "admin_management/model_logs.html" + context["title"] = "User Actions Dashboard" # OPTIMIZATION: Get the QuerySet but don't evaluate it yet - model_events_queryset = CRUDEvent.objects.all().order_by('-datetime') + model_events_queryset = CRUDEvent.objects.all().order_by("-datetime") # 1. Paginate the raw QuerySet FIRST paginator = Paginator(model_events_queryset, logs_per_page) @@ -8724,15 +8807,17 @@ def AuditLogDashboardView(request): # 2. Now, process 'field_changes' ONLY for the events on the current page processed_model_events_for_page = [] - for event in page_obj_raw.object_list: # Loop only through the current page's items + for ( + event + ) in page_obj_raw.object_list: # Loop only through the current page's items event_data = { - 'datetime': event.datetime, - 'user': event.user, - 'event_type_display': event.get_event_type_display(), - 'model_name': event.content_type.model, - 'object_id': event.object_id, - 'object_repr': event.object_repr, - 'field_changes': [] + "datetime": event.datetime, + "user": event.user, + "event_type_display": event.get_event_type_display(), + "model_name": event.content_type.model, + "object_id": event.object_id, + "object_repr": event.object_repr, + "field_changes": [], } if event.changed_fields: @@ -8740,47 +8825,61 @@ def AuditLogDashboardView(request): changes = json.loads(event.changed_fields) if isinstance(changes, dict): for field_name, values in changes.items(): - old_value = values[0] if isinstance(values, list) and len(values) > 0 else None - new_value = values[1] if isinstance(values, list) and len(values) > 1 else None - event_data['field_changes'].append({ - 'field': field_name, - 'old': old_value, - 'new': new_value - }) + old_value = ( + values[0] + if isinstance(values, list) and len(values) > 0 + else None + ) + new_value = ( + values[1] + if isinstance(values, list) and len(values) > 1 + else None + ) + event_data["field_changes"].append( + { + "field": field_name, + "old": old_value, + "new": new_value, + } + ) elif changes is None: - event_data['field_changes'].append({ - 'field': 'Info', - 'old': '', - 'new': 'No specific field changes recorded (JSON was null)' - }) - else: # Handle valid JSON but not a dictionary (e.g., "[]", 123) - event_data['field_changes'].append({ - 'field': 'Error', - 'old': '', - 'new': f'Unexpected JSON format: {type(changes).__name__}' - }) + event_data["field_changes"].append( + { + "field": "Info", + "old": "", + "new": "No specific field changes recorded (JSON was null)", + } + ) + else: # Handle valid JSON but not a dictionary (e.g., "[]", 123) + event_data["field_changes"].append( + { + "field": "Error", + "old": "", + "new": f"Unexpected JSON format: {type(changes).__name__}", + } + ) except json.JSONDecodeError: # Handle invalid JSON; you might log this error - event_data['field_changes'].append({ - 'field': 'Error', - 'old': '', - 'new': 'Invalid JSON in changed_fields' - }) + event_data["field_changes"].append( + { + "field": "Error", + "old": "", + "new": "Invalid JSON in changed_fields", + } + ) processed_model_events_for_page.append(event_data) # 3. Replace the object_list of the original page_obj with the processed data # This keeps all pagination properties (has_next, number, etc.) intact. page_obj_raw.object_list = processed_model_events_for_page - page_obj = page_obj_raw # This will be passed to the context + page_obj = page_obj_raw # This will be passed to the context # Pass the final page object to the context - context['page_obj'] = page_obj + context["page_obj"] = page_obj return render(request, template_name, context) - - def activate_account(request, content_type, slug): try: model = apps.get_model(f"inventory.{content_type}") @@ -8827,26 +8926,29 @@ def permenant_delete_account(request, content_type, slug): def PurchaseOrderCreateView(request): dealer = get_user_type(request) entity = dealer.entity - if(request.method == "POST"): + if request.method == "POST": po = entity.create_purchase_order(po_title=request.POST.get("po_title")) po.entity = entity po.save() messages.success(request, _("Purchase order created successfully")) - return redirect('purchase_order_detail', pk=po.pk) + return redirect("purchase_order_detail", pk=po.pk) - form = PurchaseOrderModelCreateForm(entity_slug=entity.slug, user_model=entity.admin) + form = PurchaseOrderModelCreateForm( + entity_slug=entity.slug, user_model=entity.admin + ) return render(request, "purchase_orders/po_form.html", {"form": form}) + def InventoryItemCreateView(request): - for_po = request.GET.get('for_po') + for_po = request.GET.get("for_po") dealer = get_user_type(request) entity = dealer.entity coa = entity.get_default_coa() - inventory_accounts = entity.get_coa_accounts().filter(role='asset_ca_inv') - cogs_accounts = entity.get_coa_accounts().filter(role='cogs_regular') + inventory_accounts = entity.get_coa_accounts().filter(role="asset_ca_inv") + cogs_accounts = entity.get_coa_accounts().filter(role="cogs_regular") - if(request.method == "POST"): + if request.method == "POST": name = request.POST.get("name") account = request.POST.get("account") account = inventory_accounts.get(pk=account) @@ -8859,8 +8961,12 @@ def InventoryItemCreateView(request): serie = request.POST.get("serie") trim = request.POST.get("trim") year = request.POST.get("year") - exterior = models.ExteriorColors.objects.get(pk=request.POST.get("exterior")) - interior = models.InteriorColors.objects.get(pk=request.POST.get("interior")) + exterior = models.ExteriorColors.objects.get( + pk=request.POST.get("exterior") + ) + interior = models.InteriorColors.objects.get( + pk=request.POST.get("interior") + ) make_name = models.CarMake.objects.get(pk=make) model_name = models.CarModel.objects.get(pk=model) @@ -8868,28 +8974,44 @@ def InventoryItemCreateView(request): trim_name = models.CarTrim.objects.get(pk=trim) inventory_name = f"{make_name.name} || {model_name.name} || {serie_name.name} || {trim_name.name} || {year} || {exterior.name} || {interior.name}" - uom = entity.get_uom_all().get(name='Unit') + if inventory := entity.get_items_inventory().filter(name=inventory_name).first(): + messages.error(request, _("Inventory item already exists")) + return redirect(f"{reverse('inventory_item_create')}?for_po={for_po}") + uom = entity.get_uom_all().get(name="Unit") entity.create_item_inventory( name=inventory_name, uom_model=uom, item_type=ItemModel.ITEM_TYPE_MATERIAL, inventory_account=account, - coa_model=coa + coa_model=coa, ) messages.success(request, _("Inventory item created successfully")) - return redirect('purchase_order_list') + return redirect("purchase_order_list") if for_po: form = forms.CSVUploadForm() - context = {"make_data":models.CarMake.objects.all(),"inventory_accounts":inventory_accounts,"cogs_accounts":cogs_accounts,"form":form} - return render(request,'purchase_orders/car_inventory_item_form.html',context) - return render(request,'purchase_orders/inventory_item_form.html',{"make_data":models.CarMake.objects.all(),"inventory_accounts":inventory_accounts,"cogs_accounts":cogs_accounts}) + context = { + "make_data": models.CarMake.objects.all(), + "inventory_accounts": inventory_accounts, + "cogs_accounts": cogs_accounts, + "form": form, + } + return render(request, "purchase_orders/car_inventory_item_form.html", context) + return render( + request, + "purchase_orders/inventory_item_form.html", + { + "make_data": models.CarMake.objects.all(), + "inventory_accounts": inventory_accounts, + "cogs_accounts": cogs_accounts, + }, + ) def inventory_items_filter(request): dealer = get_user_type(request) - make = request.GET.get('make') - model = request.GET.get('model') - serie = request.GET.get('serie') + make = request.GET.get("make") + model = request.GET.get("model") + serie = request.GET.get("serie") model_data = models.CarModel.objects.none() serie_data = models.CarSerie.objects.none() @@ -8904,31 +9026,30 @@ def inventory_items_filter(request): serie = models.CarSerie.objects.get(pk=serie) trim_data = serie.cartrim_set.all() context = { - 'model_data': model_data, - 'serie_data': serie_data, - 'trim_data': trim_data, + "model_data": model_data, + "serie_data": serie_data, + "trim_data": trim_data, # 'inventory_items': dealer.entity.get_items_inventory(), # 'entity_slug': dealer.entity.slug, - } + } return render(request, "purchase_orders/car_inventory_item_form.html", context) -class PurchaseOrderDetailView(PurchaseOrderModelDetailViewBase): - template_name = 'purchase_orders/po_detail.html' - context_object_name = 'po_model' +class PurchaseOrderDetailView(PurchaseOrderModelDetailViewBase): + template_name = "purchase_orders/po_detail.html" + context_object_name = "po_model" def get_queryset(self): dealer = get_user_type(self.request) self.queryset = PurchaseOrderModel.objects.for_entity( - entity_slug=dealer.entity.slug, - user_model=dealer.entity.admin - ).select_related('entity', 'ce_model') + entity_slug=dealer.entity.slug, user_model=dealer.entity.admin + ).select_related("entity", "ce_model") return super().get_queryset() def get_context_data(self, **kwargs): dealer = get_user_type(self.request) context = super().get_context_data(**kwargs) - context['entity_slug'] = dealer.entity.slug + context["entity_slug"] = dealer.entity.slug return context @@ -8979,43 +9100,48 @@ class PurchaseOrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListVie def get_context_data(self, **kwargs): dealer = get_user_type(self.request) context = super().get_context_data(**kwargs) - context['entity_slug'] = dealer.entity.slug + context["entity_slug"] = dealer.entity.slug return context + class PurchaseOrderUpdateView(PurchaseOrderModelUpdateViewBase): - template_name = 'purchase_orders/po_update.html' - context_object_name = 'po_model' + template_name = "purchase_orders/po_update.html" + context_object_name = "po_model" + def get_context_data(self, **kwargs): dealer = get_user_type(self.request) context = super().get_context_data(**kwargs) - context['entity_slug'] = dealer.entity.slug - context['make_data'] = models.CarMake.objects.all() - context['model_data'] = models.CarModel.objects.none() - context['serie_data'] = models.CarSerie.objects.none() - context['trim_data'] = models.CarTrim.objects.none() + context["entity_slug"] = dealer.entity.slug + context["make_data"] = models.CarMake.objects.all() + context["model_data"] = models.CarModel.objects.none() + context["serie_data"] = models.CarSerie.objects.none() + context["trim_data"] = models.CarTrim.objects.none() return context def get_success_url(self): - return reverse('purchase_order_update', - kwargs={ - 'entity_slug': self.kwargs['entity_slug'], - 'po_pk': self.kwargs['po_pk'] - }) + return reverse( + "purchase_order_update", + kwargs={ + "entity_slug": self.kwargs["entity_slug"], + "po_pk": self.kwargs["po_pk"], + }, + ) + def get(self, request, entity_slug, po_pk, *args, **kwargs): if self.action_update_items: return HttpResponseRedirect( - redirect_to=reverse('purchase_order_update', - kwargs={ - 'entity_slug': entity_slug, - 'po_pk': po_pk - }) + redirect_to=reverse( + "purchase_order_update", + kwargs={"entity_slug": entity_slug, "po_pk": po_pk}, + ) ) - return super(PurchaseOrderModelUpdateViewBase, self).get(request, entity_slug, po_pk, *args, **kwargs) + return super(PurchaseOrderModelUpdateViewBase, self).get( + request, entity_slug, po_pk, *args, **kwargs + ) def post(self, request, entity_slug, *args, **kwargs): dealer = get_user_type(self.request) if self.action_update_items: - if not request.user.is_authenticated: return HttpResponseForbidden() @@ -9023,28 +9149,32 @@ class PurchaseOrderUpdateView(PurchaseOrderModelUpdateViewBase): po_model: PurchaseOrderModel = self.get_object(queryset=queryset) self.object = po_model po_itemtxs_formset_class = get_po_itemtxs_formset_class(po_model) - itemtxs_formset = po_itemtxs_formset_class(request.POST, - user_model=dealer.entity.admin, - po_model=po_model, - entity_slug=entity_slug) + itemtxs_formset = po_itemtxs_formset_class( + request.POST, + user_model=dealer.entity.admin, + po_model=po_model, + entity_slug=entity_slug, + ) if itemtxs_formset.has_changed(): if itemtxs_formset.is_valid(): itemtxs_list = itemtxs_formset.save(commit=False) create_bill_uuids = [ - str(i['uuid'].uuid) for i in itemtxs_formset.cleaned_data if i and i['create_bill'] is True + str(i["uuid"].uuid) + for i in itemtxs_formset.cleaned_data + if i and i["create_bill"] is True ] if create_bill_uuids: - item_uuids = ','.join(create_bill_uuids) + item_uuids = ",".join(create_bill_uuids) redirect_url = reverse( - 'bill-create-po', + "bill-create-po", kwargs={ - 'entity_slug': self.kwargs['entity_slug'], - 'po_pk': po_model.uuid, - } + "entity_slug": self.kwargs["entity_slug"], + "po_pk": po_model.uuid, + }, ) - redirect_url += f'?item_uuids={item_uuids}' + redirect_url += f"?item_uuids={item_uuids}" return HttpResponseRedirect(redirect_url) for itemtxs in itemtxs_list: @@ -9055,166 +9185,182 @@ class PurchaseOrderUpdateView(PurchaseOrderModelUpdateViewBase): itemtxs_list = itemtxs_formset.save() po_model.update_state() po_model.clean() - po_model.save(update_fields=['po_amount', - 'po_amount_received', - 'updated']) + po_model.save( + update_fields=["po_amount", "po_amount_received", "updated"] + ) # if valid get saved formset from DB - messages.add_message(request, messages.SUCCESS, 'PO items updated successfully.') + messages.add_message( + request, messages.SUCCESS, "PO items updated successfully." + ) return self.render_to_response(context=self.get_context_data()) # if not valid, return formset with errors... - return self.render_to_response(context=self.get_context_data(itemtxs_formset=itemtxs_formset)) - return super(PurchaseOrderUpdateView, self).post(request,entity_slug, *args, **kwargs) + return self.render_to_response( + context=self.get_context_data(itemtxs_formset=itemtxs_formset) + ) + return super(PurchaseOrderUpdateView, self).post( + request, entity_slug, *args, **kwargs + ) def get_form(self, form_class=None): po_model: PurchaseOrderModel = self.object if po_model.is_draft(): return DraftPurchaseOrderModelUpdateForm( - entity_slug=self.kwargs['entity_slug'], + entity_slug=self.kwargs["entity_slug"], user_model=self.request.user, - **self.get_form_kwargs() + **self.get_form_kwargs(), ) elif po_model.is_review(): return ReviewPurchaseOrderModelUpdateForm( - entity_slug=self.kwargs['entity_slug'], + entity_slug=self.kwargs["entity_slug"], user_model=self.request.user, - **self.get_form_kwargs() + **self.get_form_kwargs(), ) elif po_model.is_approved(): return ApprovedPurchaseOrderModelUpdateForm( - entity_slug=self.kwargs['entity_slug'], + entity_slug=self.kwargs["entity_slug"], user_model=self.request.user, - **self.get_form_kwargs() + **self.get_form_kwargs(), ) return BasePurchaseOrderModelUpdateForm( - entity_slug=self.kwargs['entity_slug'], + entity_slug=self.kwargs["entity_slug"], user_model=self.request.user, - **self.get_form_kwargs() + **self.get_form_kwargs(), ) class BasePurchaseOrderActionActionView(BasePurchaseOrderActionActionViewBase): def get_redirect_url(self, entity_slug, po_pk, *args, **kwargs): - return reverse('purchase_order_update', - kwargs={ - 'entity_slug': entity_slug, - 'po_pk': po_pk - }) + return reverse( + "purchase_order_update", kwargs={"entity_slug": entity_slug, "po_pk": po_pk} + ) + def get(self, request, *args, **kwargs): - kwargs['user_model'] = self.request.user + kwargs["user_model"] = self.request.user if not self.action_name: - raise ImproperlyConfigured('View attribute action_name is required.') - response = super(BasePurchaseOrderActionActionView, self).get(request, *args, **kwargs) + raise ImproperlyConfigured("View attribute action_name is required.") + response = super(BasePurchaseOrderActionActionView, self).get( + request, *args, **kwargs + ) po_model: PurchaseOrderModel = self.get_object() try: getattr(po_model, self.action_name)(commit=self.commit, **kwargs) except ValidationError as e: - messages.add_message(request, - message=e.message, - level=messages.ERROR, - ) + messages.add_message( + request, + message=e.message, + level=messages.ERROR, + ) return response + class PurchaseOrderModelDeleteView(PurchaseOrderModelDeleteViewBase): - template_name = 'purchase_orders/po_delete.html' + template_name = "purchase_orders/po_delete.html" def get_success_url(self): - return reverse('purchase_order_list') + return reverse("purchase_order_list") class PurchaseOrderMarkAsDraftView(BasePurchaseOrderActionActionView): - action_name = 'mark_as_draft' + action_name = "mark_as_draft" class PurchaseOrderMarkAsReviewView(BasePurchaseOrderActionActionView): - action_name = 'mark_as_review' + action_name = "mark_as_review" class PurchaseOrderMarkAsApprovedView(BasePurchaseOrderActionActionView): - action_name = 'mark_as_approved' + action_name = "mark_as_approved" class PurchaseOrderMarkAsFulfilledView(BasePurchaseOrderActionActionView): - action_name = 'mark_as_fulfilled' + action_name = "mark_as_fulfilled" class PurchaseOrderMarkAsCanceledView(BasePurchaseOrderActionActionView): - action_name = 'mark_as_canceled' + action_name = "mark_as_canceled" class PurchaseOrderMarkAsVoidView(BasePurchaseOrderActionActionView): - action_name = 'mark_as_void' + action_name = "mark_as_void" + ##############################bil class BaseBillActionView(BaseBillActionViewBase): def get_redirect_url(self, entity_slug, bill_pk, *args, **kwargs): - return reverse('bill-update', - kwargs={ - 'entity_slug': entity_slug, - 'bill_pk': bill_pk - }) + return reverse( + "bill-update", kwargs={"entity_slug": entity_slug, "bill_pk": bill_pk} + ) + class BillModelActionMarkAsDraftView(BaseBillActionView): - action_name = 'mark_as_draft' + action_name = "mark_as_draft" class BillModelActionMarkAsInReviewView(BaseBillActionView): - action_name = 'mark_as_review' + action_name = "mark_as_review" class BillModelActionMarkAsApprovedView(BaseBillActionView): - action_name = 'mark_as_approved' + action_name = "mark_as_approved" class BillModelActionMarkAsPaidView(BaseBillActionView): - action_name = 'mark_as_paid' + action_name = "mark_as_paid" class BillModelActionDeleteView(BaseBillActionView): - action_name = 'mark_as_delete' + action_name = "mark_as_delete" class BillModelActionVoidView(BaseBillActionView): - action_name = 'mark_as_void' + action_name = "mark_as_void" class BillModelActionCanceledView(BaseBillActionView): - action_name = 'mark_as_canceled' + action_name = "mark_as_canceled" class BillModelActionLockLedgerView(BaseBillActionView): - action_name = 'lock_ledger' + action_name = "lock_ledger" class BillModelActionUnlockLedgerView(BaseBillActionView): - action_name = 'unlock_ledger' + action_name = "unlock_ledger" class BillModelActionForceMigrateView(BaseBillActionView): - action_name = 'migrate_state' + action_name = "migrate_state" + ############################################################### ############################################################### -def view_items_inventory(request,entity_slug,po_pk): + +def view_items_inventory(request, entity_slug, po_pk): dealer = get_user_type(request) po = PurchaseOrderModel.objects.get(pk=po_pk) items = po.get_itemtxs_data()[0] - return render(request,'purchase_orders/po_upload_cars.html',{'po':po,"items":items}) + return render( + request, "purchase_orders/po_upload_cars.html", {"po": po, "items": items} + ) -def upload_cars(request,pk=None): + +def upload_cars(request, pk=None): item = None dealer = get_user_type(request) - response = redirect('upload_cars') + response = redirect("upload_cars") if pk: item = get_object_or_404(ItemTransactionModel, pk=pk) - response = redirect('upload_cars', pk=pk) + response = redirect("upload_cars", pk=pk) if item.item_model.additional_info.get("uploaded"): - messages.add_message(request, messages.ERROR, 'Item already uploaded.') - return redirect('view_items_inventory', entity_slug=dealer.slug, po_pk=item.po_model.pk) + messages.add_message(request, messages.ERROR, "Item already uploaded.") + return redirect( + "view_items_inventory", entity_slug=dealer.slug, po_pk=item.po_model.pk + ) - if request.method == 'POST': - csv_file = request.FILES.get('csv_file') + if request.method == "POST": + csv_file = request.FILES.get("csv_file") try: if item: @@ -9222,7 +9368,9 @@ def upload_cars(request,pk=None): data = [x.strip() for x in item.item_model.name.split("||")] make = models.CarMake.objects.get(name=data[0]) model = make.carmodel_set.get(name=data[1]) - trim = models.CarTrim.objects.filter(name=data[3],id_car_serie__id_car_model=model.id_car_model).first() + trim = models.CarTrim.objects.filter( + name=data[3], id_car_serie__id_car_model=model.id_car_model + ).first() serie = trim.id_car_serie year = data[4] exterior = models.ExteriorColors.objects.get(name=data[5]) @@ -9235,43 +9383,55 @@ def upload_cars(request,pk=None): model = models.CarModel.objects.get(pk=request.POST.get("model")) serie = models.CarSerie.objects.get(pk=request.POST.get("serie")) trim = models.CarTrim.objects.get(pk=request.POST.get("trim")) - exterior = models.ExteriorColors.objects.get(pk=request.POST.get("exterior")) - interior = models.InteriorColors.objects.get(pk=request.POST.get("interior")) + exterior = models.ExteriorColors.objects.get( + pk=request.POST.get("exterior") + ) + interior = models.InteriorColors.objects.get( + pk=request.POST.get("interior") + ) year = request.POST.get("year") - receiving_date = datetime.strptime(request.POST.get("receiving_date"), "%Y-%m-%d") + receiving_date = datetime.strptime( + request.POST.get("receiving_date"), "%Y-%m-%d" + ) vendor = models.Vendor.objects.get(pk=request.POST.get("vendor")) - except Exception as e: messages.error(request, f"Error processing CSV: {str(e)}") return response - if not csv_file.name.endswith('.csv'): + if not csv_file.name.endswith(".csv"): messages.error(request, "Please upload a CSV file") - return redirect('upload_cars') + return redirect("upload_cars") try: # Read the file content - file_content = csv_file.read().decode('utf-8') + file_content = csv_file.read().decode("utf-8") csv_data = io.StringIO(file_content) reader = csv.DictReader(csv_data) data = [x for x in reader] for row in data: - if result := decodevin(row['vin']): - if models.Car.objects.filter(vin=row['vin']).exists(): + if result := decodevin(row["vin"]): + if models.Car.objects.filter(vin=row["vin"]).exists(): messages.error(request, f"vin {row['vin']} already exists") return response manufacturer_name, model_name, year_model = result.values() car_make = get_make(manufacturer_name) car_model = get_model(model_name, car_make) - if not all([car_make, car_model]) or (make.pk != car_make.pk) or (model.pk != car_model.pk): - messages.error(request, f"invalid data at vin {row['vin']}, Please upload a valid CSV file") + if ( + not all([car_make, car_model]) + or (make.pk != car_make.pk) + or (model.pk != car_model.pk) + ): + messages.error( + request, + f"invalid data at vin {row['vin']}, Please upload a valid CSV file", + ) return response cars_created = 0 for row in data: car = models.Car.objects.create( dealer=dealer, - vin=row['vin'], + vin=row["vin"], id_car_make=make, id_car_model=model, id_car_serie=serie, @@ -9294,15 +9454,22 @@ def upload_cars(request,pk=None): form = forms.CSVUploadForm() form.fields["vendor"].queryset = dealer.vendors.all() - return render(request, 'csv_upload.html',{"make_data":models.CarMake.objects.all(),"form":form,"item":item}) + return render( + request, + "csv_upload.html", + {"make_data": models.CarMake.objects.all(), "form": form, "item": item}, + ) + + ############################################################### ############################################################### + @require_http_methods(["POST"]) def bulk_update_car_price(request): if request.method == "POST": - cars = request.POST.getlist('car') - price = request.POST.get('price') + cars = request.POST.getlist("car") + price = request.POST.get("price") if not price or int(price) <= 0: messages.error(request, "Please enter a valid price") @@ -9310,13 +9477,31 @@ def bulk_update_car_price(request): messages.error(request, "No cars selected for price update") else: for car_pk in cars: - car_finance , created = models.CarFinance.objects.get_or_create(car__pk=car_pk, cost_price=Decimal(price),selling_price=0) - if not created: - car_finance.cost_price = Decimal(price) - car_finance.selling_price = 0 - car_finance.save() + car = models.Car.objects.get(pk=car_pk) + if not hasattr(car, "finances"): + models.CarFinance.objects.create( + car=car, cost_price=Decimal(price), selling_price=0 + ) + else: + car.finances.cost_price = Decimal(price) + car.finances.selling_price = 0 + car.finances.save() messages.success(request, "Price updated successfully") response = HttpResponse() - response['HX-Redirect'] = reverse('car_list') + response["HX-Redirect"] = reverse("car_list") return response + + + +class InventoryListView(InventoryListViewBase): + template_name = "inventory/list.html" + + def get_queryset(self): + dealer = get_user_type(self.request) + + if self.queryset is None: + self.queryset = ItemTransactionModel.objects.inventory_pipeline_aggregate( + entity_slug=dealer.entity.slug, + ) + return super().get_queryset() \ No newline at end of file diff --git a/load_json_data.py b/load_json_data.py index 5afdd7e1..1c54bd75 100644 --- a/load_json_data.py +++ b/load_json_data.py @@ -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"], diff --git a/manage.py b/manage.py index c42f6d31..1aa0e173 100755 --- a/manage.py +++ b/manage.py @@ -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() diff --git a/merge_db.py b/merge_db.py index 934d148c..1819eb5d 100644 --- a/merge_db.py +++ b/merge_db.py @@ -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) \ No newline at end of file + print("Error:", e) diff --git a/populate.py b/populate.py index 9d9b09af..cfb31ec5 100644 --- a/populate.py +++ b/populate.py @@ -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")) diff --git a/run_haikal_qa.py b/run_haikal_qa.py index 83b11ff7..7ca28286 100644 --- a/run_haikal_qa.py +++ b/run_haikal_qa.py @@ -43,10 +43,7 @@ Provide a clear step-by-step guide with numbered instructions. Include: Helpful Step-by-Step Instructions:""" -PROMPT = PromptTemplate( - template=template, - input_variables=["context", "question"] -) +PROMPT = PromptTemplate(template=template, input_variables=["context", "question"]) # Setup QA chain qa = RetrievalQA.from_chain_type( @@ -54,23 +51,25 @@ qa = RetrievalQA.from_chain_type( chain_type="stuff", retriever=index.vectorstore.as_retriever(), return_source_documents=True, - chain_type_kwargs={"prompt": PROMPT} + chain_type_kwargs={"prompt": PROMPT}, ) + # Function to run a query def ask_haikal(query): response = qa.invoke({"query": query}) - print("\n" + "="*50) + print("\n" + "=" * 50) print(f"Question: {query}") - print("="*50) + print("=" * 50) print("\nAnswer:") print(response["result"]) print("\nSources:") for doc in response["source_documents"]: print(f"- {doc.metadata.get('source', 'Unknown source')}") - print("="*50) + print("=" * 50) return response["result"] + # Example query if __name__ == "__main__": query = "How do I add a new car to the inventory? answer in Arabic" diff --git a/scripts/carapi.py b/scripts/carapi.py index 1467ff1c..fe5c9558 100644 --- a/scripts/carapi.py +++ b/scripts/carapi.py @@ -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) diff --git a/scripts/create_json.py b/scripts/create_json.py index 5051e7f5..05cb5921 100644 --- a/scripts/create_json.py +++ b/scripts/create_json.py @@ -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) \ No newline at end of file + with open(f"{table_name}.json", "w") as f: + json.dump(data, f) diff --git a/scripts/generate.py b/scripts/generate.py index 347058c5..a97f295e 100644 --- a/scripts/generate.py +++ b/scripts/generate.py @@ -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"]) diff --git a/scripts/msg.py b/scripts/msg.py index 5aacab48..b8bbc559 100644 --- a/scripts/msg.py +++ b/scripts/msg.py @@ -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() - - - \ No newline at end of file diff --git a/scripts/new_wmis.py b/scripts/new_wmis.py index 27a6112d..7019506a 100644 --- a/scripts/new_wmis.py +++ b/scripts/new_wmis.py @@ -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) \ No newline at end of file +print("\nUpdated WMI dictionary source code:\n", wmi_source_code) diff --git a/scripts/one_time_shit.py b/scripts/one_time_shit.py index b04d9461..9937bb34 100644 --- a/scripts/one_time_shit.py +++ b/scripts/one_time_shit.py @@ -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() \ No newline at end of file + main() diff --git a/scripts/r.py b/scripts/r.py index ce7ded46..a626ac0a 100644 --- a/scripts/r.py +++ b/scripts/r.py @@ -1,6 +1,7 @@ from django.contrib.auth.models import User from inventory.models import Notification + def run(): user = User.objects.first() print(Notification.get_notification_data(user)) @@ -35,4 +36,3 @@ def run(): # ) # handle_payment(request,order) - diff --git a/scripts/report.py b/scripts/report.py index b8ccb1e3..7e889bae 100644 --- a/scripts/report.py +++ b/scripts/report.py @@ -1,9 +1,9 @@ from django_ledger.report.balance_sheet import BalanceSheetReport from django_ledger.models import EntityModel + def run(): - entity = EntityModel.objects.first() report = BalanceSheetReport(entity=entity) - print(report) \ No newline at end of file + print(report) diff --git a/scripts/run.py b/scripts/run.py index e8f1b5d7..0da08e00 100644 --- a/scripts/run.py +++ b/scripts/run.py @@ -3,6 +3,7 @@ from django_ledger.models import ( EntityModel, AccountModel, ) + # from rich import print from inventory.models import ( CarMake, @@ -15,8 +16,6 @@ User = get_user_model() load_dotenv(".env") - - def run(): # print(Service.objects.first().pk) # print(Appointment.objects.first().client) diff --git a/scripts/run1.py b/scripts/run1.py index 10d22438..911a9dd8 100644 --- a/scripts/run1.py +++ b/scripts/run1.py @@ -1,5 +1,6 @@ from dotenv import load_dotenv from django_ledger.models import EntityModel + # from rich import print from inventory.models import CarMake from django.contrib.auth import get_user_model @@ -8,6 +9,8 @@ from django_ledger.io import roles User = get_user_model() load_dotenv(".env") + + def run(): car_makes = CarMake.objects.all()[:10] @@ -16,11 +19,16 @@ def run(): coa = entity.get_default_coa() cogs = entity.get_default_coa_accounts().filter(role=roles.COGS).first() - last_account = entity.get_all_accounts().filter(role=roles.LIABILITY_CL_ACC_PAYABLE).order_by('-created').first() + 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}" + code = f"{int(last_account.code) + 1}" print(code) @@ -52,4 +60,3 @@ def run(): # 'active': True, # 'coa_model': coa # Ensure the COA model is included # } - diff --git a/scripts/run2.py b/scripts/run2.py index 82833dfb..92f38bba 100644 --- a/scripts/run2.py +++ b/scripts/run2.py @@ -8,16 +8,16 @@ from django.contrib.auth import get_user_model User = get_user_model() load_dotenv(".env") + + def run(): - invoice = InvoiceModel.objects.filter(invoice_number='I-2025-0000000001').first() - calculator = CarFinanceCalculator(invoice) - finance_data = calculator.get_finance_data() - - for i in invoice.get_itemtxs_data()[0]: - car = Car.objects.get(vin=invoice.get_itemtxs_data()[0].first().item_model.name) - print(car.finances.total + car.finances.total_additionals) - print(car.finances.total_additionals) - print(finance_data.get("total_vat_amount")) - print(finance_data.get("total")) - + invoice = InvoiceModel.objects.filter(invoice_number="I-2025-0000000001").first() + calculator = CarFinanceCalculator(invoice) + finance_data = calculator.get_finance_data() + for i in invoice.get_itemtxs_data()[0]: + car = Car.objects.get(vin=invoice.get_itemtxs_data()[0].first().item_model.name) + print(car.finances.total + car.finances.total_additionals) + print(car.finances.total_additionals) + print(finance_data.get("total_vat_amount")) + print(finance_data.get("total")) diff --git a/scripts/set_plans.py b/scripts/set_plans.py index 8a3bc95d..a5593359 100644 --- a/scripts/set_plans.py +++ b/scripts/set_plans.py @@ -1,30 +1,31 @@ from plans.models import Plan, Quota from decimal import Decimal + def run(): # Create quotas first basic_quota = Quota.objects.create( - codename='basic_quota', - name='Basic Features', - description='Basic plan features', + codename="basic_quota", + name="Basic Features", + description="Basic plan features", is_boolean=True, - url='pricing' + url="pricing", ) pro_quota = Quota.objects.create( - codename='pro_quota', - name='Pro Features', - description='Pro plan features', + codename="pro_quota", + name="Pro Features", + description="Pro plan features", is_boolean=True, - url='pricing' + url="pricing", ) premium_quota = Quota.objects.create( - codename='premium_quota', - name='Premium Features', - description='Premium plan features', + codename="premium_quota", + name="Premium Features", + description="Premium plan features", is_boolean=True, - url='pricing' + url="pricing", ) # Create the plans @@ -36,7 +37,7 @@ def run(): default=True, available=True, visible=True, - order=1 + order=1, ) basic_plan.quotas.add(basic_quota) @@ -58,6 +59,6 @@ def run(): period=30, available=True, visible=True, - order=3 + order=3, ) - premium_plan.quotas.add(basic_quota, pro_quota, premium_quota) \ No newline at end of file + premium_plan.quotas.add(basic_quota, pro_quota, premium_quota) diff --git a/slug_data.py b/slug_data.py index fdd3c1d1..9cb79a43 100644 --- a/slug_data.py +++ b/slug_data.py @@ -3,6 +3,7 @@ import time from pathlib import Path from slugify import slugify + def process_json_file(input_file, output_file=None, batch_size=10000): """Add slugs to JSON data file with optimal performance""" if output_file is None: @@ -10,7 +11,7 @@ def process_json_file(input_file, output_file=None, batch_size=10000): start_time = time.time() - with open(input_file, 'r', encoding='utf-8') as f: + with open(input_file, "r", encoding="utf-8") as f: data = json.load(f) total = len(data) @@ -20,37 +21,39 @@ def process_json_file(input_file, output_file=None, batch_size=10000): for item in data: # Generate slug from name field - name = item['fields'].get('name', '') - pk = item['pk'] + name = item["fields"].get("name", "") + pk = item["pk"] if name: slug = slugify(name)[:50] # Truncate to 50 chars # Append PK to ensure uniqueness - item['fields']['slug'] = f"{slug}-{pk}" + item["fields"]["slug"] = f"{slug}-{pk}" else: # Fallback to model-pk if name is empty - model_name = item['model'].split('.')[-1] - item['fields']['slug'] = f"{model_name}-{pk}" + model_name = item["model"].split(".")[-1] + item["fields"]["slug"] = f"{model_name}-{pk}" processed += 1 if processed % batch_size == 0: print(f"Processed {processed}/{total} records...") # Save the modified data - with open(output_file, 'w', encoding='utf-8') as f: + with open(output_file, "w", encoding="utf-8") as f: json.dump(data, f, indent=2, ensure_ascii=False) print(f"Completed in {time.time() - start_time:.2f} seconds") print(f"Output saved to {output_file}") + if __name__ == "__main__": import argparse parser = argparse.ArgumentParser() - parser.add_argument('input_file', help='Path to input JSON file') - parser.add_argument('-o', '--output', help='Output file path') - parser.add_argument('-b', '--batch', type=int, default=10000, - help='Progress reporting batch size') + parser.add_argument("input_file", help="Path to input JSON file") + parser.add_argument("-o", "--output", help="Output file path") + parser.add_argument( + "-b", "--batch", type=int, default=10000, help="Progress reporting batch size" + ) args = parser.parse_args() - process_json_file(args.input_file, args.output, args.batch) \ No newline at end of file + process_json_file(args.input_file, args.output, args.batch) diff --git a/sql_agent.py b/sql_agent.py index bb38f081..e5214177 100644 --- a/sql_agent.py +++ b/sql_agent.py @@ -12,19 +12,20 @@ import os import logfire -logfire.configure(send_to_logfire='if-token-present') +logfire.configure(send_to_logfire="if-token-present") logfire.instrument_pydantic_ai() # Define the OpenAI model (replace with your actual model if needed) model = OpenAIModel( model_name="qwen2.5:14b", # Or your preferred model - provider=OpenAIProvider(base_url='http://localhost:11434/v1') # Or your provider + provider=OpenAIProvider(base_url="http://localhost:11434/v1"), # Or your provider ) class DatabaseSchema(BaseModel): tables: Dict[str, List[Dict[str, str]]] = Field( - description="A dictionary where keys are table names and values are lists of column dictionaries (name, type)") + description="A dictionary where keys are table names and values are lists of column dictionaries (name, type)" + ) # Agent to get the database schema @@ -37,7 +38,7 @@ schema_agent = Agent( Your ONLY response should be the raw JSON string representing the database schema. Do not include any other text. The JSON should be a dictionary where keys are table names, and values are lists of column dictionaries. Each column dictionary should include 'name', 'type', 'notnull', 'dflt_value', and 'pk' keys. - If there is an error, return a JSON string containing an "error" key with a list of error messages.""" + If there is an error, return a JSON string containing an "error" key with a list of error messages.""", ) @@ -97,7 +98,7 @@ Follow these strict steps: 7. **Error Handling:** If there's any error in generating or executing the SQL, return a JSON string with an "error" key and a list of error messages. -""" +""", ) # Example: # Schema: {'Country': [{'name': 'id', 'type': 'INTEGER'}, {'name': 'name', 'type': 'TEXT'}]} @@ -105,10 +106,11 @@ Follow these strict steps: # Generated SQL: SELECT name FROM Country; # Expected Answer: The countries are Belgium, England, France, ... + @sql_agent.tool async def execute_sql_query(ctx: RunContext[DatabaseSchema], query: str) -> str: """Executes the SQL query and returns a simple string answer.""" - db_path = os.path.join(os.getcwd(), 'db.sqlite3') + db_path = os.path.join(os.getcwd(), "db.sqlite3") print(query) try: conn = sqlite3.connect(db_path) @@ -125,7 +127,7 @@ async def execute_sql_query(ctx: RunContext[DatabaseSchema], query: str) -> str: async def main(): - db_path = os.path.join(os.getcwd(), 'db.sqlite3') + db_path = os.path.join(os.getcwd(), "db.sqlite3") print(f"Database path: {db_path}") user_question = "how many cars do we have in the inventory" @@ -144,7 +146,9 @@ async def main(): print("Parsed Database Schema:", database_schema) # 2. Use the schema to answer the user question - sql_response = await sql_agent.run(user_question, database_schema=database_schema.tables) + sql_response = await sql_agent.run( + user_question, database_schema=database_schema.tables + ) print("SQL Agent Response:", sql_response) print("SQL Agent Output:", sql_response.output) @@ -152,8 +156,10 @@ async def main(): print(f"Error executing SQL: {sql_response.output}") except json.JSONDecodeError: - print(f"Error: Could not parse schema agent response as JSON: {schema_result.output}") + print( + f"Error: Could not parse schema agent response as JSON: {schema_result.output}" + ) if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file + asyncio.run(main()) diff --git a/t1.py b/t1.py index cf875d79..dcfda023 100644 --- a/t1.py +++ b/t1.py @@ -3,7 +3,9 @@ import requests def get_models_for_make(): make = "honda" - url = f"https://vpic.nhtsa.dot.gov/api/vehicles/GetModelsForMake/{make}/?format=json" + url = ( + f"https://vpic.nhtsa.dot.gov/api/vehicles/GetModelsForMake/{make}/?format=json" + ) resp = requests.get(url) @@ -15,7 +17,6 @@ def get_models_for_make(): return {"Error": f"Request failed with status code {resp.status_code}"} - models = get_models_for_make() for model in models: - print(model["Model_Name"]) \ No newline at end of file + print(model["Model_Name"]) diff --git a/templates/inventory/list.html b/templates/inventory/list.html new file mode 100644 index 00000000..0df8ddf3 --- /dev/null +++ b/templates/inventory/list.html @@ -0,0 +1,83 @@ +{% extends 'base.html' %} +{% load i18n %} +{% load static %} +{% load custom_filters %} + +{% block content %} +
+
+ +
+
+
+

+ {% trans "Inventory Ordered" %} +

+
+
+ {% if inventory_ordered %} +
+ {% inventory_table inventory_ordered %} +
+ {% else %} +
+ +

{% trans "No inventory in ordered status." %}

+
+ {% endif %} +
+
+
+ + +
+
+
+

+ {% trans "Inventory In Transit" %} +

+
+
+ {% if inventory_in_transit %} +
+ {% inventory_table inventory_in_transit %} +
+ {% else %} +
+ +

{% trans "No inventory in transit." %}

+
+ {% endif %} +
+
+
+ + +
+
+
+

+ {% trans "Inventory Received" %} +

+
+
+ {% if inventory_received %} +
+ {% inventory_table inventory_received %} +
+ {% else %} +
+ +

{% trans "No inventory in received status." %}

+
+ {% endif %} +
+
+
+ + +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/inventory/tags/inventory_table.html b/templates/inventory/tags/inventory_table.html new file mode 100644 index 00000000..904a56c1 --- /dev/null +++ b/templates/inventory/tags/inventory_table.html @@ -0,0 +1,36 @@ +{% load custom_filters %} +{% load i18n %} + +
+ + + + + + + + + + + {% for i in inventory_list %} + + + + + + + {% endfor %} + + + + + + + + +
{% trans "Item" %}{% trans "UOM" %}{% trans "Quantity" %}{% trans "Value" %}
{{ i.item_model__name }}{{ i.item_model__uom__name }}{{ i.total_quantity | floatformat:3 }} + {{CURRENCY}}{{ i.total_value | currency_format }} +
{% trans "Total Value" %} + {{CURRENCY}}{{ inventory_total_value | currency_format }} +
+
\ No newline at end of file diff --git a/templates/sales/estimates/sale_order_form1.html b/templates/sales/estimates/sale_order_form1.html index 94479bb5..55a747ee 100644 --- a/templates/sales/estimates/sale_order_form1.html +++ b/templates/sales/estimates/sale_order_form1.html @@ -109,7 +109,7 @@
{% if form.customer %} - {{form.customer}} + {{form.customer|as_crispy_field}} {% endif %}
@@ -128,9 +128,6 @@ - -
- {{form.payment_method|as_crispy_field}}
@@ -146,25 +143,6 @@

Financial Details

- -
- -
- {{form.agreed_price|as_crispy_field}} -
- - -
-
- {{form.down_payment_amount|as_crispy_field}} -
-
- -
- {{form.loan_amount|as_crispy_field}} -
- -
@@ -174,8 +152,10 @@
- +
+ {{form.order_date|as_crispy_field}} +
{{form.expected_delivery_date|as_crispy_field}}
@@ -206,7 +186,7 @@
- + {% endblock %} diff --git a/templates/sales/saleorder_detail.html b/templates/sales/saleorder_detail.html new file mode 100644 index 00000000..27ee1d7f --- /dev/null +++ b/templates/sales/saleorder_detail.html @@ -0,0 +1,269 @@ +{% extends "base.html" %} +{% load i18n %} +{% load custom_filters %} + +{% block title %}{{ page_title }} - {{ sale_order.formatted_order_id }}{% endblock %} + +{% block content %} +
+
+
+

+ {% trans "Sales Order" %} #{{ sale_order.formatted_order_id }} + + {{ status_choices|get_item:sale_order.status }} + +

+
+ +
+ +
+
+

{% trans "Customer Information" %}

+

+ {% trans "Name" %}: {{ sale_order.full_name }}
+ {% if sale_order.customer %} + {% trans "Contact" %}: {{ sale_order.customer.phone_number }}
+ {% trans "Email" %}: {{ sale_order.customer.email }} + {% endif %} +

+
+ +
+

{% trans "Order Details" %}

+

+ {% trans "Order Date" %}: {{ sale_order.order_date|date }}
+ {% trans "Dealer" %}: {{ sale_order.dealer.name }}
+ {% trans "Created By" %}: {{ sale_order.created_by }} +

+
+
+ + + {% if sale_order.estimate %} +
+
+

{% trans "Estimate Information" %}

+
+
+
+
+

+ {% trans "Estimate Number" %}: {{ sale_order.estimate.estimate_number }}
+ {% trans "Date" %}: {{ sale_order.estimate.created|date }}
+ {% trans "Status" %}: {{ sale_order.estimate.get_status_display }} +

+
+
+ +
+
+ {% if sale_order.estimate.notes %} +
+ {% trans "Notes" %}: +
{{ sale_order.estimate.notes|linebreaks }}
+
+ {% endif %} +
+
+
+
+ {% endif %} + + + {% if sale_order.invoice %} +
+
+

{% trans "Invoice Information" %}

+
+
+
+
+

+ {% trans "Invoice Number" %}: {{ sale_order.invoice.invoice_number }}
+ {% trans "Date" %}: {{ sale_order.invoice.created|date }}
+ {% trans "Status" %}: + + {{ sale_order.invoice.invoice_status|capfirst }} + +

+
+
+

+ + + + + + + + + + + + + + + + + + + +
{% trans "Amount Paid" %}:{{CURRENCY}}{{ sale_order.invoice.amount_paid|floatformat:2 }}
{% trans "Balance Due" %}:{{CURRENCY}}{{ sale_order.invoice.amount_due|floatformat:2 }}
{% trans "Amount Unearned" %}:{{CURRENCY}}{{ sale_order.invoice.amount_unearned|floatformat:2 }}
{% trans "Amount Receivable" %}:{{CURRENCY}}{{ sale_order.invoice.amount_receivable|floatformat:2 }}
+

+
+
+ {% if sale_order.invoice.notes %} +
+ {% trans "Notes" %}: +
{{ sale_order.invoice.notes|linebreaks }}
+
+ {% endif %} +
+
+
+
+ {% endif %} + + + {% if sale_order.invoice.ledger %} +
+
+

{% trans "Ledger Information" %}

+
+
+
+
+

+ {% trans "Ledger Number" %}: {{ sale_order.invoice.ledger }}
+ {% trans "Date" %}: {{ sale_order.invoice.ledger.created|date }}
+

+
+
+

+ + + {% for je in sale_order.invoice.ledger.journal_entries.all %} + + + + {% endfor %} + +
{{je}}
+

+
+
+ {% if sale_order.invoice.notes %} +
+ {% trans "Notes" %}: +
{{ sale_order.invoice.notes|linebreaks }}
+
+ {% endif %} +
+
+
+
+ {% endif %} + + + {% if sale_order.expected_delivery_date or sale_order.actual_delivery_date %} +
+
+

{% trans "Delivery Information" %}

+

+ {% if sale_order.expected_delivery_date %} + {% trans "Expected Delivery" %}: {{ sale_order.expected_delivery_date|date:"DATE_FORMAT" }}
+ {% endif %} + {% if sale_order.actual_delivery_date %} + {% trans "Actual Delivery" %}: {{ sale_order.actual_delivery_date|date:"DATETIME_FORMAT" }} + {% endif %} +

+
+
+ {% endif %} + + + {% if sale_order.cars %} +
+
+

{% trans "Vehicles" %}

+
+ + + + + + + + + + + + {% for car in sale_order.cars %} + + + + + + + + {% endfor %} + +
{% trans "Make" %}{% trans "Model" %}{% trans "Year" %}{% trans "VIN" %}{% trans "Price" %}
{{ car.id_car_make }}{{ car.id_car_model }}{{ car.year }}{{ car.vin }}{{CURRENCY}}{{ car.finances.selling_price|floatformat:2 }}
+
+
+
+ {% endif %} + + + {% if is_cancelled %} +
+
+
+

{% trans "Order Cancelled" %}

+

+ {% trans "Cancellation Date" %}: {{ sale_order.cancelled_date|date:"DATETIME_FORMAT" }}
+ {% trans "Reason" %}: {{ sale_order.cancellation_reason }} +

+
+
+
+ {% endif %} + + + {% if sale_order.comments %} +
+
+

{% trans "Comments" %}

+
+
+ {{ sale_order.comments|linebreaks }} +
+
+
+
+ {% endif %} +
+ + +
+
+{% endblock %} \ No newline at end of file diff --git a/templates/sales/sales_list.html b/templates/sales/sales_list.html index 8d88ea03..2d88831e 100644 --- a/templates/sales/sales_list.html +++ b/templates/sales/sales_list.html @@ -125,13 +125,13 @@ {% for tx in txs %} -

{{tx.customer.customer_name}}

+

{{tx.customer}}

-

{{tx.customer.address_1}}

+

{{tx.customer.address}}

-

{{tx.customer.phone}}

+

{{tx.customer.phone_number}}

{{tx.info.make}} @@ -147,7 +147,7 @@

{{tx.finance.total}}

- {% if tx.has_estimate %} + {% if tx.estimate %}

{{tx.estimate.estimate_number}} @@ -165,7 +165,7 @@ {% endif %} - {% if tx.has_invoice %} + {% if tx.invoice %}

{{tx.invoice.invoice_number}} @@ -204,7 +204,7 @@

- diff --git a/test_ollama.py b/test_ollama.py index c1cd8e01..31826eb7 100644 --- a/test_ollama.py +++ b/test_ollama.py @@ -15,26 +15,21 @@ from haikalbot.views import ModelAnalystView from haikalbot.models import AnalysisCache - - - class ModelAnalystViewTest(TestCase): def setUp(self): self.factory = RequestFactory() self.user = User.objects.create_user( - username='testuser', email='test@example.com', password='testpass' + username="testuser", email="test@example.com", password="testpass" ) self.superuser = User.objects.create_superuser( - username='admin', email='admin@example.com', password='adminpass' + username="admin", email="admin@example.com", password="adminpass" ) self.view = ModelAnalystView() def test_post_without_prompt(self): """Test that the view returns an error when no prompt is provided.""" request = self.factory.post( - '/analyze/', - data=json.dumps({}), - content_type='application/json' + "/analyze/", data=json.dumps({}), content_type="application/json" ) request.user = self.user @@ -42,15 +37,13 @@ class ModelAnalystViewTest(TestCase): self.assertEqual(response.status_code, 400) content = json.loads(response.content) - self.assertEqual(content['status'], 'error') - self.assertEqual(content['message'], 'Prompt is required') + self.assertEqual(content["status"], "error") + self.assertEqual(content["message"], "Prompt is required") def test_post_with_invalid_json(self): """Test that the view handles invalid JSON properly.""" request = self.factory.post( - '/analyze/', - data='invalid json', - content_type='application/json' + "/analyze/", data="invalid json", content_type="application/json" ) request.user = self.user @@ -58,32 +51,37 @@ class ModelAnalystViewTest(TestCase): self.assertEqual(response.status_code, 400) content = json.loads(response.content) - self.assertEqual(content['status'], 'error') - self.assertEqual(content['message'], 'Invalid JSON in request body') + self.assertEqual(content["status"], "error") + self.assertEqual(content["message"], "Invalid JSON in request body") - @patch('ai_analyst.views.ModelAnalystView._process_prompt') - @patch('ai_analyst.views.ModelAnalystView._check_permissions') - @patch('ai_analyst.views.ModelAnalystView._generate_hash') - @patch('ai_analyst.views.ModelAnalystView._get_cached_result') - @patch('ai_analyst.views.ModelAnalystView._cache_result') - def test_post_with_valid_prompt(self, mock_cache_result, mock_get_cached, - mock_generate_hash, mock_check_permissions, - mock_process_prompt): + @patch("ai_analyst.views.ModelAnalystView._process_prompt") + @patch("ai_analyst.views.ModelAnalystView._check_permissions") + @patch("ai_analyst.views.ModelAnalystView._generate_hash") + @patch("ai_analyst.views.ModelAnalystView._get_cached_result") + @patch("ai_analyst.views.ModelAnalystView._cache_result") + def test_post_with_valid_prompt( + self, + mock_cache_result, + mock_get_cached, + mock_generate_hash, + mock_check_permissions, + mock_process_prompt, + ): """Test that the view processes a valid prompt correctly.""" # Setup mocks mock_check_permissions.return_value = True - mock_generate_hash.return_value = 'test_hash' + mock_generate_hash.return_value = "test_hash" mock_get_cached.return_value = None mock_process_prompt.return_value = { - 'status': 'success', - 'insights': [{'type': 'test_insight'}] + "status": "success", + "insights": [{"type": "test_insight"}], } # Create request request = self.factory.post( - '/analyze/', - data=json.dumps({'prompt': 'How many cars do we have?', 'dealer_id': 1}), - content_type='application/json' + "/analyze/", + data=json.dumps({"prompt": "How many cars do we have?", "dealer_id": 1}), + content_type="application/json", ) request.user = self.user @@ -93,35 +91,39 @@ class ModelAnalystViewTest(TestCase): # Assertions self.assertEqual(response.status_code, 200) content = json.loads(response.content) - self.assertEqual(content['status'], 'success') - self.assertEqual(len(content['insights']), 1) + self.assertEqual(content["status"], "success") + self.assertEqual(len(content["insights"]), 1) # Verify function calls mock_check_permissions.assert_called_once_with(self.user, 1) - mock_generate_hash.assert_called_once_with('How many cars do we have?', 1) - mock_get_cached.assert_called_once_with('test_hash', self.user, 1) - mock_process_prompt.assert_called_once_with('How many cars do we have?', self.user, 1) + mock_generate_hash.assert_called_once_with("How many cars do we have?", 1) + mock_get_cached.assert_called_once_with("test_hash", self.user, 1) + mock_process_prompt.assert_called_once_with( + "How many cars do we have?", self.user, 1 + ) mock_cache_result.assert_called_once() - @patch('ai_analyst.views.ModelAnalystView._get_cached_result') - @patch('ai_analyst.views.ModelAnalystView._check_permissions') - @patch('ai_analyst.views.ModelAnalystView._generate_hash') - def test_post_with_cached_result(self, mock_generate_hash, mock_check_permissions, mock_get_cached): + @patch("ai_analyst.views.ModelAnalystView._get_cached_result") + @patch("ai_analyst.views.ModelAnalystView._check_permissions") + @patch("ai_analyst.views.ModelAnalystView._generate_hash") + def test_post_with_cached_result( + self, mock_generate_hash, mock_check_permissions, mock_get_cached + ): """Test that the view returns cached results when available.""" # Setup mocks mock_check_permissions.return_value = True - mock_generate_hash.return_value = 'test_hash' + mock_generate_hash.return_value = "test_hash" mock_get_cached.return_value = { - 'status': 'success', - 'insights': [{'type': 'cached_insight'}], - 'cached': True + "status": "success", + "insights": [{"type": "cached_insight"}], + "cached": True, } # Create request request = self.factory.post( - '/analyze/', - data=json.dumps({'prompt': 'How many cars do we have?', 'dealer_id': 1}), - content_type='application/json' + "/analyze/", + data=json.dumps({"prompt": "How many cars do we have?", "dealer_id": 1}), + content_type="application/json", ) request.user = self.user @@ -131,13 +133,13 @@ class ModelAnalystViewTest(TestCase): # Assertions self.assertEqual(response.status_code, 200) content = json.loads(response.content) - self.assertEqual(content['status'], 'success') - self.assertEqual(content['cached'], True) + self.assertEqual(content["status"], "success") + self.assertEqual(content["cached"], True) # Verify function calls mock_check_permissions.assert_called_once_with(self.user, 1) - mock_generate_hash.assert_called_once_with('How many cars do we have?', 1) - mock_get_cached.assert_called_once_with('test_hash', self.user, 1) + mock_generate_hash.assert_called_once_with("How many cars do we have?", 1) + mock_get_cached.assert_called_once_with("test_hash", self.user, 1) def test_check_permissions_superuser(self): """Test that superusers have permission to access any dealer data.""" @@ -149,46 +151,56 @@ class ModelAnalystViewTest(TestCase): def test_analyze_prompt_count(self): """Test that the prompt analyzer correctly identifies count queries.""" - analysis_type, target_models, query_params = self.view._analyze_prompt("How many cars do we have?") - self.assertEqual(analysis_type, 'count') - self.assertEqual(target_models, ['Car']) + analysis_type, target_models, query_params = self.view._analyze_prompt( + "How many cars do we have?" + ) + self.assertEqual(analysis_type, "count") + self.assertEqual(target_models, ["Car"]) self.assertEqual(query_params, {}) analysis_type, target_models, query_params = self.view._analyze_prompt( - "Count the number of users with active status") - self.assertEqual(analysis_type, 'count') - self.assertEqual(target_models, ['User']) - self.assertTrue('active' in query_params or 'status' in query_params) + "Count the number of users with active status" + ) + self.assertEqual(analysis_type, "count") + self.assertEqual(target_models, ["User"]) + self.assertTrue("active" in query_params or "status" in query_params) def test_analyze_prompt_relationship(self): """Test that the prompt analyzer correctly identifies relationship queries.""" analysis_type, target_models, query_params = self.view._analyze_prompt( - "Show relationship between User and Profile") - self.assertEqual(analysis_type, 'relationship') - self.assertTrue('User' in target_models and 'Profile' in target_models) + "Show relationship between User and Profile" + ) + self.assertEqual(analysis_type, "relationship") + self.assertTrue("User" in target_models and "Profile" in target_models) analysis_type, target_models, query_params = self.view._analyze_prompt( - "What is the User to Order relationship?") - self.assertEqual(analysis_type, 'relationship') - self.assertTrue('User' in target_models and 'Order' in target_models) + "What is the User to Order relationship?" + ) + self.assertEqual(analysis_type, "relationship") + self.assertTrue("User" in target_models and "Order" in target_models) def test_analyze_prompt_statistics(self): """Test that the prompt analyzer correctly identifies statistics queries.""" - analysis_type, target_models, query_params = self.view._analyze_prompt("What is the average price of cars?") - self.assertEqual(analysis_type, 'statistics') - self.assertEqual(target_models, ['Car']) - self.assertEqual(query_params['field'], 'price') - self.assertEqual(query_params['operation'], 'average') + analysis_type, target_models, query_params = self.view._analyze_prompt( + "What is the average price of cars?" + ) + self.assertEqual(analysis_type, "statistics") + self.assertEqual(target_models, ["Car"]) + self.assertEqual(query_params["field"], "price") + self.assertEqual(query_params["operation"], "average") - analysis_type, target_models, query_params = self.view._analyze_prompt("Show maximum age of users") - self.assertEqual(analysis_type, 'statistics') - self.assertEqual(target_models, ['User']) - self.assertEqual(query_params['field'], 'age') - self.assertEqual(query_params['operation'], 'maximum') + analysis_type, target_models, query_params = self.view._analyze_prompt( + "Show maximum age of users" + ) + self.assertEqual(analysis_type, "statistics") + self.assertEqual(target_models, ["User"]) + self.assertEqual(query_params["field"], "age") + self.assertEqual(query_params["operation"], "maximum") def test_normalize_model_name(self): """Test that model names are correctly normalized.""" - self.assertEqual(self.view._normalize_model_name('users'), 'User') - self.assertEqual(self.view._normalize_model_name('car'), 'Car') - self.assertEqual(self.view._normalize_model_name('orderItems'), - 'OrderItem') # This would actually need more logic to handle camelCase + self.assertEqual(self.view._normalize_model_name("users"), "User") + self.assertEqual(self.view._normalize_model_name("car"), "Car") + self.assertEqual( + self.view._normalize_model_name("orderItems"), "OrderItem" + ) # This would actually need more logic to handle camelCase diff --git a/tours/admin.py b/tours/admin.py index 0bdb93a8..ef3c9eca 100644 --- a/tours/admin.py +++ b/tours/admin.py @@ -3,4 +3,4 @@ from . import models # Register your models here. admin.site.register(models.Tour) -admin.site.register(models.TourCompletion) \ No newline at end of file +admin.site.register(models.TourCompletion) diff --git a/tours/apps.py b/tours/apps.py index 57f8071a..eec37a0c 100644 --- a/tours/apps.py +++ b/tours/apps.py @@ -2,5 +2,5 @@ from django.apps import AppConfig class ToursConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'tours' + default_auto_field = "django.db.models.BigAutoField" + name = "tours" diff --git a/tours/management/commands/generate_tours.py b/tours/management/commands/generate_tours.py index b7fa82e7..2b67893f 100644 --- a/tours/management/commands/generate_tours.py +++ b/tours/management/commands/generate_tours.py @@ -10,16 +10,18 @@ class Command(BaseCommand): def handle(self, *args, **kwargs): input_file = "haikal_kb.yaml" - output_dir = os.path.join(settings.BASE_DIR, 'static', 'js', 'tours') + output_dir = os.path.join(settings.BASE_DIR, "static", "js", "tours") # Create output directory if it doesn't exist os.makedirs(output_dir, exist_ok=True) try: - with open(input_file, 'r', encoding='utf-8') as f: + with open(input_file, "r", encoding="utf-8") as f: kb = yaml.safe_load(f) except Exception as e: - self.stdout.write(self.style.ERROR(f"Error reading knowledge base file: {e}")) + self.stdout.write( + self.style.ERROR(f"Error reading knowledge base file: {e}") + ) return workflows = kb.get("user_workflows", {}) @@ -62,7 +64,7 @@ class Command(BaseCommand): tour_step = { "title": f"Step {i + 1}", "intro": step, - "position": "bottom" + "position": "bottom", } if element: @@ -71,14 +73,24 @@ class Command(BaseCommand): tour_steps.append(tour_step) # Save the tour definition as JSON - tour_filename = workflow_name.lower().replace(' ', '_') + '_tour.json' - with open(os.path.join(output_dir, tour_filename), 'w', encoding='utf-8') as f: - json.dump({ - "name": workflow_name, - "description": workflow.get("description", ""), - "steps": tour_steps - }, f, indent=2) + tour_filename = workflow_name.lower().replace(" ", "_") + "_tour.json" + with open( + os.path.join(output_dir, tour_filename), "w", encoding="utf-8" + ) as f: + json.dump( + { + "name": workflow_name, + "description": workflow.get("description", ""), + "steps": tour_steps, + }, + f, + indent=2, + ) tours_created += 1 - self.stdout.write(self.style.SUCCESS(f"✅ Created {tours_created} IntroJS tour definitions in {output_dir}")) + self.stdout.write( + self.style.SUCCESS( + f"✅ Created {tours_created} IntroJS tour definitions in {output_dir}" + ) + ) diff --git a/tours/management/commands/generate_ui_map.py b/tours/management/commands/generate_ui_map.py index 30258ec3..6de3bad5 100644 --- a/tours/management/commands/generate_ui_map.py +++ b/tours/management/commands/generate_ui_map.py @@ -9,7 +9,7 @@ class Command(BaseCommand): help = "Generate UI element map for IntroJS tours" def handle(self, *args, **kwargs): - output_file = os.path.join('static', 'js', 'tours', 'ui_element_map.json') + output_file = os.path.join("static", "js", "tours", "ui_element_map.json") ui_map = { "pages": {}, @@ -17,72 +17,83 @@ class Command(BaseCommand): "navigation": { "inventory": "#inventory-menu, .inventory-nav, nav .inventory", "finance": "#finance-menu, .finance-nav, nav .finance", - "customers": "#customers-menu, .customers-nav, nav .customers" + "customers": "#customers-menu, .customers-nav, nav .customers", }, "actions": { "add": ".btn-add, .add-button, button:contains('Add')", "edit": ".btn-edit, .edit-button, button:contains('Edit')", "save": ".btn-save, button[type='submit'], #save-button", "cancel": ".btn-cancel, #cancel-button, button:contains('Cancel')", - "delete": ".btn-delete, .delete-button, button:contains('Delete')" + "delete": ".btn-delete, .delete-button, button:contains('Delete')", }, "forms": { "search": "#search-form, .search-input, input[name='q']", "date_range": ".date-range, input[type='date']", - "dropdown": "select, .dropdown, .select-field" - } - } + "dropdown": "select, .dropdown, .select-field", + }, + }, } # Extract URL patterns to identify pages resolver = get_resolver() for url_pattern in resolver.url_patterns: - if hasattr(url_pattern, 'name') and url_pattern.name: + if hasattr(url_pattern, "name") and url_pattern.name: pattern_name = url_pattern.name # Skip admin and API URLs - if pattern_name.startswith(('admin:', 'api:')): + if pattern_name.startswith(("admin:", "api:")): continue ui_map["pages"][pattern_name] = { "url_pattern": str(url_pattern.pattern), - "elements": {} + "elements": {}, } # Scan templates for UI elements with IDs - template_dirs = get_app_template_dirs('templates') + template_dirs = get_app_template_dirs("templates") for template_dir in template_dirs: for root, dirs, files in os.walk(template_dir): for file in files: - if not file.endswith(('.html', '.htm')): + if not file.endswith((".html", ".htm")): continue try: - with open(os.path.join(root, file), 'r', encoding='utf-8') as f: + with open(os.path.join(root, file), "r", encoding="utf-8") as f: content = f.read() # Try to identify the page/view this template is for - template_path = os.path.relpath(os.path.join(root, file), template_dir) - page_key = template_path.replace('/', '_').replace('.html', '') + template_path = os.path.relpath( + os.path.join(root, file), template_dir + ) + page_key = template_path.replace("/", "_").replace( + ".html", "" + ) # Create page entry if it doesn't exist if page_key not in ui_map["pages"]: ui_map["pages"][page_key] = { "template": template_path, - "elements": {} + "elements": {}, } # Extract elements with IDs import re + id_matches = re.findall(r'id=["\']([^"\']+)["\']', content) for id_match in id_matches: - ui_map["pages"][page_key]["elements"][id_match] = f"#{id_match}" + ui_map["pages"][page_key]["elements"][id_match] = ( + f"#{id_match}" + ) except Exception as e: - self.stdout.write(self.style.WARNING(f"Error processing template {file}: {e}")) + self.stdout.write( + self.style.WARNING(f"Error processing template {file}: {e}") + ) # Save UI map as JSON os.makedirs(os.path.dirname(output_file), exist_ok=True) - with open(output_file, 'w', encoding='utf-8') as f: + with open(output_file, "w", encoding="utf-8") as f: json.dump(ui_map, f, indent=2) - self.stdout.write(self.style.SUCCESS(f"✅ UI element map saved to {output_file}")) + self.stdout.write( + self.style.SUCCESS(f"✅ UI element map saved to {output_file}") + ) diff --git a/tours/management/commands/import_tours.py b/tours/management/commands/import_tours.py index 16474a30..4630dfd8 100644 --- a/tours/management/commands/import_tours.py +++ b/tours/management/commands/import_tours.py @@ -12,10 +12,12 @@ class Command(BaseCommand): input_file = "haikal_kb.yaml" try: - with open(input_file, 'r', encoding='utf-8') as f: + with open(input_file, "r", encoding="utf-8") as f: kb = yaml.safe_load(f) except Exception as e: - self.stdout.write(self.style.ERROR(f"Error reading knowledge base file: {e}")) + self.stdout.write( + self.style.ERROR(f"Error reading knowledge base file: {e}") + ) return workflows = kb.get("user_workflows", {}) @@ -29,11 +31,11 @@ class Command(BaseCommand): tour, created = Tour.objects.update_or_create( slug=slug, defaults={ - 'name': workflow_name, - 'description': workflow.get('description', ''), - 'tour_file': tour_file, - 'is_active': True - } + "name": workflow_name, + "description": workflow.get("description", ""), + "tour_file": tour_file, + "is_active": True, + }, ) if created: @@ -42,4 +44,6 @@ class Command(BaseCommand): else: self.stdout.write(self.style.SUCCESS(f"Updated tour: {workflow_name}")) - self.stdout.write(self.style.SUCCESS(f"✅ Imported {tours_created} tours from knowledge base")) + self.stdout.write( + self.style.SUCCESS(f"✅ Imported {tours_created} tours from knowledge base") + ) diff --git a/tours/migrations/0001_initial.py b/tours/migrations/0001_initial.py index d0939f6e..55ca1db9 100644 --- a/tours/migrations/0001_initial.py +++ b/tours/migrations/0001_initial.py @@ -6,7 +6,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - initial = True dependencies = [ @@ -15,26 +14,53 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Tour', + name="Tour", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100)), - ('description', models.TextField(blank=True)), - ('slug', models.SlugField(unique=True)), - ('tour_file', models.CharField(max_length=255)), - ('is_active', models.BooleanField(default=True)), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100)), + ("description", models.TextField(blank=True)), + ("slug", models.SlugField(unique=True)), + ("tour_file", models.CharField(max_length=255)), + ("is_active", models.BooleanField(default=True)), ], ), migrations.CreateModel( - name='TourCompletion', + name="TourCompletion", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('completed_on', models.DateTimeField(auto_now_add=True)), - ('tour', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tours.tour')), - ('user', models.ForeignKey(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", + ), + ), + ("completed_on", models.DateTimeField(auto_now_add=True)), + ( + "tour", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="tours.tour" + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'unique_together': {('tour', 'user')}, + "unique_together": {("tour", "user")}, }, ), ] diff --git a/tours/models.py b/tours/models.py index 5179072f..4c0db541 100644 --- a/tours/models.py +++ b/tours/models.py @@ -14,9 +14,8 @@ class Tour(models.Model): class TourCompletion(models.Model): tour = models.ForeignKey(Tour, on_delete=models.CASCADE) - user = models.ForeignKey('auth.User', on_delete=models.CASCADE) + user = models.ForeignKey("auth.User", on_delete=models.CASCADE) completed_on = models.DateTimeField(auto_now_add=True) class Meta: - unique_together = ('tour', 'user') - + unique_together = ("tour", "user") diff --git a/tours/urls.py b/tours/urls.py index ffa32531..d3ddf5e2 100644 --- a/tours/urls.py +++ b/tours/urls.py @@ -2,8 +2,8 @@ from django.urls import path from . import views urlpatterns = [ - path('', views.tour_list, name='tour_list'), - path('data//', views.get_tour_data, name='get_tour_data'), - path('complete//', views.mark_tour_completed, name='mark_tour_complete'), - path('start//', views.start_tour_view, name='start_tour'), + path("", views.tour_list, name="tour_list"), + path("data//", views.get_tour_data, name="get_tour_data"), + path("complete//", views.mark_tour_completed, name="mark_tour_complete"), + path("start//", views.start_tour_view, name="start_tour"), ] diff --git a/tours/views.py b/tours/views.py index 06a6839d..719bdeff 100644 --- a/tours/views.py +++ b/tours/views.py @@ -10,7 +10,7 @@ from .models import Tour, TourCompletion @login_required def tour_list(request): tours = Tour.objects.filter(is_active=True) - return render(request, 'tours/tour_list.html', {'tours': tours}) + return render(request, "tours/tour_list.html", {"tours": tours}) @login_required @@ -21,35 +21,34 @@ def get_tour_data(request, slug): completed = TourCompletion.objects.filter(tour=tour, user=request.user).exists() # Load the tour data from JSON file - tour_file_path = os.path.join(settings.BASE_DIR, 'static', 'js', 'tours', tour.tour_file) + tour_file_path = os.path.join( + settings.BASE_DIR, "static", "js", "tours", tour.tour_file + ) try: - with open(tour_file_path, 'r') as f: + with open(tour_file_path, "r") as f: tour_data = json.load(f) except (FileNotFoundError, json.JSONDecodeError): - return JsonResponse({'error': 'Tour data not found or invalid'}, status=404) + return JsonResponse({"error": "Tour data not found or invalid"}, status=404) - return JsonResponse({ - 'tour': tour_data, - 'completed': completed - }) + return JsonResponse({"tour": tour_data, "completed": completed}) @login_required def mark_tour_completed(request, slug): - if request.method != 'POST': - return JsonResponse({'error': 'Method not allowed'}, status=405) + if request.method != "POST": + return JsonResponse({"error": "Method not allowed"}, status=405) tour = get_object_or_404(Tour, slug=slug, is_active=True) # Mark the tour as completed for this user TourCompletion.objects.get_or_create(tour=tour, user=request.user) - return JsonResponse({'status': 'success'}) + return JsonResponse({"status": "success"}) @login_required def start_tour_view(request, slug): tour = get_object_or_404(Tour, slug=slug, is_active=True) # Redirect to the page where the tour should start - return render(request, 'tours/start_tour.html', {'tour': tour}) + return render(request, "tours/start_tour.html", {"tour": tour})