update
This commit is contained in:
commit
10cb1235d6
@ -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"
|
||||
|
||||
@ -32,4 +32,4 @@
|
||||
# await self.send(text_data=json.dumps({
|
||||
# 'message': 'VIN received',
|
||||
# 'vin': vin
|
||||
# }))
|
||||
# }))
|
||||
|
||||
@ -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"),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
@ -8,4 +8,3 @@ class CarVIN(models.Model):
|
||||
|
||||
def __str__(self):
|
||||
return self.vin
|
||||
|
||||
|
||||
@ -6,78 +6,93 @@ from inventory import models as inventory_models
|
||||
class CarVINSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = models.CarVIN
|
||||
fields = ['vin']
|
||||
fields = ["vin"]
|
||||
|
||||
def create(self, validated_data):
|
||||
vin = validated_data.pop('vin')
|
||||
vin = validated_data.pop("vin")
|
||||
return models.CarVIN.objects.create(vin=vin, **validated_data)
|
||||
|
||||
|
||||
class CarMakeSerializer(serializers.ModelSerializer):
|
||||
car_models = serializers.PrimaryKeyRelatedField(many=True, read_only=True, source='carmodel_set')
|
||||
car_models = serializers.PrimaryKeyRelatedField(
|
||||
many=True, read_only=True, source="carmodel_set"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = inventory_models.CarMake
|
||||
fields = '__all__'
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class CarModelSerializer(serializers.ModelSerializer):
|
||||
car_series = serializers.PrimaryKeyRelatedField(many=True, read_only=True, source='carserie_set')
|
||||
car_series = serializers.PrimaryKeyRelatedField(
|
||||
many=True, read_only=True, source="carserie_set"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = inventory_models.CarModel
|
||||
fields = '__all__'
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class CarSerieSerializer(serializers.ModelSerializer):
|
||||
car_trims = serializers.PrimaryKeyRelatedField(many=True, read_only=True, source='cartrim_set')
|
||||
car_trims = serializers.PrimaryKeyRelatedField(
|
||||
many=True, read_only=True, source="cartrim_set"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = inventory_models.CarSerie
|
||||
fields = '__all__'
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class CarTrimSerializer(serializers.ModelSerializer):
|
||||
car_equipments = serializers.PrimaryKeyRelatedField(many=True, read_only=True, source='carequipment_set')
|
||||
car_specification_values = serializers.PrimaryKeyRelatedField(many=True, read_only=True,
|
||||
source='carspecificationvalue_set')
|
||||
car_equipments = serializers.PrimaryKeyRelatedField(
|
||||
many=True, read_only=True, source="carequipment_set"
|
||||
)
|
||||
car_specification_values = serializers.PrimaryKeyRelatedField(
|
||||
many=True, read_only=True, source="carspecificationvalue_set"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = inventory_models.CarTrim
|
||||
fields = '__all__'
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class CarEquipmentSerializer(serializers.ModelSerializer):
|
||||
car_option_values = serializers.PrimaryKeyRelatedField(many=True, read_only=True, source='caroptionvalue_set')
|
||||
car_option_values = serializers.PrimaryKeyRelatedField(
|
||||
many=True, read_only=True, source="caroptionvalue_set"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = inventory_models.CarEquipment
|
||||
fields = '__all__'
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class CarSpecificationSerializer(serializers.ModelSerializer):
|
||||
child_specifications = serializers.PrimaryKeyRelatedField(many=True, read_only=True, source='carspecification_set')
|
||||
child_specifications = serializers.PrimaryKeyRelatedField(
|
||||
many=True, read_only=True, source="carspecification_set"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = inventory_models.CarSpecification
|
||||
fields = '__all__'
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class CarSpecificationValueSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = inventory_models.CarSpecificationValue
|
||||
fields = '__all__'
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class CarOptionSerializer(serializers.ModelSerializer):
|
||||
child_options = serializers.PrimaryKeyRelatedField(many=True, read_only=True, source='caroption_set')
|
||||
child_options = serializers.PrimaryKeyRelatedField(
|
||||
many=True, read_only=True, source="caroption_set"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = inventory_models.CarOption
|
||||
fields = '__all__'
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class CarOptionValueSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = inventory_models.CarOptionValue
|
||||
fields = '__all__'
|
||||
fields = "__all__"
|
||||
|
||||
@ -2,19 +2,12 @@ import requests
|
||||
|
||||
|
||||
def get_bearer():
|
||||
|
||||
api_token = "f5204a00-6f31-4de2-96d8-ed998e0d230c"
|
||||
api_secret = "8c11320781a5b8f4f327b6937e6f8241"
|
||||
|
||||
url = "https://carapi.app/api/auth/login"
|
||||
headers = {
|
||||
"accept": "text/plain",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
data = {
|
||||
"api_token": api_token,
|
||||
"api_secret": api_secret
|
||||
}
|
||||
headers = {"accept": "text/plain", "Content-Type": "application/json"}
|
||||
data = {"api_token": api_token, "api_secret": api_secret}
|
||||
|
||||
response = requests.post(url, headers=headers, json=data)
|
||||
|
||||
@ -26,11 +19,8 @@ def get_bearer():
|
||||
|
||||
|
||||
def get_car_data(vin):
|
||||
|
||||
url = f"https://carapi.app/api/vin/{vin}?verbose=no&all_trims=no"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {get_bearer()}"
|
||||
}
|
||||
headers = {"Authorization": f"Bearer {get_bearer()}"}
|
||||
|
||||
try:
|
||||
response = requests.get(url, headers=headers)
|
||||
@ -52,10 +42,8 @@ def get_from_cardatabase(vin):
|
||||
url = "https://api.vehicledatabases.com/premium/vin-decode/{vin}"
|
||||
|
||||
payload = {}
|
||||
headers = {
|
||||
'x-AuthKey': '3cefdfd4272445f1929b5801c55d8fa5'
|
||||
}
|
||||
headers = {"x-AuthKey": "3cefdfd4272445f1929b5801c55d8fa5"}
|
||||
|
||||
response = requests.request("GET", url, headers=headers, data=payload)
|
||||
|
||||
print(response.text)
|
||||
print(response.text)
|
||||
|
||||
@ -1,2 +1 @@
|
||||
|
||||
# Create your tests here.
|
||||
# Create your tests here.
|
||||
|
||||
24
api/urls.py
24
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"),
|
||||
]
|
||||
|
||||
31
api/views.py
31
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)
|
||||
return Response({"success": True, "data": vin_data}, status=status.HTTP_200_OK)
|
||||
|
||||
@ -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)),
|
||||
}
|
||||
)
|
||||
|
||||
@ -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)),
|
||||
)
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -5,6 +5,7 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "car_inventory.settings")
|
||||
django.setup()
|
||||
|
||||
from inventory.models import *
|
||||
|
||||
# from rich import print
|
||||
import random
|
||||
import datetime
|
||||
@ -31,7 +32,6 @@ def run():
|
||||
"LGJE1EE0XSM333551",
|
||||
"LGJE1EE02SM333561",
|
||||
"LGJE1EE0XSM333565",
|
||||
|
||||
]
|
||||
for vin in vin_list:
|
||||
try:
|
||||
@ -69,5 +69,6 @@ def run():
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
run()
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -20,10 +20,10 @@ from sqlalchemy.orm import relationship
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Configuration settings
|
||||
LLM_MODEL = getattr(settings, 'MODEL_ANALYZER_LLM_MODEL', 'qwen3:8b')
|
||||
LLM_TEMPERATURE = getattr(settings, 'MODEL_ANALYZER_LLM_TEMPERATURE', 0.3)
|
||||
LLM_MAX_TOKENS = getattr(settings, 'MODEL_ANALYZER_LLM_MAX_TOKENS', 2048)
|
||||
CACHE_TIMEOUT = getattr(settings, 'MODEL_ANALYZER_CACHE_TIMEOUT', 3600)
|
||||
LLM_MODEL = getattr(settings, "MODEL_ANALYZER_LLM_MODEL", "qwen3:8b")
|
||||
LLM_TEMPERATURE = getattr(settings, "MODEL_ANALYZER_LLM_TEMPERATURE", 0.3)
|
||||
LLM_MAX_TOKENS = getattr(settings, "MODEL_ANALYZER_LLM_MAX_TOKENS", 2048)
|
||||
CACHE_TIMEOUT = getattr(settings, "MODEL_ANALYZER_CACHE_TIMEOUT", 3600)
|
||||
|
||||
system_instruction = """
|
||||
You are a specialized AI agent designed to analyze Django models and extract relevant information based on user input in Arabic or English. You must:
|
||||
@ -91,52 +91,55 @@ class ModelAnalysis:
|
||||
class DjangoModelAnalyzer:
|
||||
def __init__(self):
|
||||
self.analysis_patterns = {
|
||||
'count': {
|
||||
'patterns': [r'\b(count|number|how many)\b'],
|
||||
'fields': ['id'],
|
||||
'weight': 1.0
|
||||
"count": {
|
||||
"patterns": [r"\b(count|number|how many)\b"],
|
||||
"fields": ["id"],
|
||||
"weight": 1.0,
|
||||
},
|
||||
'aggregate': {
|
||||
'patterns': [r'\b(average|avg|mean|sum|total)\b'],
|
||||
'fields': ['price', 'amount', 'value', 'cost', 'quantity'],
|
||||
'weight': 0.8
|
||||
"aggregate": {
|
||||
"patterns": [r"\b(average|avg|mean|sum|total)\b"],
|
||||
"fields": ["price", "amount", "value", "cost", "quantity"],
|
||||
"weight": 0.8,
|
||||
},
|
||||
"temporal": {
|
||||
"patterns": [r"\b(date|time|when|period)\b"],
|
||||
"fields": ["created_at", "updated_at", "date", "timestamp"],
|
||||
"weight": 0.7,
|
||||
},
|
||||
'temporal': {
|
||||
'patterns': [r'\b(date|time|when|period)\b'],
|
||||
'fields': ['created_at', 'updated_at', 'date', 'timestamp'],
|
||||
'weight': 0.7
|
||||
}
|
||||
}
|
||||
|
||||
def analyze_prompt(self, prompt: str, model_structure: List) -> ModelAnalysis:
|
||||
# Initialize LLM
|
||||
llm = ChatOllama(
|
||||
model=LLM_MODEL,
|
||||
temperature=LLM_TEMPERATURE
|
||||
)
|
||||
llm = ChatOllama(model=LLM_MODEL, temperature=LLM_TEMPERATURE)
|
||||
|
||||
# Get model analysis from LLM
|
||||
messages = [
|
||||
SystemMessage(content=system_instruction),
|
||||
HumanMessage(content=prompt)
|
||||
HumanMessage(content=prompt),
|
||||
]
|
||||
|
||||
try:
|
||||
response = llm.invoke(messages)
|
||||
|
||||
if not response or not hasattr(response, 'content') or response.content is None:
|
||||
if (
|
||||
not response
|
||||
or not hasattr(response, "content")
|
||||
or response.content is None
|
||||
):
|
||||
raise ValueError("Empty response from LLM")
|
||||
|
||||
analysis_requirements = self._parse_llm_response(response.content)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in LLM analysis: {e}")
|
||||
analysis_requirements = self._pattern_based_analysis(prompt, model_structure)
|
||||
analysis_requirements = self._pattern_based_analysis(
|
||||
prompt, model_structure
|
||||
)
|
||||
|
||||
return self._enhance_analysis(analysis_requirements, model_structure)
|
||||
|
||||
def _parse_llm_response(self, response: str) -> Dict:
|
||||
try:
|
||||
json_match = re.search(r'({.*})', response.replace('\n', ' '), re.DOTALL)
|
||||
json_match = re.search(r"({.*})", response.replace("\n", " "), re.DOTALL)
|
||||
if json_match:
|
||||
return json.loads(json_match.group(1))
|
||||
return {}
|
||||
@ -149,20 +152,22 @@ class DjangoModelAnalyzer:
|
||||
relevant_fields = []
|
||||
|
||||
for analysis_name, config in self.analysis_patterns.items():
|
||||
for pattern in config['patterns']:
|
||||
for pattern in config["patterns"]:
|
||||
if re.search(pattern, prompt.lower()):
|
||||
relevant_fields.extend(config['fields'])
|
||||
relevant_fields.extend(config["fields"])
|
||||
analysis_type = analysis_name
|
||||
break
|
||||
if analysis_type:
|
||||
break
|
||||
|
||||
return {
|
||||
'analysis_type': analysis_type or 'basic',
|
||||
'fields': list(set(relevant_fields)) or ['id', 'name']
|
||||
"analysis_type": analysis_type or "basic",
|
||||
"fields": list(set(relevant_fields)) or ["id", "name"],
|
||||
}
|
||||
|
||||
def _enhance_analysis(self, requirements: Dict, model_structure: List) -> ModelAnalysis:
|
||||
def _enhance_analysis(
|
||||
self, requirements: Dict, model_structure: List
|
||||
) -> ModelAnalysis:
|
||||
app_label = requirements.get("analysis_requirements", {}).get("app_label")
|
||||
model_name = requirements.get("analysis_requirements", {}).get("model_name")
|
||||
fields = requirements.get("analysis_requirements", {}).get("fields") or []
|
||||
@ -185,26 +190,31 @@ class DjangoModelAnalyzer:
|
||||
field_analysis = FieldAnalysis(
|
||||
name=field_name,
|
||||
field_type=field.get_internal_type(),
|
||||
is_required=not field.null if hasattr(field, 'null') else True,
|
||||
is_required=not field.null if hasattr(field, "null") else True,
|
||||
is_relation=field.is_relation,
|
||||
related_model=field.related_model.__name__ if field.is_relation and hasattr(field,
|
||||
'related_model') and field.related_model else None
|
||||
related_model=field.related_model.__name__
|
||||
if field.is_relation
|
||||
and hasattr(field, "related_model")
|
||||
and field.related_model
|
||||
else None,
|
||||
)
|
||||
|
||||
field_analysis.analysis_relevance = self._calculate_field_relevance(
|
||||
field_analysis,
|
||||
requirements.get('analysis_type', 'basic')
|
||||
field_analysis, requirements.get("analysis_type", "basic")
|
||||
)
|
||||
|
||||
relevant_fields.append(field_analysis)
|
||||
|
||||
if field.is_relation:
|
||||
relationships.append({
|
||||
'field': field_name,
|
||||
'type': field.get_internal_type(),
|
||||
'to': field.related_model.__name__ if hasattr(field,
|
||||
'related_model') and field.related_model else ''
|
||||
})
|
||||
relationships.append(
|
||||
{
|
||||
"field": field_name,
|
||||
"type": field.get_internal_type(),
|
||||
"to": field.related_model.__name__
|
||||
if hasattr(field, "related_model") and field.related_model
|
||||
else "",
|
||||
}
|
||||
)
|
||||
|
||||
except FieldDoesNotExist:
|
||||
logger.warning(f"Field {field_name} not found in {model_name}")
|
||||
@ -212,16 +222,20 @@ class DjangoModelAnalyzer:
|
||||
return ModelAnalysis(
|
||||
app_label=app_label,
|
||||
model_name=model_name,
|
||||
relevant_fields=sorted(relevant_fields, key=lambda x: x.analysis_relevance, reverse=True),
|
||||
relevant_fields=sorted(
|
||||
relevant_fields, key=lambda x: x.analysis_relevance, reverse=True
|
||||
),
|
||||
relationships=relationships,
|
||||
confidence_score=self._calculate_confidence_score(relevant_fields)
|
||||
confidence_score=self._calculate_confidence_score(relevant_fields),
|
||||
)
|
||||
|
||||
def _calculate_field_relevance(self, field: FieldAnalysis, analysis_type: str) -> float:
|
||||
def _calculate_field_relevance(
|
||||
self, field: FieldAnalysis, analysis_type: str
|
||||
) -> float:
|
||||
base_score = 0.5
|
||||
if analysis_type in self.analysis_patterns:
|
||||
if field.name in self.analysis_patterns[analysis_type]['fields']:
|
||||
base_score += self.analysis_patterns[analysis_type]['weight']
|
||||
if field.name in self.analysis_patterns[analysis_type]["fields"]:
|
||||
base_score += self.analysis_patterns[analysis_type]["weight"]
|
||||
if field.is_required:
|
||||
base_score += 0.2
|
||||
if field.is_relation:
|
||||
@ -257,26 +271,32 @@ def get_all_model_structures(filtered_apps: Optional[List[str]] = None) -> List[
|
||||
if field.is_relation:
|
||||
# Get related model name safely
|
||||
related_model_name = None
|
||||
if hasattr(field, 'related_model') and field.related_model:
|
||||
if hasattr(field, "related_model") and field.related_model:
|
||||
related_model_name = field.related_model.__name__
|
||||
elif hasattr(field, 'model') and field.model:
|
||||
elif hasattr(field, "model") and field.model:
|
||||
related_model_name = field.model.__name__
|
||||
|
||||
if related_model_name: # Only add relationship if we have a valid related model
|
||||
relationships.append({
|
||||
"field": field.name,
|
||||
"type": field.get_internal_type(),
|
||||
"to": related_model_name
|
||||
})
|
||||
if (
|
||||
related_model_name
|
||||
): # Only add relationship if we have a valid related model
|
||||
relationships.append(
|
||||
{
|
||||
"field": field.name,
|
||||
"type": field.get_internal_type(),
|
||||
"to": related_model_name,
|
||||
}
|
||||
)
|
||||
else:
|
||||
fields[field.name] = field.get_internal_type()
|
||||
|
||||
structures.append({
|
||||
"app_label": app_label,
|
||||
"model_name": model.__name__,
|
||||
"fields": fields,
|
||||
"relationships": relationships
|
||||
})
|
||||
structures.append(
|
||||
{
|
||||
"app_label": app_label,
|
||||
"model_name": model.__name__,
|
||||
"fields": fields,
|
||||
"relationships": relationships,
|
||||
}
|
||||
)
|
||||
|
||||
return structures
|
||||
|
||||
@ -332,25 +352,42 @@ def apply_filters(queryset: QuerySet, filters: Dict[str, Any]) -> QuerySet:
|
||||
for key, value in filters.items():
|
||||
if isinstance(value, dict):
|
||||
# Handle complex filters
|
||||
operation = value.get('operation', 'exact')
|
||||
filter_value = value.get('value')
|
||||
operation = value.get("operation", "exact")
|
||||
filter_value = value.get("value")
|
||||
|
||||
if not filter_value and operation != 'isnull':
|
||||
if not filter_value and operation != "isnull":
|
||||
continue
|
||||
|
||||
if operation == 'contains':
|
||||
if operation == "contains":
|
||||
q_objects.append(Q(**{f"{key}__icontains": filter_value}))
|
||||
elif operation == 'in':
|
||||
elif operation == "in":
|
||||
if isinstance(filter_value, list) and filter_value:
|
||||
q_objects.append(Q(**{f"{key}__in": filter_value}))
|
||||
elif operation in ['gt', 'gte', 'lt', 'lte', 'exact', 'iexact', 'startswith', 'endswith']:
|
||||
elif operation in [
|
||||
"gt",
|
||||
"gte",
|
||||
"lt",
|
||||
"lte",
|
||||
"exact",
|
||||
"iexact",
|
||||
"startswith",
|
||||
"endswith",
|
||||
]:
|
||||
q_objects.append(Q(**{f"{key}__{operation}": filter_value}))
|
||||
elif operation == 'between' and isinstance(filter_value, list) and len(filter_value) >= 2:
|
||||
q_objects.append(Q(**{
|
||||
f"{key}__gte": filter_value[0],
|
||||
f"{key}__lte": filter_value[1]
|
||||
}))
|
||||
elif operation == 'isnull':
|
||||
elif (
|
||||
operation == "between"
|
||||
and isinstance(filter_value, list)
|
||||
and len(filter_value) >= 2
|
||||
):
|
||||
q_objects.append(
|
||||
Q(
|
||||
**{
|
||||
f"{key}__gte": filter_value[0],
|
||||
f"{key}__lte": filter_value[1],
|
||||
}
|
||||
)
|
||||
)
|
||||
elif operation == "isnull":
|
||||
q_objects.append(Q(**{f"{key}__isnull": bool(filter_value)}))
|
||||
else:
|
||||
# Simple exact match
|
||||
@ -363,10 +400,10 @@ def apply_filters(queryset: QuerySet, filters: Dict[str, Any]) -> QuerySet:
|
||||
|
||||
|
||||
def process_aggregation(
|
||||
queryset: QuerySet,
|
||||
aggregation: str,
|
||||
fields: List[str],
|
||||
group_by: Optional[List[str]] = None
|
||||
queryset: QuerySet,
|
||||
aggregation: str,
|
||||
fields: List[str],
|
||||
group_by: Optional[List[str]] = None,
|
||||
) -> Union[Dict[str, Any], List[Dict[str, Any]]]:
|
||||
"""
|
||||
Process aggregation queries with support for grouping.
|
||||
@ -383,13 +420,7 @@ def process_aggregation(
|
||||
if not fields:
|
||||
return {"error": "No fields specified for aggregation"}
|
||||
|
||||
agg_func_map = {
|
||||
"sum": Sum,
|
||||
"avg": Avg,
|
||||
"count": Count,
|
||||
"max": Max,
|
||||
"min": Min
|
||||
}
|
||||
agg_func_map = {"sum": Sum, "avg": Avg, "count": Count, "max": Max, "min": Min}
|
||||
|
||||
agg_func = agg_func_map.get(aggregation.lower())
|
||||
if not agg_func:
|
||||
@ -404,22 +435,25 @@ def process_aggregation(
|
||||
agg_dict[f"{aggregation}_{field}"] = agg_func(field)
|
||||
|
||||
if not agg_dict:
|
||||
return {"error": "No valid fields for aggregation after excluding group_by fields"}
|
||||
return {
|
||||
"error": "No valid fields for aggregation after excluding group_by fields"
|
||||
}
|
||||
|
||||
# Apply group_by and aggregation
|
||||
return list(queryset.values(*group_by).annotate(**agg_dict))
|
||||
else:
|
||||
# Simple aggregation without grouping
|
||||
return queryset.aggregate(**{
|
||||
f"{aggregation}_{field}": agg_func(field)
|
||||
for field in fields
|
||||
})
|
||||
return queryset.aggregate(
|
||||
**{f"{aggregation}_{field}": agg_func(field) for field in fields}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Aggregation error: {e}")
|
||||
return {"error": f"Aggregation failed: {str(e)}"}
|
||||
|
||||
|
||||
def prepare_chart_data(data: List[Dict], fields: List[str], chart_type: str) -> Optional[Dict[str, Any]]:
|
||||
def prepare_chart_data(
|
||||
data: List[Dict], fields: List[str], chart_type: str
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Prepare data for chart visualization.
|
||||
|
||||
@ -449,15 +483,18 @@ def prepare_chart_data(data: List[Dict], fields: List[str], chart_type: str) ->
|
||||
return {
|
||||
"type": chart_type,
|
||||
"labels": [str(label).replace(f"{fields[0]}_", "") for label in labels],
|
||||
"data": [float(value) if isinstance(value, (int, float)) else 0 for value in values],
|
||||
"data": [
|
||||
float(value) if isinstance(value, (int, float)) else 0
|
||||
for value in values
|
||||
],
|
||||
"backgroundColor": [
|
||||
"rgba(54, 162, 235, 0.6)",
|
||||
"rgba(255, 99, 132, 0.6)",
|
||||
"rgba(255, 206, 86, 0.6)",
|
||||
"rgba(75, 192, 192, 0.6)",
|
||||
"rgba(153, 102, 255, 0.6)",
|
||||
"rgba(255, 159, 64, 0.6)"
|
||||
]
|
||||
"rgba(255, 159, 64, 0.6)",
|
||||
],
|
||||
}
|
||||
|
||||
# For regular query results as list of dictionaries
|
||||
@ -483,13 +520,14 @@ def prepare_chart_data(data: List[Dict], fields: List[str], chart_type: str) ->
|
||||
"labels": labels,
|
||||
"data": data_values,
|
||||
"backgroundColor": [
|
||||
"rgba(54, 162, 235, 0.6)",
|
||||
"rgba(255, 99, 132, 0.6)",
|
||||
"rgba(255, 206, 86, 0.6)",
|
||||
"rgba(75, 192, 192, 0.6)",
|
||||
"rgba(153, 102, 255, 0.6)",
|
||||
"rgba(255, 159, 64, 0.6)"
|
||||
] * (len(data_values) // 6 + 1) # Repeat colors as needed
|
||||
"rgba(54, 162, 235, 0.6)",
|
||||
"rgba(255, 99, 132, 0.6)",
|
||||
"rgba(255, 206, 86, 0.6)",
|
||||
"rgba(75, 192, 192, 0.6)",
|
||||
"rgba(153, 102, 255, 0.6)",
|
||||
"rgba(255, 159, 64, 0.6)",
|
||||
]
|
||||
* (len(data_values) // 6 + 1), # Repeat colors as needed
|
||||
}
|
||||
else:
|
||||
# For other charts, create dataset for each field after the first
|
||||
@ -501,17 +539,13 @@ def prepare_chart_data(data: List[Dict], fields: List[str], chart_type: str) ->
|
||||
"data": [float(item.get(field, 0) or 0) for item in data],
|
||||
"backgroundColor": f"rgba({50 + i * 50}, {100 + i * 40}, 235, 0.6)",
|
||||
"borderColor": f"rgba({50 + i * 50}, {100 + i * 40}, 235, 1.0)",
|
||||
"borderWidth": 1
|
||||
"borderWidth": 1,
|
||||
}
|
||||
datasets.append(dataset)
|
||||
except (ValueError, TypeError) as e:
|
||||
logger.warning(f"Error processing field {field} for chart: {e}")
|
||||
|
||||
return {
|
||||
"type": chart_type,
|
||||
"labels": labels,
|
||||
"datasets": datasets
|
||||
}
|
||||
return {"type": chart_type, "labels": labels, "datasets": datasets}
|
||||
except Exception as e:
|
||||
logger.error(f"Error preparing chart data: {e}")
|
||||
return None
|
||||
@ -556,7 +590,7 @@ def query_django_model(parsed: Dict[str, Any]) -> Dict[str, Any]:
|
||||
return {
|
||||
"status": "error",
|
||||
"error": "app_label and model are required",
|
||||
"language": language
|
||||
"language": language,
|
||||
}
|
||||
|
||||
# Get model class
|
||||
@ -566,7 +600,7 @@ def query_django_model(parsed: Dict[str, Any]) -> Dict[str, Any]:
|
||||
return {
|
||||
"status": "error",
|
||||
"error": f"Model '{model_name}' not found in app '{app_label}'",
|
||||
"language": language
|
||||
"language": language,
|
||||
}
|
||||
|
||||
# Validate fields against model
|
||||
@ -592,7 +626,7 @@ def query_django_model(parsed: Dict[str, Any]) -> Dict[str, Any]:
|
||||
return {
|
||||
"status": "error",
|
||||
"error": f"Invalid filters: {str(e)}",
|
||||
"language": language
|
||||
"language": language,
|
||||
}
|
||||
|
||||
# Handle aggregations
|
||||
@ -603,7 +637,7 @@ def query_django_model(parsed: Dict[str, Any]) -> Dict[str, Any]:
|
||||
return {
|
||||
"status": "error",
|
||||
"error": result["error"],
|
||||
"language": language
|
||||
"language": language,
|
||||
}
|
||||
|
||||
chart_data = None
|
||||
@ -614,7 +648,7 @@ def query_django_model(parsed: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"status": "success",
|
||||
"data": result,
|
||||
"chart": chart_data,
|
||||
"language": language
|
||||
"language": language,
|
||||
}
|
||||
|
||||
# Handle regular queries
|
||||
@ -649,9 +683,9 @@ def query_django_model(parsed: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"total_count": len(data),
|
||||
"fields": fields,
|
||||
"model": model_name,
|
||||
"app": app_label
|
||||
"app": app_label,
|
||||
},
|
||||
"language": language
|
||||
"language": language,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
@ -659,7 +693,7 @@ def query_django_model(parsed: Dict[str, Any]) -> Dict[str, Any]:
|
||||
return {
|
||||
"status": "error",
|
||||
"error": f"Query execution failed: {str(e)}",
|
||||
"language": language
|
||||
"language": language,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
@ -667,11 +701,13 @@ def query_django_model(parsed: Dict[str, Any]) -> Dict[str, Any]:
|
||||
return {
|
||||
"status": "error",
|
||||
"error": f"Unexpected error: {str(e)}",
|
||||
"language": parsed.get("language", "en")
|
||||
"language": parsed.get("language", "en"),
|
||||
}
|
||||
|
||||
|
||||
def determine_aggregation_type(prompt: str, fields: List[FieldAnalysis]) -> Optional[str]:
|
||||
def determine_aggregation_type(
|
||||
prompt: str, fields: List[FieldAnalysis]
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Determine the appropriate aggregation type based on the prompt and fields.
|
||||
|
||||
@ -682,21 +718,39 @@ def determine_aggregation_type(prompt: str, fields: List[FieldAnalysis]) -> Opti
|
||||
Returns:
|
||||
Aggregation type or None
|
||||
"""
|
||||
if any(pattern in prompt.lower() for pattern in ['average', 'avg', 'mean', 'معدل', 'متوسط']):
|
||||
return 'avg'
|
||||
elif any(pattern in prompt.lower() for pattern in ['sum', 'total', 'مجموع', 'إجمالي']):
|
||||
return 'sum'
|
||||
elif any(pattern in prompt.lower() for pattern in ['count', 'number', 'how many', 'عدد', 'كم']):
|
||||
return 'count'
|
||||
elif any(pattern in prompt.lower() for pattern in ['maximum', 'max', 'highest', 'أقصى', 'أعلى']):
|
||||
return 'max'
|
||||
elif any(pattern in prompt.lower() for pattern in ['minimum', 'min', 'lowest', 'أدنى', 'أقل']):
|
||||
return 'min'
|
||||
if any(
|
||||
pattern in prompt.lower()
|
||||
for pattern in ["average", "avg", "mean", "معدل", "متوسط"]
|
||||
):
|
||||
return "avg"
|
||||
elif any(
|
||||
pattern in prompt.lower() for pattern in ["sum", "total", "مجموع", "إجمالي"]
|
||||
):
|
||||
return "sum"
|
||||
elif any(
|
||||
pattern in prompt.lower()
|
||||
for pattern in ["count", "number", "how many", "عدد", "كم"]
|
||||
):
|
||||
return "count"
|
||||
elif any(
|
||||
pattern in prompt.lower()
|
||||
for pattern in ["maximum", "max", "highest", "أقصى", "أعلى"]
|
||||
):
|
||||
return "max"
|
||||
elif any(
|
||||
pattern in prompt.lower()
|
||||
for pattern in ["minimum", "min", "lowest", "أدنى", "أقل"]
|
||||
):
|
||||
return "min"
|
||||
|
||||
# Check field types for numeric fields to determine default aggregation
|
||||
numeric_fields = [field for field in fields if field.field_type in ['DecimalField', 'FloatField', 'IntegerField']]
|
||||
numeric_fields = [
|
||||
field
|
||||
for field in fields
|
||||
if field.field_type in ["DecimalField", "FloatField", "IntegerField"]
|
||||
]
|
||||
if numeric_fields:
|
||||
return 'sum' # Default to sum for numeric fields
|
||||
return "sum" # Default to sum for numeric fields
|
||||
|
||||
return None
|
||||
|
||||
@ -713,32 +767,47 @@ def determine_chart_type(prompt: str, fields: List[FieldAnalysis]) -> Optional[s
|
||||
Chart type or None
|
||||
"""
|
||||
# Check for explicit chart type mentions in prompt
|
||||
if any(term in prompt.lower() for term in ['line chart', 'time series', 'trend', 'رسم خطي', 'اتجاه']):
|
||||
return 'line'
|
||||
elif any(term in prompt.lower() for term in ['bar chart', 'histogram', 'column', 'رسم شريطي', 'أعمدة']):
|
||||
return 'bar'
|
||||
elif any(term in prompt.lower() for term in ['pie chart', 'circle chart', 'رسم دائري', 'فطيرة']):
|
||||
return 'pie'
|
||||
elif any(term in prompt.lower() for term in ['doughnut', 'دونات']):
|
||||
return 'doughnut'
|
||||
elif any(term in prompt.lower() for term in ['radar', 'spider', 'رادار']):
|
||||
return 'radar'
|
||||
if any(
|
||||
term in prompt.lower()
|
||||
for term in ["line chart", "time series", "trend", "رسم خطي", "اتجاه"]
|
||||
):
|
||||
return "line"
|
||||
elif any(
|
||||
term in prompt.lower()
|
||||
for term in ["bar chart", "histogram", "column", "رسم شريطي", "أعمدة"]
|
||||
):
|
||||
return "bar"
|
||||
elif any(
|
||||
term in prompt.lower()
|
||||
for term in ["pie chart", "circle chart", "رسم دائري", "فطيرة"]
|
||||
):
|
||||
return "pie"
|
||||
elif any(term in prompt.lower() for term in ["doughnut", "دونات"]):
|
||||
return "doughnut"
|
||||
elif any(term in prompt.lower() for term in ["radar", "spider", "رادار"]):
|
||||
return "radar"
|
||||
|
||||
# Determine chart type based on field types and count
|
||||
date_fields = [field for field in fields if field.field_type in ['DateField', 'DateTimeField']]
|
||||
numeric_fields = [field for field in fields if field.field_type in ['DecimalField', 'FloatField', 'IntegerField']]
|
||||
date_fields = [
|
||||
field for field in fields if field.field_type in ["DateField", "DateTimeField"]
|
||||
]
|
||||
numeric_fields = [
|
||||
field
|
||||
for field in fields
|
||||
if field.field_type in ["DecimalField", "FloatField", "IntegerField"]
|
||||
]
|
||||
|
||||
if date_fields and numeric_fields:
|
||||
return 'line' # Time series data
|
||||
return "line" # Time series data
|
||||
elif len(fields) == 2 and len(numeric_fields) >= 1:
|
||||
return 'bar' # Category and value
|
||||
return "bar" # Category and value
|
||||
elif len(fields) == 1 or (len(fields) == 2 and len(numeric_fields) == 1):
|
||||
return 'pie' # Single dimension data
|
||||
return "pie" # Single dimension data
|
||||
elif len(fields) > 2:
|
||||
return 'bar' # Multi-dimensional data
|
||||
return "bar" # Multi-dimensional data
|
||||
|
||||
# Default
|
||||
return 'bar'
|
||||
return "bar"
|
||||
|
||||
|
||||
def analyze_prompt(prompt: str) -> Dict[str, Any]:
|
||||
@ -752,8 +821,8 @@ def analyze_prompt(prompt: str) -> Dict[str, Any]:
|
||||
Dictionary with query results
|
||||
"""
|
||||
# Detect language
|
||||
language = "ar" if bool(re.search(r'[\u0600-\u06FF]', prompt)) else "en"
|
||||
filtered_apps = ['inventory']
|
||||
language = "ar" if bool(re.search(r"[\u0600-\u06FF]", prompt)) else "en"
|
||||
filtered_apps = ["inventory"]
|
||||
try:
|
||||
analyzer = DjangoModelAnalyzer()
|
||||
model_structure = get_all_model_structures(filtered_apps=filtered_apps)
|
||||
@ -761,23 +830,27 @@ def analyze_prompt(prompt: str) -> Dict[str, Any]:
|
||||
analysis = analyzer.analyze_prompt(prompt, model_structure)
|
||||
print(analysis)
|
||||
|
||||
|
||||
if not analysis or not analysis.app_label or not analysis.model_name:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "تعذر العثور على النموذج المطلوب" if language == "ar" else "Missing model information",
|
||||
"language": language
|
||||
"message": "تعذر العثور على النموذج المطلوب"
|
||||
if language == "ar"
|
||||
else "Missing model information",
|
||||
"language": language,
|
||||
}
|
||||
|
||||
query_params = {
|
||||
"app_label": analysis.app_label,
|
||||
"model_name": analysis.model_name,
|
||||
"fields": [field.name for field in analysis.relevant_fields],
|
||||
"joins": [{"path": rel["field"], "type": rel["type"]} for rel in analysis.relationships],
|
||||
"joins": [
|
||||
{"path": rel["field"], "type": rel["type"]}
|
||||
for rel in analysis.relationships
|
||||
],
|
||||
"filters": {},
|
||||
"aggregation": determine_aggregation_type(prompt, analysis.relevant_fields),
|
||||
"chart": determine_chart_type(prompt, analysis.relevant_fields),
|
||||
"language": language
|
||||
"language": language,
|
||||
}
|
||||
|
||||
return query_django_model(query_params)
|
||||
@ -785,6 +858,8 @@ def analyze_prompt(prompt: str) -> Dict[str, Any]:
|
||||
logger.error(f"Error analyzing prompt: {e}")
|
||||
return {
|
||||
"status": "error",
|
||||
"error": "حدث خطأ أثناء تحليل الاستعلام" if language == "ar" else f"Error analyzing prompt: {str(e)}",
|
||||
"language": language
|
||||
}
|
||||
"error": "حدث خطأ أثناء تحليل الاستعلام"
|
||||
if language == "ar"
|
||||
else f"Error analyzing prompt: {str(e)}",
|
||||
"language": language,
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -63,7 +63,6 @@ class DatabaseType(Enum):
|
||||
MYSQL = "mysql"
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class DatabaseConnection:
|
||||
db_type: DatabaseType
|
||||
@ -95,18 +94,23 @@ class DatabaseSchema(BaseModel):
|
||||
description="Database schema with table names as keys and column info as values"
|
||||
)
|
||||
relationships: Optional[List[Dict[str, Any]]] = Field(
|
||||
default=None,
|
||||
description="Foreign key relationships between tables"
|
||||
default=None, description="Foreign key relationships between tables"
|
||||
)
|
||||
|
||||
|
||||
class InsightRequest(BaseModel):
|
||||
prompt: str = Field(description="Natural language query from user")
|
||||
database_path: Optional[str] = Field(default=None, description="Path to database file (for SQLite)")
|
||||
database_path: Optional[str] = Field(
|
||||
default=None, description="Path to database file (for SQLite)"
|
||||
)
|
||||
chart_type: Optional[str] = Field(default=None, description="Preferred chart type")
|
||||
limit: Optional[int] = Field(default=1000, description="Maximum number of results")
|
||||
language: Optional[str] = Field(default="auto", description="Response language preference")
|
||||
use_django: Optional[bool] = Field(default=True, description="Use Django database if available")
|
||||
language: Optional[str] = Field(
|
||||
default="auto", description="Response language preference"
|
||||
)
|
||||
use_django: Optional[bool] = Field(
|
||||
default=True, description="Use Django database if available"
|
||||
)
|
||||
|
||||
|
||||
class DatabaseInsightSystem:
|
||||
@ -114,7 +118,7 @@ class DatabaseInsightSystem:
|
||||
self.config = config or DatabaseConfig()
|
||||
self.model = OpenAIModel(
|
||||
model_name=self.config.LLM_MODEL,
|
||||
provider=OpenAIProvider(base_url=self.config.LLM_BASE_URL)
|
||||
provider=OpenAIProvider(base_url=self.config.LLM_BASE_URL),
|
||||
)
|
||||
self.db_connection = None
|
||||
self._setup_agents()
|
||||
@ -148,7 +152,7 @@ class DatabaseInsightSystem:
|
||||
}
|
||||
|
||||
Handle both English and Arabic prompts. For Arabic text, respond in Arabic.
|
||||
Focus on providing actionable insights, not just raw data."""
|
||||
Focus on providing actionable insights, not just raw data.""",
|
||||
)
|
||||
|
||||
def _get_django_database_config(self) -> Optional[DatabaseConnection]:
|
||||
@ -158,27 +162,31 @@ class DatabaseInsightSystem:
|
||||
|
||||
try:
|
||||
# Get default database configuration
|
||||
db_config = settings.DATABASES.get('default', {})
|
||||
db_config = settings.DATABASES.get("default", {})
|
||||
if not db_config:
|
||||
logger.warning("No default database configuration found in Django settings")
|
||||
logger.warning(
|
||||
"No default database configuration found in Django settings"
|
||||
)
|
||||
return None
|
||||
|
||||
engine = db_config.get('ENGINE', '')
|
||||
db_name = db_config.get('NAME', '')
|
||||
host = db_config.get('HOST', 'localhost')
|
||||
port = db_config.get('PORT', None)
|
||||
user = db_config.get('USER', '')
|
||||
password = db_config.get('PASSWORD', '')
|
||||
engine = db_config.get("ENGINE", "")
|
||||
db_name = db_config.get("NAME", "")
|
||||
host = db_config.get("HOST", "localhost")
|
||||
port = db_config.get("PORT", None)
|
||||
user = db_config.get("USER", "")
|
||||
password = db_config.get("PASSWORD", "")
|
||||
|
||||
# Determine database type from engine
|
||||
if 'sqlite' in engine.lower():
|
||||
if "sqlite" in engine.lower():
|
||||
db_type = DatabaseType.SQLITE
|
||||
connection_string = db_name # For SQLite, NAME is the file path
|
||||
elif 'postgresql' in engine.lower():
|
||||
elif "postgresql" in engine.lower():
|
||||
db_type = DatabaseType.POSTGRESQL
|
||||
port = port or 5432
|
||||
connection_string = f"postgresql://{user}:{password}@{host}:{port}/{db_name}"
|
||||
elif 'mysql' in engine.lower():
|
||||
connection_string = (
|
||||
f"postgresql://{user}:{password}@{host}:{port}/{db_name}"
|
||||
)
|
||||
elif "mysql" in engine.lower():
|
||||
db_type = DatabaseType.MYSQL
|
||||
port = port or 3306
|
||||
connection_string = f"mysql://{user}:{password}@{host}:{port}/{db_name}"
|
||||
@ -193,7 +201,7 @@ class DatabaseInsightSystem:
|
||||
host=host,
|
||||
port=port,
|
||||
user=user,
|
||||
password=password
|
||||
password=password,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
@ -218,8 +226,7 @@ class DatabaseInsightSystem:
|
||||
if request.database_path:
|
||||
# Assume SQLite for direct file path
|
||||
self.db_connection = DatabaseConnection(
|
||||
db_type=DatabaseType.SQLITE,
|
||||
connection_string=request.database_path
|
||||
db_type=DatabaseType.SQLITE, connection_string=request.database_path
|
||||
)
|
||||
return await self._analyze_sqlite_schema(request.database_path)
|
||||
|
||||
@ -247,24 +254,28 @@ class DatabaseInsightSystem:
|
||||
cursor.execute(f"PRAGMA table_info({table})")
|
||||
columns = []
|
||||
for col in cursor.fetchall():
|
||||
columns.append({
|
||||
"name": col[1],
|
||||
"type": col[2],
|
||||
"notnull": bool(col[3]),
|
||||
"default_value": col[4],
|
||||
"primary_key": bool(col[5])
|
||||
})
|
||||
columns.append(
|
||||
{
|
||||
"name": col[1],
|
||||
"type": col[2],
|
||||
"notnull": bool(col[3]),
|
||||
"default_value": col[4],
|
||||
"primary_key": bool(col[5]),
|
||||
}
|
||||
)
|
||||
schema_data[table] = columns
|
||||
|
||||
# Get foreign key relationships
|
||||
cursor.execute(f"PRAGMA foreign_key_list({table})")
|
||||
for fk in cursor.fetchall():
|
||||
relationships.append({
|
||||
"from_table": table,
|
||||
"from_column": fk[3],
|
||||
"to_table": fk[2],
|
||||
"to_column": fk[4]
|
||||
})
|
||||
relationships.append(
|
||||
{
|
||||
"from_table": table,
|
||||
"from_column": fk[3],
|
||||
"to_table": fk[2],
|
||||
"to_column": fk[4],
|
||||
}
|
||||
)
|
||||
|
||||
conn.close()
|
||||
return DatabaseSchema(tables=schema_data, relationships=relationships)
|
||||
@ -287,27 +298,33 @@ class DatabaseInsightSystem:
|
||||
|
||||
for field in model._meta.get_fields():
|
||||
if not field.is_relation:
|
||||
columns.append({
|
||||
"name": field.name,
|
||||
"type": field.get_internal_type(),
|
||||
"notnull": not getattr(field, 'null', True),
|
||||
"primary_key": getattr(field, 'primary_key', False)
|
||||
})
|
||||
columns.append(
|
||||
{
|
||||
"name": field.name,
|
||||
"type": field.get_internal_type(),
|
||||
"notnull": not getattr(field, "null", True),
|
||||
"primary_key": getattr(field, "primary_key", False),
|
||||
}
|
||||
)
|
||||
else:
|
||||
# Handle relationships
|
||||
if hasattr(field, 'related_model') and field.related_model:
|
||||
relationships.append({
|
||||
"from_table": table_name,
|
||||
"from_column": field.name,
|
||||
"to_table": field.related_model._meta.db_table,
|
||||
"relationship_type": field.get_internal_type()
|
||||
})
|
||||
if hasattr(field, "related_model") and field.related_model:
|
||||
relationships.append(
|
||||
{
|
||||
"from_table": table_name,
|
||||
"from_column": field.name,
|
||||
"to_table": field.related_model._meta.db_table,
|
||||
"relationship_type": field.get_internal_type(),
|
||||
}
|
||||
)
|
||||
|
||||
schema_data[table_name] = columns
|
||||
|
||||
return DatabaseSchema(tables=schema_data, relationships=relationships)
|
||||
|
||||
async def _analyze_postgresql_schema(self, connection_string: str) -> DatabaseSchema:
|
||||
async def _analyze_postgresql_schema(
|
||||
self, connection_string: str
|
||||
) -> DatabaseSchema:
|
||||
"""Analyze PostgreSQL database schema."""
|
||||
if not POSTGRESQL_AVAILABLE:
|
||||
raise ImportError("psycopg2 is not available")
|
||||
@ -325,47 +342,56 @@ class DatabaseInsightSystem:
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
""")
|
||||
tables = [row['table_name'] for row in cursor.fetchall()]
|
||||
tables = [row["table_name"] for row in cursor.fetchall()]
|
||||
|
||||
schema_data = {}
|
||||
relationships = []
|
||||
|
||||
for table in tables:
|
||||
# Get column information
|
||||
cursor.execute("""
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT column_name, data_type, is_nullable, column_default
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = %s
|
||||
ORDER BY ordinal_position
|
||||
""", (table,))
|
||||
""",
|
||||
(table,),
|
||||
)
|
||||
|
||||
columns = []
|
||||
for col in cursor.fetchall():
|
||||
columns.append({
|
||||
"name": col['column_name'],
|
||||
"type": col['data_type'],
|
||||
"notnull": col['is_nullable'] == 'NO',
|
||||
"default_value": col['column_default'],
|
||||
"primary_key": False # Will be updated below
|
||||
})
|
||||
columns.append(
|
||||
{
|
||||
"name": col["column_name"],
|
||||
"type": col["data_type"],
|
||||
"notnull": col["is_nullable"] == "NO",
|
||||
"default_value": col["column_default"],
|
||||
"primary_key": False, # Will be updated below
|
||||
}
|
||||
)
|
||||
|
||||
# Get primary key information
|
||||
cursor.execute("""
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT column_name
|
||||
FROM information_schema.key_column_usage
|
||||
WHERE table_name = %s
|
||||
AND constraint_name LIKE '%_pkey'
|
||||
""", (table,))
|
||||
""",
|
||||
(table,),
|
||||
)
|
||||
|
||||
pk_columns = [row['column_name'] for row in cursor.fetchall()]
|
||||
pk_columns = [row["column_name"] for row in cursor.fetchall()]
|
||||
for col in columns:
|
||||
if col['name'] in pk_columns:
|
||||
col['primary_key'] = True
|
||||
if col["name"] in pk_columns:
|
||||
col["primary_key"] = True
|
||||
|
||||
schema_data[table] = columns
|
||||
|
||||
# Get foreign key relationships
|
||||
cursor.execute("""
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT kcu.column_name,
|
||||
ccu.table_name AS foreign_table_name,
|
||||
ccu.column_name AS foreign_column_name
|
||||
@ -376,15 +402,19 @@ class DatabaseInsightSystem:
|
||||
ON ccu.constraint_name = tc.constraint_name
|
||||
WHERE tc.constraint_type = 'FOREIGN KEY'
|
||||
AND tc.table_name = %s
|
||||
""", (table,))
|
||||
""",
|
||||
(table,),
|
||||
)
|
||||
|
||||
for fk in cursor.fetchall():
|
||||
relationships.append({
|
||||
"from_table": table,
|
||||
"from_column": fk['column_name'],
|
||||
"to_table": fk['foreign_table_name'],
|
||||
"to_column": fk['foreign_column_name']
|
||||
})
|
||||
relationships.append(
|
||||
{
|
||||
"from_table": table,
|
||||
"from_column": fk["column_name"],
|
||||
"to_table": fk["foreign_table_name"],
|
||||
"to_column": fk["foreign_column_name"],
|
||||
}
|
||||
)
|
||||
|
||||
conn.close()
|
||||
return DatabaseSchema(tables=schema_data, relationships=relationships)
|
||||
@ -404,6 +434,7 @@ class DatabaseInsightSystem:
|
||||
# Parse connection string to get connection parameters
|
||||
# Format: mysql://user:password@host:port/database
|
||||
import urllib.parse
|
||||
|
||||
parsed = urllib.parse.urlparse(connection_string)
|
||||
|
||||
conn = pymysql.connect(
|
||||
@ -412,7 +443,7 @@ class DatabaseInsightSystem:
|
||||
user=parsed.username,
|
||||
password=parsed.password,
|
||||
database=parsed.path[1:], # Remove leading slash
|
||||
cursorclass=pymysql.cursors.DictCursor
|
||||
cursorclass=pymysql.cursors.DictCursor,
|
||||
)
|
||||
|
||||
cursor = conn.cursor()
|
||||
@ -429,13 +460,15 @@ class DatabaseInsightSystem:
|
||||
cursor.execute(f"DESCRIBE {table}")
|
||||
columns = []
|
||||
for col in cursor.fetchall():
|
||||
columns.append({
|
||||
"name": col['Field'],
|
||||
"type": col['Type'],
|
||||
"notnull": col['Null'] == 'NO',
|
||||
"default_value": col['Default'],
|
||||
"primary_key": col['Key'] == 'PRI'
|
||||
})
|
||||
columns.append(
|
||||
{
|
||||
"name": col["Field"],
|
||||
"type": col["Type"],
|
||||
"notnull": col["Null"] == "NO",
|
||||
"default_value": col["Default"],
|
||||
"primary_key": col["Key"] == "PRI",
|
||||
}
|
||||
)
|
||||
|
||||
schema_data[table] = columns
|
||||
|
||||
@ -451,12 +484,14 @@ class DatabaseInsightSystem:
|
||||
""")
|
||||
|
||||
for fk in cursor.fetchall():
|
||||
relationships.append({
|
||||
"from_table": table,
|
||||
"from_column": fk['COLUMN_NAME'],
|
||||
"to_table": fk['REFERENCED_TABLE_NAME'],
|
||||
"to_column": fk['REFERENCED_COLUMN_NAME']
|
||||
})
|
||||
relationships.append(
|
||||
{
|
||||
"from_table": table,
|
||||
"from_column": fk["COLUMN_NAME"],
|
||||
"to_table": fk["REFERENCED_TABLE_NAME"],
|
||||
"to_column": fk["REFERENCED_COLUMN_NAME"],
|
||||
}
|
||||
)
|
||||
|
||||
conn.close()
|
||||
return DatabaseSchema(tables=schema_data, relationships=relationships)
|
||||
@ -467,7 +502,7 @@ class DatabaseInsightSystem:
|
||||
|
||||
def _detect_language(self, text: str) -> str:
|
||||
"""Detect if text is Arabic or English."""
|
||||
arabic_chars = re.findall(r'[\u0600-\u06FF]', text)
|
||||
arabic_chars = re.findall(r"[\u0600-\u06FF]", text)
|
||||
return "ar" if len(arabic_chars) > len(text) * 0.3 else "en"
|
||||
|
||||
def _execute_query_sync(self, query: str) -> List[Dict]:
|
||||
@ -480,13 +515,19 @@ class DatabaseInsightSystem:
|
||||
raise ValueError("No database connection established")
|
||||
|
||||
if self.db_connection.db_type == DatabaseType.SQLITE:
|
||||
return await self._execute_sqlite_query(self.db_connection.connection_string, query)
|
||||
return await self._execute_sqlite_query(
|
||||
self.db_connection.connection_string, query
|
||||
)
|
||||
# elif self.db_connection.db_type == DatabaseType.DJANGO and DJANGO_AVAILABLE:
|
||||
# return await self._execute_django_query(query)
|
||||
elif self.db_connection.db_type == DatabaseType.POSTGRESQL:
|
||||
return await self._execute_postgresql_query(self.db_connection.connection_string, query)
|
||||
return await self._execute_postgresql_query(
|
||||
self.db_connection.connection_string, query
|
||||
)
|
||||
elif self.db_connection.db_type == DatabaseType.MYSQL:
|
||||
return await self._execute_mysql_query(self.db_connection.connection_string, query)
|
||||
return await self._execute_mysql_query(
|
||||
self.db_connection.connection_string, query
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Unsupported database type: {self.db_connection.db_type}")
|
||||
|
||||
@ -528,7 +569,9 @@ class DatabaseInsightSystem:
|
||||
logger.error(f"Django query execution failed: {e}")
|
||||
raise
|
||||
|
||||
async def _execute_postgresql_query(self, connection_string: str, query: str) -> List[Dict]:
|
||||
async def _execute_postgresql_query(
|
||||
self, connection_string: str, query: str
|
||||
) -> List[Dict]:
|
||||
"""Execute SQL query on PostgreSQL database."""
|
||||
try:
|
||||
import psycopg2
|
||||
@ -548,7 +591,9 @@ class DatabaseInsightSystem:
|
||||
logger.error(f"PostgreSQL query execution failed: {e}")
|
||||
raise
|
||||
|
||||
async def _execute_mysql_query(self, connection_string: str, query: str) -> List[Dict]:
|
||||
async def _execute_mysql_query(
|
||||
self, connection_string: str, query: str
|
||||
) -> List[Dict]:
|
||||
"""Execute SQL query on MySQL database."""
|
||||
try:
|
||||
import pymysql
|
||||
@ -562,7 +607,7 @@ class DatabaseInsightSystem:
|
||||
user=parsed.username,
|
||||
password=parsed.password,
|
||||
database=parsed.path[1:],
|
||||
cursorclass=pymysql.cursors.DictCursor
|
||||
cursorclass=pymysql.cursors.DictCursor,
|
||||
)
|
||||
|
||||
cursor = conn.cursor()
|
||||
@ -576,7 +621,9 @@ class DatabaseInsightSystem:
|
||||
logger.error(f"MySQL query execution failed: {e}")
|
||||
raise
|
||||
|
||||
def _prepare_chart_data(self, data: List[Dict], chart_type: str, fields: List[str]) -> Optional[Dict]:
|
||||
def _prepare_chart_data(
|
||||
self, data: List[Dict], chart_type: str, fields: List[str]
|
||||
) -> Optional[Dict]:
|
||||
"""Prepare data for chart visualization."""
|
||||
if not data or not fields:
|
||||
return None
|
||||
@ -613,7 +660,7 @@ class DatabaseInsightSystem:
|
||||
"backgroundColor": [
|
||||
f"rgba({50 + i * 30}, {100 + i * 25}, {200 + i * 20}, 0.7)"
|
||||
for i in range(len(values))
|
||||
]
|
||||
],
|
||||
}
|
||||
else:
|
||||
# Multiple datasets for other chart types
|
||||
@ -627,21 +674,19 @@ class DatabaseInsightSystem:
|
||||
value = 0
|
||||
dataset_values.append(value)
|
||||
|
||||
datasets.append({
|
||||
"label": field,
|
||||
"data": dataset_values,
|
||||
"backgroundColor": f"rgba({50 + i * 40}, {100 + i * 30}, 235, 0.6)",
|
||||
"borderColor": f"rgba({50 + i * 40}, {100 + i * 30}, 235, 1.0)",
|
||||
"borderWidth": 2
|
||||
})
|
||||
datasets.append(
|
||||
{
|
||||
"label": field,
|
||||
"data": dataset_values,
|
||||
"backgroundColor": f"rgba({50 + i * 40}, {100 + i * 30}, 235, 0.6)",
|
||||
"borderColor": f"rgba({50 + i * 40}, {100 + i * 30}, 235, 1.0)",
|
||||
"borderWidth": 2,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error processing field {field}: {e}")
|
||||
|
||||
return {
|
||||
"type": chart_type,
|
||||
"labels": labels,
|
||||
"datasets": datasets
|
||||
}
|
||||
return {"type": chart_type, "labels": labels, "datasets": datasets}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Chart preparation failed: {e}")
|
||||
@ -659,14 +704,18 @@ class DatabaseInsightSystem:
|
||||
"data": [],
|
||||
"metadata": {},
|
||||
"error": str(e),
|
||||
"language": "en"
|
||||
"language": "en",
|
||||
}
|
||||
|
||||
async def get_insights(self, request: InsightRequest) -> QueryResult:
|
||||
"""Main method to get database insights from natural language prompt."""
|
||||
try:
|
||||
# Detect language
|
||||
language = self._detect_language(request.prompt) if request.language == "auto" else request.language
|
||||
language = (
|
||||
self._detect_language(request.prompt)
|
||||
if request.language == "auto"
|
||||
else request.language
|
||||
)
|
||||
|
||||
# Analyze database schema
|
||||
schema = await self.analyze_database_schema(request)
|
||||
@ -674,7 +723,7 @@ class DatabaseInsightSystem:
|
||||
# Generate query plan using AI
|
||||
query_response = await self.query_agent.run(
|
||||
f"User prompt: {request.prompt}\nLanguage: {language}",
|
||||
database_schema=schema
|
||||
database_schema=schema,
|
||||
)
|
||||
|
||||
# Parse AI response
|
||||
@ -682,13 +731,15 @@ class DatabaseInsightSystem:
|
||||
query_plan = json.loads(query_response.output)
|
||||
except json.JSONDecodeError:
|
||||
# Fallback: extract SQL from response
|
||||
sql_match = re.search(r'SELECT.*?;', query_response.output, re.IGNORECASE | re.DOTALL)
|
||||
sql_match = re.search(
|
||||
r"SELECT.*?;", query_response.output, re.IGNORECASE | re.DOTALL
|
||||
)
|
||||
if sql_match:
|
||||
query_plan = {
|
||||
"sql_query": sql_match.group(0),
|
||||
"chart_suggestion": "bar",
|
||||
"expected_fields": [],
|
||||
"language": language
|
||||
"language": language,
|
||||
}
|
||||
else:
|
||||
raise ValueError("Could not parse AI response")
|
||||
@ -710,21 +761,25 @@ class DatabaseInsightSystem:
|
||||
elif data:
|
||||
# Use first few fields if no specific fields suggested
|
||||
available_fields = list(data[0].keys()) if data else []
|
||||
chart_data = self._prepare_chart_data(data, chart_type, available_fields[:3])
|
||||
chart_data = self._prepare_chart_data(
|
||||
data, chart_type, available_fields[:3]
|
||||
)
|
||||
|
||||
# Prepare result
|
||||
return QueryResult(
|
||||
status="success",
|
||||
data=data[:request.limit] if data else [],
|
||||
data=data[: request.limit] if data else [],
|
||||
metadata={
|
||||
"total_count": len(data) if data else 0,
|
||||
"query": sql_query,
|
||||
"analysis": query_plan.get("analysis", ""),
|
||||
"fields": expected_fields or (list(data[0].keys()) if data else []),
|
||||
"database_type": self.db_connection.db_type.value if self.db_connection else "unknown"
|
||||
"database_type": self.db_connection.db_type.value
|
||||
if self.db_connection
|
||||
else "unknown",
|
||||
},
|
||||
chart_data=chart_data,
|
||||
language=language
|
||||
language=language,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
@ -734,7 +789,7 @@ class DatabaseInsightSystem:
|
||||
data=[],
|
||||
metadata={},
|
||||
error=str(e),
|
||||
language=language if 'language' in locals() else "en"
|
||||
language=language if "language" in locals() else "en",
|
||||
)
|
||||
|
||||
# # Static method for Django view compatibility
|
||||
@ -798,4 +853,4 @@ def analyze_prompt_sync(prompt: str, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
system = DatabaseInsightSystem()
|
||||
request = InsightRequest(prompt=prompt, **kwargs)
|
||||
return system.get_insights_sync(request)
|
||||
return system.get_insights_sync(request)
|
||||
|
||||
@ -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}")
|
||||
)
|
||||
|
||||
@ -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",
|
||||
)
|
||||
],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
@ -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"
|
||||
|
||||
|
||||
@ -1,2 +1 @@
|
||||
|
||||
# Create your tests here.
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
@ -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]
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def currency_context(request):
|
||||
"""
|
||||
Provides a context dictionary containing the currency setting. This is typically
|
||||
@ -14,9 +15,7 @@ def currency_context(request):
|
||||
project's CURRENCY setting.
|
||||
:rtype: dict
|
||||
"""
|
||||
return {
|
||||
'CURRENCY': settings.CURRENCY
|
||||
}
|
||||
return {"CURRENCY": settings.CURRENCY}
|
||||
|
||||
|
||||
def breadcrumbs(request):
|
||||
@ -37,8 +36,8 @@ def breadcrumbs(request):
|
||||
:rtype: dict
|
||||
"""
|
||||
breadcrumbs = []
|
||||
path = request.path.strip('/').split('/')
|
||||
path = request.path.strip("/").split("/")
|
||||
for i in range(len(path)):
|
||||
url = '/' + '/'.join(path[:i+1]) + '/'
|
||||
breadcrumbs.append({'name': path[i].capitalize(), 'url': url})
|
||||
return {'breadcrumbs': breadcrumbs}
|
||||
url = "/" + "/".join(path[: i + 1]) + "/"
|
||||
breadcrumbs.append({"name": path[i].capitalize(), "url": url})
|
||||
return {"breadcrumbs": breadcrumbs}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
from django_ledger.models import AccountModel
|
||||
import django_filters
|
||||
|
||||
|
||||
class AccountModelFilter(django_filters.FilterSet):
|
||||
"""
|
||||
Handles filtering functionality for the AccountModel.
|
||||
@ -17,6 +18,7 @@ class AccountModelFilter(django_filters.FilterSet):
|
||||
:ivar fields: List of fields defined in the model that can be filtered.
|
||||
:type fields: list(str)
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = AccountModel
|
||||
fields = ['code', 'name','role']
|
||||
fields = ["code", "name", "role"]
|
||||
|
||||
@ -1270,6 +1270,7 @@ class OpportunityForm(forms.ModelForm):
|
||||
widgets = {
|
||||
"expected_revenue": forms.NumberInput(attrs={"readonly": "readonly"}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Add a visible number input to display the current value
|
||||
@ -1354,19 +1355,11 @@ class SaleOrderForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = SaleOrder
|
||||
fields = [
|
||||
"estimate",
|
||||
"payment_method",
|
||||
"opportunity",
|
||||
"agreed_price",
|
||||
"down_payment_amount",
|
||||
"loan_amount",
|
||||
"expected_delivery_date",
|
||||
"comments",
|
||||
"status"
|
||||
]
|
||||
"customer","expected_delivery_date","estimate","opportunity","comments","order_date","status"]
|
||||
widgets = {
|
||||
"comments": forms.Textarea(attrs={"rows": 3}),
|
||||
"expected_delivery_date": forms.DateInput(attrs={"type": "date"}),
|
||||
"expected_delivery_date": forms.DateInput(attrs={"type": "date", "label": _("Expected Delivery Date")}),
|
||||
"order_date": forms.DateInput(attrs={"type": "date", "label": _("Order Date")}),
|
||||
"customer": forms.Select(attrs={"class": "form-control","label": _("Customer"),}),
|
||||
}
|
||||
|
||||
|
||||
@ -1909,6 +1902,7 @@ class StaffTaskForm(forms.ModelForm):
|
||||
|
||||
#############################################################
|
||||
|
||||
|
||||
class ItemInventoryForm(forms.Form):
|
||||
make = forms.ModelChoiceField(
|
||||
queryset=CarMake.objects.all(),
|
||||
@ -1932,65 +1926,64 @@ class ItemInventoryForm(forms.Form):
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
#####################################################################
|
||||
|
||||
|
||||
class CSVUploadForm(forms.Form):
|
||||
dealer = forms.ModelChoiceField(
|
||||
queryset=Dealer.objects.all(),
|
||||
label=_('Dealer'),
|
||||
widget=forms.HiddenInput()
|
||||
queryset=Dealer.objects.all(), label=_("Dealer"), widget=forms.HiddenInput()
|
||||
)
|
||||
vendor = forms.ModelChoiceField(
|
||||
queryset=Vendor.objects.all(),
|
||||
label=_('Vendor'),
|
||||
widget=forms.Select(attrs={'class': 'form-select'}),
|
||||
required=True
|
||||
label=_("Vendor"),
|
||||
widget=forms.Select(attrs={"class": "form-select"}),
|
||||
required=True,
|
||||
)
|
||||
year = forms.IntegerField(
|
||||
label=_('Year'),
|
||||
widget=forms.NumberInput(attrs={'class': 'form-control'}),
|
||||
required=True
|
||||
label=_("Year"),
|
||||
widget=forms.NumberInput(attrs={"class": "form-control"}),
|
||||
required=True,
|
||||
)
|
||||
exterior = forms.ModelChoiceField(
|
||||
queryset=ExteriorColors.objects.all(),
|
||||
label=_('Exterior Color'),
|
||||
widget=forms.RadioSelect(attrs={'class': 'form-select'}),
|
||||
required=True
|
||||
label=_("Exterior Color"),
|
||||
widget=forms.RadioSelect(attrs={"class": "form-select"}),
|
||||
required=True,
|
||||
)
|
||||
interior = forms.ModelChoiceField(
|
||||
queryset=InteriorColors.objects.all(),
|
||||
label=_('Interior Color'),
|
||||
widget=forms.RadioSelect(attrs={'class': 'form-select'}),
|
||||
required=True
|
||||
label=_("Interior Color"),
|
||||
widget=forms.RadioSelect(attrs={"class": "form-select"}),
|
||||
required=True,
|
||||
)
|
||||
receiving_date = forms.DateField(
|
||||
label=_('Receiving Date'),
|
||||
widget=forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
|
||||
required=True
|
||||
label=_("Receiving Date"),
|
||||
widget=forms.DateInput(attrs={"type": "date", "class": "form-control"}),
|
||||
required=True,
|
||||
)
|
||||
|
||||
def clean_csv_file(self):
|
||||
csv_file = self.cleaned_data['csv_file']
|
||||
if not csv_file.name.endswith('.csv'):
|
||||
raise forms.ValidationError(_('File is not a CSV file'))
|
||||
csv_file = self.cleaned_data["csv_file"]
|
||||
if not csv_file.name.endswith(".csv"):
|
||||
raise forms.ValidationError(_("File is not a CSV file"))
|
||||
|
||||
# Read and validate CSV structure
|
||||
try:
|
||||
csv_data = TextIOWrapper(csv_file.file, encoding='utf-8')
|
||||
csv_data = TextIOWrapper(csv_file.file, encoding="utf-8")
|
||||
reader = csv.DictReader(csv_data)
|
||||
|
||||
required_fields = ['vin', 'make', 'model', 'year']
|
||||
required_fields = ["vin", "make", "model", "year"]
|
||||
if not all(field in reader.fieldnames for field in required_fields):
|
||||
missing = set(required_fields) - set(reader.fieldnames)
|
||||
raise forms.ValidationError(
|
||||
_('CSV is missing required columns: %(missing)s'),
|
||||
params={'missing': ', '.join(missing)}
|
||||
_("CSV is missing required columns: %(missing)s"),
|
||||
params={"missing": ", ".join(missing)},
|
||||
)
|
||||
except Exception as e:
|
||||
raise forms.ValidationError(_('Error reading CSV file: %(error)s') % {'error': str(e)})
|
||||
raise forms.ValidationError(
|
||||
_("Error reading CSV file: %(error)s") % {"error": str(e)}
|
||||
)
|
||||
|
||||
# Reset file pointer for later processing
|
||||
csv_file.file.seek(0)
|
||||
return csv_file
|
||||
return csv_file
|
||||
|
||||
@ -3,15 +3,57 @@ import re
|
||||
|
||||
def vin_year(vin_char: str) -> int:
|
||||
YEAR_MAPPING = {
|
||||
'A': 1980, 'B': 1981, 'C': 1982, 'D': 1983, 'E': 1984, 'F': 1985,
|
||||
'G': 1986, 'H': 1987, 'J': 1988, 'K': 1989, 'L': 1990, 'M': 1991,
|
||||
'N': 1992, 'P': 1993, 'R': 1994, 'S': 1995, 'T': 1996, 'V': 1997,
|
||||
'W': 1998, 'X': 1999, 'Y': 2000, '1': 2001, '2': 2002, '3': 2003,
|
||||
'4': 2004, '5': 2005, '6': 2006, '7': 2007, '8': 2008, '9': 2009,
|
||||
'A': 2010, 'B': 2011, 'C': 2012, 'D': 2013, 'E': 2014, 'F': 2015,
|
||||
'G': 2016, 'H': 2017, 'J': 2018, 'K': 2019, 'L': 2020, 'M': 2021,
|
||||
'N': 2022, 'P': 2023, 'R': 2024, 'S': 2025, 'T': 2026, 'V': 2027,
|
||||
'W': 2028, 'X': 2029, 'Y': 2030,
|
||||
"A": 1980,
|
||||
"B": 1981,
|
||||
"C": 1982,
|
||||
"D": 1983,
|
||||
"E": 1984,
|
||||
"F": 1985,
|
||||
"G": 1986,
|
||||
"H": 1987,
|
||||
"J": 1988,
|
||||
"K": 1989,
|
||||
"L": 1990,
|
||||
"M": 1991,
|
||||
"N": 1992,
|
||||
"P": 1993,
|
||||
"R": 1994,
|
||||
"S": 1995,
|
||||
"T": 1996,
|
||||
"V": 1997,
|
||||
"W": 1998,
|
||||
"X": 1999,
|
||||
"Y": 2000,
|
||||
"1": 2001,
|
||||
"2": 2002,
|
||||
"3": 2003,
|
||||
"4": 2004,
|
||||
"5": 2005,
|
||||
"6": 2006,
|
||||
"7": 2007,
|
||||
"8": 2008,
|
||||
"9": 2009,
|
||||
"A": 2010,
|
||||
"B": 2011,
|
||||
"C": 2012,
|
||||
"D": 2013,
|
||||
"E": 2014,
|
||||
"F": 2015,
|
||||
"G": 2016,
|
||||
"H": 2017,
|
||||
"J": 2018,
|
||||
"K": 2019,
|
||||
"L": 2020,
|
||||
"M": 2021,
|
||||
"N": 2022,
|
||||
"P": 2023,
|
||||
"R": 2024,
|
||||
"S": 2025,
|
||||
"T": 2026,
|
||||
"V": 2027,
|
||||
"W": 2028,
|
||||
"X": 2029,
|
||||
"Y": 2030,
|
||||
}
|
||||
|
||||
# Normalize the input character to uppercase
|
||||
@ -19,7 +61,9 @@ def vin_year(vin_char: str) -> int:
|
||||
|
||||
# Check if the character is valid
|
||||
if vin_char not in YEAR_MAPPING:
|
||||
raise ValueError(f"Invalid year character: '{vin_char}'. Expected one of {list(YEAR_MAPPING.keys())}.")
|
||||
raise ValueError(
|
||||
f"Invalid year character: '{vin_char}'. Expected one of {list(YEAR_MAPPING.keys())}."
|
||||
)
|
||||
|
||||
# Return the corresponding year
|
||||
return YEAR_MAPPING[vin_char]
|
||||
@ -1355,452 +1399,425 @@ wmi_manufacturer_mapping = {
|
||||
"MM0": "Mazda",
|
||||
"MM6": "Mazda",
|
||||
"MM7": "Mazda",
|
||||
"MM8": "Mazda"
|
||||
"MM8": "Mazda",
|
||||
}
|
||||
|
||||
|
||||
def decode_vds(manufacturer, vds):
|
||||
# Mapping of manufacturers to their VDS positions and corresponding models
|
||||
vds_model_mapping = {
|
||||
'Honda': {
|
||||
"Honda": {
|
||||
1: {
|
||||
'F': 'Agila',
|
||||
'G': 'Insignia',
|
||||
'J': 'Mokka',
|
||||
'L': 'Antara',
|
||||
'M': 'Movano',
|
||||
'P': ['Astra J', 'Zafira C'],
|
||||
'R': 'Astra GTC J',
|
||||
'S': 'Meriva',
|
||||
'V': 'Combo II',
|
||||
'W': 'Cascada',
|
||||
"F": "Agila",
|
||||
"G": "Insignia",
|
||||
"J": "Mokka",
|
||||
"L": "Antara",
|
||||
"M": "Movano",
|
||||
"P": ["Astra J", "Zafira C"],
|
||||
"R": "Astra GTC J",
|
||||
"S": "Meriva",
|
||||
"V": "Combo II",
|
||||
"W": "Cascada",
|
||||
}
|
||||
},
|
||||
|
||||
'Kia': {
|
||||
"Kia": {
|
||||
1: {
|
||||
'A': ['Rio', 'EV9'],
|
||||
'C': ['Niro', 'EV6'],
|
||||
'D': 'Rio',
|
||||
'E': ['Stinger', 'Seltos'],
|
||||
'F': ['Forte', 'K4'],
|
||||
'G': ['Optima', 'Magentis', 'K5'],
|
||||
'H': 'Rondo',
|
||||
'J': 'Soul',
|
||||
'K': ['Mohave', 'Sorento', 'Sportage'],
|
||||
'L': ['Cadenza', 'K9'],
|
||||
'M': 'Sedona',
|
||||
'N': 'Carnival',
|
||||
'P': ['Sportage', 'Sorento', 'Telluride'],
|
||||
'R': 'Sorento',
|
||||
'S': 'K9'
|
||||
"A": ["Rio", "EV9"],
|
||||
"C": ["Niro", "EV6"],
|
||||
"D": "Rio",
|
||||
"E": ["Stinger", "Seltos"],
|
||||
"F": ["Forte", "K4"],
|
||||
"G": ["Optima", "Magentis", "K5"],
|
||||
"H": "Rondo",
|
||||
"J": "Soul",
|
||||
"K": ["Mohave", "Sorento", "Sportage"],
|
||||
"L": ["Cadenza", "K9"],
|
||||
"M": "Sedona",
|
||||
"N": "Carnival",
|
||||
"P": ["Sportage", "Sorento", "Telluride"],
|
||||
"R": "Sorento",
|
||||
"S": "K9",
|
||||
}
|
||||
},
|
||||
|
||||
'Peugeot': {
|
||||
"Peugeot": {
|
||||
1: {
|
||||
'A': '604',
|
||||
'2': '206',
|
||||
'8': ['406', '508'],
|
||||
'6': '407',
|
||||
'9': '308',
|
||||
"4": '308',
|
||||
'B': 'Expert',
|
||||
'C': ['208', '504'],
|
||||
'D': '301',
|
||||
'3': '307',
|
||||
'7': ['306', 'Partner'],
|
||||
'U': '2008',
|
||||
'M': '3008',
|
||||
"A": "604",
|
||||
"2": "206",
|
||||
"8": ["406", "508"],
|
||||
"6": "407",
|
||||
"9": "308",
|
||||
"4": "308",
|
||||
"B": "Expert",
|
||||
"C": ["208", "504"],
|
||||
"D": "301",
|
||||
"3": "307",
|
||||
"7": ["306", "Partner"],
|
||||
"U": "2008",
|
||||
"M": "3008",
|
||||
}
|
||||
},
|
||||
|
||||
'Toyota': {
|
||||
"Toyota": {
|
||||
1: { # 5th character in VDS
|
||||
'0': 'MR2 Spyder',
|
||||
'1': 'Tundra',
|
||||
'3': ['Echo', 'Yaris'],
|
||||
'A': ['Highlander', 'Sequoia', 'Celica', 'Supra'],
|
||||
'B': 'Avalon',
|
||||
'C': ['Sienna', 'Previa'],
|
||||
'D': 'T100',
|
||||
'E': ['Corolla', 'Matrix'],
|
||||
'F': 'FJ Cruiser',
|
||||
'G': 'Hilux',
|
||||
'H': 'Highlander',
|
||||
'J': 'Land Cruiser',
|
||||
'K': 'Camry',
|
||||
'L': ['Tercel', 'Paseo'],
|
||||
'M': 'Previa',
|
||||
'N': 'Tacoma',
|
||||
'P': 'Camry',
|
||||
'R': ['4Runner', 'Corolla'],
|
||||
'T': 'Celica FWD',
|
||||
'U': 'Prius',
|
||||
'V': 'RAV4',
|
||||
'W': 'MR2 non Spyder',
|
||||
'X': 'Cressida',
|
||||
"0": "MR2 Spyder",
|
||||
"1": "Tundra",
|
||||
"3": ["Echo", "Yaris"],
|
||||
"A": ["Highlander", "Sequoia", "Celica", "Supra"],
|
||||
"B": "Avalon",
|
||||
"C": ["Sienna", "Previa"],
|
||||
"D": "T100",
|
||||
"E": ["Corolla", "Matrix"],
|
||||
"F": "FJ Cruiser",
|
||||
"G": "Hilux",
|
||||
"H": "Highlander",
|
||||
"J": "Land Cruiser",
|
||||
"K": "Camry",
|
||||
"L": ["Tercel", "Paseo"],
|
||||
"M": "Previa",
|
||||
"N": "Tacoma",
|
||||
"P": "Camry",
|
||||
"R": ["4Runner", "Corolla"],
|
||||
"T": "Celica FWD",
|
||||
"U": "Prius",
|
||||
"V": "RAV4",
|
||||
"W": "MR2 non Spyder",
|
||||
"X": "Cressida",
|
||||
}
|
||||
},
|
||||
|
||||
'Nissan': {
|
||||
"Nissan": {
|
||||
2: {
|
||||
'A': ['Armada', 'Titan', 'Maxima'],
|
||||
'B': 'Sentra',
|
||||
'C': 'Versa (07-11)',
|
||||
'D': ['Truck', 'Xterra (00-04)', 'Frontier'],
|
||||
'J': 'Maxima',
|
||||
'L': 'Altima',
|
||||
'N': 'Xterra (05-11)',
|
||||
'P': 'Kicks',
|
||||
'R': 'Pathfinder',
|
||||
'S': ['240SX', 'Rogue (08-11)'],
|
||||
'T': 'X Trail',
|
||||
'U': 'Altima',
|
||||
'Y': 'Patrol',
|
||||
'Z': ['300Z', '350Z', 'Murano'],
|
||||
"A": ["Armada", "Titan", "Maxima"],
|
||||
"B": "Sentra",
|
||||
"C": "Versa (07-11)",
|
||||
"D": ["Truck", "Xterra (00-04)", "Frontier"],
|
||||
"J": "Maxima",
|
||||
"L": "Altima",
|
||||
"N": "Xterra (05-11)",
|
||||
"P": "Kicks",
|
||||
"R": "Pathfinder",
|
||||
"S": ["240SX", "Rogue (08-11)"],
|
||||
"T": "X Trail",
|
||||
"U": "Altima",
|
||||
"Y": "Patrol",
|
||||
"Z": ["300Z", "350Z", "Murano"],
|
||||
}
|
||||
},
|
||||
|
||||
'Renault': {
|
||||
"Renault": {
|
||||
2: { # 5th character in VDS
|
||||
'0': 'Twingo',
|
||||
'1': 'R4',
|
||||
'2': 'R25',
|
||||
'3': 'R4',
|
||||
'4': ['R21', 'Express'],
|
||||
'5': ['Clio I', 'Laguna', 'R19', 'Safrane'],
|
||||
'A': ['Megane I', 'Master'],
|
||||
'B': 'Clio II',
|
||||
'C': 'Kangoo',
|
||||
'D': 'Master',
|
||||
'E': ['Espace III', 'Avantime'],
|
||||
'G': 'Laguna II',
|
||||
'H': 'Master Propulsion',
|
||||
'J': ['Vel Satis', 'New Trafic'],
|
||||
'K': 'Espace IV',
|
||||
'L': 'Trafic',
|
||||
'M': 'Megan II',
|
||||
'P': 'Modus',
|
||||
'S': ['Logan', 'Sandero', 'Duster', 'Dokker', 'Lodgy'],
|
||||
'Y': 'Koleos',
|
||||
"0": "Twingo",
|
||||
"1": "R4",
|
||||
"2": "R25",
|
||||
"3": "R4",
|
||||
"4": ["R21", "Express"],
|
||||
"5": ["Clio I", "Laguna", "R19", "Safrane"],
|
||||
"A": ["Megane I", "Master"],
|
||||
"B": "Clio II",
|
||||
"C": "Kangoo",
|
||||
"D": "Master",
|
||||
"E": ["Espace III", "Avantime"],
|
||||
"G": "Laguna II",
|
||||
"H": "Master Propulsion",
|
||||
"J": ["Vel Satis", "New Trafic"],
|
||||
"K": "Espace IV",
|
||||
"L": "Trafic",
|
||||
"M": "Megan II",
|
||||
"P": "Modus",
|
||||
"S": ["Logan", "Sandero", "Duster", "Dokker", "Lodgy"],
|
||||
"Y": "Koleos",
|
||||
}
|
||||
},
|
||||
|
||||
# New Manufacturers Added Below
|
||||
'Ford': {
|
||||
"Ford": {
|
||||
3: {
|
||||
'A': ['Fiesta', 'Focus'],
|
||||
'B': ['Mustang', 'Explorer'],
|
||||
'C': 'Ranger',
|
||||
'D': ['Escape', 'Edge'],
|
||||
'E': 'F-150',
|
||||
'F': 'Transit',
|
||||
'G': 'Bronco',
|
||||
'H': 'Expedition',
|
||||
"A": ["Fiesta", "Focus"],
|
||||
"B": ["Mustang", "Explorer"],
|
||||
"C": "Ranger",
|
||||
"D": ["Escape", "Edge"],
|
||||
"E": "F-150",
|
||||
"F": "Transit",
|
||||
"G": "Bronco",
|
||||
"H": "Expedition",
|
||||
}
|
||||
},
|
||||
|
||||
'BMW': {
|
||||
"BMW": {
|
||||
4: {
|
||||
'1': '1 Series',
|
||||
'2': '2 Series',
|
||||
'3': '3 Series',
|
||||
'4': '4 Series',
|
||||
'5': '5 Series',
|
||||
'6': '6 Series',
|
||||
'7': '7 Series',
|
||||
'8': '8 Series',
|
||||
'X': 'X Series (SUV)',
|
||||
'Z': 'Z Series (Roadster)',
|
||||
"1": "1 Series",
|
||||
"2": "2 Series",
|
||||
"3": "3 Series",
|
||||
"4": "4 Series",
|
||||
"5": "5 Series",
|
||||
"6": "6 Series",
|
||||
"7": "7 Series",
|
||||
"8": "8 Series",
|
||||
"X": "X Series (SUV)",
|
||||
"Z": "Z Series (Roadster)",
|
||||
}
|
||||
},
|
||||
|
||||
'Mercedes-Benz': {
|
||||
"Mercedes-Benz": {
|
||||
3: {
|
||||
'A': 'A-Class',
|
||||
'B': 'B-Class',
|
||||
'C': 'C-Class',
|
||||
'E': 'E-Class',
|
||||
'G': 'G-Class',
|
||||
'S': 'S-Class',
|
||||
'V': 'V-Class',
|
||||
'X': 'GL-Class',
|
||||
"A": "A-Class",
|
||||
"B": "B-Class",
|
||||
"C": "C-Class",
|
||||
"E": "E-Class",
|
||||
"G": "G-Class",
|
||||
"S": "S-Class",
|
||||
"V": "V-Class",
|
||||
"X": "GL-Class",
|
||||
}
|
||||
},
|
||||
|
||||
'Volkswagen': {
|
||||
"Volkswagen": {
|
||||
2: {
|
||||
'1': 'Golf',
|
||||
'2': 'Jetta',
|
||||
'3': 'Passat',
|
||||
'4': 'Tiguan',
|
||||
'5': 'Polo',
|
||||
'6': 'Arteon',
|
||||
'7': 'Atlas',
|
||||
'8': 'Touareg',
|
||||
'9': 'Beetle',
|
||||
"1": "Golf",
|
||||
"2": "Jetta",
|
||||
"3": "Passat",
|
||||
"4": "Tiguan",
|
||||
"5": "Polo",
|
||||
"6": "Arteon",
|
||||
"7": "Atlas",
|
||||
"8": "Touareg",
|
||||
"9": "Beetle",
|
||||
}
|
||||
},
|
||||
|
||||
'Hyundai': {
|
||||
"Hyundai": {
|
||||
1: {
|
||||
'A': 'Accent',
|
||||
'B': 'Elantra',
|
||||
'C': 'Sonata',
|
||||
'D': 'Tucson',
|
||||
'E': 'Santa Fe',
|
||||
'F': 'Kona',
|
||||
'G': 'Palisade',
|
||||
'H': 'Veloster',
|
||||
'J': 'Genesis',
|
||||
"A": "Accent",
|
||||
"B": "Elantra",
|
||||
"C": "Sonata",
|
||||
"D": "Tucson",
|
||||
"E": "Santa Fe",
|
||||
"F": "Kona",
|
||||
"G": "Palisade",
|
||||
"H": "Veloster",
|
||||
"J": "Genesis",
|
||||
}
|
||||
},
|
||||
|
||||
'Chevrolet': {
|
||||
"Chevrolet": {
|
||||
2: {
|
||||
'A': 'Camaro',
|
||||
'B': 'Corvette',
|
||||
'C': 'Cruze',
|
||||
'D': 'Malibu',
|
||||
'E': 'Equinox',
|
||||
'F': 'Traverse',
|
||||
'G': 'Silverado',
|
||||
'H': 'Tahoe',
|
||||
'J': 'Suburban',
|
||||
"A": "Camaro",
|
||||
"B": "Corvette",
|
||||
"C": "Cruze",
|
||||
"D": "Malibu",
|
||||
"E": "Equinox",
|
||||
"F": "Traverse",
|
||||
"G": "Silverado",
|
||||
"H": "Tahoe",
|
||||
"J": "Suburban",
|
||||
}
|
||||
},
|
||||
|
||||
'Audi': {
|
||||
"Audi": {
|
||||
3: {
|
||||
'1': 'A1',
|
||||
'2': 'A2',
|
||||
'3': 'A3',
|
||||
'4': 'A4',
|
||||
'5': 'A5',
|
||||
'6': 'A6',
|
||||
'7': 'A7',
|
||||
'8': 'A8',
|
||||
'Q': 'Q Series (SUV)',
|
||||
'T': 'TT',
|
||||
"1": "A1",
|
||||
"2": "A2",
|
||||
"3": "A3",
|
||||
"4": "A4",
|
||||
"5": "A5",
|
||||
"6": "A6",
|
||||
"7": "A7",
|
||||
"8": "A8",
|
||||
"Q": "Q Series (SUV)",
|
||||
"T": "TT",
|
||||
}
|
||||
},
|
||||
|
||||
'Subaru': {
|
||||
"Subaru": {
|
||||
2: {
|
||||
'A': 'Impreza',
|
||||
'B': 'Legacy',
|
||||
'C': 'Outback',
|
||||
'D': 'Forester',
|
||||
'E': 'Crosstrek',
|
||||
'F': 'BRZ',
|
||||
'G': 'Ascent',
|
||||
"A": "Impreza",
|
||||
"B": "Legacy",
|
||||
"C": "Outback",
|
||||
"D": "Forester",
|
||||
"E": "Crosstrek",
|
||||
"F": "BRZ",
|
||||
"G": "Ascent",
|
||||
}
|
||||
},
|
||||
|
||||
'Mazda': {
|
||||
"Mazda": {
|
||||
1: {
|
||||
'A': 'Mazda3',
|
||||
'B': 'Mazda6',
|
||||
'C': 'CX-5',
|
||||
'D': 'CX-9',
|
||||
'E': 'MX-5 Miata',
|
||||
'F': 'CX-30',
|
||||
'G': 'RX-8',
|
||||
"A": "Mazda3",
|
||||
"B": "Mazda6",
|
||||
"C": "CX-5",
|
||||
"D": "CX-9",
|
||||
"E": "MX-5 Miata",
|
||||
"F": "CX-30",
|
||||
"G": "RX-8",
|
||||
}
|
||||
},
|
||||
|
||||
'Dongfeng': {
|
||||
"Dongfeng": {
|
||||
1: {
|
||||
'A': 'A-Series',
|
||||
'B': 'SHINE',
|
||||
'C': 'C-Series',
|
||||
'D': 'MAGE',
|
||||
'E': ['CAPTAIN E', 'E32'],
|
||||
'F': 'CAPTAIN C',
|
||||
'G': 'S50',
|
||||
'H': 'Dongfeng Fengshen AX3',
|
||||
'J': 'Dongfeng Joyear SUV',
|
||||
'K': 'Dongfeng Rich 6',
|
||||
'L': 'Dongfeng Sokon',
|
||||
'M': 'Dongfeng Glory 580',
|
||||
"A": "A-Series",
|
||||
"B": "SHINE",
|
||||
"C": "C-Series",
|
||||
"D": "MAGE",
|
||||
"E": ["CAPTAIN E", "E32"],
|
||||
"F": "CAPTAIN C",
|
||||
"G": "S50",
|
||||
"H": "Dongfeng Fengshen AX3",
|
||||
"J": "Dongfeng Joyear SUV",
|
||||
"K": "Dongfeng Rich 6",
|
||||
"L": "Dongfeng Sokon",
|
||||
"M": "Dongfeng Glory 580",
|
||||
},
|
||||
|
||||
2: {
|
||||
'3': 'C35', # Specific models in C-Series
|
||||
'1': 'C31',
|
||||
'2': 'C32',
|
||||
'7': 'C72',
|
||||
'6': 'A60', # Specific models in A-Series
|
||||
'3': 'A30',
|
||||
'X': ['AX7', 'AX4'],
|
||||
"3": "C35", # Specific models in C-Series
|
||||
"1": "C31",
|
||||
"2": "C32",
|
||||
"7": "C72",
|
||||
"6": "A60", # Specific models in A-Series
|
||||
"3": "A30",
|
||||
"X": ["AX7", "AX4"],
|
||||
},
|
||||
3: { # Third character (for AX models)
|
||||
'7': 'AX7', # Resolves AX7
|
||||
'4': 'AX4', # Resolves AX4
|
||||
}
|
||||
|
||||
"7": "AX7", # Resolves AX7
|
||||
"4": "AX4", # Resolves AX4
|
||||
},
|
||||
},
|
||||
|
||||
'Changan': {
|
||||
"Changan": {
|
||||
1: {
|
||||
'A': 'Changan CS35',
|
||||
'B': 'Changan CS55',
|
||||
'C': 'Changan CS75',
|
||||
'D': 'Changan CS85',
|
||||
'E': 'Changan CS95',
|
||||
'F': 'Changan Eado',
|
||||
'G': 'Changan Raeton',
|
||||
'H': 'Changan Alsvin',
|
||||
'J': 'Changan UNI-T',
|
||||
'K': 'Changan UNI-K',
|
||||
'L': 'Changan Oushang',
|
||||
'M': 'Changan Benni',
|
||||
"A": "Changan CS35",
|
||||
"B": "Changan CS55",
|
||||
"C": "Changan CS75",
|
||||
"D": "Changan CS85",
|
||||
"E": "Changan CS95",
|
||||
"F": "Changan Eado",
|
||||
"G": "Changan Raeton",
|
||||
"H": "Changan Alsvin",
|
||||
"J": "Changan UNI-T",
|
||||
"K": "Changan UNI-K",
|
||||
"L": "Changan Oushang",
|
||||
"M": "Changan Benni",
|
||||
}
|
||||
},
|
||||
|
||||
'Chery': {
|
||||
"Chery": {
|
||||
1: {
|
||||
'A': 'Chery Arrizo 5',
|
||||
'B': 'Chery Arrizo 7',
|
||||
'C': 'Chery Tiggo 3',
|
||||
'D': 'Chery Tiggo 5',
|
||||
'E': 'Chery Tiggo 7',
|
||||
'F': 'Chery Tiggo 8',
|
||||
'G': 'Chery QQ',
|
||||
'H': 'Chery Fulwin',
|
||||
'J': 'Chery Cowin',
|
||||
'K': 'Chery eQ1',
|
||||
'L': 'Chery Exeed TX',
|
||||
'M': 'Chery Exeed LX',
|
||||
"A": "Chery Arrizo 5",
|
||||
"B": "Chery Arrizo 7",
|
||||
"C": "Chery Tiggo 3",
|
||||
"D": "Chery Tiggo 5",
|
||||
"E": "Chery Tiggo 7",
|
||||
"F": "Chery Tiggo 8",
|
||||
"G": "Chery QQ",
|
||||
"H": "Chery Fulwin",
|
||||
"J": "Chery Cowin",
|
||||
"K": "Chery eQ1",
|
||||
"L": "Chery Exeed TX",
|
||||
"M": "Chery Exeed LX",
|
||||
}
|
||||
},
|
||||
|
||||
'MG': {
|
||||
"MG": {
|
||||
1: {
|
||||
'A': 'MG 3',
|
||||
'B': 'MG 5',
|
||||
'C': 'MG 6',
|
||||
'D': 'MG ZS',
|
||||
'E': 'MG HS',
|
||||
'F': 'MG RX5',
|
||||
'G': 'MG Marvel R',
|
||||
'H': 'MG EZS',
|
||||
'J': 'MG GT',
|
||||
'K': 'MG TF',
|
||||
'L': 'MG Cyberster',
|
||||
"A": "MG 3",
|
||||
"B": "MG 5",
|
||||
"C": "MG 6",
|
||||
"D": "MG ZS",
|
||||
"E": "MG HS",
|
||||
"F": "MG RX5",
|
||||
"G": "MG Marvel R",
|
||||
"H": "MG EZS",
|
||||
"J": "MG GT",
|
||||
"K": "MG TF",
|
||||
"L": "MG Cyberster",
|
||||
}
|
||||
},
|
||||
|
||||
'JMC': {
|
||||
"JMC": {
|
||||
1: {
|
||||
'A': 'JMC Yusheng',
|
||||
'B': 'JMC Vigus',
|
||||
'C': 'JMC Baodian',
|
||||
'D': 'JMC Ford Transit',
|
||||
'E': 'JMC S350',
|
||||
'F': 'JMC Teshun',
|
||||
'G': 'JMC Realm',
|
||||
'H': 'JMC Yuhu',
|
||||
'J': 'JMC E200',
|
||||
'K': 'JMC E400',
|
||||
"A": "JMC Yusheng",
|
||||
"B": "JMC Vigus",
|
||||
"C": "JMC Baodian",
|
||||
"D": "JMC Ford Transit",
|
||||
"E": "JMC S350",
|
||||
"F": "JMC Teshun",
|
||||
"G": "JMC Realm",
|
||||
"H": "JMC Yuhu",
|
||||
"J": "JMC E200",
|
||||
"K": "JMC E400",
|
||||
}
|
||||
},
|
||||
|
||||
'JAC': {
|
||||
"JAC": {
|
||||
1: {
|
||||
'A': 'JAC J3',
|
||||
'B': 'JAC J4',
|
||||
'C': 'JAC J5',
|
||||
'D': 'JAC J6',
|
||||
'E': 'JAC J7',
|
||||
'F': 'JAC S2',
|
||||
'G': 'JAC S3',
|
||||
'H': 'JAC S4',
|
||||
'J': 'JAC S5',
|
||||
'K': 'JAC S7',
|
||||
'L': 'JAC iEV7S',
|
||||
'M': 'JAC iEVS4',
|
||||
"A": "JAC J3",
|
||||
"B": "JAC J4",
|
||||
"C": "JAC J5",
|
||||
"D": "JAC J6",
|
||||
"E": "JAC J7",
|
||||
"F": "JAC S2",
|
||||
"G": "JAC S3",
|
||||
"H": "JAC S4",
|
||||
"J": "JAC S5",
|
||||
"K": "JAC S7",
|
||||
"L": "JAC iEV7S",
|
||||
"M": "JAC iEVS4",
|
||||
}
|
||||
},
|
||||
|
||||
'BYD': {
|
||||
"BYD": {
|
||||
1: {
|
||||
'A': 'BYD F3',
|
||||
'B': 'BYD F6',
|
||||
'C': 'BYD S6',
|
||||
'D': 'BYD Tang',
|
||||
'E': 'BYD Song',
|
||||
'F': 'BYD Yuan',
|
||||
'G': 'BYD Qin',
|
||||
'H': 'BYD Han',
|
||||
'J': 'BYD e5',
|
||||
'K': 'BYD e6',
|
||||
'L': 'BYD Dolphin',
|
||||
'M': 'BYD Seal',
|
||||
"A": "BYD F3",
|
||||
"B": "BYD F6",
|
||||
"C": "BYD S6",
|
||||
"D": "BYD Tang",
|
||||
"E": "BYD Song",
|
||||
"F": "BYD Yuan",
|
||||
"G": "BYD Qin",
|
||||
"H": "BYD Han",
|
||||
"J": "BYD e5",
|
||||
"K": "BYD e6",
|
||||
"L": "BYD Dolphin",
|
||||
"M": "BYD Seal",
|
||||
}
|
||||
},
|
||||
|
||||
'Geely': {
|
||||
"Geely": {
|
||||
1: {
|
||||
'A': 'Geely Emgrand EC7',
|
||||
'B': 'Geely Emgrand GS',
|
||||
'C': 'Geely Emgrand GL',
|
||||
'D': 'Geely Boyue',
|
||||
'E': 'Geely Xingyue',
|
||||
'F': 'Geely Binrui',
|
||||
'G': 'Geely Borui',
|
||||
'H': 'Geely Vision',
|
||||
'J': 'Geely Coolray',
|
||||
'K': 'Geely Monjaro',
|
||||
'L': 'Geely Geometry A',
|
||||
'M': 'Geely Geometry C',
|
||||
"A": "Geely Emgrand EC7",
|
||||
"B": "Geely Emgrand GS",
|
||||
"C": "Geely Emgrand GL",
|
||||
"D": "Geely Boyue",
|
||||
"E": "Geely Xingyue",
|
||||
"F": "Geely Binrui",
|
||||
"G": "Geely Borui",
|
||||
"H": "Geely Vision",
|
||||
"J": "Geely Coolray",
|
||||
"K": "Geely Monjaro",
|
||||
"L": "Geely Geometry A",
|
||||
"M": "Geely Geometry C",
|
||||
}
|
||||
},
|
||||
|
||||
'Great Wall Motors (GWM)': {
|
||||
"Great Wall Motors (GWM)": {
|
||||
1: {
|
||||
'A': 'GWM Haval H6',
|
||||
'B': 'GWM Haval H9',
|
||||
'C': 'GWM Haval Jolion',
|
||||
'D': 'GWM WEY VV5',
|
||||
'E': 'GWM WEY VV6',
|
||||
'F': 'GWM WEY VV7',
|
||||
'G': 'GWM Ora R1',
|
||||
'H': 'GWM Ora Good Cat',
|
||||
'J': 'GWM Poer',
|
||||
'K': 'GWM Tank 300',
|
||||
'L': 'GWM Tank 500',
|
||||
"A": "GWM Haval H6",
|
||||
"B": "GWM Haval H9",
|
||||
"C": "GWM Haval Jolion",
|
||||
"D": "GWM WEY VV5",
|
||||
"E": "GWM WEY VV6",
|
||||
"F": "GWM WEY VV7",
|
||||
"G": "GWM Ora R1",
|
||||
"H": "GWM Ora Good Cat",
|
||||
"J": "GWM Poer",
|
||||
"K": "GWM Tank 300",
|
||||
"L": "GWM Tank 500",
|
||||
}
|
||||
},
|
||||
|
||||
'FAW': {
|
||||
"FAW": {
|
||||
1: {
|
||||
'A': 'FAW Besturn B50',
|
||||
'B': 'FAW Besturn X40',
|
||||
'C': 'FAW Besturn X80',
|
||||
'D': 'FAW Hongqi H5',
|
||||
'E': 'FAW Hongqi H7',
|
||||
'F': 'FAW Hongqi HS5',
|
||||
'G': 'FAW Hongqi HS7',
|
||||
'H': 'FAW Jiefang Truck',
|
||||
'J': 'FAW Oley',
|
||||
'K': 'FAW Vita',
|
||||
"A": "FAW Besturn B50",
|
||||
"B": "FAW Besturn X40",
|
||||
"C": "FAW Besturn X80",
|
||||
"D": "FAW Hongqi H5",
|
||||
"E": "FAW Hongqi H7",
|
||||
"F": "FAW Hongqi HS5",
|
||||
"G": "FAW Hongqi HS7",
|
||||
"H": "FAW Jiefang Truck",
|
||||
"J": "FAW Oley",
|
||||
"K": "FAW Vita",
|
||||
}
|
||||
},
|
||||
|
||||
'SAIC Motor': {
|
||||
"SAIC Motor": {
|
||||
1: {
|
||||
'A': 'SAIC Maxus G10',
|
||||
'B': 'SAIC Maxus G50',
|
||||
'C': 'SAIC Maxus T60',
|
||||
'D': 'SAIC Roewe RX5',
|
||||
'E': 'SAIC Roewe i5',
|
||||
'F': 'SAIC Roewe i6',
|
||||
'G': 'SAIC MG ZS',
|
||||
'H': 'SAIC MG HS',
|
||||
'J': 'SAIC IM LS7',
|
||||
'K': 'SAIC IM Marvel R',
|
||||
"A": "SAIC Maxus G10",
|
||||
"B": "SAIC Maxus G50",
|
||||
"C": "SAIC Maxus T60",
|
||||
"D": "SAIC Roewe RX5",
|
||||
"E": "SAIC Roewe i5",
|
||||
"F": "SAIC Roewe i6",
|
||||
"G": "SAIC MG ZS",
|
||||
"H": "SAIC MG HS",
|
||||
"J": "SAIC IM LS7",
|
||||
"K": "SAIC IM Marvel R",
|
||||
}
|
||||
},
|
||||
}
|
||||
@ -1827,7 +1844,9 @@ def decode_vin_haikalna(vin):
|
||||
|
||||
pattern = r"^[A-HJ-NPR-Z0-9]{17}$"
|
||||
if not re.match(pattern, vin):
|
||||
raise Exception("VIN number must only contain alphanumeric symbols except 'I', 'O', and 'Q' ")
|
||||
raise Exception(
|
||||
"VIN number must only contain alphanumeric symbols except 'I', 'O', and 'Q' "
|
||||
)
|
||||
|
||||
vin = vin.upper()
|
||||
|
||||
@ -1842,10 +1861,9 @@ def decode_vin_haikalna(vin):
|
||||
model = decode_vds(manufacturer, vds)
|
||||
|
||||
data = {
|
||||
'maker': manufacturer,
|
||||
'model': model,
|
||||
'modelYear': year,
|
||||
|
||||
"maker": manufacturer,
|
||||
"model": model,
|
||||
"modelYear": year,
|
||||
}
|
||||
print(data)
|
||||
return data
|
||||
return data
|
||||
|
||||
@ -6,51 +6,71 @@ from inventory.models import CarTrim
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Add id_car_model column to CarTrim model and populate it using a CSV file.'
|
||||
help = "Add id_car_model column to CarTrim model and populate it using a CSV file."
|
||||
|
||||
def handle(self, *args, **options):
|
||||
# Define the file path relative to the project directory
|
||||
base_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
file_path = os.path.join(base_dir, 'data/mappings.csv')
|
||||
base_dir = os.path.dirname(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
)
|
||||
file_path = os.path.join(base_dir, "data/mappings.csv")
|
||||
|
||||
# Step 1: Add the new column if it does not exist
|
||||
with connection.cursor() as cursor:
|
||||
try:
|
||||
cursor.execute("ALTER TABLE inventory_cartrim ADD COLUMN id_car_model INTEGER")
|
||||
self.stdout.write(self.style.SUCCESS("Column 'id_car_model' added successfully."))
|
||||
cursor.execute(
|
||||
"ALTER TABLE inventory_cartrim ADD COLUMN id_car_model INTEGER"
|
||||
)
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS("Column 'id_car_model' added successfully.")
|
||||
)
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.WARNING(f"Column 'id_car_model' might already exist: {e}"))
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
f"Column 'id_car_model' might already exist: {e}"
|
||||
)
|
||||
)
|
||||
|
||||
# Step 2: Read and process the CSV file
|
||||
try:
|
||||
with open(file_path, mode='r', encoding='utf-8-sig') as csvfile:
|
||||
with open(file_path, mode="r", encoding="utf-8-sig") as csvfile:
|
||||
reader = csv.DictReader(csvfile)
|
||||
|
||||
for row in reader:
|
||||
# Extract id_car_serie and id_car_model from the current row
|
||||
id_car_serie = row.get('id_car_serie')
|
||||
id_car_model = row.get('id_car_model')
|
||||
id_car_serie = row.get("id_car_serie")
|
||||
id_car_model = row.get("id_car_model")
|
||||
|
||||
if not id_car_serie or not id_car_model:
|
||||
self.stdout.write(self.style.WARNING(f"Skipping row with missing data: {row}"))
|
||||
self.stdout.write(
|
||||
self.style.WARNING(f"Skipping row with missing data: {row}")
|
||||
)
|
||||
continue
|
||||
|
||||
# Step 3: Update CarTrim rows based on the id_car_serie
|
||||
updated_count = CarTrim.objects.filter(id_car_serie=id_car_serie).update(id_car_model=id_car_model)
|
||||
updated_count = CarTrim.objects.filter(
|
||||
id_car_serie=id_car_serie
|
||||
).update(id_car_model=id_car_model)
|
||||
|
||||
# Output progress
|
||||
if updated_count > 0:
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f"Updated {updated_count} rows for id_car_serie={id_car_serie} with id_car_model={id_car_model}."
|
||||
))
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"Updated {updated_count} rows for id_car_serie={id_car_serie} with id_car_model={id_car_model}."
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.stdout.write(self.style.WARNING(
|
||||
f"No rows found for id_car_serie={id_car_serie}."
|
||||
))
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
f"No rows found for id_car_serie={id_car_serie}."
|
||||
)
|
||||
)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("All rows have been processed successfully!"))
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS("All rows have been processed successfully!")
|
||||
)
|
||||
|
||||
except FileNotFoundError:
|
||||
self.stdout.write(self.style.ERROR(f"File not found: {file_path}"))
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.ERROR(f"An error occurred: {e}"))
|
||||
self.stdout.write(self.style.ERROR(f"An error occurred: {e}"))
|
||||
|
||||
@ -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}%)"
|
||||
)
|
||||
|
||||
@ -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(
|
||||
"####################################################################################################"
|
||||
)
|
||||
)
|
||||
|
||||
@ -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"
|
||||
)
|
||||
)
|
||||
|
||||
@ -7,14 +7,15 @@ from tqdm import tqdm # Progress bar support
|
||||
|
||||
# Database connection details
|
||||
db_config = {
|
||||
'host': 'localhost',
|
||||
'user': 'root',
|
||||
'password': "Kfsh&rc9788",
|
||||
'database': 'car2db_june'
|
||||
"host": "localhost",
|
||||
"user": "root",
|
||||
"password": "Kfsh&rc9788",
|
||||
"database": "car2db_june",
|
||||
}
|
||||
|
||||
EXCLUDED_TABLES = {"car_serie", "car_generation"} # Tables to exclude from direct dump
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Merge car_serie with car_generation, include in final JSON dump with a progress bar."
|
||||
|
||||
@ -22,7 +23,9 @@ class Command(BaseCommand):
|
||||
try:
|
||||
self.stdout.write(self.style.SUCCESS("Connecting to database..."))
|
||||
# Create SQLAlchemy engine
|
||||
engine = create_engine(f"mysql+pymysql://{db_config['user']}:{db_config['password']}@{db_config['host']}/{db_config['database']}")
|
||||
engine = create_engine(
|
||||
f"mysql+pymysql://{db_config['user']}:{db_config['password']}@{db_config['host']}/{db_config['database']}"
|
||||
)
|
||||
|
||||
# Load car_generation table
|
||||
self.stdout.write(self.style.SUCCESS("Loading car_generation data..."))
|
||||
@ -35,22 +38,39 @@ class Command(BaseCommand):
|
||||
car_serie_df = pd.read_sql(car_serie_query, engine)
|
||||
|
||||
# Perform a LEFT JOIN to keep all car series and merge with car generations
|
||||
self.stdout.write(self.style.SUCCESS("Merging car_serie with car_generation..."))
|
||||
merged_df = pd.merge(car_serie_df, car_generation_df, on="id_car_generation", how="left")
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS("Merging car_serie with car_generation...")
|
||||
)
|
||||
merged_df = pd.merge(
|
||||
car_serie_df, car_generation_df, on="id_car_generation", how="left"
|
||||
)
|
||||
|
||||
# Select and rename the relevant columns
|
||||
final_df = merged_df.rename(columns={
|
||||
"id_car_serie": "id_car_serie",
|
||||
"id_car_model_x": "id_car_model",
|
||||
"name_y": "generation_name",
|
||||
"name_x": "serie_name",
|
||||
"year_begin": "year_begin",
|
||||
"year_end": "year_end"
|
||||
})[["id_car_serie", "id_car_model", "generation_name", "serie_name", "year_begin", "year_end"]]
|
||||
final_df = merged_df.rename(
|
||||
columns={
|
||||
"id_car_serie": "id_car_serie",
|
||||
"id_car_model_x": "id_car_model",
|
||||
"name_y": "generation_name",
|
||||
"name_x": "serie_name",
|
||||
"year_begin": "year_begin",
|
||||
"year_end": "year_end",
|
||||
}
|
||||
)[
|
||||
[
|
||||
"id_car_serie",
|
||||
"id_car_model",
|
||||
"generation_name",
|
||||
"serie_name",
|
||||
"year_begin",
|
||||
"year_end",
|
||||
]
|
||||
]
|
||||
|
||||
# Convert merged data to a JSON-ready format
|
||||
self.stdout.write(self.style.SUCCESS("Processing merged data..."))
|
||||
car_serie_json = list(tqdm(final_df.to_dict(orient="records"), desc="Processing car_serie"))
|
||||
car_serie_json = list(
|
||||
tqdm(final_df.to_dict(orient="records"), desc="Processing car_serie")
|
||||
)
|
||||
|
||||
# Export the full database including merged car_serie
|
||||
self.export_database_to_json(car_serie_json)
|
||||
@ -59,7 +79,7 @@ class Command(BaseCommand):
|
||||
self.stdout.write(self.style.ERROR(f"Error: {e}"))
|
||||
|
||||
def export_database_to_json(self, car_serie_data):
|
||||
""" Export the entire MariaDB database to JSON, replacing car_serie with merged data """
|
||||
"""Export the entire MariaDB database to JSON, replacing car_serie with merged data"""
|
||||
try:
|
||||
self.stdout.write(self.style.SUCCESS("Exporting database to JSON..."))
|
||||
# Connect to the database using pymysql
|
||||
@ -90,13 +110,17 @@ class Command(BaseCommand):
|
||||
|
||||
# Save the JSON to a file
|
||||
self.stdout.write(self.style.SUCCESS("Saving database_export.json..."))
|
||||
with open('database_export.json', 'w', encoding='utf-8') as json_file:
|
||||
with open("database_export.json", "w", encoding="utf-8") as json_file:
|
||||
json.dump(database_json, json_file, indent=4, ensure_ascii=False)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("✅ Database exported to JSON successfully! (Including merged car_serie)"))
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
"✅ Database exported to JSON successfully! (Including merged car_serie)"
|
||||
)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.ERROR(f"Error exporting database: {e}"))
|
||||
finally:
|
||||
if connection:
|
||||
connection.close()
|
||||
connection.close()
|
||||
|
||||
@ -2,17 +2,17 @@ from django.core.management.base import BaseCommand
|
||||
from django.utils import timezone
|
||||
from plans.models import UserPlan
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Deactivates expired user plans'
|
||||
help = "Deactivates expired user plans"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
expired_plans = UserPlan.objects.filter(
|
||||
active=True,
|
||||
expire__lt=timezone.now()
|
||||
)
|
||||
expired_plans = UserPlan.objects.filter(active=True, expire__lt=timezone.now())
|
||||
|
||||
count = expired_plans.count()
|
||||
for plan in expired_plans:
|
||||
plan.expire_account()
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f'Successfully deactivated {count} expired plans'))
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f"Successfully deactivated {count} expired plans")
|
||||
)
|
||||
|
||||
@ -4,69 +4,72 @@ from django.db import transaction, models
|
||||
from django.utils.text import slugify
|
||||
from django.db.models import Case, When, Value
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Generate slugs for model instances with proper empty value handling'
|
||||
help = "Generate slugs for model instances with proper empty value handling"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--model',
|
||||
"--model",
|
||||
type=str,
|
||||
required=True,
|
||||
help='Model name (format: "app_label.ModelName")'
|
||||
help='Model name (format: "app_label.ModelName")',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--field',
|
||||
"--field",
|
||||
type=str,
|
||||
default='name',
|
||||
help='Field to use as slug source (default: "name")'
|
||||
default="name",
|
||||
help='Field to use as slug source (default: "name")',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--batch-size',
|
||||
"--batch-size",
|
||||
type=int,
|
||||
default=1000,
|
||||
help='Number of records to process at once (default: 1000)'
|
||||
help="Number of records to process at once (default: 1000)",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--dry-run',
|
||||
action='store_true',
|
||||
help='Test without actually saving changes'
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Test without actually saving changes",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--fill-empty',
|
||||
action='store_true',
|
||||
help='Fill empty slugs with model-ID when source field is empty'
|
||||
"--fill-empty",
|
||||
action="store_true",
|
||||
help="Fill empty slugs with model-ID when source field is empty",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
model = self.get_model(options['model'])
|
||||
source_field = options['field']
|
||||
batch_size = options['batch_size']
|
||||
dry_run = options['dry_run']
|
||||
fill_empty = options['fill_empty']
|
||||
model = self.get_model(options["model"])
|
||||
source_field = options["field"]
|
||||
batch_size = options["batch_size"]
|
||||
dry_run = options["dry_run"]
|
||||
fill_empty = options["fill_empty"]
|
||||
|
||||
queryset = model.objects.filter(models.Q(slug__isnull=True) | models.Q(slug=''))
|
||||
queryset = model.objects.filter(models.Q(slug__isnull=True) | models.Q(slug=""))
|
||||
total_count = queryset.count()
|
||||
processed = 0
|
||||
empty_source = 0
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f'Generating slugs for {total_count} {model._meta.model_name} records '
|
||||
f"Generating slugs for {total_count} {model._meta.model_name} records "
|
||||
f'using field "{source_field}" (batch size: {batch_size})'
|
||||
)
|
||||
)
|
||||
|
||||
with transaction.atomic():
|
||||
if dry_run:
|
||||
self.stdout.write(self.style.WARNING('DRY RUN - No changes will be saved'))
|
||||
self.stdout.write(
|
||||
self.style.WARNING("DRY RUN - No changes will be saved")
|
||||
)
|
||||
transaction.set_rollback(True)
|
||||
|
||||
for offset in range(0, total_count, batch_size):
|
||||
batch = queryset[offset:offset + batch_size]
|
||||
batch = queryset[offset : offset + batch_size]
|
||||
updates = []
|
||||
|
||||
for obj in batch:
|
||||
source_value = getattr(obj, source_field, '')
|
||||
source_value = getattr(obj, source_field, "")
|
||||
|
||||
if not source_value:
|
||||
if fill_empty:
|
||||
@ -76,12 +79,14 @@ class Command(BaseCommand):
|
||||
else:
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
f'Skipping {obj} (empty {source_field})'
|
||||
f"Skipping {obj} (empty {source_field})"
|
||||
)
|
||||
)
|
||||
continue
|
||||
else:
|
||||
slug_base = slugify(str(source_value))[:50] # Ensure string and truncate
|
||||
slug_base = slugify(str(source_value))[
|
||||
:50
|
||||
] # Ensure string and truncate
|
||||
new_slug = f"{slug_base}-{obj.pk}" # Guaranteed unique
|
||||
|
||||
updates.append((obj.pk, new_slug))
|
||||
@ -94,27 +99,26 @@ class Command(BaseCommand):
|
||||
)
|
||||
|
||||
self.stdout.write(
|
||||
f'Processed batch {offset//batch_size + 1}: '
|
||||
f'{min(offset + batch_size, total_count)}/{total_count}'
|
||||
f"Processed batch {offset // batch_size + 1}: "
|
||||
f"{min(offset + batch_size, total_count)}/{total_count}"
|
||||
)
|
||||
|
||||
stats = [
|
||||
f"Total processed: {processed}",
|
||||
f"Records with empty source field: {empty_source}",
|
||||
f"Skipped records: {total_count - processed - empty_source}"
|
||||
f"Skipped records: {total_count - processed - empty_source}",
|
||||
]
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS('\n'.join(stats))
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS("\n".join(stats)))
|
||||
|
||||
def get_model(self, model_path):
|
||||
"""Get model class from 'app_label.ModelName' string"""
|
||||
from django.apps import apps
|
||||
|
||||
try:
|
||||
app_label, model_name = model_path.split('.')
|
||||
app_label, model_name = model_path.split(".")
|
||||
return apps.get_model(app_label, model_name)
|
||||
except ValueError:
|
||||
raise self.style.ERROR('Model must be specified as "app_label.ModelName"')
|
||||
except LookupError as e:
|
||||
raise self.style.ERROR(f'Model not found: {e}')
|
||||
raise self.style.ERROR(f"Model not found: {e}")
|
||||
|
||||
@ -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
|
||||
|
||||
@ -8,8 +8,15 @@ django.setup()
|
||||
import json
|
||||
from tqdm import tqdm
|
||||
from inventory.models import (
|
||||
CarMake, CarModel, CarSerie, CarTrim, CarEquipment,
|
||||
CarSpecification, CarSpecificationValue, CarOption, CarOptionValue
|
||||
CarMake,
|
||||
CarModel,
|
||||
CarSerie,
|
||||
CarTrim,
|
||||
CarEquipment,
|
||||
CarSpecification,
|
||||
CarSpecificationValue,
|
||||
CarOption,
|
||||
CarOptionValue,
|
||||
)
|
||||
|
||||
# Load the cleaned JSON data
|
||||
@ -26,7 +33,7 @@ for item in tqdm(data["car_make"], desc="Inserting CarMake"):
|
||||
# "arabic_name": item.get("arabic_name", ""),
|
||||
# "logo": item.get("Logo", ""),
|
||||
# "is_sa_import": item.get("is_sa_import", False),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Step 2: Insert CarModel
|
||||
@ -38,7 +45,7 @@ for item in tqdm(data["car_model"], desc="Inserting CarModel"):
|
||||
"id_car_make_id": item["id_car_make"],
|
||||
"name": item["name"],
|
||||
# "arabic_name": item.get("arabic_name", ""),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Step 3: Insert CarSerie
|
||||
@ -53,7 +60,7 @@ for item in tqdm(data["car_serie"], desc="Inserting CarSerie"):
|
||||
"year_begin": item.get("year_begin"),
|
||||
"year_end": item.get("year_end"),
|
||||
"generation_name": item.get("generation_name", ""),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Step 4: Insert CarTrim
|
||||
@ -67,7 +74,7 @@ for item in tqdm(data["car_trim"], desc="Inserting CarTrim"):
|
||||
# "arabic_name": item.get("arabic_name", ""),
|
||||
"start_production_year": item["start_production_year"],
|
||||
"end_production_year": item["end_production_year"],
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Step 5: Insert CarEquipment
|
||||
@ -79,12 +86,14 @@ for item in tqdm(data["car_equipment"], desc="Inserting CarEquipment"):
|
||||
"id_car_trim_id": item["id_car_trim"],
|
||||
"name": item["name"],
|
||||
"year_begin": item.get("year"),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Step 6: Insert CarSpecification (Parent specifications first)
|
||||
parent_specs = [item for item in data["car_specification"] if item["id_parent"] is None]
|
||||
child_specs = [item for item in data["car_specification"] if item["id_parent"] is not None]
|
||||
child_specs = [
|
||||
item for item in data["car_specification"] if item["id_parent"] is not None
|
||||
]
|
||||
|
||||
for item in tqdm(parent_specs, desc="Inserting Parent CarSpecifications"):
|
||||
CarSpecification.objects.update_or_create(
|
||||
@ -92,8 +101,8 @@ for item in tqdm(parent_specs, desc="Inserting Parent CarSpecifications"):
|
||||
defaults={
|
||||
"name": item["name"],
|
||||
# "arabic_name": item.get("arabic_name", ""),
|
||||
"id_parent_id": None
|
||||
}
|
||||
"id_parent_id": None,
|
||||
},
|
||||
)
|
||||
|
||||
for item in tqdm(child_specs, desc="Inserting Child CarSpecifications"):
|
||||
@ -103,12 +112,14 @@ for item in tqdm(child_specs, desc="Inserting Child CarSpecifications"):
|
||||
defaults={
|
||||
"name": item["name"],
|
||||
# "arabic_name": item.get("arabic_name", ""),
|
||||
"id_parent_id": item["id_parent"]
|
||||
}
|
||||
"id_parent_id": item["id_parent"],
|
||||
},
|
||||
)
|
||||
|
||||
# Step 7: Insert CarSpecificationValue
|
||||
for item in tqdm(data["car_specification_value"], desc="Inserting CarSpecificationValue"):
|
||||
for item in tqdm(
|
||||
data["car_specification_value"], desc="Inserting CarSpecificationValue"
|
||||
):
|
||||
CarTrim.objects.get(id_car_trim=item["id_car_trim"])
|
||||
CarSpecification.objects.get(id_car_specification=item["id_car_specification"])
|
||||
CarSpecificationValue.objects.update_or_create(
|
||||
@ -118,7 +129,7 @@ for item in tqdm(data["car_specification_value"], desc="Inserting CarSpecificati
|
||||
"id_car_specification_id": item["id_car_specification"],
|
||||
"value": item["value"],
|
||||
"unit": item.get("unit", ""),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Step 8: Insert CarOption (Parent options first)
|
||||
@ -131,8 +142,8 @@ for item in tqdm(parent_options, desc="Inserting Parent CarOptions"):
|
||||
defaults={
|
||||
"name": item["name"],
|
||||
# "arabic_name": item.get("arabic_name", ""),
|
||||
"id_parent_id": None
|
||||
}
|
||||
"id_parent_id": None,
|
||||
},
|
||||
)
|
||||
|
||||
for item in tqdm(child_options, desc="Inserting Child CarOptions"):
|
||||
@ -142,8 +153,8 @@ for item in tqdm(child_options, desc="Inserting Child CarOptions"):
|
||||
defaults={
|
||||
"name": item["name"],
|
||||
# "arabic_name": item.get("arabic_name", ""),
|
||||
"id_parent_id": item["id_parent"]
|
||||
}
|
||||
"id_parent_id": item["id_parent"],
|
||||
},
|
||||
)
|
||||
|
||||
# Step 9: Insert CarOptionValue
|
||||
@ -156,7 +167,7 @@ for item in tqdm(data["car_option_value"], desc="Inserting CarOptionValue"):
|
||||
"id_car_option_id": item["id_car_option"],
|
||||
"id_car_equipment_id": item["id_car_equipment"],
|
||||
"is_base": item["is_base"],
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
print("Data population completed successfully.")
|
||||
print("Data population completed successfully.")
|
||||
|
||||
@ -2,11 +2,31 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from appointment.models import Service
|
||||
import datetime
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'create initial services offered'
|
||||
help = "create initial services offered"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
Service.objects.all().delete()
|
||||
Service.objects.create(name='call', price=0,duration=datetime.timedelta(minutes=10),currency='SAR',description='15 min call')
|
||||
Service.objects.create(name='meeting', price=0,duration=datetime.timedelta(minutes=30),currency='SAR',description='30 min meeting')
|
||||
Service.objects.create(name='email', price=0,duration=datetime.timedelta(minutes=30),currency='SAR',description='30 min visit')
|
||||
Service.objects.create(
|
||||
name="call",
|
||||
price=0,
|
||||
duration=datetime.timedelta(minutes=10),
|
||||
currency="SAR",
|
||||
description="15 min call",
|
||||
)
|
||||
Service.objects.create(
|
||||
name="meeting",
|
||||
price=0,
|
||||
duration=datetime.timedelta(minutes=30),
|
||||
currency="SAR",
|
||||
description="30 min meeting",
|
||||
)
|
||||
Service.objects.create(
|
||||
name="email",
|
||||
price=0,
|
||||
duration=datetime.timedelta(minutes=30),
|
||||
currency="SAR",
|
||||
description="30 min visit",
|
||||
)
|
||||
|
||||
@ -48,7 +48,9 @@ class Command(BaseCommand):
|
||||
if created:
|
||||
self.stdout.write(f"Added Exterior Color: {obj.name} ({obj.rgb})")
|
||||
else:
|
||||
self.stdout.write(f"Exterior Color already exists: {obj.name} ({obj.rgb})")
|
||||
self.stdout.write(
|
||||
f"Exterior Color already exists: {obj.name} ({obj.rgb})"
|
||||
)
|
||||
|
||||
self.stdout.write("Populating Interior Colors...")
|
||||
for color in self.interior_colors:
|
||||
@ -58,6 +60,8 @@ class Command(BaseCommand):
|
||||
if created:
|
||||
self.stdout.write(f"Added Interior Color: {obj.name} ({obj.rgb})")
|
||||
else:
|
||||
self.stdout.write(f"Interior Color already exists: {obj.name} ({obj.rgb})")
|
||||
self.stdout.write(
|
||||
f"Interior Color already exists: {obj.name} ({obj.rgb})"
|
||||
)
|
||||
|
||||
self.stdout.write("Finished populating colors.")
|
||||
self.stdout.write("Finished populating colors.")
|
||||
|
||||
@ -2,24 +2,27 @@ from django.core.management.base import BaseCommand
|
||||
from faker import Faker
|
||||
from inventory.models import Customer, Dealer
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Seed the Customer model with 20 records'
|
||||
help = "Seed the Customer model with 20 records"
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
fake = Faker()
|
||||
dealers = Dealer.objects.all()
|
||||
|
||||
if not dealers.exists():
|
||||
self.stdout.write(self.style.ERROR('No dealers found. Please create dealers first.'))
|
||||
self.stdout.write(
|
||||
self.style.ERROR("No dealers found. Please create dealers first.")
|
||||
)
|
||||
return
|
||||
|
||||
for _ in range(20):
|
||||
dealer = fake.random_element(elements=dealers)
|
||||
first_name = fake.first_name()
|
||||
middle_name = fake.first_name() if fake.boolean() else ''
|
||||
middle_name = fake.first_name() if fake.boolean() else ""
|
||||
last_name = fake.last_name()
|
||||
email = fake.unique.email()
|
||||
national_id = fake.unique.bothify(text='##########')
|
||||
national_id = fake.unique.bothify(text="##########")
|
||||
phone_number = fake.unique.phone_number()
|
||||
address = fake.address()
|
||||
|
||||
@ -31,7 +34,7 @@ class Command(BaseCommand):
|
||||
email=email,
|
||||
national_id=national_id,
|
||||
phone_number=phone_number,
|
||||
address=address
|
||||
address=address,
|
||||
)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS('Successfully seeded 20 customers.'))
|
||||
self.stdout.write(self.style.SUCCESS("Successfully seeded 20 customers."))
|
||||
|
||||
@ -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."))
|
||||
|
||||
@ -3,38 +3,53 @@ import csv
|
||||
from django.core.management.base import BaseCommand
|
||||
from inventory.models import CarSerie, CarModel
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Update or add CarSerie entries from a merged CSV file"
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
# Path to the merged CSV file
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
file_path = os.path.join(base_dir, "../../data/Updated_Merged_Car_Generation_and_Serie_Data.csv") # Adjust the path if needed
|
||||
file_path = os.path.join(
|
||||
base_dir, "../../data/Updated_Merged_Car_Generation_and_Serie_Data.csv"
|
||||
) # Adjust the path if needed
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
self.stdout.write(self.style.ERROR(f"File not found: {file_path}"))
|
||||
return
|
||||
|
||||
with open(file_path, newline='', encoding='utf-8') as csvfile:
|
||||
with open(file_path, newline="", encoding="utf-8") as csvfile:
|
||||
reader = csv.DictReader(csvfile)
|
||||
for row in reader:
|
||||
try:
|
||||
car_model = CarModel.objects.get(pk=row['id_car_model'])
|
||||
car_model = CarModel.objects.get(pk=row["id_car_model"])
|
||||
except CarModel.DoesNotExist:
|
||||
self.stdout.write(self.style.WARNING(f"CarModel with ID {row['id_car_model']} not found"))
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
f"CarModel with ID {row['id_car_model']} not found"
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
car_serie, created = CarSerie.objects.update_or_create(
|
||||
id_car_serie=row['id_car_serie'],
|
||||
id_car_serie=row["id_car_serie"],
|
||||
defaults={
|
||||
'id_car_model': car_model,
|
||||
'name': row['name'],
|
||||
'arabic_name': "-",
|
||||
'year_begin': int(float(row['year_begin'])) if row['year_begin'] else None,
|
||||
'year_end': int(float(row['year_end'])) if row['year_end'] else None,
|
||||
'generation_name': row['generation_name'],
|
||||
"id_car_model": car_model,
|
||||
"name": row["name"],
|
||||
"arabic_name": "-",
|
||||
"year_begin": int(float(row["year_begin"]))
|
||||
if row["year_begin"]
|
||||
else None,
|
||||
"year_end": int(float(row["year_end"]))
|
||||
if row["year_end"]
|
||||
else None,
|
||||
"generation_name": row["generation_name"],
|
||||
},
|
||||
)
|
||||
|
||||
action = "Created" if created else "Updated"
|
||||
self.stdout.write(self.style.SUCCESS(f"{action} CarSerie with ID {car_serie.id_car_serie}"))
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"{action} CarSerie with ID {car_serie.id_car_serie}"
|
||||
)
|
||||
)
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from inventory.models import VatRate
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def handle(self, *args, **kwargs):
|
||||
VatRate.objects.get_or_create(rate=Decimal('0.15'), is_active=True)
|
||||
VatRate.objects.get_or_create(rate=Decimal("0.15"), is_active=True)
|
||||
|
||||
@ -4,24 +4,23 @@ from plans.models import Plan, Quota, Pricing, PlanPricing
|
||||
from decimal import Decimal
|
||||
from django.db.models import Q
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Create basic subscription plans structure'
|
||||
help = "Create basic subscription plans structure"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--reset',
|
||||
action='store_true',
|
||||
help='Delete existing plans and quotas before creating new ones'
|
||||
"--reset",
|
||||
action="store_true",
|
||||
help="Delete existing plans and quotas before creating new ones",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if options['reset']:
|
||||
self.stdout.write(self.style.WARNING('Resetting existing plans data...'))
|
||||
if options["reset"]:
|
||||
self.stdout.write(self.style.WARNING("Resetting existing plans data..."))
|
||||
Plan.objects.all().delete()
|
||||
Quota.objects.filter(
|
||||
Q(codename='basic') |
|
||||
Q(codename='pro') |
|
||||
Q(codename='premium')
|
||||
Q(codename="basic") | Q(codename="pro") | Q(codename="premium")
|
||||
).delete()
|
||||
|
||||
# Ensure no existing plans are marked as default
|
||||
@ -32,93 +31,89 @@ class Command(BaseCommand):
|
||||
|
||||
# Create core quotas
|
||||
basic_quota, _ = Quota.objects.update_or_create(
|
||||
codename='basic',
|
||||
codename="basic",
|
||||
defaults={
|
||||
'name': 'Basic Features',
|
||||
'description': 'Essential platform access',
|
||||
'is_boolean': True
|
||||
}
|
||||
"name": "Basic Features",
|
||||
"description": "Essential platform access",
|
||||
"is_boolean": True,
|
||||
},
|
||||
)
|
||||
|
||||
pro_quota, _ = Quota.objects.update_or_create(
|
||||
codename='pro',
|
||||
codename="pro",
|
||||
defaults={
|
||||
'name': 'Pro Features',
|
||||
'description': 'Advanced functionality',
|
||||
'is_boolean': True
|
||||
}
|
||||
"name": "Pro Features",
|
||||
"description": "Advanced functionality",
|
||||
"is_boolean": True,
|
||||
},
|
||||
)
|
||||
|
||||
premium_quota, _ = Quota.objects.update_or_create(
|
||||
codename='premium',
|
||||
codename="premium",
|
||||
defaults={
|
||||
'name': 'Premium Features',
|
||||
'description': 'Full platform access',
|
||||
'is_boolean': True
|
||||
}
|
||||
"name": "Premium Features",
|
||||
"description": "Full platform access",
|
||||
"is_boolean": True,
|
||||
},
|
||||
)
|
||||
|
||||
# Create pricing period
|
||||
monthly_pricing, _ = Pricing.objects.update_or_create(
|
||||
name='Monthly',
|
||||
defaults={'period': 30}
|
||||
name="Monthly", defaults={"period": 30}
|
||||
)
|
||||
|
||||
# Define plan structure
|
||||
plans = [
|
||||
{
|
||||
'name': 'Basic',
|
||||
'description': 'Entry-level plan',
|
||||
'price': Decimal('0.00'),
|
||||
'period': None,
|
||||
'quotas': [basic_quota],
|
||||
'default': True
|
||||
"name": "Basic",
|
||||
"description": "Entry-level plan",
|
||||
"price": Decimal("0.00"),
|
||||
"period": None,
|
||||
"quotas": [basic_quota],
|
||||
"default": True,
|
||||
},
|
||||
{
|
||||
'name': 'Pro',
|
||||
'description': 'Professional plan',
|
||||
'price': Decimal('29.00'),
|
||||
'period': 30,
|
||||
'quotas': [basic_quota, pro_quota],
|
||||
'default': False
|
||||
"name": "Pro",
|
||||
"description": "Professional plan",
|
||||
"price": Decimal("29.00"),
|
||||
"period": 30,
|
||||
"quotas": [basic_quota, pro_quota],
|
||||
"default": False,
|
||||
},
|
||||
{
|
||||
'name': 'Premium',
|
||||
'description': 'Full access plan',
|
||||
'price': Decimal('99.00'),
|
||||
'period': 30,
|
||||
'quotas': [basic_quota, pro_quota, premium_quota],
|
||||
'default': None
|
||||
}
|
||||
"name": "Premium",
|
||||
"description": "Full access plan",
|
||||
"price": Decimal("99.00"),
|
||||
"period": 30,
|
||||
"quotas": [basic_quota, pro_quota, premium_quota],
|
||||
"default": None,
|
||||
},
|
||||
]
|
||||
|
||||
# Create plans and associations
|
||||
for plan_data in plans:
|
||||
plan, created = Plan.objects.update_or_create(
|
||||
name=plan_data['name'],
|
||||
name=plan_data["name"],
|
||||
defaults={
|
||||
'description': plan_data['description'],
|
||||
'default': plan_data.get('default', False),
|
||||
'available': True,
|
||||
'visible': True
|
||||
}
|
||||
"description": plan_data["description"],
|
||||
"default": plan_data.get("default", False),
|
||||
"available": True,
|
||||
"visible": True,
|
||||
},
|
||||
)
|
||||
|
||||
# Set quotas
|
||||
plan.quotas.set(plan_data['quotas'])
|
||||
plan.quotas.set(plan_data["quotas"])
|
||||
|
||||
# Create pricing if applicable
|
||||
if plan_data['price'] > 0:
|
||||
if plan_data["price"] > 0:
|
||||
PlanPricing.objects.update_or_create(
|
||||
plan=plan,
|
||||
pricing=monthly_pricing,
|
||||
defaults={
|
||||
'price': plan_data['price'],
|
||||
'visible': True
|
||||
}
|
||||
defaults={"price": plan_data["price"], "visible": True},
|
||||
)
|
||||
|
||||
status = 'Created' if created else 'Updated'
|
||||
self.stdout.write(self.style.SUCCESS(f'{status} {plan.name} plan'))
|
||||
status = "Created" if created else "Updated"
|
||||
self.stdout.write(self.style.SUCCESS(f"{status} {plan.name} plan"))
|
||||
|
||||
self.stdout.write(self.style.SUCCESS('Successfully created plans structure'))
|
||||
self.stdout.write(self.style.SUCCESS("Successfully created plans structure"))
|
||||
|
||||
@ -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,
|
||||
# )
|
||||
|
||||
@ -4,8 +4,9 @@ from inventory.tasks import create_coa_accounts
|
||||
from inventory.models import Dealer
|
||||
|
||||
User = get_user_model()
|
||||
class Command(BaseCommand):
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def handle(self, *args, **kwargs):
|
||||
# user = User.objects.last()
|
||||
# print(user.email)
|
||||
@ -17,4 +18,4 @@ class Command(BaseCommand):
|
||||
# result = re.match(r'^05\d{8}$', '0625252522')
|
||||
# print(result)
|
||||
dealer = Dealer.objects.last()
|
||||
create_coa_accounts(dealer.pk)
|
||||
create_coa_accounts(dealer.pk)
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -5,17 +5,19 @@ from django.conf import settings
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Translates car model names to Arabic and saves them in the arabic_name field.'
|
||||
help = (
|
||||
"Translates car model names to Arabic and saves them in the arabic_name field."
|
||||
)
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
client = OpenAI(api_key=settings.OPENAI_API_KEY)
|
||||
car_models = CarModel.objects.all()
|
||||
|
||||
total = car_models.count()
|
||||
print(f'Translating {total} names...')
|
||||
print(f"Translating {total} names...")
|
||||
|
||||
for index, car_model in enumerate(car_models, start=1):
|
||||
if not car_model.arabic_name or car_model.arabic_name == '-':
|
||||
if not car_model.arabic_name or car_model.arabic_name == "-":
|
||||
if isinstance(car_model.name, int):
|
||||
car_model.arabic_name = car_model.name
|
||||
car_model.save()
|
||||
@ -31,12 +33,9 @@ class Command(BaseCommand):
|
||||
"You are an assistant that translates English car names to Arabic."
|
||||
"If the name is purely numeric, keep it as is."
|
||||
"For mixed names like 'D9', translate them as 'دي 9'."
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": car_model.name
|
||||
}
|
||||
{"role": "user", "content": car_model.name},
|
||||
],
|
||||
temperature=0.2,
|
||||
)
|
||||
@ -45,4 +44,4 @@ class Command(BaseCommand):
|
||||
car_model.save()
|
||||
print(f"[{index}/{total}] .. Done")
|
||||
except Exception as e:
|
||||
print(f"Error translating '{car_model.name}': {e}")
|
||||
print(f"Error translating '{car_model.name}': {e}")
|
||||
|
||||
@ -2,32 +2,37 @@ from django.core.management.base import BaseCommand
|
||||
from inventory.models import CarMake
|
||||
import json
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Update CarMake model with data from a JSON file'
|
||||
help = "Update CarMake model with data from a JSON file"
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
# Load the JSON data from the file
|
||||
with open('carmake_updated_backup.json', 'r', encoding='utf-8') as file:
|
||||
with open("carmake_updated_backup.json", "r", encoding="utf-8") as file:
|
||||
car_makes_data = json.load(file)
|
||||
|
||||
# Iterate over the data and update the CarMake model
|
||||
for car_make_data in car_makes_data:
|
||||
pk = car_make_data['pk']
|
||||
fields = car_make_data['fields']
|
||||
pk = car_make_data["pk"]
|
||||
fields = car_make_data["fields"]
|
||||
|
||||
# Get or create the CarMake instance
|
||||
car_make, created = CarMake.objects.get_or_create(pk=pk)
|
||||
|
||||
# Update the fields
|
||||
car_make.name = fields['name']
|
||||
car_make.arabic_name = fields['arabic_name']
|
||||
car_make.logo = fields['logo']
|
||||
car_make.is_sa_import = fields['is_sa_import']
|
||||
car_make.name = fields["name"]
|
||||
car_make.arabic_name = fields["arabic_name"]
|
||||
car_make.logo = fields["logo"]
|
||||
car_make.is_sa_import = fields["is_sa_import"]
|
||||
|
||||
# Save the updated instance
|
||||
car_make.save()
|
||||
|
||||
if created:
|
||||
self.stdout.write(self.style.SUCCESS(f'Created CarMake: {car_make.name}'))
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f"Created CarMake: {car_make.name}")
|
||||
)
|
||||
else:
|
||||
self.stdout.write(self.style.SUCCESS(f'Updated CarMake: {car_make.name}'))
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f"Updated CarMake: {car_make.name}")
|
||||
)
|
||||
|
||||
@ -3,35 +3,46 @@ import csv
|
||||
from django.core.management.base import BaseCommand
|
||||
from inventory.models import CarModel, CarMake
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Update or add CarModel entries from a CSV file"
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
# Path to the car_model CSV file
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
file_path = os.path.join(base_dir, "../../data/car_model.csv") # Adjust path if needed
|
||||
file_path = os.path.join(
|
||||
base_dir, "../../data/car_model.csv"
|
||||
) # Adjust path if needed
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
self.stdout.write(self.style.ERROR(f"File not found: {file_path}"))
|
||||
return
|
||||
|
||||
with open(file_path, newline='', encoding='utf-8') as csvfile:
|
||||
with open(file_path, newline="", encoding="utf-8") as csvfile:
|
||||
reader = csv.DictReader(csvfile)
|
||||
for row in reader:
|
||||
try:
|
||||
car_make = CarMake.objects.get(pk=row['id_car_make'])
|
||||
car_make = CarMake.objects.get(pk=row["id_car_make"])
|
||||
except CarMake.DoesNotExist:
|
||||
self.stdout.write(self.style.WARNING(f"CarMake with ID {row['id_car_make']} not found"))
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
f"CarMake with ID {row['id_car_make']} not found"
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
car_model, created = CarModel.objects.update_or_create(
|
||||
id_car_model=row['id_car_model'],
|
||||
id_car_model=row["id_car_model"],
|
||||
defaults={
|
||||
'id_car_make': car_make,
|
||||
'name': row['name'],
|
||||
'arabic_name': row.get('arabic_name', ''),
|
||||
"id_car_make": car_make,
|
||||
"name": row["name"],
|
||||
"arabic_name": row.get("arabic_name", ""),
|
||||
},
|
||||
)
|
||||
|
||||
action = "Created" if created else "Updated"
|
||||
self.stdout.write(self.style.SUCCESS(f"{action} CarModel with ID {car_model.id_car_model}"))
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"{action} CarModel with ID {car_model.id_car_model}"
|
||||
)
|
||||
)
|
||||
|
||||
@ -3,42 +3,61 @@ import csv
|
||||
from django.core.management.base import BaseCommand
|
||||
from inventory.models import CarSpecificationValue, CarSpecification, CarTrim
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Update or add CarSpecificationValue entries from a CSV file"
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
# Path to the car_specification_value CSV file
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
file_path = os.path.join(base_dir, "../../data/car_specification_value.csv") # Adjust this path if needed
|
||||
file_path = os.path.join(
|
||||
base_dir, "../../data/car_specification_value.csv"
|
||||
) # Adjust this path if needed
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
self.stdout.write(self.style.ERROR(f"File not found: {file_path}"))
|
||||
return
|
||||
|
||||
with open(file_path, newline='', encoding='utf-8') as csvfile:
|
||||
with open(file_path, newline="", encoding="utf-8") as csvfile:
|
||||
reader = csv.DictReader(csvfile)
|
||||
for row in reader:
|
||||
try:
|
||||
car_trim = CarTrim.objects.get(pk=row['id_car_trim'])
|
||||
car_trim = CarTrim.objects.get(pk=row["id_car_trim"])
|
||||
except CarTrim.DoesNotExist:
|
||||
self.stdout.write(self.style.WARNING(f"CarTrim with ID {row['id_car_trim']} not found"))
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
f"CarTrim with ID {row['id_car_trim']} not found"
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
try:
|
||||
car_specification = CarSpecification.objects.get(pk=row['id_car_specification'])
|
||||
car_specification = CarSpecification.objects.get(
|
||||
pk=row["id_car_specification"]
|
||||
)
|
||||
except CarSpecification.DoesNotExist:
|
||||
self.stdout.write(self.style.WARNING(f"CarSpecification with ID {row['id_car_specification']} not found"))
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
f"CarSpecification with ID {row['id_car_specification']} not found"
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
car_specification_value, created = CarSpecificationValue.objects.update_or_create(
|
||||
id_car_specification_value=row['id_car_specification_value'],
|
||||
defaults={
|
||||
'id_car_trim': car_trim,
|
||||
'id_car_specification': car_specification,
|
||||
'value': row['value'],
|
||||
'unit': row.get('unit', ''),
|
||||
},
|
||||
car_specification_value, created = (
|
||||
CarSpecificationValue.objects.update_or_create(
|
||||
id_car_specification_value=row["id_car_specification_value"],
|
||||
defaults={
|
||||
"id_car_trim": car_trim,
|
||||
"id_car_specification": car_specification,
|
||||
"value": row["value"],
|
||||
"unit": row.get("unit", ""),
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
action = "Created" if created else "Updated"
|
||||
self.stdout.write(self.style.SUCCESS(f"{action} CarSpecificationValue with ID {car_specification_value.id_car_specification_value}"))
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"{action} CarSpecificationValue with ID {car_specification_value.id_car_specification_value}"
|
||||
)
|
||||
)
|
||||
|
||||
@ -3,45 +3,66 @@ import csv
|
||||
from django.core.management.base import BaseCommand
|
||||
from inventory.models import CarTrim, CarSerie, CarModel
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Update or add CarTrim entries from a CSV file"
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
# Path to the car_trim CSV file
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
file_path = os.path.join(base_dir, "../../data/car_trim.csv") # Adjust path if needed
|
||||
file_path = os.path.join(
|
||||
base_dir, "../../data/car_trim.csv"
|
||||
) # Adjust path if needed
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
self.stdout.write(self.style.ERROR(f"File not found: {file_path}"))
|
||||
return
|
||||
|
||||
with open(file_path, newline='', encoding='utf-8') as csvfile:
|
||||
with open(file_path, newline="", encoding="utf-8") as csvfile:
|
||||
reader = csv.DictReader(csvfile)
|
||||
for row in reader:
|
||||
try:
|
||||
car_serie = CarSerie.objects.get(pk=row['id_car_serie'])
|
||||
car_serie = CarSerie.objects.get(pk=row["id_car_serie"])
|
||||
except CarSerie.DoesNotExist:
|
||||
self.stdout.write(self.style.WARNING(f"CarSerie with ID {row['id_car_serie']} not found"))
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
f"CarSerie with ID {row['id_car_serie']} not found"
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
car_model = None
|
||||
if row['id_car_model']:
|
||||
if row["id_car_model"]:
|
||||
try:
|
||||
car_model = CarModel.objects.get(pk=row['id_car_model'])
|
||||
car_model = CarModel.objects.get(pk=row["id_car_model"])
|
||||
except CarModel.DoesNotExist:
|
||||
self.stdout.write(self.style.WARNING(f"CarModel with ID {row['id_car_model']} not found"))
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
f"CarModel with ID {row['id_car_model']} not found"
|
||||
)
|
||||
)
|
||||
|
||||
car_trim, created = CarTrim.objects.update_or_create(
|
||||
id_car_trim=row['id_car_trim'],
|
||||
id_car_trim=row["id_car_trim"],
|
||||
defaults={
|
||||
'id_car_serie': car_serie,
|
||||
'id_car_model': car_model,
|
||||
'name': row['name'],
|
||||
'arabic_name': row.get('arabic_name', ''),
|
||||
'start_production_year': int(float(row['start_production_year'])) if row['start_production_year'] else None,
|
||||
'end_production_year': int(float(row['end_production_year'])) if row['end_production_year'] else None,
|
||||
"id_car_serie": car_serie,
|
||||
"id_car_model": car_model,
|
||||
"name": row["name"],
|
||||
"arabic_name": row.get("arabic_name", ""),
|
||||
"start_production_year": int(
|
||||
float(row["start_production_year"])
|
||||
)
|
||||
if row["start_production_year"]
|
||||
else None,
|
||||
"end_production_year": int(float(row["end_production_year"]))
|
||||
if row["end_production_year"]
|
||||
else None,
|
||||
},
|
||||
)
|
||||
|
||||
action = "Created" if created else "Updated"
|
||||
self.stdout.write(self.style.SUCCESS(f"{action} CarTrim with ID {car_trim.id_car_trim}"))
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"{action} CarTrim with ID {car_trim.id_car_trim}"
|
||||
)
|
||||
)
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
880
inventory/migrations/0001_initial.py
Normal file
880
inventory/migrations/0001_initial.py
Normal file
@ -0,0 +1,880 @@
|
||||
# Generated by Django 5.2.1 on 2025-06-18 15:44
|
||||
|
||||
import datetime
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import inventory.mixins
|
||||
import inventory.models
|
||||
import phonenumber_field.modelfields
|
||||
import uuid
|
||||
from decimal import Decimal
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('appointment', '0001_initial'),
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('django_ledger', '0022_alter_billmodel_bill_items_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CarEquipment',
|
||||
fields=[
|
||||
('id_car_equipment', models.AutoField(primary_key=True, serialize=False)),
|
||||
('name', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('arabic_name', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('year_begin', models.IntegerField(blank=True, null=True)),
|
||||
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Equipment',
|
||||
},
|
||||
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CarMake',
|
||||
fields=[
|
||||
('id_car_make', models.AutoField(primary_key=True, serialize=False)),
|
||||
('name', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
|
||||
('arabic_name', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('logo', models.ImageField(blank=True, null=True, upload_to='car_make', verbose_name='logo')),
|
||||
('is_sa_import', models.BooleanField(default=False)),
|
||||
('car_type', models.SmallIntegerField(blank=True, choices=[(1, 'Car'), (2, 'Light Commercial'), (3, 'Heavy-Duty Tractors'), (4, 'Trailers'), (5, 'Medium Trucks'), (6, 'Buses'), (20, 'Motorcycles'), (21, 'Buggy'), (22, 'Moto ATV'), (23, 'Scooters'), (24, 'Karting'), (25, 'ATV'), (26, 'Snowmobiles')], null=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Make',
|
||||
},
|
||||
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ExteriorColors',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, verbose_name='Name')),
|
||||
('arabic_name', models.CharField(max_length=255, verbose_name='Arabic Name')),
|
||||
('rgb', models.CharField(blank=True, max_length=24, null=True, verbose_name='RGB')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Exterior Colors',
|
||||
'verbose_name_plural': 'Exterior Colors',
|
||||
},
|
||||
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='InteriorColors',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, verbose_name='Name')),
|
||||
('arabic_name', models.CharField(max_length=255, verbose_name='Arabic Name')),
|
||||
('rgb', models.CharField(blank=True, max_length=24, null=True, verbose_name='RGB')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Interior Colors',
|
||||
'verbose_name_plural': 'Interior Colors',
|
||||
},
|
||||
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Payment',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='amount')),
|
||||
('payment_method', models.CharField(choices=[('cash', 'cash'), ('credit', 'credit'), ('transfer', 'transfer'), ('debit', 'debit'), ('sadad', 'SADAD')], max_length=50, verbose_name='method')),
|
||||
('reference_number', models.CharField(blank=True, max_length=100, null=True, verbose_name='reference number')),
|
||||
('payment_date', models.DateField(auto_now_add=True, verbose_name='date')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'payment',
|
||||
'verbose_name_plural': 'payments',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='VatRate',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('rate', models.DecimalField(decimal_places=2, default=Decimal('0.15'), max_digits=5)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AdditionalServices',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, verbose_name='Name')),
|
||||
('arabic_name', models.CharField(max_length=255, verbose_name='Arabic Name')),
|
||||
('description', models.TextField(verbose_name='Description')),
|
||||
('price', models.DecimalField(decimal_places=2, max_digits=14, verbose_name='Price')),
|
||||
('taxable', models.BooleanField(default=False, verbose_name='taxable')),
|
||||
('uom', models.CharField(choices=[('EA', 'Each'), ('PR', 'Pair'), ('SET', 'Set'), ('GAL', 'Gallon'), ('L', 'Liter'), ('M', 'Meter'), ('KG', 'Kilogram'), ('HR', 'Hour'), ('BX', 'Box'), ('RL', 'Roll'), ('PKG', 'Package'), ('DZ', 'Dozen'), ('SQ_M', 'Square Meter'), ('PC', 'Piece'), ('BDL', 'Bundle')], max_length=10, verbose_name='Unit of Measurement')),
|
||||
('item', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='django_ledger.itemmodel', verbose_name='Item')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Additional Services',
|
||||
'verbose_name_plural': 'Additional Services',
|
||||
},
|
||||
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Car',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True, verbose_name='Primary Key')),
|
||||
('slug', models.SlugField(blank=True, help_text='Slug for the object. If not provided, it will be generated automatically.', null=True, unique=True, verbose_name='Slug')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
|
||||
('vin', models.CharField(max_length=17, unique=True, verbose_name='VIN')),
|
||||
('year', models.IntegerField(verbose_name='Year')),
|
||||
('status', models.CharField(choices=[('available', 'Available'), ('sold', 'Sold'), ('hold', 'Hold'), ('damaged', 'Damaged'), ('reserved', 'Reserved'), ('transfer', 'Transfer')], default='available', max_length=10, verbose_name='Status')),
|
||||
('stock_type', models.CharField(choices=[('new', 'New'), ('used', 'Used')], default='new', max_length=10, verbose_name='Stock Type')),
|
||||
('remarks', models.TextField(blank=True, null=True, verbose_name='Remarks')),
|
||||
('mileage', models.IntegerField(blank=True, null=True, verbose_name='Mileage')),
|
||||
('receiving_date', models.DateTimeField(verbose_name='Receiving Date')),
|
||||
('hash', models.CharField(blank=True, max_length=64, null=True, verbose_name='Hash')),
|
||||
('item_model', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='django_ledger.itemmodel', verbose_name='Item Model')),
|
||||
('id_car_make', models.ForeignKey(blank=True, db_column='id_car_make', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmake', verbose_name='Make')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Car',
|
||||
'verbose_name_plural': 'Cars',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CarFinance',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('cost_price', models.DecimalField(decimal_places=2, max_digits=14, verbose_name='Cost Price')),
|
||||
('selling_price', models.DecimalField(decimal_places=2, max_digits=14, verbose_name='Selling Price')),
|
||||
('discount_amount', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=14, verbose_name='Discount Amount')),
|
||||
('is_sold', models.BooleanField(default=False)),
|
||||
('additional_services', models.ManyToManyField(blank=True, related_name='additional_finances', to='inventory.additionalservices')),
|
||||
('car', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='finances', to='inventory.car')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Car Financial Details',
|
||||
'verbose_name_plural': 'Car Financial Details',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CarModel',
|
||||
fields=[
|
||||
('id_car_model', models.AutoField(primary_key=True, serialize=False)),
|
||||
('name', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('arabic_name', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
|
||||
('id_car_make', models.ForeignKey(db_column='id_car_make', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmake')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Model',
|
||||
},
|
||||
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='car',
|
||||
name='id_car_model',
|
||||
field=models.ForeignKey(blank=True, db_column='id_car_model', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmodel', verbose_name='Model'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CarOption',
|
||||
fields=[
|
||||
('id_car_option', models.AutoField(primary_key=True, serialize=False)),
|
||||
('name', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('arabic_name', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
|
||||
('id_parent', models.ForeignKey(blank=True, db_column='id_parent', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.caroption')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Option',
|
||||
},
|
||||
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CarOptionValue',
|
||||
fields=[
|
||||
('id_car_option_value', models.AutoField(primary_key=True, serialize=False)),
|
||||
('value', models.CharField(max_length=500)),
|
||||
('unit', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('is_base', models.IntegerField()),
|
||||
('id_car_equipment', models.ForeignKey(db_column='id_car_equipment', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carequipment')),
|
||||
('id_car_option', models.ForeignKey(db_column='id_car_option', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.caroption')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Option Value',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CarRegistration',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('plate_number', models.IntegerField(verbose_name='Plate Number')),
|
||||
('text1', models.CharField(max_length=1, verbose_name='Text 1')),
|
||||
('text2', models.CharField(blank=True, max_length=1, null=True, verbose_name='Text 2')),
|
||||
('text3', models.CharField(blank=True, max_length=1, null=True, verbose_name='Text 3')),
|
||||
('registration_date', models.DateTimeField(verbose_name='Registration Date')),
|
||||
('car', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='registrations', to='inventory.car', verbose_name='Car')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Registration',
|
||||
'verbose_name_plural': 'Registrations',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CarSerie',
|
||||
fields=[
|
||||
('id_car_serie', models.AutoField(primary_key=True, serialize=False)),
|
||||
('name', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('arabic_name', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('year_begin', models.IntegerField(blank=True, null=True)),
|
||||
('year_end', models.IntegerField(blank=True, null=True)),
|
||||
('generation_name', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
|
||||
('id_car_model', models.ForeignKey(db_column='id_car_model', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmodel')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Series',
|
||||
},
|
||||
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='car',
|
||||
name='id_car_serie',
|
||||
field=models.ForeignKey(blank=True, db_column='id_car_serie', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carserie', verbose_name='Series'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CarSpecification',
|
||||
fields=[
|
||||
('id_car_specification', models.AutoField(primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('arabic_name', models.CharField(max_length=255)),
|
||||
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
|
||||
('id_parent', models.ForeignKey(blank=True, db_column='id_parent', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carspecification')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Specification',
|
||||
},
|
||||
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CarTrim',
|
||||
fields=[
|
||||
('id_car_trim', models.AutoField(primary_key=True, serialize=False)),
|
||||
('name', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('arabic_name', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('start_production_year', models.IntegerField(blank=True, null=True)),
|
||||
('end_production_year', models.IntegerField(blank=True, null=True)),
|
||||
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
|
||||
('id_car_serie', models.ForeignKey(db_column='id_car_serie', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carserie')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Trim',
|
||||
},
|
||||
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CarSpecificationValue',
|
||||
fields=[
|
||||
('id_car_specification_value', models.AutoField(primary_key=True, serialize=False)),
|
||||
('value', models.CharField(max_length=500)),
|
||||
('unit', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('id_car_specification', models.ForeignKey(db_column='id_car_specification', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carspecification')),
|
||||
('id_car_trim', models.ForeignKey(db_column='id_car_trim', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.cartrim')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Specification Value',
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='carequipment',
|
||||
name='id_car_trim',
|
||||
field=models.ForeignKey(db_column='id_car_trim', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.cartrim'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='car',
|
||||
name='id_car_trim',
|
||||
field=models.ForeignKey(blank=True, db_column='id_car_trim', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.cartrim', verbose_name='Trim'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CustomCard',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('custom_number', models.CharField(max_length=255, verbose_name='Custom Number')),
|
||||
('custom_date', models.DateField(verbose_name='Custom Date')),
|
||||
('car', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='custom_cards', to='inventory.car', verbose_name='Car')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Custom Card',
|
||||
'verbose_name_plural': 'Custom Cards',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Dealer',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('crn', models.CharField(blank=True, max_length=10, null=True, verbose_name='Commercial Registration Number')),
|
||||
('vrn', models.CharField(blank=True, max_length=15, null=True, verbose_name='VAT Registration Number')),
|
||||
('arabic_name', models.CharField(max_length=255, verbose_name='Arabic Name')),
|
||||
('name', models.CharField(max_length=255, verbose_name='English Name')),
|
||||
('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')),
|
||||
('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='Address')),
|
||||
('logo', models.ImageField(blank=True, null=True, upload_to='logos/users', verbose_name='Logo')),
|
||||
('joined_at', models.DateTimeField(auto_now_add=True, verbose_name='Joined At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
|
||||
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
|
||||
('entity', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='django_ledger.entitymodel')),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='dealer', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Dealer',
|
||||
'verbose_name_plural': 'Dealers',
|
||||
},
|
||||
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
|
||||
managers=[
|
||||
('objects', inventory.models.DealerUserManager()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CustomGroup',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('group', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='auth.group', verbose_name='Group')),
|
||||
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='groups', to='inventory.dealer')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Customer',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(choices=[('mr', 'Mr'), ('mrs', 'Mrs'), ('ms', 'Ms'), ('miss', 'Miss'), ('dr', 'Dr'), ('prof', 'Prof'), ('prince', 'Prince'), ('princess', 'Princess'), ('company', 'Company'), ('na', 'N/A')], default='na', max_length=10, verbose_name='Title')),
|
||||
('first_name', models.CharField(max_length=50, verbose_name='First Name')),
|
||||
('last_name', models.CharField(max_length=50, verbose_name='Last Name')),
|
||||
('gender', models.CharField(choices=[('m', 'Male'), ('f', 'Female')], max_length=1, verbose_name='Gender')),
|
||||
('dob', models.DateField(blank=True, null=True, verbose_name='Date of Birth')),
|
||||
('email', models.EmailField(max_length=254, unique=True, verbose_name='Email')),
|
||||
('national_id', models.CharField(blank=True, max_length=10, null=True, unique=True, verbose_name='National ID')),
|
||||
('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', unique=True, verbose_name='Phone Number')),
|
||||
('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='Address')),
|
||||
('active', models.BooleanField(default=True, verbose_name='Active')),
|
||||
('image', models.ImageField(blank=True, null=True, upload_to='customers/', verbose_name='Image')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
|
||||
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
|
||||
('slug', models.SlugField(blank=True, editable=False, max_length=255, null=True, unique=True)),
|
||||
('customer_model', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='django_ledger.customermodel')),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='customer_profile', to=settings.AUTH_USER_MODEL)),
|
||||
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='customers', to='inventory.dealer')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Customer',
|
||||
'verbose_name_plural': 'Customers',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CarTransfer',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('transfer_date', models.DateTimeField(auto_now_add=True, verbose_name='Transfer Date')),
|
||||
('quantity', models.IntegerField(default=1, verbose_name='Quantity')),
|
||||
('remarks', models.TextField(blank=True, null=True, verbose_name='Remarks')),
|
||||
('status', models.CharField(default='draft', max_length=10, verbose_name=[('draft', 'Draft'), ('approved', 'Approved'), ('pending', 'Pending'), ('accepted', 'Accepted'), ('success', 'Success'), ('reject', 'Reject'), ('cancelled', 'Cancelled')])),
|
||||
('is_approved', models.BooleanField(default=False)),
|
||||
('active', models.BooleanField(default=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
|
||||
('car', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transfer_logs', to='inventory.car', verbose_name='Car')),
|
||||
('from_dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transfers_out', to='inventory.dealer', verbose_name='From Dealer')),
|
||||
('to_dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transfers_in', to='inventory.dealer', verbose_name='To Dealer')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Car Transfer Log',
|
||||
'verbose_name_plural': 'Car Transfer Logs',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CarLocation',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('description', models.TextField(blank=True, help_text='Optional description about the showroom placement.', null=True, verbose_name='Description')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Updated')),
|
||||
('car', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='location', to='inventory.car', verbose_name='Car')),
|
||||
('owner', models.ForeignKey(help_text='Dealer who owns the car.', on_delete=django.db.models.deletion.CASCADE, related_name='owned_cars', to='inventory.dealer', verbose_name='Owner')),
|
||||
('showroom', models.ForeignKey(help_text='Dealer where the car is displayed (can be the owner).', on_delete=django.db.models.deletion.CASCADE, related_name='showroom_cars', to='inventory.dealer', verbose_name='Showroom')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Car Location',
|
||||
'verbose_name_plural': 'Car Locations',
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='car',
|
||||
name='dealer',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='cars', to='inventory.dealer', verbose_name='Dealer'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='additionalservices',
|
||||
name='dealer',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.dealer', verbose_name='Dealer'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Activity',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('object_id', models.PositiveIntegerField()),
|
||||
('activity_type', models.CharField(choices=[('call', 'Call'), ('sms', 'SMS'), ('email', 'Email'), ('meeting', 'Meeting'), ('whatsapp', 'WhatsApp'), ('visit', 'Visit'), ('negotiation', 'Negotiation'), ('follow_up', 'Follow Up'), ('won', 'Won'), ('lost', 'Lost'), ('closed', 'Closed'), ('converted', 'Converted'), ('transfer', 'Transfer'), ('add_car', 'Add Car'), ('sale_car', 'Sale Car'), ('reserve_car', 'Reserve Car'), ('transfer_car', 'Transfer Car'), ('remove_car', 'Remove Car'), ('create_quotation', 'Create Quotation'), ('cancel_quotation', 'Cancel Quotation'), ('create_order', 'Create Order'), ('cancel_order', 'Cancel Order'), ('create_invoice', 'Create Invoice'), ('cancel_invoice', 'Cancel Invoice')], max_length=50, verbose_name='Activity Type')),
|
||||
('notes', models.TextField(blank=True, null=True, verbose_name='Notes')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
|
||||
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
|
||||
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='contenttypes.contenttype')),
|
||||
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='activities_created_by', to=settings.AUTH_USER_MODEL)),
|
||||
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='activities', to='inventory.dealer')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Activity',
|
||||
'verbose_name_plural': 'Activities',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DealerSettings',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('additional_info', models.JSONField(blank=True, default=dict, null=True)),
|
||||
('bill_cash_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bill_cash', to='django_ledger.accountmodel')),
|
||||
('bill_prepaid_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bill_prepaid', to='django_ledger.accountmodel')),
|
||||
('bill_unearned_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bill_unearned', to='django_ledger.accountmodel')),
|
||||
('dealer', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='settings', to='inventory.dealer')),
|
||||
('invoice_cash_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoice_cash', to='django_ledger.accountmodel')),
|
||||
('invoice_prepaid_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoice_prepaid', to='django_ledger.accountmodel')),
|
||||
('invoice_unearned_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoice_unearned', to='django_ledger.accountmodel')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Email',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('object_id', models.UUIDField()),
|
||||
('from_email', models.TextField(blank=True, null=True, verbose_name='From Email')),
|
||||
('to_email', models.TextField(blank=True, null=True, verbose_name='To Email')),
|
||||
('subject', models.TextField(blank=True, null=True, verbose_name='Subject')),
|
||||
('message', models.TextField(blank=True, null=True, verbose_name='Message')),
|
||||
('status', models.CharField(choices=[('SENT', 'Sent'), ('FAILED', 'Failed'), ('DELIVERED', 'Delivered'), ('OPEN', 'Open'), ('DRAFT', 'Draft')], default='OPEN', max_length=20, verbose_name='Status')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
|
||||
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
|
||||
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
|
||||
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='emails_created', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Email',
|
||||
'verbose_name_plural': 'Emails',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Lead',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('first_name', models.CharField(max_length=50, verbose_name='First Name')),
|
||||
('last_name', models.CharField(max_length=50, verbose_name='Last Name')),
|
||||
('email', models.EmailField(max_length=254, verbose_name='Email')),
|
||||
('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')),
|
||||
('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='Address')),
|
||||
('lead_type', models.CharField(choices=[('customer', 'Customer'), ('organization', 'Organization')], default='customer', max_length=50, verbose_name='Lead Type')),
|
||||
('source', models.CharField(choices=[('referrals', 'Referrals'), ('whatsapp', 'WhatsApp'), ('showroom', 'Showroom'), ('tiktok', 'TikTok'), ('instagram', 'Instagram'), ('x', 'X'), ('facebook', 'Facebook'), ('motory', 'Motory'), ('influencers', 'Influencers'), ('youtube', 'Youtube'), ('campaign', 'Campaign')], max_length=50, verbose_name='Source')),
|
||||
('channel', models.CharField(choices=[('walk_in', 'Walk In'), ('toll_free', 'Toll Free'), ('website', 'Website'), ('email', 'Email'), ('form', 'Form')], max_length=50, verbose_name='Channel')),
|
||||
('status', models.CharField(choices=[('new', 'New'), ('contacted', 'Contacted'), ('qualified', 'Qualified'), ('unqualified', 'Unqualified'), ('converted', 'Converted')], db_index=True, default='new', max_length=50, verbose_name='Status')),
|
||||
('next_action', models.CharField(blank=True, max_length=255, null=True, verbose_name='Next Action')),
|
||||
('next_action_date', models.DateTimeField(blank=True, null=True, verbose_name='Next Action Date')),
|
||||
('is_converted', models.BooleanField(default=False)),
|
||||
('converted_at', models.DateTimeField(blank=True, null=True)),
|
||||
('created', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Created')),
|
||||
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
|
||||
('slug', models.SlugField(blank=True, null=True, unique=True)),
|
||||
('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='customer_leads', to='inventory.customer')),
|
||||
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='leads', to='inventory.dealer')),
|
||||
('id_car_make', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmake', verbose_name='Make')),
|
||||
('id_car_model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmodel', verbose_name='Model')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Lead',
|
||||
'verbose_name_plural': 'Leads',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Notes',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('object_id', models.UUIDField()),
|
||||
('note', models.TextField(verbose_name='Note')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
|
||||
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
|
||||
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
|
||||
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='notes_created', to=settings.AUTH_USER_MODEL)),
|
||||
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='inventory.dealer')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Note',
|
||||
'verbose_name_plural': 'Notes',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Notification',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('message', models.CharField(max_length=255, verbose_name='Message')),
|
||||
('is_read', models.BooleanField(default=False, verbose_name='Is Read')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Notification',
|
||||
'verbose_name_plural': 'Notifications',
|
||||
'ordering': ['-created'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Organization',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, verbose_name='Name')),
|
||||
('arabic_name', models.CharField(max_length=255, verbose_name='Arabic Name')),
|
||||
('crn', models.CharField(max_length=15, verbose_name='Commercial Registration Number')),
|
||||
('vrn', models.CharField(max_length=15, verbose_name='VAT Registration Number')),
|
||||
('email', models.EmailField(max_length=254, verbose_name='Email')),
|
||||
('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')),
|
||||
('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='Address')),
|
||||
('logo', models.ImageField(blank=True, null=True, upload_to='logos', verbose_name='Logo')),
|
||||
('active', models.BooleanField(default=True, verbose_name='Active')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
|
||||
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
|
||||
('slug', models.SlugField(blank=True, editable=False, max_length=255, null=True, unique=True)),
|
||||
('customer_model', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='django_ledger.customermodel')),
|
||||
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='organizations', to='inventory.dealer')),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='organization_profile', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Organization',
|
||||
'verbose_name_plural': 'Organizations',
|
||||
},
|
||||
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Opportunity',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('crn', models.CharField(blank=True, max_length=20, null=True, verbose_name='CRN')),
|
||||
('vrn', models.CharField(blank=True, max_length=20, null=True, verbose_name='VRN')),
|
||||
('salary', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Salary')),
|
||||
('priority', models.CharField(choices=[('high', 'High'), ('medium', 'Medium'), ('low', 'Low')], default='medium', max_length=20, verbose_name='Priority')),
|
||||
('stage', models.CharField(choices=[('qualification', 'Qualification'), ('test_drive', 'Test Drive'), ('quotation', 'Quotation'), ('negotiation', 'Negotiation'), ('financing', 'Financing'), ('closed_won', 'Closed Won'), ('closed_lost', 'Closed Lost'), ('on_hold', 'On Hold')], max_length=20, verbose_name='Stage')),
|
||||
('probability', models.PositiveIntegerField(validators=[inventory.models.validate_probability])),
|
||||
('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Amount')),
|
||||
('expected_revenue', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Expected Revenue')),
|
||||
('vehicle_of_interest_make', models.CharField(blank=True, max_length=50, null=True)),
|
||||
('vehicle_of_interest_model', models.CharField(blank=True, max_length=100, null=True)),
|
||||
('expected_close_date', models.DateField(blank=True, null=True)),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
|
||||
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
|
||||
('slug', models.SlugField(blank=True, help_text='Unique slug for the opportunity.', null=True, unique=True, verbose_name='Slug')),
|
||||
('loss_reason', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('car', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='inventory.car', verbose_name='Car')),
|
||||
('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='opportunities', to='inventory.customer')),
|
||||
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='opportunities', to='inventory.dealer')),
|
||||
('estimate', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='opportunity', to='django_ledger.estimatemodel')),
|
||||
('lead', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='opportunity', to='inventory.lead')),
|
||||
('organization', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='inventory.organization', verbose_name='Organization')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Opportunity',
|
||||
'verbose_name_plural': 'Opportunities',
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='lead',
|
||||
name='organization',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='organization_leads', to='inventory.organization'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Refund',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='amount')),
|
||||
('reason', models.TextField(blank=True, verbose_name='reason')),
|
||||
('refund_date', models.DateField(auto_now_add=True, verbose_name='refund date')),
|
||||
('payment', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='refund', to='inventory.payment')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'refund',
|
||||
'verbose_name_plural': 'refunds',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Representative',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, verbose_name='Name')),
|
||||
('arabic_name', models.CharField(max_length=255, verbose_name='Arabic Name')),
|
||||
('id_number', models.CharField(max_length=10, unique=True, verbose_name='ID Number')),
|
||||
('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')),
|
||||
('email', models.EmailField(max_length=255, verbose_name='Email Address')),
|
||||
('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='Address')),
|
||||
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='representatives', to='inventory.dealer')),
|
||||
('organization', models.ManyToManyField(related_name='representatives', to='inventory.organization')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Representative',
|
||||
'verbose_name_plural': 'Representatives',
|
||||
},
|
||||
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SaleOrder',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('payment_method', models.CharField(choices=[('cash', 'Cash'), ('finance', 'Finance'), ('lease', 'Lease'), ('credit_card', 'Credit Card'), ('bank_transfer', 'Bank Transfer'), ('sadad', 'SADAD')], max_length=20)),
|
||||
('comments', models.TextField(blank=True, null=True)),
|
||||
('formatted_order_id', models.CharField(editable=False, max_length=10, unique=True)),
|
||||
('agreed_price', models.DecimalField(decimal_places=2, help_text='The final agreed-upon selling price of the vehicle.', max_digits=12)),
|
||||
('down_payment_amount', models.DecimalField(decimal_places=2, default=0.0, help_text='The initial payment made by the customer.', max_digits=12)),
|
||||
('trade_in_value', models.DecimalField(decimal_places=2, default=0.0, help_text='The value of any vehicle traded in by the customer.', max_digits=12)),
|
||||
('loan_amount', models.DecimalField(decimal_places=2, default=0.0, help_text='The amount financed by a bank or third-party lender.', max_digits=12)),
|
||||
('total_paid_amount', models.DecimalField(decimal_places=2, default=0.0, help_text='Sum of down payment, trade-in value, and loan amount received so far.', max_digits=12)),
|
||||
('remaining_balance', models.DecimalField(decimal_places=2, default=0.0, help_text='The remaining amount due from the customer or financing.', max_digits=12)),
|
||||
('status', models.CharField(choices=[('PENDING_APPROVAL', 'Pending Approval'), ('APPROVED', 'Approved'), ('IN_FINANCING', 'In Financing'), ('PARTIALLY_PAID', 'Partially Paid'), ('FULLY_PAID', 'Fully Paid'), ('PENDING_DELIVERY', 'Pending Delivery'), ('DELIVERED', 'Delivered'), ('CANCELLED', 'Cancelled')], default='PENDING_APPROVAL', help_text='Current status of the sales order.', max_length=20)),
|
||||
('order_date', models.DateTimeField(default=django.utils.timezone.now, help_text='The date and time the sales order was created.')),
|
||||
('expected_delivery_date', models.DateField(blank=True, help_text='The planned date for vehicle delivery.', null=True)),
|
||||
('actual_delivery_date', models.DateTimeField(blank=True, help_text='The actual date and time the vehicle was delivered.', null=True)),
|
||||
('cancelled_date', models.DateTimeField(blank=True, help_text='The date and time the order was cancelled, if applicable.', null=True)),
|
||||
('cancellation_reason', models.TextField(blank=True, help_text='Reason for cancellation, if applicable.', null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('car', models.ForeignKey(blank=True, help_text='The specific vehicle (VIN) being sold.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='sales_orders', to='inventory.car')),
|
||||
('created_by', models.ForeignKey(help_text='The user who created this sales order.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_sales_orders', to=settings.AUTH_USER_MODEL)),
|
||||
('customer', models.ForeignKey(help_text='The customer making the purchase.', on_delete=django.db.models.deletion.PROTECT, related_name='sales_orders', to='inventory.customer')),
|
||||
('estimate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sale_orders', to='django_ledger.estimatemodel', verbose_name='Estimate')),
|
||||
('invoice', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sale_orders', to='django_ledger.invoicemodel', verbose_name='Invoice')),
|
||||
('journal_entry', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='django_ledger.journalentrymodel')),
|
||||
('last_modified_by', models.ForeignKey(help_text='The user who last modified this sales order.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='modified_sales_orders', to=settings.AUTH_USER_MODEL)),
|
||||
('opportunity', models.OneToOneField(help_text='The associated sales opportunity for this order.', on_delete=django.db.models.deletion.CASCADE, related_name='sales_order', to='inventory.opportunity')),
|
||||
('trade_in_vehicle', models.ForeignKey(blank=True, help_text='The vehicle traded in by the customer, if any.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='traded_in_on_orders', to='inventory.car')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Sales Order',
|
||||
'verbose_name_plural': 'Sales Orders',
|
||||
'ordering': ['-order_date'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Schedule',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('purpose', models.CharField(choices=[('product_demo', 'Product Demo'), ('follow_up_call', 'Follow-Up Call'), ('contract_discussion', 'Contract Discussion'), ('sales_meeting', 'Sales Meeting'), ('support_call', 'Support Call'), ('other', 'Other')], max_length=200)),
|
||||
('scheduled_at', models.DateTimeField()),
|
||||
('scheduled_type', models.CharField(choices=[('call', 'Call'), ('meeting', 'Meeting'), ('email', 'Email')], default='Call', max_length=200)),
|
||||
('duration', models.DurationField(default=datetime.timedelta(seconds=300))),
|
||||
('notes', models.TextField(blank=True, null=True)),
|
||||
('status', models.CharField(choices=[('scheduled', 'Scheduled'), ('completed', 'Completed'), ('canceled', 'Canceled')], default='Scheduled', max_length=200)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='schedules', to='django_ledger.customermodel')),
|
||||
('lead', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='schedules', to='inventory.lead')),
|
||||
('scheduled_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-scheduled_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Staff',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, verbose_name='Name')),
|
||||
('arabic_name', models.CharField(max_length=255, verbose_name='Arabic Name')),
|
||||
('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')),
|
||||
('staff_type', models.CharField(choices=[('inventory', 'Inventory'), ('accountant', 'Accountant'), ('sales', 'Sales')], max_length=255, verbose_name='Staff Type')),
|
||||
('active', models.BooleanField(default=True, verbose_name='Active')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
|
||||
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
|
||||
('slug', models.SlugField(blank=True, editable=False, max_length=255, null=True, unique=True)),
|
||||
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='staff', to='inventory.dealer')),
|
||||
('staff_member', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='staff', to='appointment.staffmember')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Staff',
|
||||
'verbose_name_plural': 'Staff',
|
||||
'permissions': [],
|
||||
},
|
||||
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
|
||||
managers=[
|
||||
('objects', inventory.models.StaffUserManager()),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='opportunity',
|
||||
name='staff',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owner', to='inventory.staff', verbose_name='Owner'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='LeadStatusHistory',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('old_status', models.CharField(choices=[('new', 'New'), ('contacted', 'Contacted'), ('qualified', 'Qualified'), ('unqualified', 'Unqualified'), ('converted', 'Converted')], max_length=50, verbose_name='Old Status')),
|
||||
('new_status', models.CharField(choices=[('new', 'New'), ('contacted', 'Contacted'), ('qualified', 'Qualified'), ('unqualified', 'Unqualified'), ('converted', 'Converted')], max_length=50, verbose_name='New Status')),
|
||||
('changed_at', models.DateTimeField(auto_now_add=True, verbose_name='Changed At')),
|
||||
('lead', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='status_history', to='inventory.lead')),
|
||||
('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='status_changes', to='inventory.staff')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Lead Status History',
|
||||
'verbose_name_plural': 'Lead Status Histories',
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='lead',
|
||||
name='staff',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned', to='inventory.staff', verbose_name='Assigned'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Tasks',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('object_id', models.UUIDField()),
|
||||
('title', models.CharField(max_length=255, verbose_name='Title')),
|
||||
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
|
||||
('due_date', models.DateField(verbose_name='Due Date')),
|
||||
('completed', models.BooleanField(default=False, verbose_name='Completed')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
|
||||
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
|
||||
('assigned_to', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='tasks_assigned', to=settings.AUTH_USER_MODEL)),
|
||||
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
|
||||
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='tasks_created', to=settings.AUTH_USER_MODEL)),
|
||||
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tasks', to='inventory.dealer')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Task',
|
||||
'verbose_name_plural': 'Tasks',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserActivityLog',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('action', models.TextField()),
|
||||
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'User Activity Log',
|
||||
'verbose_name_plural': 'User Activity Logs',
|
||||
'ordering': ['-timestamp'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Vendor',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('crn', models.CharField(max_length=10, unique=True, verbose_name='Commercial Registration Number')),
|
||||
('vrn', models.CharField(max_length=15, unique=True, verbose_name='VAT Registration Number')),
|
||||
('arabic_name', models.CharField(max_length=255, verbose_name='Arabic Name')),
|
||||
('name', models.CharField(max_length=255, verbose_name='English Name')),
|
||||
('contact_person', models.CharField(max_length=100, verbose_name='Contact Person')),
|
||||
('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')),
|
||||
('email', models.EmailField(max_length=255, verbose_name='Email Address')),
|
||||
('address', models.CharField(max_length=200, verbose_name='Address')),
|
||||
('logo', models.ImageField(blank=True, null=True, upload_to='logos/vendors', verbose_name='Logo')),
|
||||
('active', models.BooleanField(default=True, verbose_name='Active')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True, verbose_name='Slug')),
|
||||
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='vendors', to='inventory.dealer')),
|
||||
('vendor_model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='django_ledger.vendormodel', verbose_name='Vendor Model')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Vendor',
|
||||
'verbose_name_plural': 'Vendors',
|
||||
},
|
||||
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='car',
|
||||
name='vendor',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='cars', to='inventory.vendor', verbose_name='Vendor'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CarReservation',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('reserved_at', models.DateTimeField(auto_now_add=True, verbose_name='Reserved At')),
|
||||
('reserved_until', models.DateTimeField(verbose_name='Reserved Until')),
|
||||
('car', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to='inventory.car', verbose_name='Car')),
|
||||
('reserved_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to=settings.AUTH_USER_MODEL, verbose_name='Reserved By')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Car Reservation',
|
||||
'verbose_name_plural': 'Car Reservations',
|
||||
'ordering': ['-reserved_at'],
|
||||
'unique_together': {('car', 'reserved_until')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DealersMake',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('added_at', models.DateTimeField(auto_now_add=True)),
|
||||
('car_make', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='car_dealers', to='inventory.carmake')),
|
||||
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='dealer_makes', to='inventory.dealer')),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('dealer', 'car_make')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CarColors',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('car', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='colors', to='inventory.car')),
|
||||
('exterior', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='colors', to='inventory.exteriorcolors')),
|
||||
('interior', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='colors', to='inventory.interiorcolors')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Color',
|
||||
'verbose_name_plural': 'Colors',
|
||||
'unique_together': {('car', 'exterior', 'interior')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PaymentHistory',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('user_data', models.JSONField(blank=True, null=True)),
|
||||
('amount', models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(0.01)])),
|
||||
('currency', models.CharField(default='SAR', max_length=3)),
|
||||
('payment_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('status', models.CharField(choices=[('initiated', 'initiated'), ('pending', 'Pending'), ('completed', 'Completed'), ('paid', 'Paid'), ('failed', 'Failed'), ('refunded', 'Refunded'), ('cancelled', 'Cancelled')], default='pending', max_length=10)),
|
||||
('payment_method', models.CharField(choices=[('credit_card', 'Credit Card'), ('debit_card', 'Debit Card'), ('paypal', 'PayPal'), ('bank_transfer', 'Bank Transfer'), ('crypto', 'Cryptocurrency'), ('other', 'Other')], max_length=20)),
|
||||
('transaction_id', models.CharField(blank=True, max_length=100, null=True, unique=True)),
|
||||
('invoice_number', models.CharField(blank=True, max_length=50, null=True)),
|
||||
('order_reference', models.CharField(blank=True, max_length=100, null=True)),
|
||||
('gateway_response', models.JSONField(blank=True, null=True)),
|
||||
('gateway_name', models.CharField(blank=True, max_length=50, null=True)),
|
||||
('description', models.TextField(blank=True, null=True)),
|
||||
('is_recurring', models.BooleanField(default=False)),
|
||||
('billing_email', models.EmailField(blank=True, max_length=254, null=True)),
|
||||
('billing_address', models.TextField(blank=True, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Payment History',
|
||||
'verbose_name_plural': 'Payment Histories',
|
||||
'ordering': ['-payment_date'],
|
||||
'indexes': [models.Index(fields=['transaction_id'], name='inventory_p_transac_9469f3_idx'), models.Index(fields=['user'], name='inventory_p_user_id_c31626_idx'), models.Index(fields=['status'], name='inventory_p_status_abcb77_idx'), models.Index(fields=['payment_date'], name='inventory_p_payment_b3068c_idx')],
|
||||
},
|
||||
),
|
||||
]
|
||||
17
inventory/migrations/0002_remove_saleorder_journal_entry.py
Normal file
17
inventory/migrations/0002_remove_saleorder_journal_entry.py
Normal file
@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.2.1 on 2025-06-18 15:46
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='saleorder',
|
||||
name='journal_entry',
|
||||
),
|
||||
]
|
||||
20
inventory/migrations/0003_saleorder_journal_entry.py
Normal file
20
inventory/migrations/0003_saleorder_journal_entry.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Generated by Django 5.2.1 on 2025-06-18 15:46
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('django_ledger', '0022_alter_billmodel_bill_items_and_more'),
|
||||
('inventory', '0002_remove_saleorder_journal_entry'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='saleorder',
|
||||
name='journal_entry',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='django_ledger.journalentrymodel'),
|
||||
),
|
||||
]
|
||||
@ -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',
|
||||
),
|
||||
]
|
||||
17
inventory/migrations/0005_remove_saleorder_car.py
Normal file
17
inventory/migrations/0005_remove_saleorder_car.py
Normal file
@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.2.1 on 2025-06-19 13:34
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0004_remove_saleorder_agreed_price_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='saleorder',
|
||||
name='car',
|
||||
),
|
||||
]
|
||||
@ -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,
|
||||
),
|
||||
]
|
||||
20
inventory/migrations/0007_saleorder_estimate.py
Normal file
20
inventory/migrations/0007_saleorder_estimate.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Generated by Django 5.2.1 on 2025-06-19 13:59
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('django_ledger', '0022_alter_billmodel_bill_items_and_more'),
|
||||
('inventory', '0006_saleorder_dealer'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='saleorder',
|
||||
name='estimate',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sale_orders', to='django_ledger.estimatemodel', verbose_name='Estimate'),
|
||||
),
|
||||
]
|
||||
19
inventory/migrations/0008_saleorder_opportunity.py
Normal file
19
inventory/migrations/0008_saleorder_opportunity.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.2.1 on 2025-06-19 14:00
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0007_saleorder_estimate'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='saleorder',
|
||||
name='opportunity',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sale_orders', to='inventory.opportunity', verbose_name='Opportunity'),
|
||||
),
|
||||
]
|
||||
19
inventory/migrations/0009_saleorder_customer.py
Normal file
19
inventory/migrations/0009_saleorder_customer.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.2.1 on 2025-06-19 14:19
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0008_saleorder_opportunity'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='saleorder',
|
||||
name='customer',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sale_orders', to='inventory.customer', verbose_name='Customer'),
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
@ -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')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@ -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')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
@ -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',
|
||||
),
|
||||
]
|
||||
0
inventory/migrations/__init__.py
Normal file
0
inventory/migrations/__init__.py
Normal file
@ -1,5 +1,6 @@
|
||||
from django.utils.translation import get_language
|
||||
|
||||
|
||||
class AddClassMixin:
|
||||
"""
|
||||
Provides functionality for automatically adding CSS classes to form field widgets.
|
||||
@ -10,6 +11,7 @@ class AddClassMixin:
|
||||
apply different CSS classes.
|
||||
|
||||
"""
|
||||
|
||||
def add_class_to_fields(self):
|
||||
"""
|
||||
Adds the class to the fields of the model.
|
||||
@ -20,8 +22,10 @@ class AddClassMixin:
|
||||
# existing_classes = field.widget.attrs.get('class', '')
|
||||
# field.widget.attrs['class'] = f"{existing_classes} form-select form-select-sm".strip()
|
||||
# else:
|
||||
existing_classes = field.widget.attrs.get('class', '')
|
||||
field.widget.attrs['class'] = f"{existing_classes} form-control form-control-sm".strip()
|
||||
existing_classes = field.widget.attrs.get("class", "")
|
||||
field.widget.attrs["class"] = (
|
||||
f"{existing_classes} form-control form-control-sm".strip()
|
||||
)
|
||||
|
||||
|
||||
class LocalizedNameMixin:
|
||||
@ -38,13 +42,14 @@ class LocalizedNameMixin:
|
||||
:ivar name: Default name used for non-Arabic languages.
|
||||
:type name: Optional[str]
|
||||
"""
|
||||
|
||||
def get_local_name(self):
|
||||
"""
|
||||
Returns the localized name based on the current language.
|
||||
"""
|
||||
if get_language() == 'ar':
|
||||
return getattr(self, 'arabic_name', None)
|
||||
return getattr(self, 'name', None)
|
||||
if get_language() == "ar":
|
||||
return getattr(self, "arabic_name", None)
|
||||
return getattr(self, "name", None)
|
||||
|
||||
|
||||
# class AddDealerInstanceMixin:
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
######################################################################################################
|
||||
######################################################################################################
|
||||
######################################################################################################
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
@ -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"):
|
||||
|
||||
@ -47,17 +47,29 @@ class CarTable(tables.Table):
|
||||
with badge-style formatting based on the status value.
|
||||
:type status: tables.Column
|
||||
"""
|
||||
|
||||
stock_type = tables.Column(verbose_name=_("Stock Type"))
|
||||
vin = tables.LinkColumn("car_detail", args=[tables.A("pk")], verbose_name=_("VIN"), attrs={"td": {"class": "fw-bold"}})
|
||||
vin = tables.LinkColumn(
|
||||
"car_detail",
|
||||
args=[tables.A("pk")],
|
||||
verbose_name=_("VIN"),
|
||||
attrs={"td": {"class": "fw-bold"}},
|
||||
)
|
||||
id_car_make = tables.Column(verbose_name=_("Make"))
|
||||
id_car_model = tables.Column(verbose_name=_("Model"))
|
||||
year = tables.Column(verbose_name=_("Year"))
|
||||
id_car_serie = tables.Column(verbose_name=_("Series"))
|
||||
id_car_trim = tables.Column(verbose_name=_("Trim"))
|
||||
mileage = tables.Column(verbose_name=_("Mileage"))
|
||||
selling_price = tables.Column(accessor="finances.selling_price", verbose_name=_("Price"))
|
||||
exterior_color = tables.Column(accessor="colors.exterior.name", verbose_name=_("Exterior Color"))
|
||||
interior_color = tables.Column(accessor="colors.interior.name", verbose_name=_("Interior Color"))
|
||||
selling_price = tables.Column(
|
||||
accessor="finances.selling_price", verbose_name=_("Price")
|
||||
)
|
||||
exterior_color = tables.Column(
|
||||
accessor="colors.exterior.name", verbose_name=_("Exterior Color")
|
||||
)
|
||||
interior_color = tables.Column(
|
||||
accessor="colors.interior.name", verbose_name=_("Interior Color")
|
||||
)
|
||||
receiving_date = tables.Column(verbose_name=_("Age"))
|
||||
status = tables.Column(verbose_name=_("Status"))
|
||||
|
||||
@ -111,7 +123,9 @@ class CarTable(tables.Table):
|
||||
"transfer": "badge-phoenix-warning",
|
||||
}
|
||||
badge_class = status_badges.get(value.lower(), "badge-secondary")
|
||||
return format_html('<span class="badge badge-phoenix fs-11 {}">{}</span>', badge_class, value)
|
||||
return format_html(
|
||||
'<span class="badge badge-phoenix fs-11 {}">{}</span>', badge_class, value
|
||||
)
|
||||
|
||||
def render_stock_type(self, value):
|
||||
type_badges = {
|
||||
@ -119,4 +133,6 @@ class CarTable(tables.Table):
|
||||
"used": "badge-phoenix-warning",
|
||||
}
|
||||
badge_class = type_badges.get(value.lower(), "badge-secondary")
|
||||
return format_html('<span class="badge badge-phoenix fs-11 {}">{}</span>', badge_class, value)
|
||||
return format_html(
|
||||
'<span class="badge badge-phoenix fs-11 {}">{}</span>', badge_class, value
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -4,195 +4,210 @@ from django import template
|
||||
from django.urls import reverse
|
||||
from calendar import month_abbr
|
||||
from django.conf import settings
|
||||
from django.db.models import Sum
|
||||
from django.forms import ValidationError
|
||||
from django.utils.formats import number_format
|
||||
from django_ledger.io.io_core import get_localdate,validate_activity
|
||||
from django_ledger.io.io_core import get_localdate, validate_activity
|
||||
from django_ledger.models import InvoiceModel, JournalEntryModel, BillModel
|
||||
from django.db.models import Case, Value, When, IntegerField
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@register.filter(name='percentage')
|
||||
|
||||
@register.filter(name="percentage")
|
||||
def percentage(value):
|
||||
if value is not None:
|
||||
return '{0:,.2f}%'.format(value * 100)
|
||||
return "{0:,.2f}%".format(value * 100)
|
||||
return None
|
||||
|
||||
|
||||
@register.filter
|
||||
def get_item(dictionary, key):
|
||||
return dictionary.get(key)
|
||||
|
||||
|
||||
@register.filter(name='add_class')
|
||||
@register.filter(name="add_class")
|
||||
def add_class(field, css_class):
|
||||
return field.as_widget(attrs={"class": css_class})
|
||||
|
||||
|
||||
@register.filter(name='attr')
|
||||
@register.filter(name="attr")
|
||||
def attr(field, args):
|
||||
attrs = {}
|
||||
definitions = args.split(',')
|
||||
definitions = args.split(",")
|
||||
for definition in definitions:
|
||||
if ':' in definition:
|
||||
key, val = definition.split(':')
|
||||
if ":" in definition:
|
||||
key, val = definition.split(":")
|
||||
attrs[key.strip()] = val.strip()
|
||||
else:
|
||||
attrs[definition.strip()] = True
|
||||
return field.as_widget(attrs=attrs)
|
||||
|
||||
|
||||
@register.inclusion_tag('ledger/reports/components/period_navigator.html', takes_context=True)
|
||||
@register.inclusion_tag(
|
||||
"ledger/reports/components/period_navigator.html", takes_context=True
|
||||
)
|
||||
def period_navigation(context, base_url: str):
|
||||
print(context, base_url)
|
||||
kwargs = dict()
|
||||
entity_slug = context['view'].kwargs['entity_slug']
|
||||
kwargs['entity_slug'] = entity_slug
|
||||
entity_slug = context["view"].kwargs["entity_slug"]
|
||||
kwargs["entity_slug"] = entity_slug
|
||||
|
||||
if context['view'].kwargs.get('ledger_pk'):
|
||||
kwargs['ledger_pk'] = context['view'].kwargs.get('ledger_pk')
|
||||
if context["view"].kwargs.get("ledger_pk"):
|
||||
kwargs["ledger_pk"] = context["view"].kwargs.get("ledger_pk")
|
||||
|
||||
if context['view'].kwargs.get('account_pk'):
|
||||
kwargs['account_pk'] = context['view'].kwargs.get('account_pk')
|
||||
if context["view"].kwargs.get("account_pk"):
|
||||
kwargs["account_pk"] = context["view"].kwargs.get("account_pk")
|
||||
|
||||
if context['view'].kwargs.get('unit_slug'):
|
||||
kwargs['unit_slug'] = context['view'].kwargs.get('unit_slug')
|
||||
if context["view"].kwargs.get("unit_slug"):
|
||||
kwargs["unit_slug"] = context["view"].kwargs.get("unit_slug")
|
||||
|
||||
if context['view'].kwargs.get('coa_slug'):
|
||||
kwargs['coa_slug'] = context['view'].kwargs.get('coa_slug')
|
||||
if context["view"].kwargs.get("coa_slug"):
|
||||
kwargs["coa_slug"] = context["view"].kwargs.get("coa_slug")
|
||||
|
||||
ctx = dict()
|
||||
ctx['year'] = context['year']
|
||||
ctx['has_year'] = context.get('has_year')
|
||||
ctx['has_quarter'] = context.get('has_quarter')
|
||||
ctx['has_month'] = context.get('has_month')
|
||||
ctx['has_date'] = context.get('has_date')
|
||||
ctx['previous_year'] = context['previous_year']
|
||||
ctx["year"] = context["year"]
|
||||
ctx["has_year"] = context.get("has_year")
|
||||
ctx["has_quarter"] = context.get("has_quarter")
|
||||
ctx["has_month"] = context.get("has_month")
|
||||
ctx["has_date"] = context.get("has_date")
|
||||
ctx["previous_year"] = context["previous_year"]
|
||||
|
||||
kwargs['year'] = context['previous_year']
|
||||
ctx['previous_year_url'] = reverse(f'{base_url}-year', kwargs=kwargs)
|
||||
ctx['next_year'] = context['next_year']
|
||||
kwargs["year"] = context["previous_year"]
|
||||
ctx["previous_year_url"] = reverse(f"{base_url}-year", kwargs=kwargs)
|
||||
ctx["next_year"] = context["next_year"]
|
||||
|
||||
kwargs['year'] = context['next_year']
|
||||
ctx['next_year_url'] = reverse(f'{base_url}-year', kwargs=kwargs)
|
||||
kwargs["year"] = context["next_year"]
|
||||
ctx["next_year_url"] = reverse(f"{base_url}-year", kwargs=kwargs)
|
||||
|
||||
kwargs['year'] = context['year']
|
||||
ctx['current_year_url'] = reverse(f'{base_url}-year', kwargs=kwargs)
|
||||
kwargs["year"] = context["year"]
|
||||
ctx["current_year_url"] = reverse(f"{base_url}-year", kwargs=kwargs)
|
||||
|
||||
dt = get_localdate()
|
||||
|
||||
KWARGS_CURRENT_MONTH = {
|
||||
'entity_slug': context['view'].kwargs['entity_slug'],
|
||||
'year': dt.year,
|
||||
'month': dt.month
|
||||
"entity_slug": context["view"].kwargs["entity_slug"],
|
||||
"year": dt.year,
|
||||
"month": dt.month,
|
||||
}
|
||||
|
||||
if 'unit_slug' in kwargs:
|
||||
KWARGS_CURRENT_MONTH['unit_slug'] = kwargs['unit_slug']
|
||||
if 'account_pk' in kwargs:
|
||||
KWARGS_CURRENT_MONTH['account_pk'] = kwargs['account_pk']
|
||||
if 'ledger_pk' in kwargs:
|
||||
KWARGS_CURRENT_MONTH['ledger_pk'] = kwargs['ledger_pk']
|
||||
if 'coa_slug' in kwargs:
|
||||
KWARGS_CURRENT_MONTH['coa_slug'] = kwargs['coa_slug']
|
||||
if "unit_slug" in kwargs:
|
||||
KWARGS_CURRENT_MONTH["unit_slug"] = kwargs["unit_slug"]
|
||||
if "account_pk" in kwargs:
|
||||
KWARGS_CURRENT_MONTH["account_pk"] = kwargs["account_pk"]
|
||||
if "ledger_pk" in kwargs:
|
||||
KWARGS_CURRENT_MONTH["ledger_pk"] = kwargs["ledger_pk"]
|
||||
if "coa_slug" in kwargs:
|
||||
KWARGS_CURRENT_MONTH["coa_slug"] = kwargs["coa_slug"]
|
||||
|
||||
ctx['current_month_url'] = reverse(f'{base_url}-month',
|
||||
kwargs=KWARGS_CURRENT_MONTH)
|
||||
ctx["current_month_url"] = reverse(f"{base_url}-month", kwargs=KWARGS_CURRENT_MONTH)
|
||||
|
||||
quarter_urls = list()
|
||||
ctx['quarter'] = context.get('quarter')
|
||||
ctx["quarter"] = context.get("quarter")
|
||||
for Q in range(1, 5):
|
||||
kwargs['quarter'] = Q
|
||||
quarter_urls.append({
|
||||
'url': reverse(f'{base_url}-quarter', kwargs=kwargs),
|
||||
'quarter': Q,
|
||||
'quarter_name': f'Q{Q}'
|
||||
})
|
||||
del kwargs['quarter']
|
||||
ctx['quarter_urls'] = quarter_urls
|
||||
kwargs["quarter"] = Q
|
||||
quarter_urls.append(
|
||||
{
|
||||
"url": reverse(f"{base_url}-quarter", kwargs=kwargs),
|
||||
"quarter": Q,
|
||||
"quarter_name": f"Q{Q}",
|
||||
}
|
||||
)
|
||||
del kwargs["quarter"]
|
||||
ctx["quarter_urls"] = quarter_urls
|
||||
|
||||
month_urls = list()
|
||||
ctx['month'] = context.get('month')
|
||||
ctx["month"] = context.get("month")
|
||||
for M in range(1, 13):
|
||||
kwargs['month'] = M
|
||||
month_urls.append({
|
||||
'url': reverse(f'{base_url}-month', kwargs=kwargs),
|
||||
'month': M,
|
||||
'month_abbr': month_abbr[M]
|
||||
})
|
||||
ctx['month_urls'] = month_urls
|
||||
ctx['from_date'] = context['from_date']
|
||||
ctx['to_date'] = context['to_date']
|
||||
kwargs["month"] = M
|
||||
month_urls.append(
|
||||
{
|
||||
"url": reverse(f"{base_url}-month", kwargs=kwargs),
|
||||
"month": M,
|
||||
"month_abbr": month_abbr[M],
|
||||
}
|
||||
)
|
||||
ctx["month_urls"] = month_urls
|
||||
ctx["from_date"] = context["from_date"]
|
||||
ctx["to_date"] = context["to_date"]
|
||||
ctx.update(kwargs)
|
||||
|
||||
ctx['date_navigation_url'] = context.get('date_navigation_url')
|
||||
ctx["date_navigation_url"] = context.get("date_navigation_url")
|
||||
|
||||
return ctx
|
||||
|
||||
|
||||
@register.inclusion_tag('ledger/reports/tags/balance_sheet_statement.html', takes_context=True)
|
||||
@register.inclusion_tag(
|
||||
"ledger/reports/tags/balance_sheet_statement.html", takes_context=True
|
||||
)
|
||||
def balance_sheet_statement(context, io_model, to_date=None):
|
||||
user_model = context['user']
|
||||
activity = context['request'].GET.get('activity')
|
||||
entity_slug = context['view'].kwargs.get('entity_slug')
|
||||
user_model = context["user"]
|
||||
activity = context["request"].GET.get("activity")
|
||||
entity_slug = context["view"].kwargs.get("entity_slug")
|
||||
if not to_date:
|
||||
to_date = context['to_date']
|
||||
to_date = context["to_date"]
|
||||
|
||||
io_digest = io_model.digest(
|
||||
activity=activity,
|
||||
user_model=user_model,
|
||||
equity_only=False,
|
||||
entity_slug=entity_slug,
|
||||
unit_slug=context['unit_slug'],
|
||||
by_unit=context['by_unit'],
|
||||
unit_slug=context["unit_slug"],
|
||||
by_unit=context["by_unit"],
|
||||
to_date=to_date,
|
||||
signs=True,
|
||||
process_groups=True,
|
||||
balance_sheet_statement=True)
|
||||
balance_sheet_statement=True,
|
||||
)
|
||||
|
||||
return {
|
||||
'entity_slug': entity_slug,
|
||||
'user_model': user_model,
|
||||
'tx_digest': io_digest.get_io_data(),
|
||||
"entity_slug": entity_slug,
|
||||
"user_model": user_model,
|
||||
"tx_digest": io_digest.get_io_data(),
|
||||
}
|
||||
|
||||
@register.inclusion_tag('ledger/reports/tags/income_statement.html', takes_context=True)
|
||||
|
||||
@register.inclusion_tag("ledger/reports/tags/income_statement.html", takes_context=True)
|
||||
def income_statement_table(context, io_model, from_date=None, to_date=None):
|
||||
user_model = context['user']
|
||||
activity = context['request'].GET.get('activity')
|
||||
user_model = context["user"]
|
||||
activity = context["request"].GET.get("activity")
|
||||
activity = validate_activity(activity, raise_404=True)
|
||||
entity_slug = context['view'].kwargs.get('entity_slug')
|
||||
entity_slug = context["view"].kwargs.get("entity_slug")
|
||||
|
||||
if not from_date:
|
||||
from_date = context['from_date']
|
||||
from_date = context["from_date"]
|
||||
if not to_date:
|
||||
to_date = context['to_date']
|
||||
to_date = context["to_date"]
|
||||
|
||||
io_digest = io_model.digest(
|
||||
activity=activity,
|
||||
user_model=user_model,
|
||||
entity_slug=entity_slug,
|
||||
unit_slug=context['unit_slug'],
|
||||
by_unit=context['by_unit'],
|
||||
unit_slug=context["unit_slug"],
|
||||
by_unit=context["by_unit"],
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
equity_only=True,
|
||||
process_groups=True,
|
||||
income_statement=True,
|
||||
signs=True
|
||||
signs=True,
|
||||
)
|
||||
|
||||
return {
|
||||
'entity_slug': entity_slug,
|
||||
'user_model': user_model,
|
||||
'tx_digest': io_digest.get_io_data()
|
||||
"entity_slug": entity_slug,
|
||||
"user_model": user_model,
|
||||
"tx_digest": io_digest.get_io_data(),
|
||||
}
|
||||
|
||||
@register.inclusion_tag('ledger/reports/tags/cash_flow_statement.html', takes_context=True)
|
||||
|
||||
@register.inclusion_tag(
|
||||
"ledger/reports/tags/cash_flow_statement.html", takes_context=True
|
||||
)
|
||||
def cash_flow_statement(context, io_model):
|
||||
user_model = context['user']
|
||||
entity_slug = context['view'].kwargs.get('entity_slug')
|
||||
from_date = context['from_date']
|
||||
to_date = context['to_date']
|
||||
user_model = context["user"]
|
||||
entity_slug = context["view"].kwargs.get("entity_slug")
|
||||
from_date = context["from_date"]
|
||||
to_date = context["to_date"]
|
||||
|
||||
io_digest = io_model.digest(
|
||||
cash_flow_statement=True,
|
||||
@ -201,57 +216,75 @@ def cash_flow_statement(context, io_model):
|
||||
equity_only=False,
|
||||
signs=True,
|
||||
entity_slug=entity_slug,
|
||||
unit_slug=context['unit_slug'],
|
||||
by_unit=context['by_unit'],
|
||||
unit_slug=context["unit_slug"],
|
||||
by_unit=context["by_unit"],
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
process_groups=True)
|
||||
process_groups=True,
|
||||
)
|
||||
|
||||
return {
|
||||
'entity_slug': entity_slug,
|
||||
'user_model': user_model,
|
||||
'tx_digest': io_digest.get_io_data()
|
||||
"entity_slug": entity_slug,
|
||||
"user_model": user_model,
|
||||
"tx_digest": io_digest.get_io_data(),
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@register.inclusion_tag('django_ledger/components/date_picker.html', takes_context=True)
|
||||
@register.inclusion_tag("django_ledger/components/date_picker.html", takes_context=True)
|
||||
def date_picker(context, nav_url=None, date_picker_id=None):
|
||||
try:
|
||||
entity_slug = context['view'].kwargs.get('entity_slug')
|
||||
entity_slug = context["view"].kwargs.get("entity_slug")
|
||||
except KeyError:
|
||||
entity_slug = context['entity_slug']
|
||||
entity_slug = context["entity_slug"]
|
||||
|
||||
if not date_picker_id:
|
||||
date_picker_id = f'djl-datepicker-{randint(10000, 99999)}'
|
||||
date_picker_id = f"djl-datepicker-{randint(10000, 99999)}"
|
||||
|
||||
if 'date_picker_ids' not in context:
|
||||
context['date_picker_ids'] = list()
|
||||
context['date_picker_ids'].append(date_picker_id)
|
||||
if "date_picker_ids" not in context:
|
||||
context["date_picker_ids"] = list()
|
||||
context["date_picker_ids"].append(date_picker_id)
|
||||
|
||||
date_navigation_url = nav_url if nav_url else context.get('date_navigation_url')
|
||||
date_navigation_url = nav_url if nav_url else context.get("date_navigation_url")
|
||||
return {
|
||||
'entity_slug': entity_slug,
|
||||
'date_picker_id': date_picker_id,
|
||||
'date_navigation_url': date_navigation_url
|
||||
"entity_slug": entity_slug,
|
||||
"date_picker_id": date_picker_id,
|
||||
"date_navigation_url": date_navigation_url,
|
||||
}
|
||||
|
||||
@register.simple_tag(name='get_currency')
|
||||
|
||||
@register.simple_tag(name="get_currency")
|
||||
def get_currency():
|
||||
return settings.CURRENCY
|
||||
|
||||
|
||||
|
||||
|
||||
@register.simple_tag(name='num2words', takes_context=True)
|
||||
@register.simple_tag(name="num2words", takes_context=True)
|
||||
def number_to_words_english(number):
|
||||
"""Convert a number to words in English."""
|
||||
units = ["", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"]
|
||||
teens = ["ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen",
|
||||
"seventeen", "eighteen", "nineteen"]
|
||||
tens = ["", "ten", "twenty", "thirty", "forty", "fifty", "sixty", "seventy",
|
||||
"eighty", "ninety"]
|
||||
teens = [
|
||||
"ten",
|
||||
"eleven",
|
||||
"twelve",
|
||||
"thirteen",
|
||||
"fourteen",
|
||||
"fifteen",
|
||||
"sixteen",
|
||||
"seventeen",
|
||||
"eighteen",
|
||||
"nineteen",
|
||||
]
|
||||
tens = [
|
||||
"",
|
||||
"ten",
|
||||
"twenty",
|
||||
"thirty",
|
||||
"forty",
|
||||
"fifty",
|
||||
"sixty",
|
||||
"seventy",
|
||||
"eighty",
|
||||
"ninety",
|
||||
]
|
||||
scales = ["", "thousand", "million", "billion", "trillion"]
|
||||
|
||||
if number == 0:
|
||||
@ -282,15 +315,47 @@ def number_to_words_english(number):
|
||||
number = number // 1000
|
||||
scale_index += 1
|
||||
|
||||
return ' '.join(words)
|
||||
return " ".join(words)
|
||||
|
||||
|
||||
def number_to_words_arabic(number):
|
||||
"""Convert a number to words in Arabic."""
|
||||
units = ["", "واحد", "اثنان", "ثلاثة", "أربعة", "خمسة", "ستة", "سبعة", "ثمانية", "تسعة"]
|
||||
teens = ["عشرة", "أحد عشر", "اثنا عشر", "ثلاثة عشر", "أربعة عشر", "خمسة عشر",
|
||||
"ستة عشر", "سبعة عشر", "ثمانية عشر", "تسعة عشر"]
|
||||
tens = ["", "عشرة", "عشرون", "ثلاثون", "أربعون", "خمسون", "ستون", "سبعون",
|
||||
"ثمانون", "تسعون"]
|
||||
units = [
|
||||
"",
|
||||
"واحد",
|
||||
"اثنان",
|
||||
"ثلاثة",
|
||||
"أربعة",
|
||||
"خمسة",
|
||||
"ستة",
|
||||
"سبعة",
|
||||
"ثمانية",
|
||||
"تسعة",
|
||||
]
|
||||
teens = [
|
||||
"عشرة",
|
||||
"أحد عشر",
|
||||
"اثنا عشر",
|
||||
"ثلاثة عشر",
|
||||
"أربعة عشر",
|
||||
"خمسة عشر",
|
||||
"ستة عشر",
|
||||
"سبعة عشر",
|
||||
"ثمانية عشر",
|
||||
"تسعة عشر",
|
||||
]
|
||||
tens = [
|
||||
"",
|
||||
"عشرة",
|
||||
"عشرون",
|
||||
"ثلاثون",
|
||||
"أربعون",
|
||||
"خمسون",
|
||||
"ستون",
|
||||
"سبعون",
|
||||
"ثمانون",
|
||||
"تسعون",
|
||||
]
|
||||
scales = ["", "ألف", "مليون", "مليار", "تريليون"]
|
||||
|
||||
if number == 0:
|
||||
@ -321,7 +386,8 @@ def number_to_words_arabic(number):
|
||||
number = number // 1000
|
||||
scale_index += 1
|
||||
|
||||
return ' '.join(words)
|
||||
return " ".join(words)
|
||||
|
||||
|
||||
# @register.filter(name='num2words')
|
||||
# def num2words(number, language='en'):
|
||||
@ -331,25 +397,26 @@ def number_to_words_arabic(number):
|
||||
# else:
|
||||
# return number_to_words_english(number)
|
||||
|
||||
@register.inclusion_tag('components/date_picker.html', takes_context=True)
|
||||
|
||||
@register.inclusion_tag("components/date_picker.html", takes_context=True)
|
||||
def date_picker(context, nav_url=None, date_picker_id=None):
|
||||
try:
|
||||
entity_slug = context['view'].kwargs.get('entity_slug')
|
||||
entity_slug = context["view"].kwargs.get("entity_slug")
|
||||
except KeyError:
|
||||
entity_slug = context['entity_slug']
|
||||
entity_slug = context["entity_slug"]
|
||||
|
||||
if not date_picker_id:
|
||||
date_picker_id = f'djl-datepicker-{randint(10000, 99999)}'
|
||||
date_picker_id = f"djl-datepicker-{randint(10000, 99999)}"
|
||||
|
||||
if 'date_picker_ids' not in context:
|
||||
context['date_picker_ids'] = list()
|
||||
context['date_picker_ids'].append(date_picker_id)
|
||||
if "date_picker_ids" not in context:
|
||||
context["date_picker_ids"] = list()
|
||||
context["date_picker_ids"].append(date_picker_id)
|
||||
|
||||
date_navigation_url = nav_url if nav_url else context.get('date_navigation_url')
|
||||
date_navigation_url = nav_url if nav_url else context.get("date_navigation_url")
|
||||
return {
|
||||
'entity_slug': entity_slug,
|
||||
'date_picker_id': date_picker_id,
|
||||
'date_navigation_url': date_navigation_url
|
||||
"entity_slug": entity_slug,
|
||||
"date_picker_id": date_picker_id,
|
||||
"date_navigation_url": date_navigation_url,
|
||||
}
|
||||
|
||||
|
||||
@ -358,7 +425,8 @@ def splitlines(value):
|
||||
"""Splits text into lines"""
|
||||
return value.splitlines()
|
||||
|
||||
@register.filter(name='currency_format')
|
||||
|
||||
@register.filter(name="currency_format")
|
||||
def currency_format(value):
|
||||
if not value:
|
||||
value = 0.00
|
||||
@ -369,38 +437,47 @@ def currency_format(value):
|
||||
def filter_by_role(accounts, role_prefix):
|
||||
return [account for account in accounts if account.role.startswith(role_prefix)]
|
||||
|
||||
@register.inclusion_tag('purchase_orders/tags/po_item_table.html', takes_context=True)
|
||||
|
||||
@register.inclusion_tag("purchase_orders/tags/po_item_table.html", takes_context=True)
|
||||
def po_item_table1(context, queryset):
|
||||
return {
|
||||
'entity_slug': context['entity_slug'],
|
||||
'po_model': context['po_model'],
|
||||
'po_item_list': queryset
|
||||
"entity_slug": context["entity_slug"],
|
||||
"po_model": context["po_model"],
|
||||
"po_item_list": queryset,
|
||||
}
|
||||
|
||||
@register.inclusion_tag('purchase_orders/includes/po_item_formset.html', takes_context=True)
|
||||
|
||||
@register.inclusion_tag(
|
||||
"purchase_orders/includes/po_item_formset.html", takes_context=True
|
||||
)
|
||||
def po_item_formset_table(context, po_model, itemtxs_formset):
|
||||
return {
|
||||
'entity_slug': context['view'].kwargs['entity_slug'],
|
||||
'po_model': po_model,
|
||||
'itemtxs_formset': itemtxs_formset,
|
||||
"entity_slug": context["view"].kwargs["entity_slug"],
|
||||
"po_model": po_model,
|
||||
"itemtxs_formset": itemtxs_formset,
|
||||
}
|
||||
|
||||
|
||||
@register.inclusion_tag('bill/tags/bill_item_formset.html', takes_context=True)
|
||||
@register.inclusion_tag("bill/tags/bill_item_formset.html", takes_context=True)
|
||||
def bill_item_formset_table(context, item_formset):
|
||||
return {
|
||||
'entity_slug': context['view'].kwargs['entity_slug'],
|
||||
'bill_pk': context['view'].kwargs['bill_pk'],
|
||||
'total_amount__sum': context['total_amount__sum'],
|
||||
'item_formset': item_formset,
|
||||
"entity_slug": context["view"].kwargs["entity_slug"],
|
||||
"bill_pk": context["view"].kwargs["bill_pk"],
|
||||
"total_amount__sum": context["total_amount__sum"],
|
||||
"item_formset": item_formset,
|
||||
}
|
||||
|
||||
@register.inclusion_tag('bill/transactions/tags/txs_table.html')
|
||||
def transactions_table(object_type: Union[JournalEntryModel, BillModel, InvoiceModel], style='detail'):
|
||||
|
||||
@register.inclusion_tag("bill/transactions/tags/txs_table.html")
|
||||
def transactions_table(
|
||||
object_type: Union[JournalEntryModel, BillModel, InvoiceModel], style="detail"
|
||||
):
|
||||
if isinstance(object_type, JournalEntryModel):
|
||||
transaction_model_qs = object_type.transactionmodel_set.all().with_annotated_details().order_by(
|
||||
'-timestamp',
|
||||
)
|
||||
transaction_model_qs = (
|
||||
object_type.transactionmodel_set.all()
|
||||
.with_annotated_details()
|
||||
.order_by("-timestamp")
|
||||
)
|
||||
elif isinstance(object_type, BillModel):
|
||||
# Specific ordering for BillModel (timestamp ascending, then debit before credit)
|
||||
qs = object_type.get_transaction_queryset(annotated=True)
|
||||
@ -417,21 +494,25 @@ def transactions_table(object_type: Union[JournalEntryModel, BillModel, InvoiceM
|
||||
'pk' # Optional: Tie-breaker for consistent order
|
||||
)
|
||||
elif isinstance(object_type, InvoiceModel):
|
||||
transaction_model_qs = object_type.get_transaction_queryset(annotated=True).order_by('-timestamp')
|
||||
transaction_model_qs = object_type.get_transaction_queryset(
|
||||
annotated=True
|
||||
).order_by("-timestamp")
|
||||
else:
|
||||
raise ValidationError(
|
||||
'Cannot handle object of type {} to get transaction model queryset'.format(type(object_type))
|
||||
"Cannot handle object of type {} to get transaction model queryset".format(
|
||||
type(object_type)
|
||||
)
|
||||
)
|
||||
|
||||
total_credits = sum(tx.amount for tx in transaction_model_qs if tx.is_credit())
|
||||
total_debits = sum(tx.amount for tx in transaction_model_qs if tx.is_debit())
|
||||
|
||||
return {
|
||||
'style': style,
|
||||
'transaction_model_qs': transaction_model_qs,
|
||||
'total_debits': total_debits,
|
||||
'total_credits': total_credits,
|
||||
'object': object_type
|
||||
"style": style,
|
||||
"transaction_model_qs": transaction_model_qs,
|
||||
"total_debits": total_debits,
|
||||
"total_credits": total_credits,
|
||||
"object": object_type,
|
||||
}
|
||||
|
||||
|
||||
@ -441,67 +522,67 @@ def get_vehicle_image(car_serie):
|
||||
Returns the appropriate car image filename based on car series
|
||||
"""
|
||||
if not car_serie:
|
||||
return 'sedan.png'
|
||||
return "sedan.png"
|
||||
|
||||
serie_lower = car_serie.name.lower()
|
||||
|
||||
# SUV mapping
|
||||
if 'suv' in serie_lower:
|
||||
if 'sport' in serie_lower or '3 doors' in serie_lower:
|
||||
return 'crossover.png'
|
||||
if "suv" in serie_lower:
|
||||
if "sport" in serie_lower or "3 doors" in serie_lower:
|
||||
return "crossover.png"
|
||||
else:
|
||||
return 'suv.png'
|
||||
return "suv.png"
|
||||
|
||||
# Pickup mapping
|
||||
elif 'pickup' in serie_lower:
|
||||
if 'cabriolet' in serie_lower:
|
||||
return 'pickup_cabriolet.png'
|
||||
elif 'double' in serie_lower or 'crew' in serie_lower:
|
||||
return 'double_pickup.png'
|
||||
elif "pickup" in serie_lower:
|
||||
if "cabriolet" in serie_lower:
|
||||
return "pickup_cabriolet.png"
|
||||
elif "double" in serie_lower or "crew" in serie_lower:
|
||||
return "double_pickup.png"
|
||||
else:
|
||||
return 'single_pickup.png'
|
||||
return "single_pickup.png"
|
||||
|
||||
# Van/Minivan mapping
|
||||
elif 'minivan' in serie_lower:
|
||||
return 'minivan.png'
|
||||
elif 'van' in serie_lower:
|
||||
if 'cargo' in serie_lower:
|
||||
return 'van_cargo.png'
|
||||
elif "minivan" in serie_lower:
|
||||
return "minivan.png"
|
||||
elif "van" in serie_lower:
|
||||
if "cargo" in serie_lower:
|
||||
return "van_cargo.png"
|
||||
else:
|
||||
return 'van.png'
|
||||
elif 'compactvan' in serie_lower:
|
||||
return 'van.png'
|
||||
return "van.png"
|
||||
elif "compactvan" in serie_lower:
|
||||
return "van.png"
|
||||
|
||||
# Hatchback mapping
|
||||
elif 'hatchback' in serie_lower:
|
||||
return 'hatchback.png'
|
||||
elif "hatchback" in serie_lower:
|
||||
return "hatchback.png"
|
||||
|
||||
# Wagon mapping
|
||||
elif 'wagon' in serie_lower:
|
||||
return 'van.png' # Closest match
|
||||
elif "wagon" in serie_lower:
|
||||
return "van.png" # Closest match
|
||||
|
||||
# Coupe/Sports mapping
|
||||
elif 'cabriolet' in serie_lower:
|
||||
return 'cabriolet.png'
|
||||
elif 'coupe' in serie_lower:
|
||||
return 'coupe.png'
|
||||
elif 'speedster' in serie_lower:
|
||||
return 'sport_car.png'
|
||||
elif "cabriolet" in serie_lower:
|
||||
return "cabriolet.png"
|
||||
elif "coupe" in serie_lower:
|
||||
return "coupe.png"
|
||||
elif "speedster" in serie_lower:
|
||||
return "sport_car.png"
|
||||
|
||||
# Liftback mapping
|
||||
elif 'liftback' in serie_lower:
|
||||
return 'hatchback.png' # Closest match
|
||||
elif "liftback" in serie_lower:
|
||||
return "hatchback.png" # Closest match
|
||||
|
||||
# Sedan mapping (including 2 doors)
|
||||
elif 'sedan' in serie_lower:
|
||||
if '2 doors' in serie_lower:
|
||||
return 'coupe.png'
|
||||
elif "sedan" in serie_lower:
|
||||
if "2 doors" in serie_lower:
|
||||
return "coupe.png"
|
||||
else:
|
||||
return 'sedan.png'
|
||||
return "sedan.png"
|
||||
|
||||
# Default fallback
|
||||
else:
|
||||
return 'sedan.png'
|
||||
return "sedan.png"
|
||||
|
||||
|
||||
@register.filter
|
||||
@ -510,24 +591,49 @@ def get_vehicle_type_name(car_serie):
|
||||
Returns the vehicle type name for styling purposes
|
||||
"""
|
||||
if not car_serie:
|
||||
return 'sedan'
|
||||
return "sedan"
|
||||
|
||||
serie_lower = car_serie.name.lower()
|
||||
|
||||
if 'suv' in serie_lower:
|
||||
return 'suv'
|
||||
elif 'pickup' in serie_lower:
|
||||
return 'pickup'
|
||||
elif any(word in serie_lower for word in ['van', 'minivan']):
|
||||
return 'van'
|
||||
elif 'hatchback' in serie_lower:
|
||||
return 'hatchback'
|
||||
elif 'wagon' in serie_lower:
|
||||
return 'wagon'
|
||||
elif any(word in serie_lower for word in ['coupe', 'cabriolet', 'speedster']):
|
||||
return 'coupe'
|
||||
elif 'liftback' in serie_lower:
|
||||
return 'liftback'
|
||||
if "suv" in serie_lower:
|
||||
return "suv"
|
||||
elif "pickup" in serie_lower:
|
||||
return "pickup"
|
||||
elif any(word in serie_lower for word in ["van", "minivan"]):
|
||||
return "van"
|
||||
elif "hatchback" in serie_lower:
|
||||
return "hatchback"
|
||||
elif "wagon" in serie_lower:
|
||||
return "wagon"
|
||||
elif any(word in serie_lower for word in ["coupe", "cabriolet", "speedster"]):
|
||||
return "coupe"
|
||||
elif "liftback" in serie_lower:
|
||||
return "liftback"
|
||||
else:
|
||||
return 'sedan'
|
||||
|
||||
return "sedan"
|
||||
|
||||
|
||||
|
||||
@register.filter
|
||||
def status_badge_color(status):
|
||||
color_map = {
|
||||
'PENDING_APPROVAL': 'warning',
|
||||
'APPROVED': 'info',
|
||||
'IN_FINANCING': 'primary',
|
||||
'PARTIALLY_PAID': 'success',
|
||||
'FULLY_PAID': 'success',
|
||||
'PENDING_DELIVERY': 'warning',
|
||||
'DELIVERED': 'success',
|
||||
'CANCELLED': 'danger',
|
||||
}
|
||||
return color_map.get(status, 'secondary')
|
||||
|
||||
|
||||
@register.inclusion_tag('inventory/tags/inventory_table.html', takes_context=True)
|
||||
def inventory_table(context, queryset):
|
||||
ctx = {
|
||||
'entity_slug': context['view'].kwargs['entity_slug'],
|
||||
'inventory_list': queryset
|
||||
}
|
||||
ctx.update(queryset.aggregate(inventory_total_value=Sum('total_value')))
|
||||
return ctx
|
||||
@ -3,9 +3,10 @@ from num2words import num2words
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter
|
||||
def num_to_words(value, lang='ar'):
|
||||
def num_to_words(value, lang="ar"):
|
||||
try:
|
||||
return num2words(value, lang=lang)
|
||||
except:
|
||||
return value
|
||||
return value
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ from django_ledger.io.io_core import get_localdate
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from decimal import Decimal
|
||||
from unittest.mock import MagicMock
|
||||
from inventory.models import VatRate
|
||||
from inventory.models import VatRate
|
||||
from inventory.utils import CarFinanceCalculator
|
||||
|
||||
|
||||
@ -47,6 +47,7 @@ class ModelTest(TestCase):
|
||||
:ivar car_finances: Car finance object for the car under test.
|
||||
:type car_finances: CarFinance instance
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
email = "RkzgO@example.com"
|
||||
name = "John Doe"
|
||||
@ -178,46 +179,46 @@ class AuthenticationTest(TestCase):
|
||||
:ivar url: URL for account signup endpoint used in the test cases.
|
||||
:type url: str
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
self.url = reverse("account_signup")
|
||||
|
||||
def test_login(self):
|
||||
url = reverse("account_login")
|
||||
response = self.client.post(url, {"email": "RkzgO@example.com", "password": "password"})
|
||||
response = self.client.post(
|
||||
url, {"email": "RkzgO@example.com", "password": "password"}
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
def test_valid_data(self):
|
||||
# Create valid JSON data
|
||||
data = {
|
||||
"wizardValidationForm1": {
|
||||
"email": "test@example.com",
|
||||
"password": "password123",
|
||||
"confirm_password": "password123"
|
||||
"confirm_password": "password123",
|
||||
},
|
||||
"wizardValidationForm2": {
|
||||
"name": "John Doe",
|
||||
"arabic_name": "جون دو",
|
||||
"phone_number": "1234567890"
|
||||
"phone_number": "1234567890",
|
||||
},
|
||||
"wizardValidationForm3": {
|
||||
"crn": "123456",
|
||||
"vrn": "789012",
|
||||
"address": "123 Main St"
|
||||
}
|
||||
"address": "123 Main St",
|
||||
},
|
||||
}
|
||||
|
||||
# Send a POST request with the JSON data
|
||||
response = self.client.post(
|
||||
self.url,
|
||||
data=json.dumps(data),
|
||||
content_type='application/json'
|
||||
self.url, data=json.dumps(data), content_type="application/json"
|
||||
)
|
||||
|
||||
# Check the response
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(), {'message': 'User created successfully.'})
|
||||
|
||||
self.assertEqual(response.json(), {"message": "User created successfully."})
|
||||
|
||||
def test_passwords_do_not_match(self):
|
||||
# Create JSON data with mismatched passwords
|
||||
@ -225,25 +226,23 @@ class AuthenticationTest(TestCase):
|
||||
"wizardValidationForm1": {
|
||||
"email": "test@example.com",
|
||||
"password": "password123",
|
||||
"confirm_password": "differentpassword"
|
||||
"confirm_password": "differentpassword",
|
||||
},
|
||||
"wizardValidationForm2": {
|
||||
"name": "John Doe",
|
||||
"arabic_name": "جون دو",
|
||||
"phone_number": "1234567890"
|
||||
"phone_number": "1234567890",
|
||||
},
|
||||
"wizardValidationForm3": {
|
||||
"crn": "123456",
|
||||
"vrn": "789012",
|
||||
"address": "123 Main St"
|
||||
}
|
||||
"address": "123 Main St",
|
||||
},
|
||||
}
|
||||
|
||||
# Send a POST request with the JSON data
|
||||
response = self.client.post(
|
||||
self.url,
|
||||
data=json.dumps(data),
|
||||
content_type='application/json'
|
||||
self.url, data=json.dumps(data), content_type="application/json"
|
||||
)
|
||||
|
||||
# Check the response
|
||||
@ -261,26 +260,26 @@ class AuthenticationTest(TestCase):
|
||||
"wizardValidationForm2": {
|
||||
"name": "John Doe",
|
||||
"arabic_name": "جون دو",
|
||||
"phone_number": "1234567890"
|
||||
"phone_number": "1234567890",
|
||||
},
|
||||
"wizardValidationForm3": {
|
||||
"crn": "123456",
|
||||
"vrn": "789012",
|
||||
"address": "123 Main St"
|
||||
}
|
||||
"address": "123 Main St",
|
||||
},
|
||||
}
|
||||
|
||||
# Send a POST request with the JSON data
|
||||
response = self.client.post(
|
||||
self.url,
|
||||
data=json.dumps(data),
|
||||
content_type='application/json'
|
||||
self.url, data=json.dumps(data), content_type="application/json"
|
||||
)
|
||||
|
||||
# Check the response
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertIn("error", response.json()) # Assuming the view returns an error for missing fields
|
||||
|
||||
self.assertIn(
|
||||
"error", response.json()
|
||||
) # Assuming the view returns an error for missing fields
|
||||
|
||||
|
||||
class CarFinanceCalculatorTests(TestCase):
|
||||
"""
|
||||
@ -297,10 +296,11 @@ class CarFinanceCalculatorTests(TestCase):
|
||||
:ivar vat_rate: Active VAT rate used for testing VAT rate retrieval.
|
||||
:type vat_rate: VatRate
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
# Common setup for all tests
|
||||
self.mock_model = MagicMock()
|
||||
self.vat_rate = VatRate.objects.create(rate=Decimal('0.20'), is_active=True)
|
||||
self.vat_rate = VatRate.objects.create(rate=Decimal("0.20"), is_active=True)
|
||||
|
||||
def test_no_active_vat_rate_raises_error(self):
|
||||
VatRate.objects.all().delete() # Ensure no active VAT
|
||||
@ -309,11 +309,13 @@ class CarFinanceCalculatorTests(TestCase):
|
||||
|
||||
def test_vat_rate_retrieval(self):
|
||||
calculator = CarFinanceCalculator(self.mock_model)
|
||||
self.assertEqual(calculator.vat_rate, Decimal('0.20'))
|
||||
self.assertEqual(calculator.vat_rate, Decimal("0.20"))
|
||||
|
||||
def test_item_transactions_retrieval(self):
|
||||
mock_item = MagicMock()
|
||||
self.mock_model.get_itemtxs_data.return_value = [MagicMock(all=lambda: [mock_item])]
|
||||
self.mock_model.get_itemtxs_data.return_value = [
|
||||
MagicMock(all=lambda: [mock_item])
|
||||
]
|
||||
calculator = CarFinanceCalculator(self.mock_model)
|
||||
self.assertEqual(calculator.item_transactions, [mock_item])
|
||||
|
||||
@ -321,136 +323,152 @@ class CarFinanceCalculatorTests(TestCase):
|
||||
mock_item = MagicMock()
|
||||
mock_item.ce_quantity = 2
|
||||
mock_item.item_model = MagicMock()
|
||||
mock_item.item_model.item_number = '123'
|
||||
mock_item.item_model.item_number = "123"
|
||||
mock_item.item_model.additional_info = {
|
||||
CarFinanceCalculator.CAR_FINANCE_KEY: {
|
||||
'selling_price': '10000',
|
||||
'cost_price': '8000',
|
||||
'discount_amount': '500',
|
||||
'total_vat': '2000'
|
||||
"selling_price": "10000",
|
||||
"cost_price": "8000",
|
||||
"discount_amount": "500",
|
||||
"total_vat": "2000",
|
||||
},
|
||||
CarFinanceCalculator.CAR_INFO_KEY: {
|
||||
'vin': 'VIN123',
|
||||
'make': 'Toyota',
|
||||
'model': 'Camry',
|
||||
'year': 2020,
|
||||
'trim': 'LE',
|
||||
'mileage': 15000
|
||||
"vin": "VIN123",
|
||||
"make": "Toyota",
|
||||
"model": "Camry",
|
||||
"year": 2020,
|
||||
"trim": "LE",
|
||||
"mileage": 15000,
|
||||
},
|
||||
CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [
|
||||
{'name': 'Service 1', 'price': '200', 'taxable': True, 'price_': '240'}
|
||||
]
|
||||
{"name": "Service 1", "price": "200", "taxable": True, "price_": "240"}
|
||||
],
|
||||
}
|
||||
|
||||
calculator = CarFinanceCalculator(self.mock_model)
|
||||
car_data = calculator._get_car_data(mock_item)
|
||||
|
||||
self.assertEqual(car_data['item_number'], '123')
|
||||
self.assertEqual(car_data['vin'], 'VIN123')
|
||||
self.assertEqual(car_data['make'], 'Toyota')
|
||||
self.assertEqual(car_data['selling_price'], '10000')
|
||||
self.assertEqual(car_data['unit_price'], Decimal('10000'))
|
||||
self.assertEqual(car_data['quantity'], 2)
|
||||
self.assertEqual(car_data['total'], Decimal('20000'))
|
||||
self.assertEqual(car_data['total_vat'], '2000')
|
||||
self.assertEqual(car_data['additional_services'], [{'name': 'Service 1', 'price': '200', 'taxable': True, 'price_': '240'}])
|
||||
self.assertEqual(car_data["item_number"], "123")
|
||||
self.assertEqual(car_data["vin"], "VIN123")
|
||||
self.assertEqual(car_data["make"], "Toyota")
|
||||
self.assertEqual(car_data["selling_price"], "10000")
|
||||
self.assertEqual(car_data["unit_price"], Decimal("10000"))
|
||||
self.assertEqual(car_data["quantity"], 2)
|
||||
self.assertEqual(car_data["total"], Decimal("20000"))
|
||||
self.assertEqual(car_data["total_vat"], "2000")
|
||||
self.assertEqual(
|
||||
car_data["additional_services"],
|
||||
[{"name": "Service 1", "price": "200", "taxable": True, "price_": "240"}],
|
||||
)
|
||||
|
||||
def test_get_additional_services(self):
|
||||
mock_item1 = MagicMock()
|
||||
mock_item1.item_model.additional_info = {
|
||||
CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [
|
||||
{'name': 'Service 1', 'price': '100', 'taxable': True, 'price_': '120'}
|
||||
{"name": "Service 1", "price": "100", "taxable": True, "price_": "120"}
|
||||
]
|
||||
}
|
||||
mock_item2 = MagicMock()
|
||||
mock_item2.item_model.additional_info = {
|
||||
CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [
|
||||
{'name': 'Service 2', 'price': '200', 'taxable': False, 'price_': '200'}
|
||||
{"name": "Service 2", "price": "200", "taxable": False, "price_": "200"}
|
||||
]
|
||||
}
|
||||
|
||||
self.mock_model.get_itemtxs_data.return_value = [MagicMock(all=lambda: [mock_item1, mock_item2])]
|
||||
self.mock_model.get_itemtxs_data.return_value = [
|
||||
MagicMock(all=lambda: [mock_item1, mock_item2])
|
||||
]
|
||||
calculator = CarFinanceCalculator(self.mock_model)
|
||||
services = calculator._get_additional_services()
|
||||
|
||||
self.assertEqual(len(services), 2)
|
||||
self.assertEqual(services[0]['name'], 'Service 1')
|
||||
self.assertEqual(services[1]['name'], 'Service 2')
|
||||
self.assertEqual(services[0]['price_'], '120')
|
||||
self.assertEqual(services[1]['price_'], '200')
|
||||
self.assertEqual(services[0]["name"], "Service 1")
|
||||
self.assertEqual(services[1]["name"], "Service 2")
|
||||
self.assertEqual(services[0]["price_"], "120")
|
||||
self.assertEqual(services[1]["price_"], "200")
|
||||
|
||||
def test_calculate_totals(self):
|
||||
mock_item1 = MagicMock()
|
||||
mock_item1.ce_quantity = 2
|
||||
mock_item1.item_model.additional_info = {
|
||||
CarFinanceCalculator.CAR_FINANCE_KEY: {
|
||||
'selling_price': '10000',
|
||||
'discount_amount': '500'
|
||||
"selling_price": "10000",
|
||||
"discount_amount": "500",
|
||||
},
|
||||
CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [
|
||||
{'price_': '100'},
|
||||
{'price_': '200'}
|
||||
]
|
||||
{"price_": "100"},
|
||||
{"price_": "200"},
|
||||
],
|
||||
}
|
||||
|
||||
mock_item2 = MagicMock()
|
||||
mock_item2.quantity = 3
|
||||
mock_item2.item_model.additional_info = {
|
||||
CarFinanceCalculator.CAR_FINANCE_KEY: {
|
||||
'selling_price': '20000',
|
||||
'discount_amount': '1000'
|
||||
"selling_price": "20000",
|
||||
"discount_amount": "1000",
|
||||
},
|
||||
CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [
|
||||
{'price_': '300'}
|
||||
]
|
||||
CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [{"price_": "300"}],
|
||||
}
|
||||
|
||||
self.mock_model.get_itemtxs_data.return_value = [MagicMock(all=lambda: [mock_item1, mock_item2])]
|
||||
self.mock_model.get_itemtxs_data.return_value = [
|
||||
MagicMock(all=lambda: [mock_item1, mock_item2])
|
||||
]
|
||||
calculator = CarFinanceCalculator(self.mock_model)
|
||||
totals = calculator.calculate_totals()
|
||||
|
||||
expected_total_price = (Decimal('10000') * 2 + Decimal('20000') * 3) - (Decimal('500') + Decimal('1000'))
|
||||
expected_vat = expected_total_price * Decimal('0.15')
|
||||
expected_additionals = Decimal('100') + Decimal('200') + Decimal('300')
|
||||
expected_grand_total = (expected_total_price + expected_vat + expected_additionals).quantize(Decimal('0.00'))
|
||||
expected_total_price = (Decimal("10000") * 2 + Decimal("20000") * 3) - (
|
||||
Decimal("500") + Decimal("1000")
|
||||
)
|
||||
expected_vat = expected_total_price * Decimal("0.15")
|
||||
expected_additionals = Decimal("100") + Decimal("200") + Decimal("300")
|
||||
expected_grand_total = (
|
||||
expected_total_price + expected_vat + expected_additionals
|
||||
).quantize(Decimal("0.00"))
|
||||
|
||||
self.assertEqual(totals['total_price'], expected_total_price)
|
||||
self.assertEqual(totals['total_discount'], Decimal('1500'))
|
||||
self.assertEqual(totals['total_vat_amount'], expected_vat)
|
||||
self.assertEqual(totals['total_additionals'], expected_additionals)
|
||||
self.assertEqual(totals['grand_total'], expected_grand_total)
|
||||
self.assertEqual(totals["total_price"], expected_total_price)
|
||||
self.assertEqual(totals["total_discount"], Decimal("1500"))
|
||||
self.assertEqual(totals["total_vat_amount"], expected_vat)
|
||||
self.assertEqual(totals["total_additionals"], expected_additionals)
|
||||
self.assertEqual(totals["grand_total"], expected_grand_total)
|
||||
|
||||
def test_get_finance_data(self):
|
||||
mock_item = MagicMock()
|
||||
mock_item.ce_quantity = 1
|
||||
mock_item.item_model = MagicMock()
|
||||
mock_item.item_model.item_number = '456'
|
||||
mock_item.item_model.item_number = "456"
|
||||
mock_item.item_model.additional_info = {
|
||||
CarFinanceCalculator.CAR_FINANCE_KEY: {
|
||||
'selling_price': '15000',
|
||||
'discount_amount': '1000',
|
||||
'total_vat': '2800'
|
||||
"selling_price": "15000",
|
||||
"discount_amount": "1000",
|
||||
"total_vat": "2800",
|
||||
},
|
||||
CarFinanceCalculator.CAR_INFO_KEY: {
|
||||
'vin': 'VIN456',
|
||||
'make': 'Honda',
|
||||
'model': 'Civic',
|
||||
'year': 2021
|
||||
"vin": "VIN456",
|
||||
"make": "Honda",
|
||||
"model": "Civic",
|
||||
"year": 2021,
|
||||
},
|
||||
CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [
|
||||
{'name': 'Service', 'price': '150', 'taxable': True, 'price_': '180'}
|
||||
]
|
||||
{"name": "Service", "price": "150", "taxable": True, "price_": "180"}
|
||||
],
|
||||
}
|
||||
|
||||
self.mock_model.get_itemtxs_data.return_value = [MagicMock(all=lambda: [mock_item])]
|
||||
self.mock_model.get_itemtxs_data.return_value = [
|
||||
MagicMock(all=lambda: [mock_item])
|
||||
]
|
||||
calculator = CarFinanceCalculator(self.mock_model)
|
||||
finance_data = calculator.get_finance_data()
|
||||
|
||||
self.assertEqual(len(finance_data['cars']), 1)
|
||||
self.assertEqual(finance_data['quantity'], 1)
|
||||
self.assertEqual(finance_data['total_price'], Decimal('14000')) # 15000 - 1000
|
||||
self.assertEqual(finance_data['total_vat'], Decimal('14000') + (Decimal('14000') * Decimal('0.20')))
|
||||
self.assertEqual(finance_data['total_vat_amount'], Decimal('14000') * Decimal('0.20'))
|
||||
self.assertEqual(finance_data['total_additionals'], Decimal('180'))
|
||||
self.assertEqual(finance_data['additionals'][0]['name'], 'Service')
|
||||
self.assertEqual(finance_data['vat'], Decimal('0.20'))
|
||||
self.assertEqual(len(finance_data["cars"]), 1)
|
||||
self.assertEqual(finance_data["quantity"], 1)
|
||||
self.assertEqual(finance_data["total_price"], Decimal("14000")) # 15000 - 1000
|
||||
self.assertEqual(
|
||||
finance_data["total_vat"],
|
||||
Decimal("14000") + (Decimal("14000") * Decimal("0.20")),
|
||||
)
|
||||
self.assertEqual(
|
||||
finance_data["total_vat_amount"], Decimal("14000") * Decimal("0.20")
|
||||
)
|
||||
self.assertEqual(finance_data["total_additionals"], Decimal("180"))
|
||||
self.assertEqual(finance_data["additionals"][0]["name"], "Service")
|
||||
self.assertEqual(finance_data["vat"], Decimal("0.20"))
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from django.conf.urls import handler403,handler400,handler404,handler500
|
||||
from django.conf.urls import handler403, handler400, handler404, handler500
|
||||
from django.urls import path
|
||||
from django_tables2.export.export import TableExport
|
||||
|
||||
@ -43,30 +43,39 @@ urlpatterns = [
|
||||
# ),
|
||||
# ),
|
||||
# Tasks
|
||||
|
||||
path('tasks/', views.task_list, name='task_list'),
|
||||
path('legal/', views.terms_and_privacy, name='terms_and_privacy'),
|
||||
path("tasks/", views.task_list, name="task_list"),
|
||||
path("legal/", views.terms_and_privacy, name="terms_and_privacy"),
|
||||
# path('tasks/<int:task_id>/detail/', views.task_detail, name='task_detail'),
|
||||
# Dashboards
|
||||
# path("user/<int:pk>/settings/", views.UserSettingsView.as_view(), name="user_settings"),
|
||||
path("pricing/", views.pricing_page, name="pricing_page"),
|
||||
path("submit_plan/", views.submit_plan, name="submit_plan"),
|
||||
path('payment-callback/', views.payment_callback, name='payment_callback'),
|
||||
path("payment-callback/", views.payment_callback, name="payment_callback"),
|
||||
#
|
||||
path(
|
||||
"dealers/activity/",
|
||||
views.UserActivityLogListView.as_view(),
|
||||
name="dealer_activity",
|
||||
),
|
||||
path("dealers/<slug:slug>/settings/", views.DealerSettingsView, name="dealer_settings"),
|
||||
path(
|
||||
"dealers/<slug:slug>/settings/",
|
||||
views.DealerSettingsView,
|
||||
name="dealer_settings",
|
||||
),
|
||||
path("dealers/assign-car-makes/", views.assign_car_makes, name="assign_car_makes"),
|
||||
path("dashboards/manager/", views.ManagerDashboard.as_view(), name="manager_dashboard"),
|
||||
path(
|
||||
"dashboards/manager/",
|
||||
views.ManagerDashboard.as_view(),
|
||||
name="manager_dashboard",
|
||||
),
|
||||
path("dashboards/sales/", views.SalesDashboard.as_view(), name="sales_dashboard"),
|
||||
path("test/", views.TestView.as_view(), name="test"),
|
||||
path('cars/inventory/table/', views.CarListViewTable.as_view(), name="car_table"),
|
||||
path("cars/inventory/table/", views.CarListViewTable.as_view(), name="car_table"),
|
||||
path("export/format/", TableExport, name="export"),
|
||||
# Dealer URLs
|
||||
path("dealers/<slug:slug>/", views.DealerDetailView.as_view(), name="dealer_detail"),
|
||||
path(
|
||||
"dealers/<slug:slug>/", views.DealerDetailView.as_view(), name="dealer_detail"
|
||||
),
|
||||
path(
|
||||
"dealers/<slug:slug>/update/",
|
||||
views.DealerUpdateView.as_view(),
|
||||
@ -93,7 +102,9 @@ urlpatterns = [
|
||||
views.CustomerUpdateView.as_view(),
|
||||
name="customer_update",
|
||||
),
|
||||
path("customers/<slug:slug>/delete/", views.delete_customer, name="customer_delete"),
|
||||
path(
|
||||
"customers/<slug:slug>/delete/", views.delete_customer, name="customer_delete"
|
||||
),
|
||||
path(
|
||||
"customers/<slug:slug>/opportunities/create/",
|
||||
views.OpportunityCreateView.as_view(),
|
||||
@ -101,19 +112,26 @@ urlpatterns = [
|
||||
),
|
||||
path("crm/leads/create/", views.lead_create, name="lead_create"),
|
||||
path(
|
||||
"crm/leads/<slug:slug>/view/", views.LeadDetailView.as_view(), name="lead_detail"
|
||||
"crm/leads/<slug:slug>/view/",
|
||||
views.LeadDetailView.as_view(),
|
||||
name="lead_detail",
|
||||
),
|
||||
path('update-lead-actions/', views.update_lead_actions, name='update_lead_actions'),
|
||||
path('crm/leads/lead_tracking/', views.lead_tracking, name='lead_tracking'),
|
||||
path('crm/leads/lead_view/', views.lead_view, name='lead_view'),
|
||||
|
||||
path("update-lead-actions/", views.update_lead_actions, name="update_lead_actions"),
|
||||
path("crm/leads/lead_tracking/", views.lead_tracking, name="lead_tracking"),
|
||||
path("crm/leads/lead_view/", views.lead_view, name="lead_view"),
|
||||
path("crm/leads/", views.LeadListView.as_view(), name="lead_list"),
|
||||
path(
|
||||
"crm/leads/<slug:slug>/update/", views.LeadUpdateView.as_view(), name="lead_update"
|
||||
"crm/leads/<slug:slug>/update/",
|
||||
views.LeadUpdateView.as_view(),
|
||||
name="lead_update",
|
||||
),
|
||||
path("crm/leads/<slug:slug>/delete/", views.LeadDeleteView, name="lead_delete"),
|
||||
path("crm/leads/<slug:slug>/lead-convert/", views.lead_convert, name="lead_convert"),
|
||||
path("crm/leads/<int:pk>/delete-note/", views.delete_note, name="delete_note_to_lead"),
|
||||
path(
|
||||
"crm/leads/<slug:slug>/lead-convert/", views.lead_convert, name="lead_convert"
|
||||
),
|
||||
path(
|
||||
"crm/leads/<int:pk>/delete-note/", views.delete_note, name="delete_note_to_lead"
|
||||
),
|
||||
path(
|
||||
"crm/<int:pk>/update-note/",
|
||||
views.update_note,
|
||||
@ -211,17 +229,22 @@ urlpatterns = [
|
||||
),
|
||||
# path('crm/opportunities/<int:pk>/logs/', views.OpportunityLogsView.as_view(), name='opportunity_logs'),
|
||||
# #######################
|
||||
path('stream/', views.sse_stream, name='sse_stream'),
|
||||
path('fetch/', views.fetch_notifications, name='fetch_notifications'),
|
||||
|
||||
path("stream/", views.sse_stream, name="sse_stream"),
|
||||
path("fetch/", views.fetch_notifications, name="fetch_notifications"),
|
||||
# Mark single notification as read
|
||||
path('<int:notification_id>/mark-read/', views.mark_notification_as_read, name='mark_notification_as_read'),
|
||||
|
||||
path(
|
||||
"<int:notification_id>/mark-read/",
|
||||
views.mark_notification_as_read,
|
||||
name="mark_notification_as_read",
|
||||
),
|
||||
# Mark all notifications as read
|
||||
path('mark-all-read/', views.mark_all_notifications_as_read, name='mark_all_notifications_as_read'),
|
||||
|
||||
path(
|
||||
"mark-all-read/",
|
||||
views.mark_all_notifications_as_read,
|
||||
name="mark_all_notifications_as_read",
|
||||
),
|
||||
# Notification history
|
||||
path('history/', views.notifications_history, name='notifications_history'),
|
||||
path("history/", views.notifications_history, name="notifications_history"),
|
||||
# #######################
|
||||
path(
|
||||
"crm/notifications/",
|
||||
@ -238,7 +261,7 @@ urlpatterns = [
|
||||
views.mark_notification_as_read,
|
||||
name="mark_notification_as_read",
|
||||
),
|
||||
path('crm/calender/', views.EmployeeCalendarView.as_view(), name='calendar_list'),
|
||||
path("crm/calender/", views.EmployeeCalendarView.as_view(), name="calendar_list"),
|
||||
# Vendor URLs
|
||||
path("vendors/create/", views.VendorCreateView.as_view(), name="vendor_create"),
|
||||
path("vendors", views.VendorListView.as_view(), name="vendor_list"),
|
||||
@ -254,8 +277,8 @@ urlpatterns = [
|
||||
name="vendor_delete",
|
||||
),
|
||||
# Car URLs
|
||||
path('cars/upload_cars/', views.upload_cars, name='upload_cars'),
|
||||
path('cars/<uuid:pk>/upload_cars/', views.upload_cars, name='upload_cars'),
|
||||
path("cars/upload_cars/", views.upload_cars, name="upload_cars"),
|
||||
path("cars/<uuid:pk>/upload_cars/", views.upload_cars, name="upload_cars"),
|
||||
path("cars/add/", views.CarCreateView.as_view(), name="car_add"),
|
||||
path("cars/inventory/", views.CarInventory.as_view(), name="car_inventory_all"),
|
||||
path(
|
||||
@ -288,13 +311,17 @@ urlpatterns = [
|
||||
path(
|
||||
"cars/<slug:slug>/add-color/", views.CarColorCreate.as_view(), name="add_color"
|
||||
),
|
||||
path('car/colors/<slug:slug>/update/', views.CarColorsUpdateView.as_view(), name='car_colors_update'),
|
||||
path(
|
||||
"car/colors/<slug:slug>/update/",
|
||||
views.CarColorsUpdateView.as_view(),
|
||||
name="car_colors_update",
|
||||
),
|
||||
path(
|
||||
"cars/<slug:slug>/location/add/",
|
||||
views.CarLocationCreateView.as_view(),
|
||||
name="add_car_location",
|
||||
),
|
||||
path(
|
||||
path(
|
||||
"cars/<slug:car_pk>/location/<int:pk>/update",
|
||||
views.CarLocationUpdateView.as_view(),
|
||||
name="update_car_location",
|
||||
@ -324,13 +351,9 @@ path(
|
||||
views.CarTransferPreviewView,
|
||||
name="transfer_preview",
|
||||
),
|
||||
path("cars/inventory/search/",
|
||||
views.SearchCodeView.as_view(),
|
||||
name="car_search"),
|
||||
path("cars/inventory/search/", views.SearchCodeView.as_view(), name="car_search"),
|
||||
# path('cars/<int:car_pk>/colors/<int:pk>/update/',views.CarColorUpdateView.as_view(),name='color_update'),
|
||||
path("cars/reserve/<slug:slug>/",
|
||||
views.reserve_car_view,
|
||||
name="reserve_car"),
|
||||
path("cars/reserve/<slug:slug>/", views.reserve_car_view, name="reserve_car"),
|
||||
path(
|
||||
"reservations/<int:reservation_id>/",
|
||||
views.manage_reservation,
|
||||
@ -341,16 +364,20 @@ path(
|
||||
views.CustomCardCreateView.as_view(),
|
||||
name="add_custom_card",
|
||||
),
|
||||
path('cars/<slug:slug>/add-registration/',
|
||||
views.CarRegistrationCreateView.as_view(),
|
||||
name='add_registration'),
|
||||
|
||||
#sales list
|
||||
path(
|
||||
'sales/list/',
|
||||
views.sales_list_view,
|
||||
name='sales_list',
|
||||
"cars/<slug:slug>/add-registration/",
|
||||
views.CarRegistrationCreateView.as_view(),
|
||||
name="add_registration",
|
||||
),
|
||||
# sales list
|
||||
path(
|
||||
"sales/list/",
|
||||
views.sales_list_view,
|
||||
name="sales_list",
|
||||
),
|
||||
path('sale_orders/<int:pk>/', views.SaleOrderDetailView.as_view(), name='order_detail'),
|
||||
path('inventory/<slug:entity_slug>/list/', views.InventoryListView.as_view(), name='inventort_list'),
|
||||
|
||||
# Sales URLs quotation_create
|
||||
# path(
|
||||
# "sales/quotations/create/",
|
||||
@ -401,21 +428,26 @@ path(
|
||||
# views.payment_create,
|
||||
# name="payment_create",
|
||||
# ),
|
||||
|
||||
# Users URLs
|
||||
path("user/", views.UserListView.as_view(), name="user_list"),
|
||||
path("user/create/", views.UserCreateView.as_view(), name="user_create"),
|
||||
path("user/<slug:slug>/", views.UserDetailView.as_view(), name="user_detail"),
|
||||
path("user/<slug:slug>/groups/", views.UserGroupView, name="user_groups"),
|
||||
path("user/<slug:slug>/update/", views.UserUpdateView.as_view(), name="user_update"),
|
||||
path(
|
||||
"user/<slug:slug>/update/", views.UserUpdateView.as_view(), name="user_update"
|
||||
),
|
||||
path("user/<slug:slug>/confirm/", views.UserDeleteview, name="user_delete"),
|
||||
# Group URLs
|
||||
path("group/create/", views.GroupCreateView.as_view(), name="group_create"),
|
||||
path("group/<int:pk>/update/", views.GroupUpdateView.as_view(), name="group_update"),
|
||||
path(
|
||||
"group/<int:pk>/update/", views.GroupUpdateView.as_view(), name="group_update"
|
||||
),
|
||||
path("group/<int:pk>/", views.GroupDetailView.as_view(), name="group_detail"),
|
||||
path("group/", views.GroupListView.as_view(), name="group_list"),
|
||||
path("group/<int:pk>/confirm/", views.GroupDeleteview, name="group_delete"),
|
||||
path("group/<int:pk>/permission/", views.GroupPermissionView, name="group_permission"),
|
||||
path(
|
||||
"group/<int:pk>/permission/", views.GroupPermissionView, name="group_permission"
|
||||
),
|
||||
# Organization URLs
|
||||
path(
|
||||
"organizations/create/",
|
||||
@ -468,76 +500,109 @@ path(
|
||||
),
|
||||
# Ledger URLS
|
||||
# Ledger
|
||||
path(
|
||||
"ledgers/", views.LedgerModelListView.as_view(), name="ledger_list"
|
||||
),
|
||||
path("ledgers/", views.LedgerModelListView.as_view(), name="ledger_list"),
|
||||
path(
|
||||
"ledgers/create/", views.LedgerModelCreateView.as_view(), name="ledger_create"
|
||||
),
|
||||
path(
|
||||
"ledgers/<slug:entity_slug>/detail/<uuid:pk>/", views.LedgerModelDetailView.as_view(), name="ledger_detail"
|
||||
"ledgers/<slug:entity_slug>/detail/<uuid:pk>/",
|
||||
views.LedgerModelDetailView.as_view(),
|
||||
name="ledger_detail",
|
||||
),
|
||||
path(
|
||||
"ledgers/<slug:entity_slug>/lock_all_journals/<uuid:pk>/", views.ledger_lock_all_journals, name="lock_all_journals"
|
||||
"ledgers/<slug:entity_slug>/lock_all_journals/<uuid:pk>/",
|
||||
views.ledger_lock_all_journals,
|
||||
name="lock_all_journals",
|
||||
),
|
||||
path(
|
||||
"ledgers/<slug:entity_slug>/unlock_all_journals/<uuid:pk>/", views.ledger_unlock_all_journals, name="unlock_all_journals"
|
||||
"ledgers/<slug:entity_slug>/unlock_all_journals/<uuid:pk>/",
|
||||
views.ledger_unlock_all_journals,
|
||||
name="unlock_all_journals",
|
||||
),
|
||||
path(
|
||||
"ledgers/<slug:entity_slug>/post_all_journals/<uuid:pk>/", views.ledger_post_all_journals, name="post_all_journals"
|
||||
"ledgers/<slug:entity_slug>/post_all_journals/<uuid:pk>/",
|
||||
views.ledger_post_all_journals,
|
||||
name="post_all_journals",
|
||||
),
|
||||
path(
|
||||
"ledgers/<slug:entity_slug>/unpost_all_journals/<uuid:pk>/", views.ledger_unpost_all_journals, name="unpost_all_journals"
|
||||
"ledgers/<slug:entity_slug>/unpost_all_journals/<uuid:pk>/",
|
||||
views.ledger_unpost_all_journals,
|
||||
name="unpost_all_journals",
|
||||
),
|
||||
# path(
|
||||
# "ledgers/create/", views.LedgerModelCreateView.as_view(), name="ledger_create"
|
||||
# ),
|
||||
path(
|
||||
"journalentries/<uuid:pk>/list/", views.JournalEntryListView.as_view(), name="journalentry_list"
|
||||
"journalentries/<uuid:pk>/list/",
|
||||
views.JournalEntryListView.as_view(),
|
||||
name="journalentry_list",
|
||||
),
|
||||
path(
|
||||
"journalentries/<uuid:pk>/create/", views.JournalEntryCreateView.as_view(), name="journalentry_create"
|
||||
"journalentries/<uuid:pk>/create/",
|
||||
views.JournalEntryCreateView.as_view(),
|
||||
name="journalentry_create",
|
||||
),
|
||||
path(
|
||||
"journalentries/<uuid:pk>/delete/", views.JournalEntryDeleteView, name="journalentry_delete"
|
||||
"journalentries/<uuid:pk>/delete/",
|
||||
views.JournalEntryDeleteView,
|
||||
name="journalentry_delete",
|
||||
),
|
||||
path(
|
||||
"journalentries/<uuid:pk>/transactions/",
|
||||
views.JournalEntryTransactionsView,
|
||||
name="journalentry_transactions",
|
||||
),
|
||||
path('journalentries/<slug:entity_slug>/<uuid:ledger_pk>/detail/<uuid:je_pk>/txs/',
|
||||
views.JournalEntryModelTXSDetailView.as_view(),
|
||||
name='journalentry_txs'),
|
||||
path(
|
||||
"journalentries/<slug:entity_slug>/<uuid:ledger_pk>/detail/<uuid:je_pk>/txs/",
|
||||
views.JournalEntryModelTXSDetailView.as_view(),
|
||||
name="journalentry_txs",
|
||||
),
|
||||
# ledger actions
|
||||
|
||||
path('ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/post/',
|
||||
views.LedgerModelModelActionView.as_view(action_name='post'),
|
||||
name='ledger-action-post'),
|
||||
path('ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/post-journal-entries/',
|
||||
views.LedgerModelModelActionView.as_view(action_name='post_journal_entries'),
|
||||
name='ledger-action-post-journal-entries'),
|
||||
path('ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/unpost/',
|
||||
views.LedgerModelModelActionView.as_view(action_name='unpost'),
|
||||
name='ledger-action-unpost'),
|
||||
path('ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/lock/',
|
||||
views.LedgerModelModelActionView.as_view(action_name='lock'),
|
||||
name='ledger-action-lock'),
|
||||
path('ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/lock-journal-entries/',
|
||||
views.LedgerModelModelActionView.as_view(action_name='lock_journal_entries'),
|
||||
name='ledger-action-lock-journal-entries'),
|
||||
path('ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/unlock/',
|
||||
views.LedgerModelModelActionView.as_view(action_name='unlock'),
|
||||
name='ledger-action-unlock'),
|
||||
path('ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/hide/',
|
||||
views.LedgerModelModelActionView.as_view(action_name='hide'),
|
||||
name='ledger-action-hide'),
|
||||
path('ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/unhide/',
|
||||
views.LedgerModelModelActionView.as_view(action_name='unhide'),
|
||||
name='ledger-action-unhide'),
|
||||
path('ledgers/<slug:entity_slug>/delete/<uuid:ledger_pk>/',
|
||||
views.LedgerModelDeleteView.as_view(),
|
||||
name='ledger-delete'),
|
||||
path(
|
||||
"ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/post/",
|
||||
views.LedgerModelModelActionView.as_view(action_name="post"),
|
||||
name="ledger-action-post",
|
||||
),
|
||||
path(
|
||||
"ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/post-journal-entries/",
|
||||
views.LedgerModelModelActionView.as_view(action_name="post_journal_entries"),
|
||||
name="ledger-action-post-journal-entries",
|
||||
),
|
||||
path(
|
||||
"ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/unpost/",
|
||||
views.LedgerModelModelActionView.as_view(action_name="unpost"),
|
||||
name="ledger-action-unpost",
|
||||
),
|
||||
path(
|
||||
"ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/lock/",
|
||||
views.LedgerModelModelActionView.as_view(action_name="lock"),
|
||||
name="ledger-action-lock",
|
||||
),
|
||||
path(
|
||||
"ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/lock-journal-entries/",
|
||||
views.LedgerModelModelActionView.as_view(action_name="lock_journal_entries"),
|
||||
name="ledger-action-lock-journal-entries",
|
||||
),
|
||||
path(
|
||||
"ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/unlock/",
|
||||
views.LedgerModelModelActionView.as_view(action_name="unlock"),
|
||||
name="ledger-action-unlock",
|
||||
),
|
||||
path(
|
||||
"ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/hide/",
|
||||
views.LedgerModelModelActionView.as_view(action_name="hide"),
|
||||
name="ledger-action-hide",
|
||||
),
|
||||
path(
|
||||
"ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/unhide/",
|
||||
views.LedgerModelModelActionView.as_view(action_name="unhide"),
|
||||
name="ledger-action-unhide",
|
||||
),
|
||||
path(
|
||||
"ledgers/<slug:entity_slug>/delete/<uuid:ledger_pk>/",
|
||||
views.LedgerModelDeleteView.as_view(),
|
||||
name="ledger-delete",
|
||||
),
|
||||
# Bank Account
|
||||
path(
|
||||
"bank_accounts/", views.BankAccountListView.as_view(), name="bank_account_list"
|
||||
@ -586,7 +651,11 @@ path(
|
||||
name="estimate_detail",
|
||||
),
|
||||
path("sales/estimates/create/", views.create_estimate, name="estimate_create"),
|
||||
path("sales/estimates/create/<slug:slug>/", views.create_estimate, name="estimate_create_from_opportunity"),
|
||||
path(
|
||||
"sales/estimates/create/<slug:slug>/",
|
||||
views.create_estimate,
|
||||
name="estimate_create_from_opportunity",
|
||||
),
|
||||
path(
|
||||
"sales/estimates/<uuid:pk>/estimate_mark_as/",
|
||||
views.estimate_mark_as,
|
||||
@ -605,10 +674,21 @@ path(
|
||||
path(
|
||||
"sales/estimates/<uuid:pk>/send_email", views.send_email_view, name="send_email"
|
||||
),
|
||||
path('sales/estimates/<uuid:pk>/sale_order/', views.create_sale_order, name='create_sale_order'),
|
||||
path('sales/estimates/<uuid:pk>/sale_order/<int:order_pk>/details/', views.SaleOrderDetail.as_view(), name='sale_order_details'),
|
||||
path('sales/estimates/<uuid:pk>/sale_order/preview/', views.preview_sale_order, name='preview_sale_order'),
|
||||
|
||||
path(
|
||||
"sales/estimates/<uuid:pk>/sale_order/",
|
||||
views.create_sale_order,
|
||||
name="create_sale_order",
|
||||
),
|
||||
path(
|
||||
"sales/estimates/<uuid:pk>/sale_order/<int:order_pk>/details/",
|
||||
views.SaleOrderDetail.as_view(),
|
||||
name="sale_order_details",
|
||||
),
|
||||
path(
|
||||
"sales/estimates/<uuid:pk>/sale_order/preview/",
|
||||
views.preview_sale_order,
|
||||
name="preview_sale_order",
|
||||
),
|
||||
# Invoice
|
||||
path("sales/invoices/", views.InvoiceListView.as_view(), name="invoice_list"),
|
||||
path(
|
||||
@ -702,52 +782,82 @@ path(
|
||||
# Bills
|
||||
path("items/bills/", views.BillListView.as_view(), name="bill_list"),
|
||||
# path("items/bills/create/", views.BillModelCreateViewView.as_view(), name="bill_create"),
|
||||
path('items/bills/<slug:entity_slug>/create/',
|
||||
views.BillModelCreateView.as_view(),
|
||||
name='bill-create'),
|
||||
path('items/bills/<slug:entity_slug>/create/purchase-order/<uuid:po_pk>/',
|
||||
views.BillModelCreateView.as_view(for_purchase_order=True),
|
||||
name='bill-create-po'),
|
||||
path('items/bills/<slug:entity_slug>/create/estimate/<uuid:ce_pk>/',
|
||||
views.BillModelCreateView.as_view(for_estimate=True),
|
||||
name='bill-create-estimate'),
|
||||
path('items/bills/<slug:entity_slug>/detail/<uuid:bill_pk>/',
|
||||
views.BillModelDetailViewView.as_view(),
|
||||
name='bill-detail'),
|
||||
path('items/bills/<slug:entity_slug>/update/<uuid:bill_pk>/',
|
||||
views.BillModelUpdateViewView.as_view(),
|
||||
name='bill-update'),
|
||||
path('items/bills/<slug:entity_slug>/update/<uuid:bill_pk>/items/',
|
||||
views.BillModelUpdateViewView.as_view(action_update_items=True),
|
||||
name='bill-update-items'),
|
||||
############################################################
|
||||
path('items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/mark-as-draft/',
|
||||
views.BillModelActionMarkAsDraftView.as_view(),
|
||||
name='bill-action-mark-as-draft'),
|
||||
path('items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/mark-as-review/',
|
||||
views.BillModelActionMarkAsInReviewView.as_view(),
|
||||
name='bill-action-mark-as-review'),
|
||||
path('items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/mark-as-approved/',
|
||||
views.BillModelActionMarkAsApprovedView.as_view(),
|
||||
name='bill-action-mark-as-approved'),
|
||||
path('items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/mark-as-paid/',
|
||||
views.BillModelActionMarkAsPaidView.as_view(),
|
||||
name='bill-action-mark-as-paid'),
|
||||
path('items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/mark-as-void/',
|
||||
views.BillModelActionVoidView.as_view(),
|
||||
name='bill-action-mark-as-void'),
|
||||
path('items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/mark-as-canceled/',
|
||||
views.BillModelActionCanceledView.as_view(),
|
||||
name='bill-action-mark-as-canceled'),
|
||||
path('items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/lock-ledger/',
|
||||
views.BillModelActionLockLedgerView.as_view(),
|
||||
name='bill-action-lock-ledger'),
|
||||
path('items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/unlock-ledger/',
|
||||
views.BillModelActionUnlockLedgerView.as_view(),
|
||||
name='bill-action-unlock-ledger'),
|
||||
path('items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/force-migration/',
|
||||
views.BillModelActionForceMigrateView.as_view(),
|
||||
name='bill-action-force-migrate'),
|
||||
path(
|
||||
"items/bills/<slug:entity_slug>/create/",
|
||||
views.BillModelCreateView.as_view(),
|
||||
name="bill-create",
|
||||
),
|
||||
path(
|
||||
"items/bills/<slug:entity_slug>/create/purchase-order/<uuid:po_pk>/",
|
||||
views.BillModelCreateView.as_view(for_purchase_order=True),
|
||||
name="bill-create-po",
|
||||
),
|
||||
path(
|
||||
"items/bills/<slug:entity_slug>/create/estimate/<uuid:ce_pk>/",
|
||||
views.BillModelCreateView.as_view(for_estimate=True),
|
||||
name="bill-create-estimate",
|
||||
),
|
||||
path(
|
||||
"items/bills/<slug:entity_slug>/detail/<uuid:bill_pk>/",
|
||||
views.BillModelDetailViewView.as_view(),
|
||||
name="bill-detail",
|
||||
),
|
||||
path(
|
||||
"items/bills/<slug:entity_slug>/update/<uuid:bill_pk>/",
|
||||
views.BillModelUpdateViewView.as_view(),
|
||||
name="bill-update",
|
||||
),
|
||||
path(
|
||||
"items/bills/<slug:entity_slug>/update/<uuid:bill_pk>/items/",
|
||||
views.BillModelUpdateViewView.as_view(action_update_items=True),
|
||||
name="bill-update-items",
|
||||
),
|
||||
############################################################
|
||||
path(
|
||||
"items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/mark-as-draft/",
|
||||
views.BillModelActionMarkAsDraftView.as_view(),
|
||||
name="bill-action-mark-as-draft",
|
||||
),
|
||||
path(
|
||||
"items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/mark-as-review/",
|
||||
views.BillModelActionMarkAsInReviewView.as_view(),
|
||||
name="bill-action-mark-as-review",
|
||||
),
|
||||
path(
|
||||
"items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/mark-as-approved/",
|
||||
views.BillModelActionMarkAsApprovedView.as_view(),
|
||||
name="bill-action-mark-as-approved",
|
||||
),
|
||||
path(
|
||||
"items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/mark-as-paid/",
|
||||
views.BillModelActionMarkAsPaidView.as_view(),
|
||||
name="bill-action-mark-as-paid",
|
||||
),
|
||||
path(
|
||||
"items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/mark-as-void/",
|
||||
views.BillModelActionVoidView.as_view(),
|
||||
name="bill-action-mark-as-void",
|
||||
),
|
||||
path(
|
||||
"items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/mark-as-canceled/",
|
||||
views.BillModelActionCanceledView.as_view(),
|
||||
name="bill-action-mark-as-canceled",
|
||||
),
|
||||
path(
|
||||
"items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/lock-ledger/",
|
||||
views.BillModelActionLockLedgerView.as_view(),
|
||||
name="bill-action-lock-ledger",
|
||||
),
|
||||
path(
|
||||
"items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/unlock-ledger/",
|
||||
views.BillModelActionUnlockLedgerView.as_view(),
|
||||
name="bill-action-unlock-ledger",
|
||||
),
|
||||
path(
|
||||
"items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/force-migration/",
|
||||
views.BillModelActionForceMigrateView.as_view(),
|
||||
name="bill-action-force-migrate",
|
||||
),
|
||||
# path("items/bills/create/", views.bill_create, name="bill_create"),
|
||||
path(
|
||||
"items/bills/<uuid:pk>/bill_detail/",
|
||||
@ -775,130 +885,228 @@ path(
|
||||
views.bill_mark_as_paid,
|
||||
name="bill_mark_as_paid",
|
||||
),
|
||||
|
||||
# orders
|
||||
path("orders/", views.OrderListView.as_view(), name="order_list_view"),
|
||||
|
||||
# BALANCE SHEET Reports...
|
||||
# Entities...
|
||||
path('entity/<slug:entity_slug>/balance-sheet/',
|
||||
views.BaseBalanceSheetRedirectView.as_view(),
|
||||
name='entity-bs'),
|
||||
path('entity/<slug:entity_slug>/balance-sheet/year/<int:year>/',
|
||||
views.FiscalYearBalanceSheetViewBase.as_view(),
|
||||
name='entity-bs-year'),
|
||||
path('entity/<slug:entity_slug>/balance-sheet/quarter/<int:year>/<int:quarter>/',
|
||||
views.QuarterlyBalanceSheetView.as_view(),
|
||||
name='entity-bs-quarter'),
|
||||
path('entity/<slug:entity_slug>/balance-sheet/month/<int:year>/<int:month>/',
|
||||
views.MonthlyBalanceSheetView.as_view(),
|
||||
name='entity-bs-month'),
|
||||
path('entity/<slug:entity_slug>/balance-sheet/date/<int:year>/<int:month>/<int:day>/',
|
||||
views.DateBalanceSheetView.as_view(),
|
||||
name='entity-bs-date'),
|
||||
path(
|
||||
"entity/<slug:entity_slug>/balance-sheet/",
|
||||
views.BaseBalanceSheetRedirectView.as_view(),
|
||||
name="entity-bs",
|
||||
),
|
||||
path(
|
||||
"entity/<slug:entity_slug>/balance-sheet/year/<int:year>/",
|
||||
views.FiscalYearBalanceSheetViewBase.as_view(),
|
||||
name="entity-bs-year",
|
||||
),
|
||||
path(
|
||||
"entity/<slug:entity_slug>/balance-sheet/quarter/<int:year>/<int:quarter>/",
|
||||
views.QuarterlyBalanceSheetView.as_view(),
|
||||
name="entity-bs-quarter",
|
||||
),
|
||||
path(
|
||||
"entity/<slug:entity_slug>/balance-sheet/month/<int:year>/<int:month>/",
|
||||
views.MonthlyBalanceSheetView.as_view(),
|
||||
name="entity-bs-month",
|
||||
),
|
||||
path(
|
||||
"entity/<slug:entity_slug>/balance-sheet/date/<int:year>/<int:month>/<int:day>/",
|
||||
views.DateBalanceSheetView.as_view(),
|
||||
name="entity-bs-date",
|
||||
),
|
||||
# INCOME STATEMENT Reports ----
|
||||
# Entity .....
|
||||
path('entity/<slug:entity_slug>/income-statement/',
|
||||
views.BaseIncomeStatementRedirectViewBase.as_view(),
|
||||
name='entity-ic'),
|
||||
path('entity/<slug:entity_slug>/income-statement/year/<int:year>/',
|
||||
views.FiscalYearIncomeStatementViewBase.as_view(),
|
||||
name='entity-ic-year'),
|
||||
path('entity/<slug:entity_slug>/income-statement/quarter/<int:year>/<int:quarter>/',
|
||||
views.QuarterlyIncomeStatementView.as_view(),
|
||||
name='entity-ic-quarter'),
|
||||
path('entity/<slug:entity_slug>/income-statement/month/<int:year>/<int:month>/',
|
||||
views.MonthlyIncomeStatementView.as_view(),
|
||||
name='entity-ic-month'),
|
||||
path('entity/<slug:entity_slug>/income-statement/date/<int:year>/<int:month>/<int:day>/',
|
||||
views.MonthlyIncomeStatementView.as_view(),
|
||||
name='entity-ic-date'),
|
||||
# CASH FLOW STATEMENTS...
|
||||
path(
|
||||
"entity/<slug:entity_slug>/income-statement/",
|
||||
views.BaseIncomeStatementRedirectViewBase.as_view(),
|
||||
name="entity-ic",
|
||||
),
|
||||
path(
|
||||
"entity/<slug:entity_slug>/income-statement/year/<int:year>/",
|
||||
views.FiscalYearIncomeStatementViewBase.as_view(),
|
||||
name="entity-ic-year",
|
||||
),
|
||||
path(
|
||||
"entity/<slug:entity_slug>/income-statement/quarter/<int:year>/<int:quarter>/",
|
||||
views.QuarterlyIncomeStatementView.as_view(),
|
||||
name="entity-ic-quarter",
|
||||
),
|
||||
path(
|
||||
"entity/<slug:entity_slug>/income-statement/month/<int:year>/<int:month>/",
|
||||
views.MonthlyIncomeStatementView.as_view(),
|
||||
name="entity-ic-month",
|
||||
),
|
||||
path(
|
||||
"entity/<slug:entity_slug>/income-statement/date/<int:year>/<int:month>/<int:day>/",
|
||||
views.MonthlyIncomeStatementView.as_view(),
|
||||
name="entity-ic-date",
|
||||
),
|
||||
# CASH FLOW STATEMENTS...
|
||||
# Entities...
|
||||
path('entity/<slug:entity_slug>/cash-flow-statement/',
|
||||
views.BaseCashFlowStatementRedirectViewBase.as_view(),
|
||||
name='entity-cf'),
|
||||
path('entity/<slug:entity_slug>/cash-flow-statement/year/<int:year>/',
|
||||
views.FiscalYearCashFlowStatementViewBase.as_view(),
|
||||
name='entity-cf-year'),
|
||||
path('entity/<slug:entity_slug>/cash-flow-statement/quarter/<int:year>/<int:quarter>/',
|
||||
views.QuarterlyCashFlowStatementView.as_view(),
|
||||
name='entity-cf-quarter'),
|
||||
path('entity/<slug:entity_slug>/cash-flow-statement/month/<int:year>/<int:month>/',
|
||||
views.MonthlyCashFlowStatementView.as_view(),
|
||||
name='entity-cf-month'),
|
||||
path('entity/<slug:entity_slug>/cash-flow-statement/date/<int:year>/<int:month>/<int:day>/',
|
||||
views.DateCashFlowStatementView.as_view(),
|
||||
name='entity-cf-date'),
|
||||
#Dashboard
|
||||
path(
|
||||
"entity/<slug:entity_slug>/cash-flow-statement/",
|
||||
views.BaseCashFlowStatementRedirectViewBase.as_view(),
|
||||
name="entity-cf",
|
||||
),
|
||||
path(
|
||||
"entity/<slug:entity_slug>/cash-flow-statement/year/<int:year>/",
|
||||
views.FiscalYearCashFlowStatementViewBase.as_view(),
|
||||
name="entity-cf-year",
|
||||
),
|
||||
path(
|
||||
"entity/<slug:entity_slug>/cash-flow-statement/quarter/<int:year>/<int:quarter>/",
|
||||
views.QuarterlyCashFlowStatementView.as_view(),
|
||||
name="entity-cf-quarter",
|
||||
),
|
||||
path(
|
||||
"entity/<slug:entity_slug>/cash-flow-statement/month/<int:year>/<int:month>/",
|
||||
views.MonthlyCashFlowStatementView.as_view(),
|
||||
name="entity-cf-month",
|
||||
),
|
||||
path(
|
||||
"entity/<slug:entity_slug>/cash-flow-statement/date/<int:year>/<int:month>/<int:day>/",
|
||||
views.DateCashFlowStatementView.as_view(),
|
||||
name="entity-cf-date",
|
||||
),
|
||||
# Dashboard
|
||||
# DASHBOARD Views...
|
||||
path('<slug:entity_slug>/dashboard/',
|
||||
views.EntityModelDetailHandlerViewBase.as_view(),
|
||||
name='entity-dashboard'),
|
||||
path('<slug:entity_slug>/dashboard/year/<int:year>/',
|
||||
views.FiscalYearEntityModelDashboardView.as_view(),
|
||||
name='entity-dashboard-year'),
|
||||
path('<slug:entity_slug>/dashboard/quarter/<int:year>/<int:quarter>/',
|
||||
views.QuarterlyEntityDashboardView.as_view(),
|
||||
name='entity-dashboard-quarter'),
|
||||
path('<slug:entity_slug>/dashboard/month/<int:year>/<int:month>/',
|
||||
views.MonthlyEntityDashboardView.as_view(),
|
||||
name='entity-dashboard-month'),
|
||||
path('<slug:entity_slug>/dashboard/date/<int:year>/<int:month>/<int:day>/',
|
||||
views.DateEntityDashboardView.as_view(),
|
||||
name='entity-dashboard-date'),
|
||||
#dashboard api
|
||||
path('entity/<slug:entity_slug>/data/net-payables/',
|
||||
views.PayableNetAPIView.as_view(),
|
||||
name='entity-json-net-payables'),
|
||||
path('entity/<slug:entity_slug>/data/net-receivables/',
|
||||
views.ReceivableNetAPIView.as_view(),
|
||||
name='entity-json-net-receivables'),
|
||||
path('entity/<slug:entity_slug>/data/pnl/',
|
||||
views.PnLAPIView.as_view(),
|
||||
name='entity-json-pnl'),
|
||||
# Admin Management...
|
||||
path('management/', views.management_view, name='management'),
|
||||
path('management/user_management/', views.user_management, name='user_management'),
|
||||
path('management/<str:content_type>/<slug:slug>/activate_account/', views.activate_account, name='activate_account'),
|
||||
path('management/<str:content_type>/<slug:slug>/permenant_delete_account/', views.permenant_delete_account, name='permenant_delete_account'),
|
||||
path('management/audit_log_dashboard/', views.AuditLogDashboardView, name='audit_log_dashboard'),
|
||||
|
||||
|
||||
path(
|
||||
"<slug:entity_slug>/dashboard/",
|
||||
views.EntityModelDetailHandlerViewBase.as_view(),
|
||||
name="entity-dashboard",
|
||||
),
|
||||
path(
|
||||
"<slug:entity_slug>/dashboard/year/<int:year>/",
|
||||
views.FiscalYearEntityModelDashboardView.as_view(),
|
||||
name="entity-dashboard-year",
|
||||
),
|
||||
path(
|
||||
"<slug:entity_slug>/dashboard/quarter/<int:year>/<int:quarter>/",
|
||||
views.QuarterlyEntityDashboardView.as_view(),
|
||||
name="entity-dashboard-quarter",
|
||||
),
|
||||
path(
|
||||
"<slug:entity_slug>/dashboard/month/<int:year>/<int:month>/",
|
||||
views.MonthlyEntityDashboardView.as_view(),
|
||||
name="entity-dashboard-month",
|
||||
),
|
||||
path(
|
||||
"<slug:entity_slug>/dashboard/date/<int:year>/<int:month>/<int:day>/",
|
||||
views.DateEntityDashboardView.as_view(),
|
||||
name="entity-dashboard-date",
|
||||
),
|
||||
# dashboard api
|
||||
path(
|
||||
"entity/<slug:entity_slug>/data/net-payables/",
|
||||
views.PayableNetAPIView.as_view(),
|
||||
name="entity-json-net-payables",
|
||||
),
|
||||
path(
|
||||
"entity/<slug:entity_slug>/data/net-receivables/",
|
||||
views.ReceivableNetAPIView.as_view(),
|
||||
name="entity-json-net-receivables",
|
||||
),
|
||||
path(
|
||||
"entity/<slug:entity_slug>/data/pnl/",
|
||||
views.PnLAPIView.as_view(),
|
||||
name="entity-json-pnl",
|
||||
),
|
||||
# Admin Management...
|
||||
path("management/", views.management_view, name="management"),
|
||||
path("management/user_management/", views.user_management, name="user_management"),
|
||||
path(
|
||||
"management/<str:content_type>/<slug:slug>/activate_account/",
|
||||
views.activate_account,
|
||||
name="activate_account",
|
||||
),
|
||||
path(
|
||||
"management/<str:content_type>/<slug:slug>/permenant_delete_account/",
|
||||
views.permenant_delete_account,
|
||||
name="permenant_delete_account",
|
||||
),
|
||||
path(
|
||||
"management/audit_log_dashboard/",
|
||||
views.AuditLogDashboardView,
|
||||
name="audit_log_dashboard",
|
||||
),
|
||||
#########
|
||||
# Purchase Order
|
||||
path('purchase_orders/', views.PurchaseOrderListView.as_view(), name='purchase_order_list'),
|
||||
path('purchase_orders/new/', views.PurchaseOrderCreateView, name='purchase_order_create'),
|
||||
path('purchase_orders/<uuid:pk>/detail/', views.PurchaseOrderDetailView.as_view(), name='purchase_order_detail'),
|
||||
path('purchase_orders/<slug:entity_slug>/<uuid:po_pk>/update/', views.PurchaseOrderUpdateView.as_view(), name='purchase_order_update'),
|
||||
path('purchase_orders/<slug:entity_slug>/update/<uuid:po_pk>/update-items/',
|
||||
views.PurchaseOrderUpdateView.as_view(action_update_items=True),
|
||||
name='purchase_order_update_items'),
|
||||
path('purchase_orders/inventory_item/create/', views.InventoryItemCreateView, name='inventory_item_create'),
|
||||
path('purchase_orders/inventory_items_filter/', views.inventory_items_filter, name='inventory_items_filter'),
|
||||
path('purchase_orders/<slug:entity_slug>/delete/<uuid:po_pk>/',
|
||||
views.PurchaseOrderModelDeleteView.as_view(),
|
||||
name='po-delete'),
|
||||
path('purchase_orders/<slug:entity_slug>/<uuid:po_pk>/upload/',view=views.view_items_inventory,name='view_items_inventory'),
|
||||
path(
|
||||
"purchase_orders/",
|
||||
views.PurchaseOrderListView.as_view(),
|
||||
name="purchase_order_list",
|
||||
),
|
||||
path(
|
||||
"purchase_orders/new/",
|
||||
views.PurchaseOrderCreateView,
|
||||
name="purchase_order_create",
|
||||
),
|
||||
path(
|
||||
"purchase_orders/<uuid:pk>/detail/",
|
||||
views.PurchaseOrderDetailView.as_view(),
|
||||
name="purchase_order_detail",
|
||||
),
|
||||
path(
|
||||
"purchase_orders/<slug:entity_slug>/<uuid:po_pk>/update/",
|
||||
views.PurchaseOrderUpdateView.as_view(),
|
||||
name="purchase_order_update",
|
||||
),
|
||||
path(
|
||||
"purchase_orders/<slug:entity_slug>/update/<uuid:po_pk>/update-items/",
|
||||
views.PurchaseOrderUpdateView.as_view(action_update_items=True),
|
||||
name="purchase_order_update_items",
|
||||
),
|
||||
path(
|
||||
"purchase_orders/inventory_item/create/",
|
||||
views.InventoryItemCreateView,
|
||||
name="inventory_item_create",
|
||||
),
|
||||
path(
|
||||
"purchase_orders/inventory_items_filter/",
|
||||
views.inventory_items_filter,
|
||||
name="inventory_items_filter",
|
||||
),
|
||||
path(
|
||||
"purchase_orders/<slug:entity_slug>/delete/<uuid:po_pk>/",
|
||||
views.PurchaseOrderModelDeleteView.as_view(),
|
||||
name="po-delete",
|
||||
),
|
||||
path(
|
||||
"purchase_orders/<slug:entity_slug>/<uuid:po_pk>/upload/",
|
||||
view=views.view_items_inventory,
|
||||
name="view_items_inventory",
|
||||
),
|
||||
# Actions....
|
||||
path('<slug:entity_slug>/action/<uuid:po_pk>/mark-as-draft/',
|
||||
views.PurchaseOrderMarkAsDraftView.as_view(),
|
||||
name='po-action-mark-as-draft'),
|
||||
path('<slug:entity_slug>/action/<uuid:po_pk>/mark-as-review/',
|
||||
views.PurchaseOrderMarkAsReviewView.as_view(),
|
||||
name='po-action-mark-as-review'),
|
||||
path('<slug:entity_slug>/action/<uuid:po_pk>/mark-as-approved/',
|
||||
views.PurchaseOrderMarkAsApprovedView.as_view(),
|
||||
name='po-action-mark-as-approved'),
|
||||
path('<slug:entity_slug>/action/<uuid:po_pk>/mark-as-fulfilled/',
|
||||
views.PurchaseOrderMarkAsFulfilledView.as_view(),
|
||||
name='po-action-mark-as-fulfilled'),
|
||||
path('<slug:entity_slug>/action/<uuid:po_pk>/mark-as-canceled/',
|
||||
views.PurchaseOrderMarkAsCanceledView.as_view(),
|
||||
name='po-action-mark-as-canceled'),
|
||||
path('<slug:entity_slug>/action/<uuid:po_pk>/mark-as-void/',
|
||||
views.PurchaseOrderMarkAsVoidView.as_view(),
|
||||
name='po-action-mark-as-void'),
|
||||
path(
|
||||
"<slug:entity_slug>/action/<uuid:po_pk>/mark-as-draft/",
|
||||
views.PurchaseOrderMarkAsDraftView.as_view(),
|
||||
name="po-action-mark-as-draft",
|
||||
),
|
||||
path(
|
||||
"<slug:entity_slug>/action/<uuid:po_pk>/mark-as-review/",
|
||||
views.PurchaseOrderMarkAsReviewView.as_view(),
|
||||
name="po-action-mark-as-review",
|
||||
),
|
||||
path(
|
||||
"<slug:entity_slug>/action/<uuid:po_pk>/mark-as-approved/",
|
||||
views.PurchaseOrderMarkAsApprovedView.as_view(),
|
||||
name="po-action-mark-as-approved",
|
||||
),
|
||||
path(
|
||||
"<slug:entity_slug>/action/<uuid:po_pk>/mark-as-fulfilled/",
|
||||
views.PurchaseOrderMarkAsFulfilledView.as_view(),
|
||||
name="po-action-mark-as-fulfilled",
|
||||
),
|
||||
path(
|
||||
"<slug:entity_slug>/action/<uuid:po_pk>/mark-as-canceled/",
|
||||
views.PurchaseOrderMarkAsCanceledView.as_view(),
|
||||
name="po-action-mark-as-canceled",
|
||||
),
|
||||
path(
|
||||
"<slug:entity_slug>/action/<uuid:po_pk>/mark-as-void/",
|
||||
views.PurchaseOrderMarkAsVoidView.as_view(),
|
||||
name="po-action-mark-as-void",
|
||||
),
|
||||
]
|
||||
|
||||
handler404 = "inventory.views.custom_page_not_found_view"
|
||||
|
||||
@ -1,34 +1,41 @@
|
||||
|
||||
from decimal import Decimal
|
||||
from django.conf import settings
|
||||
from django_ledger.models.items import ItemModel
|
||||
|
||||
|
||||
def calculate_vat(value):
|
||||
"""Helper to calculate VAT dynamically for a given value."""
|
||||
vat_rate = getattr(settings, 'VAT_RATE', Decimal('0.15')) # Default VAT rate
|
||||
return (value * vat_rate).quantize(Decimal('0.01'))
|
||||
"""Helper to calculate VAT dynamically for a given value."""
|
||||
vat_rate = getattr(settings, "VAT_RATE", Decimal("0.15")) # Default VAT rate
|
||||
return (value * vat_rate).quantize(Decimal("0.01"))
|
||||
|
||||
|
||||
# def get_financial_value(instance,attribute,vat=False):
|
||||
# if vat:
|
||||
# return calculate_vat(getattr(instance, attribute, Decimal('0.00')) if instance else Decimal('0.00'))
|
||||
# return getattr(instance, attribute, Decimal('0.00')) if instance else Decimal('0.00')
|
||||
|
||||
def get_financial_value(name,vat=False):
|
||||
|
||||
def get_financial_value(name, vat=False):
|
||||
val = ItemModel.objects.filter(name=name).first()
|
||||
if not val:
|
||||
return 0
|
||||
if vat:
|
||||
return (val.price * settings.VAT_RATE).quantize(Decimal('0.01'))
|
||||
return (val.price * settings.VAT_RATE).quantize(Decimal("0.01"))
|
||||
return val.price
|
||||
|
||||
|
||||
def get_total_financials(instance,vat=False):
|
||||
|
||||
|
||||
def get_total_financials(instance, vat=False):
|
||||
total = 0
|
||||
if instance.additional_services.count() != 0:
|
||||
total_additional_services = sum(x.price for x in instance.additional_services.all())
|
||||
total_additional_services = sum(
|
||||
x.price for x in instance.additional_services.all()
|
||||
)
|
||||
total = instance.selling_price + total_additional_services
|
||||
if vat and total:
|
||||
total = (total * settings.VAT_RATE).quantize(Decimal('0.01')) + total
|
||||
total = (total * settings.VAT_RATE).quantize(Decimal("0.01")) + total
|
||||
return total
|
||||
|
||||
|
||||
|
||||
def get_total(instance):
|
||||
total = get_total_financials(instance,vat=True)
|
||||
return total
|
||||
total = get_total_financials(instance, vat=True)
|
||||
return total
|
||||
|
||||
@ -4,7 +4,6 @@ from django.conf import settings
|
||||
from plans.taxation import TaxationPolicy
|
||||
|
||||
|
||||
|
||||
class SaudiTaxationPolicy(TaxationPolicy):
|
||||
"""
|
||||
Represents the taxation policy specific to Saudi Arabia.
|
||||
@ -17,12 +16,13 @@ class SaudiTaxationPolicy(TaxationPolicy):
|
||||
:ivar settings: Configuration settings of the application.
|
||||
:type settings: module
|
||||
"""
|
||||
|
||||
def get_default_tax(self):
|
||||
return getattr(settings, 'PLANS_TAX', None)
|
||||
return getattr(settings, "PLANS_TAX", None)
|
||||
|
||||
def get_issuer_country_code(self):
|
||||
return getattr(settings, 'PLANS_TAX_COUNTRY', None)
|
||||
return getattr(settings, "PLANS_TAX_COUNTRY", None)
|
||||
|
||||
def get_tax_rate(self, tax_id, country_code, request=None):
|
||||
rate = Decimal("15")
|
||||
return rate
|
||||
return rate
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
from django.core.validators import RegexValidator
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class SaudiPhoneNumberValidator(RegexValidator):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
regex=r'^(\+9665|05)[0-9]{8}$',
|
||||
message=_("Enter a valid Saudi phone number (05XXXXXXXX or +9665XXXXXXXX)")
|
||||
)
|
||||
regex=r"^(\+9665|05)[0-9]{8}$",
|
||||
message=_("Enter a valid Saudi phone number (05XXXXXXXX or +9665XXXXXXXX)"),
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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"],
|
||||
|
||||
@ -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()
|
||||
|
||||
33
merge_db.py
33
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)
|
||||
print("Error:", e)
|
||||
|
||||
@ -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"))
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -3,10 +3,7 @@ import json
|
||||
|
||||
# Connect to MySQL
|
||||
mysql_conn = mysql.connector.connect(
|
||||
host='localhost',
|
||||
user='root',
|
||||
password='Kfsh&rc9788',
|
||||
database='trucks2db'
|
||||
host="localhost", user="root", password="Kfsh&rc9788", database="trucks2db"
|
||||
)
|
||||
cursor = mysql_conn.cursor()
|
||||
|
||||
@ -25,5 +22,5 @@ for table in tables:
|
||||
data = [dict(zip(columns, row)) for row in rows]
|
||||
|
||||
# Write to JSON file
|
||||
with open(f'{table_name}.json', 'w') as f:
|
||||
json.dump(data, f)
|
||||
with open(f"{table_name}.json", "w") as f:
|
||||
json.dump(data, f)
|
||||
|
||||
@ -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"])
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
from vininfo.utils import merge_wmi
|
||||
|
||||
|
||||
@ -1332,14 +1331,13 @@ wmi_manufacturer_mapping = {
|
||||
"MM0": "Mazda",
|
||||
"MM6": "Mazda",
|
||||
"MM7": "Mazda",
|
||||
"MM8": "Mazda"
|
||||
"MM8": "Mazda",
|
||||
}
|
||||
|
||||
|
||||
|
||||
# merge_wmi(wmi)
|
||||
|
||||
new_keys, wmi_source_code = merge_wmi(wmi_manufacturer_mapping)
|
||||
|
||||
print("New keys added:", new_keys)
|
||||
print("\nUpdated WMI dictionary source code:\n", wmi_source_code)
|
||||
print("\nUpdated WMI dictionary source code:\n", wmi_source_code)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import os
|
||||
import pymysql
|
||||
import django
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "car_inventory.settings")
|
||||
django.setup()
|
||||
|
||||
@ -8,10 +9,18 @@ import json
|
||||
from datetime import datetime
|
||||
from tqdm import tqdm
|
||||
from inventory.models import (
|
||||
CarMake, CarModel, CarSerie, CarTrim, CarEquipment,
|
||||
CarSpecification, CarSpecificationValue, CarOption, CarOptionValue
|
||||
CarMake,
|
||||
CarModel,
|
||||
CarSerie,
|
||||
CarTrim,
|
||||
CarEquipment,
|
||||
CarSpecification,
|
||||
CarSpecificationValue,
|
||||
CarOption,
|
||||
CarOptionValue,
|
||||
)
|
||||
|
||||
|
||||
# Step 1: Perform MySQL Dump
|
||||
def dump_mysql_database():
|
||||
print("Starting MySQL dump...")
|
||||
@ -22,14 +31,15 @@ def dump_mysql_database():
|
||||
os.system(f"mysqldump -u {db_user} -p{db_password} {db_name} > {dump_file}")
|
||||
print(f"MySQL dump completed: {dump_file}")
|
||||
|
||||
|
||||
# Step 2: Connect to MySQL and export data to JSON
|
||||
def export_database_to_json():
|
||||
print("Starting export to JSON...")
|
||||
db_config = {
|
||||
'host': 'localhost',
|
||||
'user': 'root',
|
||||
'password': 'Kfsh&rc9788',
|
||||
'database': 'trucks2db',
|
||||
"host": "localhost",
|
||||
"user": "root",
|
||||
"password": "Kfsh&rc9788",
|
||||
"database": "trucks2db",
|
||||
}
|
||||
|
||||
connection = pymysql.connect(**db_config)
|
||||
@ -53,6 +63,7 @@ def export_database_to_json():
|
||||
print(f"Database exported to JSON successfully: {json_file_name}")
|
||||
return json_file_name
|
||||
|
||||
|
||||
# Step 3: Run the JSON processing script (previously created)
|
||||
def process_json_data(json_file_name):
|
||||
print(f"Starting JSON processing for file: {json_file_name}")
|
||||
@ -69,24 +80,28 @@ def process_json_data(json_file_name):
|
||||
"arabic_name": item.get("arabic_name", ""),
|
||||
"logo": item.get("Logo", ""),
|
||||
"is_sa_import": item.get("is_sa_import", False),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Step 2: Populate CarModel
|
||||
for item in tqdm(data["car_model"], desc="Populating CarModel"):
|
||||
CarMake.objects.get(id_car_make=item["id_car_make"]) # Ensures foreign key exists
|
||||
CarMake.objects.get(
|
||||
id_car_make=item["id_car_make"]
|
||||
) # Ensures foreign key exists
|
||||
CarModel.objects.update_or_create(
|
||||
id_car_model=item["id_car_model"],
|
||||
defaults={
|
||||
"id_car_make_id": item["id_car_make"],
|
||||
"name": item["name"],
|
||||
"arabic_name": item.get("arabic_name", ""),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Step 3: Populate CarSerie
|
||||
for item in tqdm(data["car_serie"], desc="Populating CarSerie"):
|
||||
CarModel.objects.get(id_car_model=item["id_car_model"]) # Ensures foreign key exists
|
||||
CarModel.objects.get(
|
||||
id_car_model=item["id_car_model"]
|
||||
) # Ensures foreign key exists
|
||||
CarSerie.objects.update_or_create(
|
||||
id_car_serie=item["id_car_serie"],
|
||||
defaults={
|
||||
@ -96,12 +111,14 @@ def process_json_data(json_file_name):
|
||||
"year_begin": item.get("year_begin"),
|
||||
"year_end": item.get("year_end"),
|
||||
"generation_name": item.get("generation_name", ""),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Step 4: Populate CarTrim
|
||||
for item in tqdm(data["car_trim"], desc="Populating CarTrim"):
|
||||
CarSerie.objects.get(id_car_serie=item["id_car_serie"]) # Ensures foreign key exists
|
||||
CarSerie.objects.get(
|
||||
id_car_serie=item["id_car_serie"]
|
||||
) # Ensures foreign key exists
|
||||
CarTrim.objects.update_or_create(
|
||||
id_car_trim=item["id_car_trim"],
|
||||
defaults={
|
||||
@ -113,19 +130,21 @@ def process_json_data(json_file_name):
|
||||
"date_create": item["date_create"],
|
||||
"date_update": item["date_update"],
|
||||
"id_car_type": item.get("id_car_type", 1),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Step 5: Populate CarEquipment
|
||||
for item in tqdm(data["car_equipment"], desc="Populating CarEquipment"):
|
||||
CarTrim.objects.get(id_car_trim=item["id_car_trim"]) # Ensures foreign key exists
|
||||
CarTrim.objects.get(
|
||||
id_car_trim=item["id_car_trim"]
|
||||
) # Ensures foreign key exists
|
||||
CarEquipment.objects.update_or_create(
|
||||
id_car_equipment=item["id_car_equipment"],
|
||||
defaults={
|
||||
"id_car_trim_id": item["id_car_trim"],
|
||||
"name": item["name"],
|
||||
"year_begin": item.get("year"),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Step 6: Populate CarSpecification
|
||||
@ -136,13 +155,19 @@ def process_json_data(json_file_name):
|
||||
"name": item["name"],
|
||||
"arabic_name": item.get("arabic_name", ""),
|
||||
"id_parent_id": item.get("id_parent"),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Step 7: Populate CarSpecificationValue
|
||||
for item in tqdm(data["car_specification_value"], desc="Populating CarSpecificationValue"):
|
||||
CarTrim.objects.get(id_car_trim=item["id_car_trim"]) # Ensures foreign key exists
|
||||
CarSpecification.objects.get(id_car_specification=item["id_car_specification"]) # Ensures foreign key exists
|
||||
for item in tqdm(
|
||||
data["car_specification_value"], desc="Populating CarSpecificationValue"
|
||||
):
|
||||
CarTrim.objects.get(
|
||||
id_car_trim=item["id_car_trim"]
|
||||
) # Ensures foreign key exists
|
||||
CarSpecification.objects.get(
|
||||
id_car_specification=item["id_car_specification"]
|
||||
) # Ensures foreign key exists
|
||||
CarSpecificationValue.objects.update_or_create(
|
||||
id_car_specification_value=item["id_car_specification_value"],
|
||||
defaults={
|
||||
@ -150,7 +175,7 @@ def process_json_data(json_file_name):
|
||||
"id_car_specification_id": item["id_car_specification"],
|
||||
"value": item["value"],
|
||||
"unit": item.get("unit", ""),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Step 8: Populate CarOption
|
||||
@ -161,20 +186,24 @@ def process_json_data(json_file_name):
|
||||
"name": item["name"],
|
||||
"arabic_name": item.get("arabic_name", ""),
|
||||
"id_parent_id": item.get("id_parent"),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Step 9: Populate CarOptionValue
|
||||
for item in tqdm(data["car_option_value"], desc="Populating CarOptionValue"):
|
||||
CarEquipment.objects.get(id_car_equipment=item["id_car_equipment"]) # Ensures foreign key exists
|
||||
CarOption.objects.get(id_car_option=item["id_car_option"]) # Ensures foreign key exists
|
||||
CarEquipment.objects.get(
|
||||
id_car_equipment=item["id_car_equipment"]
|
||||
) # Ensures foreign key exists
|
||||
CarOption.objects.get(
|
||||
id_car_option=item["id_car_option"]
|
||||
) # Ensures foreign key exists
|
||||
CarOptionValue.objects.update_or_create(
|
||||
id_car_option_value=item["id_car_option_value"],
|
||||
defaults={
|
||||
"id_car_option_id": item["id_car_option"],
|
||||
"id_car_equipment_id": item["id_car_equipment"],
|
||||
"is_base": item["is_base"],
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
print("Data population completed.")
|
||||
@ -182,11 +211,13 @@ def process_json_data(json_file_name):
|
||||
os.system(f"python process_car_data.py {json_file_name}")
|
||||
print("JSON processing completed.")
|
||||
|
||||
|
||||
# Main function to run all steps
|
||||
def main():
|
||||
dump_mysql_database() # Step 1: Dump the database
|
||||
dump_mysql_database() # Step 1: Dump the database
|
||||
json_file_name = export_database_to_json() # Step 2: Export to JSON
|
||||
# process_json_data(json_file_name) # Step 3: Process the JSON
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user