diff --git a/.DS_Store b/.DS_Store index 43cb9feb..06fa0e68 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/car_inventory/__pycache__/settings.cpython-311.pyc b/car_inventory/__pycache__/settings.cpython-311.pyc index 757f9163..51bac618 100644 Binary files a/car_inventory/__pycache__/settings.cpython-311.pyc and b/car_inventory/__pycache__/settings.cpython-311.pyc differ diff --git a/car_inventory/settings.py b/car_inventory/settings.py index 87d03ebd..b12b3c77 100644 --- a/car_inventory/settings.py +++ b/car_inventory/settings.py @@ -26,7 +26,7 @@ SECRET_KEY = 'django-insecure-gc9bh4*3=b6hihdnaom0edjsbxh$5t)aap@e8p&340r7)*)qb8 # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = ['10.10.1.109','10.10.1.120', 'localhost', '127.0.0.1', '192.168.1.135', '172.20.10.4'] +ALLOWED_HOSTS = ['10.10.1.109', 'localhost', '127.0.0.1', '192.168.1.135', '172.20.10.4'] # Application definition @@ -110,9 +110,9 @@ WSGI_APPLICATION = 'car_inventory.wsgi.application' DATABASES = { "default": { "ENGINE": "django_prometheus.db.backends.postgresql", - "NAME": "haikal", - "USER": "haikal", - "PASSWORD": "haikal", + "NAME": "haikal_app", + "USER": "f95166", + "PASSWORD": "Kfsh&rc9788", "HOST": "localhost", "PORT": 5432, } diff --git a/carapi.py b/carapi.py new file mode 100644 index 00000000..1467ff1c --- /dev/null +++ b/carapi.py @@ -0,0 +1,69 @@ +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' + + +def fetch_and_save_car_makes_models(api_url, headers, output_csv): + TRANSLATION = [] + page = 1 + pages_per_batch = 1 + + while True: + responses = [] + # Fetch 100 pages in one batch + for i in range(pages_per_batch): + current_page = page + i + response = requests.get(f"{api_url}&page={current_page}", headers=headers) + if response.status_code == 200 and page < 100: + responses.append(response.json()) + else: + break + + # Process the batch of responses + for data in responses: + if 'data' not in data: + continue + + 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 + } + TRANSLATION.append(translation_entry) + + # Increment the page number for the next batch + 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']): + break + + # Save the TRANSLATION data to a CSV file + with open(output_csv, 'w', newline='', encoding='utf-8') as csvfile: + fieldnames = ['make', 'model'] + writer = csv.DictWriter(csvfile, fieldnames=fieldnames) + + writer.writeheader() + for entry in TRANSLATION: + writer.writerow(entry) + + print(f"Data saved to {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' +} +output_csv = 'car_makes_models.csv' + +fetch_and_save_car_makes_models(api_url, headers, output_csv) diff --git a/haikalna.py b/haikalna.py index 25e6e738..5c4c7de7 100644 --- a/haikalna.py +++ b/haikalna.py @@ -6,23 +6,24 @@ from typing import List def vin_years(vin_char: str) -> List[int]: letters = 'ABCDEFGHJKLMNPRSTVWXY123456789' - year_char = vin_char.upper() - - if year_char not in letters: - raise ValueError("Invalid VIN character. Must be A-Z (excluding I, O, Q) or 1-9.") + year_letter = vin_char year = 1979 - current_year = datetime.now().year + year_current = datetime.now().year + result = [] for letter in cycle(letters): year += 1 - if letter == year_char: + + if letter == year_letter: result.append(year) - if year >= current_year: + + if year == year_current: break result.sort(reverse=True) + return result @@ -150,7 +151,6 @@ wmi_manufacturer_mapping = { "J8T": "GMC", "J8Z": "Chevrolet", "KF3": "Merkavim", - "KF6": "Automotive Industries,", "KL": "Daewoo", "KLA": "Daewoo", "KLP": "CT&T United", @@ -190,9 +190,9 @@ wmi_manufacturer_mapping = { "KPT": "SsangYong", "LA6": "King Long", "LC0": "BYD", - "LBE": "Beijing-Hyundai Shouwang", + "LBE": "Hyundai Shouwang", "LBM": "Zongshen Piaggio", - "LBV": "BMW Brilliance", + "LBV": "BMW", "LB1": "Fujian Benz", "LB3": "Geely", "LCR": "Gonow", @@ -222,17 +222,15 @@ wmi_manufacturer_mapping = { "LGL": "Guilin Daewoo", "LGW": "Great Wall", "LGX": "BYD", - "LGZ": "Guangzhou Denway Bus", "LHA": "Shuanghuan", "LHB": "BAIC", "LHG": "Guangzhou Honda", "LH1": "FAW", "LJC": "Jincheng", - "LJD": "Human Horizons - HiPhi", + "LJD": "Yueda Kia", "LJN": "Zhengzhou Nissan", "LJS": "Yaxing Coach", "LJU": "Lotus Geely", - "LJV": "Chengdu Wangpai", "LJX": "JMC Ford", "LJ1": "Nio", "LJ8": "Zotye", @@ -260,7 +258,7 @@ wmi_manufacturer_mapping = { "LPE": "BYD", "LPS": "Polestar", "LRB": "SAIC Buick", - "LRD": "Foton Daimler", + "LRD": "Foton", "LRE": "SAIC Cadillac", "LRW": "Tesla", "LSC": "Changan", @@ -270,15 +268,14 @@ wmi_manufacturer_mapping = { "LSJ": "SAIC MG", "LSK": "SAIC Maxus", "LSV": "SAIC Volkswagen", - "LSY": "Brilliance Jinbei GM", + "LSY": "Brilliance", "LS4": "Changan", "LS5": "Changan", "LS6": "Changan", "LS7": "JMC", "LTA": "ZX Auto", "LTN": "Soueast", - "LTP": "National Electric Vehicle Sweden AB", - "LTV": "FAW Toyota (Tianjin)", + "LTV": "FAW Toyota", "LUC": "Honda", "LUD": "Dongfeng Nissan", "LUX": "Dongfeng Yulon", @@ -322,18 +319,15 @@ wmi_manufacturer_mapping = { "L4F": "Suzhou Eagle", "L5C": "KangDi", "L5K": "Yongkang", - "L6T": "Geely, Lynk & Co, Zeekr", + "L6T": "Lynk & Co", "L82": "Baotian", "L85": "Yongkang Huabao", "L9N": "Taotao", - "MAB": "Mahindra & Mahindra", - "MAC": "Mahindra & Mahindra", "MAH": "Fiat", "MAJ": "Ford", "MAK": "Honda", "MAL": "Hyundai", "MAT": "Tatar", - "MA1": "Mahindra & Mahindra", "MA3": "Suzuki", "MA6": "GM", "MA7": "Hindustan", @@ -348,10 +342,7 @@ wmi_manufacturer_mapping = { "MBX": "Piaggio", "MBY": "Asia Motors", "MB1": "Ashok Leyland", - "MB7": "Reva Electric Company", - "MCA": "FCA Pvt. Ltd", "MCB": "GM", - "MCD": "Mahindra Two Wheelers", "MCG": "Atul", "MC1": "Force", "MC2": "Eicher", @@ -361,7 +352,7 @@ wmi_manufacturer_mapping = { "MD6": "TVS", "MD9": "Shuttles", "MEC": "Daimler", - "MEE": "Renault Private Limited", + "MEE": "Renault", "MEG": "Harley-Davidson", "MER": "Benelli", "MET": "Piaggio", @@ -502,7 +493,7 @@ wmi_manufacturer_mapping = { "SAJ": "Jaguar", "SAL": "Land Rover", "SAM": "Morris", - "SAR": "Rover MG Rover", + "SAR": "Rover MG", "SAT": "Triumph", "SAX": "Austin-Rover", "SAZ": "Freight Rover", @@ -513,7 +504,6 @@ wmi_manufacturer_mapping = { "SBL": "Leyland", "SBM": "McLaren", "SBS": "Scammell", - "SBV": "Kenworth Peterbilt", "SB1": "Toyota", "SCA": "Rolls Royce", "SCB": "Bentley", @@ -568,13 +558,13 @@ wmi_manufacturer_mapping = { "TN9": "Karosa", "TRA": "Ikarus Bus", "TRC": "Csepel", - "TRU": "Audi Hungary", + "TRU": "Audi", "TSB": "Ikarus Bus", "TSE": "Ikarus,", "TSF": "Alfabusz", "TSM": "Suzuki Fiat", "TWG": "Ceatano Bus", - "TW1": "Toyota Caetano", + "TW1": "Toyota", "TW2": "Ford", "TW6": "Citroën", "TW7": "Mini", @@ -670,7 +660,6 @@ wmi_manufacturer_mapping = { "WBU": "Bürstner", "WBX": "BMW", "WBY": "BMW", - "WB0": "Böckmann Fahrzeugwerke GmbH", "WB1": "BMW", "WB5": "BMW", "WCD": "Freightliner", @@ -691,14 +680,11 @@ wmi_manufacturer_mapping = { "WD6": "Freightliner", "WD8": "Dodge", "WEB": "Evobus GmbH", - "WEL": "e.GO Mobile AG", "WFC": "Fendt", "WFD": "Fliegl Trailer", "WF0": "Ford", "WF1": "Merkur", "WHB": "Hobby", - "WHD": "Humbaur GmbH", - "WHW": "Hako GmbH", "WHY": "Hymer", "WJM": "Iveco", "WJR": "Irmscher", @@ -760,12 +746,10 @@ wmi_manufacturer_mapping = { "XMC": "Mitsubishi", "XMD": "Mitsubishi", "XMG": "VDL Bus", - "XMR": "Nooteboom Trailers", + "XMR": "Nooteboom", "XM4": "RAVO Holding", "XNB": "Mitsubishi", "XNC": "Mitsubishi", - "XNL": "VDL Bus & Coach", - "XPN": "Knapen Trailers", "XP7": "Tesla", "XTA": "Lada", "XTB": "Moskvitch", @@ -803,7 +787,6 @@ wmi_manufacturer_mapping = { "X9X": "Great Wall", "YAF": "Faymonville", "YAR": "Toyota", - "YA9": "Lambrecht Constructie NV", "YBW": "Volkswagen", "YB1": "Volvo", "YB2": "Volvo", @@ -812,7 +795,7 @@ wmi_manufacturer_mapping = { "YH1": "Solifer", "YH2": "BRP", "YH4": "Fisker", - "YK1": "Saab-Valmet", + "YK1": "Saab", "YSC": "Cadillac", "YSM": "Polestars", "YSP": "Volta AB", @@ -827,7 +810,6 @@ wmi_manufacturer_mapping = { "YV3": "Volvos", "YV4": "Volvo", "YYC": "Think Nordic", - "Y3J": "Belkommunmash", "Y3M": "MAZ", "Y4F": "Ford", "Y4K": "Geely", @@ -841,19 +823,19 @@ wmi_manufacturer_mapping = { "Y8A": "LAZ", "Y9H": "LAZ", "ZAA": "Autobianchi", - "ZAC": "Jeep Dodge Hornet", + "ZAC": "Jeep Dodge", "ZAM": "Maserati", "ZAP": "Piaggio", "ZAR": "Alfa Romeo", "ZAS": "Alfa Romeo", "ZBB": "Bertone", "ZBN": "Benelli", - "ZBW": "Rayton-Fissore Magnum", + "ZBW": "Rayton-Fissore", "ZCB": "Bartoletti", "ZCF": "Iveco", "ZCG": "Cagiva", "ZCM": "Menarinibus", - "ZC2": "Chrysler Maserati", + "ZC2": "Chrysler", "ZDC": "Honda", "ZDF": "Ferrari", "ZDM": "Ducati", @@ -895,8 +877,6 @@ wmi_manufacturer_mapping = { "Z8T": "PCMA", "Z9M": "Mercedes-Benz", "Z94": "Hyundai", - "1AC": "American Corporation", - "1AF": "American LaFrance", "1B3": "Dodge", "1B4": "Dodge", "1B7": "Dodge", @@ -946,7 +926,7 @@ wmi_manufacturer_mapping = { "1GY": "Cadillac", "1HD": "Harley-Davidson", "1HG": "Honda", - "1HS": "International & Caterpillar", + "1HS": "Caterpillar", "1JC": "Jeep", "1JT": "Jeep", "1JU": "Marmon", @@ -969,10 +949,7 @@ wmi_manufacturer_mapping = { "1P3": "Plymouth", "1P4": "Plymouth", "1PY": "John Deere", - "1T7": "Thomas Built Buses", - "1T8": "Thomas Built Buses", "1TC": "Coachmen", - "1TU": "Transportation Manufacturing Corporation", "1UJ": "Jayco", "1UT": "Jeep", "1VH": "Orion Bus", @@ -993,8 +970,6 @@ wmi_manufacturer_mapping = { "10T": "Oshkosh", "12A": "Avanti", "137": "Hummer", - "15G": "Gillig bus", - "16C": "Clenet Coachworks", "16X": "Vixen", "19U": "Acura", "19V": "Acura", @@ -1549,7 +1524,7 @@ def decode_vin(vin): return { 'VIN': vin, 'Manufacturer': manufacturer, - 'Year': year[0], + 'Year': year, 'Model': model } @@ -1558,7 +1533,7 @@ def decode_vin(vin): # VR3USHNLWRJ521303 # KNARH81E8P5194005 # Example usage -vin_number = 'LJD5AA1DXR0104257' +vin_number = 'LGWCBE196SB652802' decoded_vin = decode_vin(vin_number) print(decoded_vin) diff --git a/inventory/__pycache__/admin.cpython-311.pyc b/inventory/__pycache__/admin.cpython-311.pyc index dda6f1fc..39c54534 100644 Binary files a/inventory/__pycache__/admin.cpython-311.pyc and b/inventory/__pycache__/admin.cpython-311.pyc differ diff --git a/inventory/__pycache__/forms.cpython-311.pyc b/inventory/__pycache__/forms.cpython-311.pyc index 9f8cab61..dc803557 100644 Binary files a/inventory/__pycache__/forms.cpython-311.pyc and b/inventory/__pycache__/forms.cpython-311.pyc differ diff --git a/inventory/__pycache__/models.cpython-311.pyc b/inventory/__pycache__/models.cpython-311.pyc index 6d58fdc5..ae1bee03 100644 Binary files a/inventory/__pycache__/models.cpython-311.pyc and b/inventory/__pycache__/models.cpython-311.pyc differ diff --git a/inventory/__pycache__/services.cpython-311.pyc b/inventory/__pycache__/services.cpython-311.pyc index 30347aaa..0268e2cd 100644 Binary files a/inventory/__pycache__/services.cpython-311.pyc and b/inventory/__pycache__/services.cpython-311.pyc differ diff --git a/inventory/__pycache__/urls.cpython-311.pyc b/inventory/__pycache__/urls.cpython-311.pyc index 4a49f5cb..a98ee4c5 100644 Binary files a/inventory/__pycache__/urls.cpython-311.pyc and b/inventory/__pycache__/urls.cpython-311.pyc differ diff --git a/inventory/__pycache__/views.cpython-311.pyc b/inventory/__pycache__/views.cpython-311.pyc index 46468501..50f2aed0 100644 Binary files a/inventory/__pycache__/views.cpython-311.pyc and b/inventory/__pycache__/views.cpython-311.pyc differ diff --git a/inventory/admin.py b/inventory/admin.py index a6f01e1e..82d72cee 100644 --- a/inventory/admin.py +++ b/inventory/admin.py @@ -5,17 +5,22 @@ from . import models admin.site.register(models.Dealer) admin.site.register(models.Vendor) admin.site.register(models.Customer) +admin.site.register(models.SaleQuotation) +admin.site.register(models.SaleQuotationCar) admin.site.register(models.Car) admin.site.register(models.CarFinance) admin.site.register(models.CarColors) admin.site.register(models.CarRegistration) admin.site.register(models.CustomCard) admin.site.register(models.CarSpecificationValue) +admin.site.register(models.ExteriorColors) +admin.site.register(models.InteriorColors) @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',) class Meta: verbose_name = "Car Make" diff --git a/inventory/forms.py b/inventory/forms.py index c3455d4f..0c36251e 100644 --- a/inventory/forms.py +++ b/inventory/forms.py @@ -1,5 +1,6 @@ from django import forms from .mixins import AddClassMixin +from django.forms.models import inlineformset_factory from .models import ( Dealer, # Branch, @@ -9,9 +10,12 @@ from .models import ( CarFinance, CustomCard, CarRegistration, - CarColors + CarColors, + ExteriorColors, + InteriorColors, + SaleQuotation ) -from django.contrib.contenttypes.forms import generic_inlineformset_factory +from django.forms import ModelMultipleChoiceField from django.utils.translation import gettext_lazy as _ @@ -19,7 +23,7 @@ from django.utils.translation import gettext_lazy as _ class DealerForm(forms.ModelForm): class Meta: model = Dealer - fields = ['crn', 'vrn', 'arabic_name', 'name', 'phone_number', 'address', 'logo'] + fields = ['name', 'arabic_name', 'crn', 'vrn', 'phone_number', 'address', 'logo'] # Customer Form @@ -87,57 +91,10 @@ class CarUpdateForm(forms.ModelForm, AddClassMixin): class CarFinanceForm(AddClassMixin, forms.ModelForm): - profit_margin_percentage = forms.DecimalField( - max_digits=10, - decimal_places=2, - min_value=0, - max_value=100, - label="Profit Margin", - required=True, - widget=forms.NumberInput(attrs={'min': '0', 'max': '100', 'step': '0.01'}) - ) - vat_rate_percentage = forms.DecimalField( - max_digits=10, - decimal_places=2, - min_value=0, - max_value=100, - label="Vat Rate", - required=True, - widget=forms.NumberInput(attrs={'min': '0', 'max': '100', 'step': '0.01'}) - ) class Meta: model = CarFinance - fields = ['cost_price'] - - def __init__(self, *args, **kwargs): - super(CarFinanceForm, self).__init__(*args, **kwargs) - if self.instance and self.instance.pk: - # Convert profit_margin from decimal to percentage for initial display - self.fields['profit_margin_percentage'].initial = self.instance.profit_margin * 100 - self.fields['vat_rate_percentage'].initial = self.instance.vat_rate * 100 - - def clean_profit_margin_percentage(self): - profit_margin_percentage = self.cleaned_data['profit_margin_percentage'] - if not (0 <= profit_margin_percentage <= 100): - raise forms.ValidationError('Profit margin must be between 0 and 100.') - return profit_margin_percentage - - def clean_vat_rate_percentage(self): - vat_rate_percentage = self.cleaned_data['vat_rate_percentage'] - if not (0 <= vat_rate_percentage <= 100): - raise forms.ValidationError('vat rate must be between 0 and 100.') - return vat_rate_percentage - - def save(self, commit=True): - instance = super(CarFinanceForm, self).save(commit=False) - profit_margin_percentage = self.cleaned_data['profit_margin_percentage'] - vat_rate_percentage = self.cleaned_data['vat_rate_percentage'] - instance.profit_margin = profit_margin_percentage / 100 - instance.vat_rate = vat_rate_percentage / 100 - if commit: - instance.save() - return instance + exclude = ['car', 'profit_margin', 'vat_amount', 'total', 'vat_rate'] # Custom Card Form @@ -165,3 +122,52 @@ class VendorForm(forms.ModelForm): class Meta: model = Vendor exclude = ['dealer'] + + +class CarColorsForm(forms.ModelForm): + class Meta: + model = CarColors + fields = ['exterior', 'interior'] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.fields['exterior'].queryset = ExteriorColors.objects.all() + self.fields['exterior'].widget = forms.RadioSelect(attrs={'class': 'form-check-input'}) + self.fields['exterior'].choices = [ + (color.id, f"{color.get_local_name}") for color in ExteriorColors.objects.all().order_by('-name') + ] + + self.fields['interior'].queryset = InteriorColors.objects.all() + self.fields['interior'].widget = forms.RadioSelect(attrs={'class': 'form-check-input'}) + self.fields['interior'].choices = [ + (color.id, f"{color.get_local_name}") for color in InteriorColors.objects.all().order_by('-name') + ] + + def clean(self): + cleaned_data = super().clean() + exterior = cleaned_data.get("exterior") + interior = cleaned_data.get("interior") + + if not exterior or not interior: + raise forms.ValidationError(_("Both exterior and interior colors must be selected.")) + + return cleaned_data + + +class QuotationForm(forms.ModelForm): + cars = ModelMultipleChoiceField( + queryset=Car.objects.none(), # Default empty queryset + widget=forms.CheckboxSelectMultiple, + label="Select Cars" + ) + + class Meta: + model = SaleQuotation + fields = ['customer', 'cars', 'remarks'] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['cars'].queryset = Car.objects.filter( + finances__isnull=False + ).distinct() diff --git a/inventory/management/commands/populate_colors.py b/inventory/management/commands/populate_colors.py new file mode 100644 index 00000000..56228dd7 --- /dev/null +++ b/inventory/management/commands/populate_colors.py @@ -0,0 +1,100 @@ +from django.core.management.base import BaseCommand +from inventory.models import ExteriorColors, InteriorColors + + +class Command(BaseCommand): + help = "Populates the ExteriorColors and InteriorColors models with predefined data" + + exterior_colors = [ + {"rgb": "192, 192, 192", "name": "Silver Metallic", "arabic_name": "فضي معدني"}, + {"rgb": "0, 0, 0", "name": "Jet Black", "arabic_name": "أسود نفاث"}, + {"rgb": "255, 255, 255", "name": "Bright White", "arabic_name": "أبيض ناصع"}, + {"rgb": "128, 128, 128", "name": "Graphite Gray", "arabic_name": "رمادي غرافيتي"}, + {"rgb": "80, 80, 80", "name": "Gunmetal Gray", "arabic_name": "رمادي معدني"}, + {"rgb": "255, 0, 0", "name": "Racing Red", "arabic_name": "أحمر سباق"}, + {"rgb": "255, 69, 0", "name": "Inferno Orange", "arabic_name": "برتقالي جهنمي"}, + {"rgb": "0, 0, 255", "name": "Deep Blue Pearl", "arabic_name": "أزرق لؤلؤي عميق"}, + {"rgb": "75, 0, 130", "name": "Indigo Night", "arabic_name": "ليلة نيلي"}, + {"rgb": "255, 215, 0", "name": "Solar Gold", "arabic_name": "ذهبي شمسي"}, + {"rgb": "34, 139, 34", "name": "Emerald Green", "arabic_name": "أخضر زمردي"}, + {"rgb": "60, 179, 113", "name": "Forest Mist Green", "arabic_name": "أخضر ضباب الغابة"}, + {"rgb": "255, 140, 0", "name": "Burnt Amber", "arabic_name": "كهرماني محروق"}, + {"rgb": "160, 82, 45", "name": "Copper Brown", "arabic_name": "بني نحاسي"}, + {"rgb": "128, 0, 0", "name": "Crimson Maroon", "arabic_name": "ماروني قرمزي"}, + {"rgb": "245, 245, 220", "name": "Beige Champagne", "arabic_name": "بيج شامبين"}, + {"rgb": "169, 169, 169", "name": "Shadow Gray", "arabic_name": "رمادي ظل"}, + {"rgb": "255, 250, 205", "name": "Lemon Pearl", "arabic_name": "لؤلؤي ليموني"}, + {"rgb": "220, 220, 220", "name": "Platinum Silver", "arabic_name": "فضي بلاتيني"}, + {"rgb": "105, 105, 105", "name": "Charcoal Metallic", "arabic_name": "رمادي فحمي معدني"}, + {"rgb": "128, 0, 128", "name": "Royal Purple", "arabic_name": "أرجواني ملكي"}, + {"rgb": "210, 105, 30", "name": "Sunset Bronze", "arabic_name": "برونزي الغروب"}, + {"rgb": "0, 128, 128", "name": "Teal Lagoon", "arabic_name": "أزرق مخضر بحري"}, + {"rgb": "72, 61, 139", "name": "Midnight Blue", "arabic_name": "أزرق منتصف الليل"}, + {"rgb": "255, 20, 147", "name": "Blazing Pink", "arabic_name": "وردي لامع"}, + {"rgb": "192, 57, 43", "name": "Crimson Flame", "arabic_name": "لهب قرمزي"}, + {"rgb": "255, 228, 196", "name": "Cream Sand", "arabic_name": "كريم رملي"}, + {"rgb": "112, 128, 144", "name": "Steel Gray", "arabic_name": "رمادي فولاذي"}, + {"rgb": "0, 100, 0", "name": "Hunter Green", "arabic_name": "أخضر صياد"}, + {"rgb": "255, 223, 0", "name": "Bright Yellow", "arabic_name": "أصفر ساطع"}, + {"rgb": "85, 107, 47", "name": "Olive Metallic", "arabic_name": "زيتوني معدني"}, + {"rgb": "128, 128, 0", "name": "Mustard Gold", "arabic_name": "ذهبي خردلي"}, + {"rgb": "139, 69, 19", "name": "Cocoa Brown", "arabic_name": "بني كاكاو"}, + {"rgb": "255, 165, 0", "name": "Tangerine Flame", "arabic_name": "لهب المندرين"}, + {"rgb": "0, 0, 139", "name": "Navy Sapphire", "arabic_name": "كحلي ياقوتي"}, + {"rgb": "70, 130, 180", "name": "Skyline Blue", "arabic_name": "أزرق أفق"}, + {"rgb": "220, 20, 60", "name": "Crimson Passion", "arabic_name": "شغف قرمزي"}, + {"rgb": "189, 183, 107", "name": "Khaki Dune", "arabic_name": "كاكي كثيب"}, + {"rgb": "50, 205, 50", "name": "Lime Essence", "arabic_name": "ليموني نقي"}, + {"rgb": "139, 0, 139", "name": "Amethyst Glow", "arabic_name": "توهج جمشت"}, + {"rgb": "255, 215, 180", "name": "Rosé Gold", "arabic_name": "ذهبي وردي"}, + {"rgb": "46, 139, 87", "name": "Moss Green", "arabic_name": "أخضر طحلبي"}, + {"rgb": "72, 209, 204", "name": "Caribbean Aqua", "arabic_name": "أكوا كاريبي"}, + {"rgb": "255, 240, 245", "name": "Pearl Blush", "arabic_name": "تورد لؤلؤي"}, + {"rgb": "244, 164, 96", "name": "Sierra Sunset", "arabic_name": "غروب سييرا"}, + {"rgb": "139, 0, 0", "name": "Crimson Ruby", "arabic_name": "روبي قرمزي"}, + {"rgb": "192, 192, 192", "name": "Chrome", "arabic_name": "كروم"}, + {"rgb": "255, 105, 180", "name": "Hot Magenta", "arabic_name": "ماجنتا ساخن"}, + {"rgb": "0, 255, 255", "name": "Ice Blue", "arabic_name": "أزرق جليدي"}, + {"rgb": "184, 134, 11", "name": "Golden Bronze", "arabic_name": "برونزي ذهبي"}, + {"rgb": "128, 128, 64", "name": "Bronze Olive", "arabic_name": "زيتوني برونزي"}, + ] + + interior_colors = [ + {"rgb": "0, 0, 0", "name": "Jet Black", "arabic_name": "أسود نفاث"}, + {"rgb": "54, 69, 79", "name": "Charcoal Black", "arabic_name": "أسود فحمي"}, + {"rgb": "255, 255, 255", "name": "Bright White", "arabic_name": "أبيض ناصع"}, + {"rgb": "245, 245, 220", "name": "Off-White", "arabic_name": "أبيض مصفر"}, + {"rgb": "210, 180, 140", "name": "Beige", "arabic_name": "بيج"}, + {"rgb": "203, 194, 190", "name": "Sandstone", "arabic_name": "حجر رملي"}, + {"rgb": "80, 80, 80", "name": "Graphite Gray", "arabic_name": "رمادي غرافيتي"}, + {"rgb": "127, 128, 133", "name": "Ebony/Medium Slate", "arabic_name": "إيبوني/لائحة متوسطة"}, + {"rgb": "124, 79, 58", "name": "Mocha", "arabic_name": "موكا"}, + {"rgb": "193, 154, 107", "name": "Camel Tan", "arabic_name": "بيج الجمل"}, + {"rgb": "128, 0, 0", "name": "Maroon", "arabic_name": "مارون"}, + {"rgb": "0, 0, 128", "name": "Navy Blue", "arabic_name": "أزرق بحري"}, + {"rgb": "25, 25, 112", "name": "Midnight Blue", "arabic_name": "أزرق منتصف الليل"}, + {"rgb": "72, 60, 50", "name": "Taupe", "arabic_name": "توب"}, + ] + + def handle(self, *args, **kwargs): + self.stdout.write("Populating Exterior Colors...") + for color in self.exterior_colors: + obj, created = ExteriorColors.objects.get_or_create( + name=color["name"], arabic_name=color["arabic_name"], rgb=color["rgb"] + ) + 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("Populating Interior Colors...") + for color in self.interior_colors: + obj, created = InteriorColors.objects.get_or_create( + name=color["name"], arabic_name=color["arabic_name"], rgb=color["rgb"] + ) + 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("Finished populating colors.") \ No newline at end of file diff --git a/inventory/migrations/0001_initial.py b/inventory/migrations/0001_initial.py index 62d5b5f3..aa3b2b0b 100644 --- a/inventory/migrations/0001_initial.py +++ b/inventory/migrations/0001_initial.py @@ -1,8 +1,9 @@ -# Generated by Django 5.1.4 on 2024-12-04 23:43 +# Generated by Django 5.1.4 on 2024-12-09 13:58 import django.db.models.deletion import inventory.mixins import phonenumber_field.modelfields +from decimal import Decimal from django.conf import settings from django.db import migrations, models @@ -47,21 +48,56 @@ class Migration(migrations.Migration): }, 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='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')), - ('profit_margin', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Profit Margin')), - ('selling_price', models.DecimalField(decimal_places=2, editable=False, max_digits=14, verbose_name='Selling Price')), - ('vat_rate', models.DecimalField(decimal_places=2, default=0.15, max_digits=10, verbose_name='VAT Rate')), - ('vat_amount', models.DecimalField(decimal_places=2, editable=False, max_digits=12, verbose_name='VAT Amount')), - ('total', models.DecimalField(decimal_places=2, editable=False, max_digits=14, verbose_name='Total Amount')), + ('selling_price', models.DecimalField(decimal_places=2, max_digits=14, verbose_name='Selling Price')), + ('profit_margin', models.DecimalField(decimal_places=2, editable=False, max_digits=14, verbose_name='Profit Margin')), + ('discount_amount', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=14, verbose_name='Discount Amount')), + ('registration_fee', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=14, verbose_name='Registration Fee')), + ('administration_fee', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=14, verbose_name='Administration Fee')), + ('transportation_fee', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=14, verbose_name='Transportation Fee')), + ('custom_card_fee', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=14, verbose_name='Custom Card Fee')), + ('vat_rate', models.DecimalField(decimal_places=2, default=Decimal('0.15'), max_digits=14, verbose_name='VAT Rate')), + ('administration_vat_amount', models.DecimalField(decimal_places=2, default=Decimal('0.00'), editable=False, max_digits=12, verbose_name='Administration VAT')), + ('transportation_vat_amount', models.DecimalField(decimal_places=2, default=Decimal('0.00'), editable=False, max_digits=12, verbose_name='Transportation VAT')), + ('custom_card_vat_amount', models.DecimalField(decimal_places=2, default=Decimal('0.00'), editable=False, max_digits=12, verbose_name='Custom Card VAT')), + ('selling_vat_amount', models.DecimalField(decimal_places=2, default=Decimal('0.00'), editable=False, max_digits=12, verbose_name='Selling VAT')), + ('total_vat_amount', models.DecimalField(decimal_places=2, default=Decimal('0.00'), editable=False, max_digits=12, verbose_name='Total VAT')), + ('total_before_vat', models.DecimalField(decimal_places=2, default=Decimal('0.00'), editable=False, max_digits=14, verbose_name='Total Before VAT')), + ('total', models.DecimalField(decimal_places=2, default=Decimal('0.00'), editable=False, max_digits=14, verbose_name='Total Amount')), ('car', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='finances', to='inventory.car')), ], - options={ - 'verbose_name': 'Car Financial Details', - }, ), migrations.AddField( model_name='car', @@ -171,7 +207,7 @@ class Migration(migrations.Migration): 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.DateTimeField(verbose_name='Custom Date')), + ('custom_date', models.DateField(verbose_name='Custom Date')), ('car', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='custom_cards', to='inventory.car', verbose_name='Car')), ], options={ @@ -223,34 +259,22 @@ class Migration(migrations.Migration): field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='cars', to='inventory.dealer', verbose_name='Dealer'), ), migrations.CreateModel( - name='ExteriorColors', + name='SaleQuotation', 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')), - ('car', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='exteriorcolor', to='inventory.car')), + ('remarks', models.TextField(blank=True, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.customer')), ], - options={ - 'verbose_name': 'Exterior Color', - 'verbose_name_plural': 'Exterior Colors', - }, - bases=(models.Model, inventory.mixins.LocalizedNameMixin), ), migrations.CreateModel( - name='InteriorColors', + name='SaleQuotationCar', 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')), - ('car', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interiorcolor', to='inventory.car', verbose_name='Car')), + ('car', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.car')), + ('financial_details', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='quotation_finances', to='inventory.carfinance')), + ('quotation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='quotation_cars', to='inventory.salequotation')), ], - options={ - 'verbose_name': 'Interior Color', - 'verbose_name_plural': 'Interior Colors', - }, - bases=(models.Model, inventory.mixins.LocalizedNameMixin), ), migrations.CreateModel( name='Vendor', @@ -263,6 +287,7 @@ class Migration(migrations.Migration): ('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')), ('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='Address')), + ('logo', models.ImageField(blank=True, null=True, upload_to='logos/vendors', verbose_name='Logo')), ('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='vendors', to='inventory.dealer')), ], options={ @@ -290,4 +315,18 @@ class Migration(migrations.Migration): 'unique_together': {('car', 'reserved_until')}, }, ), + migrations.CreateModel( + name='CarColors', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('car', models.ForeignKey(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')}, + }, + ), ] diff --git a/inventory/migrations/0002_remove_interiorcolors_car_carcolors_and_more.py b/inventory/migrations/0002_remove_interiorcolors_car_carcolors_and_more.py deleted file mode 100644 index 141e49d9..00000000 --- a/inventory/migrations/0002_remove_interiorcolors_car_carcolors_and_more.py +++ /dev/null @@ -1,39 +0,0 @@ -# Generated by Django 5.1.4 on 2024-12-06 14:30 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('inventory', '0001_initial'), - ] - - operations = [ - migrations.RemoveField( - model_name='interiorcolors', - name='car', - ), - migrations.CreateModel( - name='CarColors', - 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')), - ('color_type', models.CharField(choices=[('exterior', 'Exterior'), ('interior', 'Interior')], default='exterior', max_length=10, verbose_name='Color Type')), - ('car', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='colors', to='inventory.car')), - ], - options={ - 'verbose_name': 'Color', - 'verbose_name_plural': 'Colors', - }, - ), - migrations.DeleteModel( - name='ExteriorColors', - ), - migrations.DeleteModel( - name='InteriorColors', - ), - ] diff --git a/inventory/migrations/0002_remove_salequotationcar_financial_details_and_more.py b/inventory/migrations/0002_remove_salequotationcar_financial_details_and_more.py new file mode 100644 index 00000000..9cc81730 --- /dev/null +++ b/inventory/migrations/0002_remove_salequotationcar_financial_details_and_more.py @@ -0,0 +1,98 @@ +# Generated by Django 5.1.4 on 2024-12-09 20:42 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='salequotationcar', + name='financial_details', + ), + migrations.AddField( + model_name='salequotation', + name='status', + field=models.CharField(choices=[('DRAFT', 'Draft'), ('CONFIRMED', 'Confirmed'), ('CANCELED', 'Canceled')], default='DRAFT', max_length=10, verbose_name='Status'), + ), + migrations.AddField( + model_name='salequotation', + name='updated_at', + field=models.DateTimeField(auto_now=True, verbose_name='Updated At'), + ), + migrations.AddField( + model_name='salequotationcar', + name='administration_fee', + field=models.DecimalField(decimal_places=2, default=150, max_digits=14, verbose_name='Administration Fee'), + preserve_default=False, + ), + migrations.AddField( + model_name='salequotationcar', + name='custom_card_fee', + field=models.DecimalField(decimal_places=2, default=70, max_digits=14, verbose_name='Custom Card Fee'), + preserve_default=False, + ), + migrations.AddField( + model_name='salequotationcar', + name='selling_price', + field=models.DecimalField(decimal_places=2, default=120000, max_digits=14, verbose_name='Selling Price'), + preserve_default=False, + ), + migrations.AddField( + model_name='salequotationcar', + name='total_amount', + field=models.DecimalField(decimal_places=2, default=135000, max_digits=14, verbose_name='Total Amount'), + preserve_default=False, + ), + migrations.AddField( + model_name='salequotationcar', + name='transportation_fee', + field=models.DecimalField(decimal_places=2, default=500, max_digits=14, verbose_name='Transportation Fee'), + preserve_default=False, + ), + migrations.AddField( + model_name='salequotationcar', + name='vat_amount', + field=models.DecimalField(decimal_places=2, default=4352, max_digits=14, verbose_name='VAT Amount'), + preserve_default=False, + ), + migrations.AlterField( + model_name='salequotation', + name='created_at', + field=models.DateTimeField(auto_now_add=True, verbose_name='Created At'), + ), + migrations.AlterField( + model_name='salequotation', + name='customer', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='quotations', to='inventory.customer', verbose_name='Customer'), + ), + migrations.AlterField( + model_name='salequotation', + name='remarks', + field=models.TextField(blank=True, null=True, verbose_name='Remarks'), + ), + migrations.AlterField( + model_name='salequotationcar', + name='car', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.car', verbose_name='Car'), + ), + migrations.AlterField( + model_name='salequotationcar', + name='quotation', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='quotation_cars', to='inventory.salequotation', verbose_name='Quotation'), + ), + migrations.CreateModel( + name='SalesOrder', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), + ('total_amount', models.DecimalField(decimal_places=2, max_digits=14, verbose_name='Total Amount')), + ('quotation', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='sales_order', to='inventory.salequotation', verbose_name='Quotation')), + ], + ), + ] diff --git a/inventory/migrations/0003_alter_customcard_custom_date.py b/inventory/migrations/0003_alter_customcard_custom_date.py deleted file mode 100644 index 5eab67c1..00000000 --- a/inventory/migrations/0003_alter_customcard_custom_date.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.1.4 on 2024-12-08 08:10 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('inventory', '0002_remove_interiorcolors_car_carcolors_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='customcard', - name='custom_date', - field=models.DateField(verbose_name='Custom Date'), - ), - ] diff --git a/inventory/migrations/0003_remove_salequotationcar_administration_fee_and_more.py b/inventory/migrations/0003_remove_salequotationcar_administration_fee_and_more.py new file mode 100644 index 00000000..82b8a1d0 --- /dev/null +++ b/inventory/migrations/0003_remove_salequotationcar_administration_fee_and_more.py @@ -0,0 +1,37 @@ +# Generated by Django 5.1.4 on 2024-12-09 21:06 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0002_remove_salequotationcar_financial_details_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='salequotationcar', + name='administration_fee', + ), + migrations.RemoveField( + model_name='salequotationcar', + name='custom_card_fee', + ), + migrations.RemoveField( + model_name='salequotationcar', + name='selling_price', + ), + migrations.RemoveField( + model_name='salequotationcar', + name='total_amount', + ), + migrations.RemoveField( + model_name='salequotationcar', + name='transportation_fee', + ), + migrations.RemoveField( + model_name='salequotationcar', + name='vat_amount', + ), + ] diff --git a/inventory/migrations/0004_remove_carfinance_administration_vat_amount_and_more.py b/inventory/migrations/0004_remove_carfinance_administration_vat_amount_and_more.py new file mode 100644 index 00000000..a0473761 --- /dev/null +++ b/inventory/migrations/0004_remove_carfinance_administration_vat_amount_and_more.py @@ -0,0 +1,37 @@ +# Generated by Django 5.1.4 on 2024-12-09 21:59 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0003_remove_salequotationcar_administration_fee_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='carfinance', + name='administration_vat_amount', + ), + migrations.RemoveField( + model_name='carfinance', + name='custom_card_vat_amount', + ), + migrations.RemoveField( + model_name='carfinance', + name='selling_vat_amount', + ), + migrations.RemoveField( + model_name='carfinance', + name='total_before_vat', + ), + migrations.RemoveField( + model_name='carfinance', + name='total_vat_amount', + ), + migrations.RemoveField( + model_name='carfinance', + name='transportation_vat_amount', + ), + ] diff --git a/inventory/migrations/__pycache__/0001_initial.cpython-311.pyc b/inventory/migrations/__pycache__/0001_initial.cpython-311.pyc index 933de09d..d1195297 100644 Binary files a/inventory/migrations/__pycache__/0001_initial.cpython-311.pyc and b/inventory/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/inventory/migrations/__pycache__/0002_remove_interiorcolors_car_carcolors_and_more.cpython-311.pyc b/inventory/migrations/__pycache__/0002_remove_interiorcolors_car_carcolors_and_more.cpython-311.pyc deleted file mode 100644 index 420d8462..00000000 Binary files a/inventory/migrations/__pycache__/0002_remove_interiorcolors_car_carcolors_and_more.cpython-311.pyc and /dev/null differ diff --git a/inventory/migrations/__pycache__/0003_alter_customcard_custom_date.cpython-311.pyc b/inventory/migrations/__pycache__/0003_alter_customcard_custom_date.cpython-311.pyc deleted file mode 100644 index de2f0f1d..00000000 Binary files a/inventory/migrations/__pycache__/0003_alter_customcard_custom_date.cpython-311.pyc and /dev/null differ diff --git a/inventory/models.py b/inventory/models.py index 5469e2de..6188a371 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -14,10 +14,12 @@ from django_ledger.models import ( UnitOfMeasureModel, CustomerModel, ItemModelQuerySet, + ) +from decimal import Decimal, InvalidOperation +from django.core.exceptions import ValidationError from phonenumber_field.modelfields import PhoneNumberField from django.contrib.contenttypes.models import ContentType -from decimal import Decimal from django.utils.timezone import now from .mixins import LocalizedNameMixin @@ -26,7 +28,7 @@ class CarMake(models.Model, LocalizedNameMixin): id_car_make = models.AutoField(primary_key=True) name = models.CharField(max_length=255) arabic_name = models.CharField(max_length=255) - logo = models.ImageField(_("logo"), upload_to="car_make", blank=True, null=True) + logo = models.ImageField(_('logo'), upload_to='car_make', blank=True, null=True) is_sa_import = models.BooleanField(default=False) def __str__(self): @@ -38,7 +40,7 @@ class CarMake(models.Model, LocalizedNameMixin): class CarModel(models.Model, LocalizedNameMixin): id_car_model = models.AutoField(primary_key=True) - id_car_make = models.ForeignKey(CarMake, models.DO_NOTHING, db_column="id_car_make") + id_car_make = models.ForeignKey(CarMake, models.DO_NOTHING, db_column='id_car_make') name = models.CharField(max_length=255) arabic_name = models.CharField(max_length=255) @@ -51,9 +53,7 @@ class CarModel(models.Model, LocalizedNameMixin): class CarSerie(models.Model, LocalizedNameMixin): id_car_serie = models.AutoField(primary_key=True) - id_car_model = models.ForeignKey( - CarModel, models.DO_NOTHING, db_column="id_car_model" - ) + id_car_model = models.ForeignKey(CarModel, models.DO_NOTHING, db_column='id_car_model') name = models.CharField(max_length=255) arabic_name = models.CharField(max_length=255) @@ -66,9 +66,7 @@ class CarSerie(models.Model, LocalizedNameMixin): class CarTrim(models.Model, LocalizedNameMixin): id_car_trim = models.AutoField(primary_key=True) - id_car_serie = models.ForeignKey( - CarSerie, models.DO_NOTHING, db_column="id_car_serie" - ) + id_car_serie = models.ForeignKey(CarSerie, models.DO_NOTHING, db_column='id_car_serie') name = models.CharField(max_length=255) arabic_name = models.CharField(max_length=255) start_production_year = models.IntegerField(blank=True, null=True) @@ -85,9 +83,7 @@ class CarSpecification(models.Model, LocalizedNameMixin): id_car_specification = models.AutoField(primary_key=True) name = models.CharField(max_length=255) arabic_name = models.CharField(max_length=255) - id_parent = models.ForeignKey( - "self", models.DO_NOTHING, db_column="id_parent", blank=True, null=True - ) + id_parent = models.ForeignKey('self', models.DO_NOTHING, db_column='id_parent', blank=True, null=True) def __str__(self): return self.name @@ -98,10 +94,8 @@ class CarSpecification(models.Model, LocalizedNameMixin): class CarSpecificationValue(models.Model): id_car_specification_value = models.AutoField(primary_key=True) - id_car_trim = models.ForeignKey(CarTrim, models.DO_NOTHING, db_column="id_car_trim") - id_car_specification = models.ForeignKey( - CarSpecification, models.DO_NOTHING, db_column="id_car_specification" - ) + id_car_trim = models.ForeignKey(CarTrim, models.DO_NOTHING, db_column='id_car_trim') + id_car_specification = models.ForeignKey(CarSpecification, models.DO_NOTHING, db_column='id_car_specification') value = models.CharField(max_length=500) unit = models.CharField(max_length=255, blank=True, null=True) @@ -114,21 +108,24 @@ class CarSpecificationValue(models.Model): # Car Model class CarStatusChoices(models.TextChoices): - AVAILABLE = "available", _("Available") - SOLD = "sold", _("Sold") - HOLD = "hold", _("Hold") - DAMAGED = "damaged", _("Damaged") + AVAILABLE = 'available', _('Available') + SOLD = 'sold', _('Sold') + HOLD = 'hold', _('Hold') + DAMAGED = 'damaged', _('Damaged') class CarStockTypeChoices(models.TextChoices): - NEW = "new", _("New") - USED = "used", _("Used") + NEW = 'new', _('New') + USED = 'used', _('Used') class Car(models.Model): vin = models.CharField(max_length=17, unique=True, verbose_name=_("VIN")) dealer = models.ForeignKey( - "Dealer", models.DO_NOTHING, related_name="cars", verbose_name=_("Dealer") + "Dealer", + models.DO_NOTHING, + related_name='cars', + verbose_name=_("Dealer") ) vendor = models.ForeignKey( @@ -136,53 +133,53 @@ class Car(models.Model): models.DO_NOTHING, null=True, blank=True, - related_name="cars", - verbose_name=_("Vendor"), + related_name='cars', + verbose_name=_("Vendor") ) id_car_make = models.ForeignKey( CarMake, models.DO_NOTHING, - db_column="id_car_make", + db_column='id_car_make', null=True, blank=True, - verbose_name=_("Make"), + verbose_name=_("Make") ) id_car_model = models.ForeignKey( CarModel, models.DO_NOTHING, - db_column="id_car_model", + db_column='id_car_model', null=True, blank=True, - verbose_name=_("Model"), + verbose_name=_("Model") ) year = models.IntegerField(verbose_name=_("Year")) id_car_serie = models.ForeignKey( CarSerie, models.DO_NOTHING, - db_column="id_car_serie", + db_column='id_car_serie', null=True, blank=True, - verbose_name=_("Series"), + verbose_name=_("Series") ) id_car_trim = models.ForeignKey( CarTrim, models.DO_NOTHING, - db_column="id_car_trim", + db_column='id_car_trim', null=True, blank=True, - verbose_name=_("Trim"), + verbose_name=_("Trim") ) status = models.CharField( max_length=10, choices=CarStatusChoices.choices, default=CarStatusChoices.AVAILABLE, - verbose_name=_("Status"), + verbose_name=_("Status") ) stock_type = models.CharField( max_length=10, choices=CarStockTypeChoices.choices, default=CarStockTypeChoices.NEW, - verbose_name=_("Stock Type"), + verbose_name=_("Stock Type") ) remarks = models.TextField(blank=True, null=True, verbose_name=_("Remarks")) mileage = models.IntegerField(blank=True, null=True, verbose_name=_("Mileage")) @@ -205,23 +202,21 @@ class Car(models.Model): @property def selling_price(self): finance = self.finances.first() - return finance.selling_price if finance else Decimal("0.00") + return finance.selling_price if finance else Decimal('0.00') @property def vat_amount(self): finance = self.finances.first() - return finance.vat_amount if finance else Decimal("0.00") + return finance.vat_amount if finance else Decimal('0.00') @property def total(self): finance = self.finances.first() - return finance.total if finance else Decimal("0.00") + return finance.total if finance else Decimal('0.00') class CarReservation(models.Model): - car = models.ForeignKey( - "Car", on_delete=models.CASCADE, related_name="reservations" - ) + car = models.ForeignKey('Car', on_delete=models.CASCADE, related_name='reservations') reserved_by = models.ForeignKey(User, on_delete=models.CASCADE) reserved_at = models.DateTimeField(auto_now_add=True) reserved_until = models.DateTimeField() @@ -230,81 +225,98 @@ class CarReservation(models.Model): return self.reserved_until > now() class Meta: - unique_together = ("car", "reserved_until") - ordering = ["-reserved_at"] + unique_together = ('car', 'reserved_until') + ordering = ['-reserved_at'] # Car Finance Model class CarFinance(models.Model): - car = models.ForeignKey(Car, on_delete=models.CASCADE, related_name="finances") - cost_price = models.DecimalField( - max_digits=14, decimal_places=2, verbose_name=_("Cost Price") - ) - profit_margin = models.DecimalField( - max_digits=10, decimal_places=2, verbose_name=_("Profit Margin") - ) - selling_price = models.DecimalField( - max_digits=14, decimal_places=2, verbose_name=_("Selling Price"), editable=False - ) - vat_rate = models.DecimalField( - max_digits=10, decimal_places=2, default=0.15, verbose_name=_("VAT Rate") - ) - vat_amount = models.DecimalField( - max_digits=12, decimal_places=2, verbose_name=_("VAT Amount"), editable=False - ) - total = models.DecimalField( - max_digits=14, decimal_places=2, verbose_name=_("Total Amount"), editable=False - ) + car = models.ForeignKey(Car, on_delete=models.CASCADE, related_name='finances') + cost_price = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Cost Price")) + selling_price = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Selling Price")) + profit_margin = models.DecimalField(max_digits=14, + decimal_places=2, + verbose_name=_("Profit Margin"), + editable=False) + discount_amount = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Discount Amount"), + default=Decimal('0.00')) + registration_fee = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Registration Fee"), + default=Decimal('0.00')) + administration_fee = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Administration Fee"), + default=Decimal('0.00')) + transportation_fee = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Transportation Fee"), + default=Decimal('0.00')) + custom_card_fee = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Custom Card Fee"), + default=Decimal('0.00')) + vat_rate = models.DecimalField(max_digits=14, decimal_places=2, default=Decimal('0.15'), verbose_name=_("VAT Rate"),) class Meta: verbose_name = _("Car Financial Details") def save(self, *args, **kwargs): self.full_clean() - self.selling_price = self.cost_price * (1 + self.profit_margin) - self.vat_amount = self.selling_price * self.vat_rate - self.total = self.selling_price + self.vat_amount + try: + self.profit_margin = self.selling_price - self.cost_price - self.discount_amount + services = self.administration_fee + self.transportation_fee + self.custom_card_fee + price_after_discount = self.selling_price - self.discount_amount + total_vat_amount = (price_after_discount + services) * self.vat_rate + self.total = price_after_discount + services + total_vat_amount + self.registration_fee + + except InvalidOperation as e: + raise ValidationError(_("Invalid decimal operation: %s") % str(e)) + super().save(*args, **kwargs) + + @property + def total_vat_amount(self): + return self.total if self.total else Decimal('0.00') + + +class ExteriorColors(models.Model, LocalizedNameMixin): + name = models.CharField(max_length=255, verbose_name=_("Name")) + arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name")) + rgb = models.CharField(max_length=24, blank=True, null=True, verbose_name=_("RGB")) + + class Meta: + verbose_name = _("Exterior Colors") + verbose_name_plural = _("Exterior Colors") + def __str__(self): - return ( - f"Car Financial Details for {self.car}: Selling Price {self.selling_price}" - ) + return f"{self.name} ({self.rgb})" + + +class InteriorColors(models.Model, LocalizedNameMixin): + name = models.CharField(max_length=255, verbose_name=_("Name")) + arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name")) + rgb = models.CharField(max_length=24, blank=True, null=True, verbose_name=_("RGB")) + + class Meta: + verbose_name = _("Interior Colors") + verbose_name_plural = _("Interior Colors") + + def __str__(self): + return f"{self.name} ({self.rgb})" # Colors Model class CarColors(models.Model): - class ColorType(models.TextChoices): - EXTERIOR = "exterior", _("Exterior") - INTERIOR = "interior", _("Interior") - - car = models.ForeignKey("Car", on_delete=models.CASCADE, related_name="colors") - name = models.CharField(max_length=255, verbose_name=_("Name")) - arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name")) - rgb = models.CharField(max_length=24, blank=True, null=True, verbose_name=_("RGB")) - color_type = models.CharField( - max_length=10, - choices=ColorType.choices, - default=ColorType.EXTERIOR, - verbose_name=_("Color Type"), - ) + car = models.ForeignKey('Car', on_delete=models.CASCADE, related_name='colors') + exterior = models.ForeignKey('ExteriorColors', on_delete=models.CASCADE, related_name='colors') + interior = models.ForeignKey('InteriorColors', on_delete=models.CASCADE, related_name='colors') class Meta: verbose_name = _("Color") verbose_name_plural = _("Colors") + unique_together = ('car', 'exterior', 'interior') def __str__(self): - return f"{self.get_color_type_display()} - {self.name} ({self.rgb})" + return f"{self.car} ({self.exterior.name}) ({self.interior.name})" # Custom Card Model class CustomCard(models.Model): - car = models.ForeignKey( - Car, - on_delete=models.CASCADE, - related_name="custom_cards", - verbose_name=_("Car"), - ) + car = models.ForeignKey(Car, on_delete=models.CASCADE, related_name='custom_cards', verbose_name=_("Car")) custom_number = models.CharField(max_length=255, verbose_name=_("Custom Number")) custom_date = models.DateField(verbose_name=_("Custom Date")) @@ -318,12 +330,7 @@ class CustomCard(models.Model): # Car Registration Model class CarRegistration(models.Model): - car = models.ForeignKey( - Car, - on_delete=models.CASCADE, - related_name="registrations", - verbose_name=_("Car"), - ) + car = models.ForeignKey(Car, on_delete=models.CASCADE, related_name='registrations', verbose_name=_("Car")) plate_number = models.IntegerField(verbose_name=_("Plate Number")) text1 = models.CharField(max_length=1, verbose_name=_("Text 1")) text2 = models.CharField(max_length=1, verbose_name=_("Text 2")) @@ -349,20 +356,14 @@ class TimestampedModel(models.Model): # Dealer Model class Dealer(models.Model, LocalizedNameMixin): - user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="dealer") - crn = models.CharField( - max_length=10, verbose_name=_("Commercial Registration Number") - ) + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='dealer') + crn = models.CharField(max_length=10, verbose_name=_("Commercial Registration Number")) vrn = models.CharField(max_length=15, 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 = PhoneNumberField(region="SA", verbose_name=_("Phone Number")) - address = models.CharField( - max_length=200, blank=True, null=True, verbose_name=_("Address") - ) - logo = models.ImageField( - upload_to="logos/users", blank=True, null=True, verbose_name=_("Logo") - ) + phone_number = PhoneNumberField(region='SA', verbose_name=_("Phone Number")) + address = models.CharField(max_length=200, blank=True, null=True, verbose_name=_("Address")) + logo = models.ImageField(upload_to="logos/users", blank=True, null=True, verbose_name=_("Logo")) class Meta: verbose_name = _("Dealer") @@ -374,20 +375,15 @@ class Dealer(models.Model, LocalizedNameMixin): # Vendor Model class Vendor(models.Model, LocalizedNameMixin): - dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="vendors") - 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") - ) + dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name='vendors') + 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 = PhoneNumberField(region="SA", verbose_name=_("Phone Number")) - address = models.CharField( - max_length=200, blank=True, null=True, verbose_name=_("Address") - ) + phone_number = PhoneNumberField(region='SA', verbose_name=_("Phone Number")) + address = models.CharField(max_length=200, blank=True, null=True, verbose_name=_("Address")) + logo = models.ImageField(upload_to="logos/vendors", blank=True, null=True, verbose_name=_("Logo")) class Meta: verbose_name = _("Vendor") @@ -399,24 +395,14 @@ class Vendor(models.Model, LocalizedNameMixin): # Customer Model class Customer(models.Model): - dealer = models.ForeignKey( - Dealer, on_delete=models.CASCADE, related_name="customers" - ) + dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name='customers') first_name = models.CharField(max_length=50, verbose_name=_("First Name")) - middle_name = models.CharField( - max_length=50, blank=True, null=True, verbose_name=_("Middle Name") - ) + middle_name = models.CharField(max_length=50, blank=True, null=True, verbose_name=_("Middle Name")) last_name = models.CharField(max_length=50, verbose_name=_("Last Name")) email = models.EmailField(unique=True, verbose_name=_("Email")) - national_id = models.CharField( - max_length=10, unique=True, verbose_name=_("National ID") - ) - phone_number = PhoneNumberField( - region="SA", unique=True, verbose_name=_("Phone Number") - ) - address = models.CharField( - max_length=200, blank=True, null=True, verbose_name=_("Address") - ) + national_id = models.CharField(max_length=10, unique=True, verbose_name=_("National ID")) + phone_number = PhoneNumberField(region='SA', unique=True, verbose_name=_("Phone Number")) + address = models.CharField(max_length=200, blank=True, null=True, verbose_name=_("Address")) created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created")) class Meta: @@ -424,9 +410,84 @@ class Customer(models.Model): verbose_name_plural = _("Customers") def __str__(self): - middle = f" {self.middle_name}" if self.middle_name else "" + middle = f" {self.middle_name}" if self.middle_name else '' return f"{self.first_name}{middle} {self.last_name}" + +class SaleQuotation(models.Model): + STATUS_CHOICES = [ + ("DRAFT", _("Draft")), + ("CONFIRMED", _("Confirmed")), + ("CANCELED", _("Canceled")), + ] + + customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name="quotations", verbose_name=_("Customer")) + remarks = models.TextField(blank=True, null=True, verbose_name=_("Remarks")) + status = models.CharField(max_length=10, choices=STATUS_CHOICES, default="DRAFT", verbose_name=_("Status")) + created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At")) + updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At")) + + def confirm(self): + """Confirm the quotation and lock financial details.""" + if self.status != "DRAFT": + raise ValueError(_("Only draft quotations can be confirmed.")) + self.status = "CONFIRMED" + self.save() + + def cancel(self): + """Cancel the quotation.""" + if self.status == "CONFIRMED": + raise ValueError(_("Cannot cancel a confirmed quotation.")) + self.status = "CANCELED" + self.save() + + def __str__(self): + return f"Quotation #{self.id} for {self.customer}" + + +class SaleQuotationCar(models.Model): + quotation = models.ForeignKey( + SaleQuotation, + on_delete=models.CASCADE, + related_name="quotation_cars", + verbose_name=_("Quotation") + ) + car = models.ForeignKey( + Car, + on_delete=models.CASCADE, + verbose_name=_("Car") + ) + + def get_financial_details(self): + """Retrieve financial details dynamically from CarFinance.""" + car_finance = self.car.finances.first() + if not car_finance: + return None + + return { + "selling_price": car_finance.selling_price, + "administration_fee": car_finance.administration_fee, + "transportation_fee": car_finance.transportation_fee, + "custom_card_fee": car_finance.custom_card_fee, + "registration_fee": car_finance.registration_fee, + "vat_rate": car_finance.vat_rate, + "discount_amount": car_finance.discount_amount, + "total_amount": car_finance.total, + } + + def __str__(self): + return f"{self.car} - Quotation #{self.quotation.id}" + + +class SalesOrder(models.Model): + quotation = models.OneToOneField(SaleQuotation, on_delete=models.CASCADE, related_name="sales_order", verbose_name=_("Quotation")) + created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At")) + total_amount = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Total Amount")) + + def __str__(self): + return f"Sales Order #{self.id} from Quotation #{self.quotation.id}" + + # Create Entity @receiver(post_save, sender=Dealer) def create_ledger_entity(sender, instance, created, **kwargs): diff --git a/inventory/services.py b/inventory/services.py index c476558a..07e0eb30 100644 --- a/inventory/services.py +++ b/inventory/services.py @@ -25,6 +25,7 @@ def decode_vin_pyvin(vin): print(data) return data + # vehicle-info # c2729afb # 6d397471920412d672af1b8a02ca52ea @@ -44,197 +45,10 @@ def elm(vin): payload = {} response = requests.request("GET", url, headers=headers, data=payload) car_info = json.loads(response.text) + print(car_info) return car_info -def get_unique_colors(api_response): - print(api_response) - colors = api_response.get("data", []) - print(colors) - - unique_colors = {} - for color in colors: - color_name = color.get("name") - rgb = color.get("rgb") - if color_name not in unique_colors: - unique_colors[color_name] = rgb - - return [{"name": name, "rgb": rgb} for name, rgb in unique_colors.items()] - - -def fetch_colors(car_data): - - car_colors = { - "data": [ - {"rgb": "192, 192, 192", "name": "Silver Metallic"}, - {"rgb": "0, 0, 0", "name": "Jet Black"}, - {"rgb": "255, 255, 255", "name": "Bright White"}, - {"rgb": "128, 128, 128", "name": "Graphite Gray"}, - {"rgb": "80, 80, 80", "name": "Gunmetal Gray"}, - {"rgb": "255, 0, 0", "name": "Racing Red"}, - {"rgb": "255, 69, 0", "name": "Inferno Orange"}, - {"rgb": "0, 0, 255", "name": "Deep Blue Pearl"}, - {"rgb": "75, 0, 130", "name": "Indigo Night"}, - {"rgb": "255, 215, 0", "name": "Solar Gold"}, - {"rgb": "34, 139, 34", "name": "Emerald Green"}, - {"rgb": "60, 179, 113", "name": "Forest Mist Green"}, - {"rgb": "255, 140, 0", "name": "Burnt Amber"}, - {"rgb": "160, 82, 45", "name": "Copper Brown"}, - {"rgb": "128, 0, 0", "name": "Crimson Maroon"}, - {"rgb": "245, 245, 220", "name": "Beige Champagne"}, - {"rgb": "169, 169, 169", "name": "Shadow Gray"}, - {"rgb": "255, 250, 205", "name": "Lemon Pearl"}, - {"rgb": "220, 220, 220", "name": "Platinum Silver"}, - {"rgb": "105, 105, 105", "name": "Charcoal Metallic"}, - {"rgb": "128, 0, 128", "name": "Royal Purple"}, - {"rgb": "210, 105, 30", "name": "Sunset Bronze"}, - {"rgb": "0, 128, 128", "name": "Teal Lagoon"}, - {"rgb": "72, 61, 139", "name": "Midnight Blue"}, - {"rgb": "255, 20, 147", "name": "Blazing Pink"}, - {"rgb": "192, 57, 43", "name": "Crimson Flame"}, - {"rgb": "255, 228, 196", "name": "Cream Sand"}, - {"rgb": "112, 128, 144", "name": "Steel Gray"}, - {"rgb": "0, 100, 0", "name": "Hunter Green"}, - {"rgb": "255, 223, 0", "name": "Bright Yellow"}, - {"rgb": "85, 107, 47", "name": "Olive Metallic"}, - {"rgb": "128, 128, 0", "name": "Mustard Gold"}, - {"rgb": "139, 69, 19", "name": "Cocoa Brown"}, - {"rgb": "255, 165, 0", "name": "Tangerine Flame"}, - {"rgb": "0, 0, 139", "name": "Navy Sapphire"}, - {"rgb": "70, 130, 180", "name": "Skyline Blue"}, - {"rgb": "220, 20, 60", "name": "Crimson Passion"}, - {"rgb": "189, 183, 107", "name": "Khaki Dune"}, - {"rgb": "50, 205, 50", "name": "Lime Essence"}, - {"rgb": "139, 0, 139", "name": "Amethyst Glow"}, - {"rgb": "255, 215, 180", "name": "Rosé Gold"}, - {"rgb": "46, 139, 87", "name": "Moss Green"}, - {"rgb": "72, 209, 204", "name": "Caribbean Aqua"}, - {"rgb": "255, 240, 245", "name": "Pearl Blush"}, - {"rgb": "244, 164, 96", "name": "Sierra Sunset"}, - {"rgb": "139, 0, 0", "name": "Crimson Ruby"}, - {"rgb": "192, 192, 192", "name": "Chrome"}, - {"rgb": "255, 105, 180", "name": "Hot Magenta"}, - {"rgb": "0, 255, 255", "name": "Ice Blue"}, - {"rgb": "184, 134, 11", "name": "Golden Bronze"}, - {"rgb": "128, 128, 64", "name": "Bronze Olive"}, - {"rgb": "245, 222, 179", "name": "Wheat Cream"} - ] - } - - jwt_token = get_jwt_token() - if not jwt_token: - print("Failed to retrieve JWT token.") - return None - - year = car_data['year'] - make = car_data['make'] - model = car_data['model'] - - url = "https://carapi.app/api/exterior-colors?year={}&make={}&model={}".format(year, make, model) - params = { - 'limit': '1000', - 'sort': 'name', - 'direction': 'asc', - 'verbose': 'no', - 'all_trims': 'no', - } - - headers = { - 'Accept': 'application/json', - 'Authorization': f'Bearer {jwt_token}', - } - - try: - response = requests.get(url, headers=headers, params=params) - color_info = response.json() - - if not color_info["data"] == []: - return get_unique_colors(color_info) - else: - return car_colors["data"] - - except requests.exceptions.RequestException as e: - print(f"Error fetching color information: {e}") - return None - - -def fetch_interior_colors(car_data): - - car_colors = { - "data": [ - {"rgb": "0, 0, 0", "name": "Jet Black"}, - {"rgb": "54, 69, 79", "name": "Charcoal Black"}, - {"rgb": "255, 255, 255", "name": "Bright White"}, - {"rgb": "245, 245, 220", "name": "Off-White"}, - {"rgb": "210, 180, 140", "name": "Beige"}, - {"rgb": "205, 133, 63", "name": "Tan"}, - {"rgb": "128, 128, 128", "name": "Gray"}, - {"rgb": "80, 80, 80", "name": "Graphite Gray"}, - {"rgb": "112, 128, 144", "name": "Gunmetal Gray"}, - {"rgb": "192, 192, 192", "name": "Silver Metallic"}, - {"rgb": "139, 69, 19", "name": "Cognac Brown"}, - {"rgb": "149, 94, 55", "name": "Chestnut Brown"}, - {"rgb": "97, 63, 43", "name": "Espresso Brown"}, - {"rgb": "72, 40, 34", "name": "Dark Chocolate"}, - {"rgb": "139, 69, 19", "name": "Saddle Brown"}, - {"rgb": "124, 79, 58", "name": "Mocha"}, - {"rgb": "193, 154, 107", "name": "Camel Tan"}, - {"rgb": "128, 0, 32", "name": "Burgundy"}, - {"rgb": "128, 0, 0", "name": "Maroon"}, - {"rgb": "139, 0, 0", "name": "Deep Red"}, - {"rgb": "0, 0, 128", "name": "Navy Blue"}, - {"rgb": "65, 105, 225", "name": "Royal Blue"}, - {"rgb": "34, 139, 34", "name": "Forest Green"}, - {"rgb": "80, 200, 120", "name": "Emerald Green"}, - {"rgb": "255, 255, 240", "name": "Ivory"}, - {"rgb": "242, 242, 242", "name": "Pearl White"}, - {"rgb": "169, 169, 169", "name": "Stone Gray"}, - {"rgb": "112, 128, 144", "name": "Slate Gray"}, - {"rgb": "150, 111, 51", "name": "Ash Brown"}, - {"rgb": "128, 128, 0", "name": "Olive Green"}, - {"rgb": "25, 25, 112", "name": "Midnight Blue"}, - {"rgb": "72, 60, 50", "name": "Taupe"} - ] - } - - jwt_token = get_jwt_token() - if not jwt_token: - print("Failed to retrieve JWT token.") - return None - - year = car_data['year'] - make = car_data['make'] - model = car_data['model'] - - url = "https://carapi.app/api/interior-colors?year={}&make={}&model={}".format(year, make, model) - params = { - 'limit': '100', - 'sort': 'name', - 'direction': 'asc', - 'verbose': 'no', - 'all_trims': 'no', - } - - headers = { - 'Accept': 'application/json', - 'Authorization': f'Bearer {jwt_token}', - } - - try: - response = requests.get(url, headers=headers, params=params) - color_info = response.json() - - if not color_info["data"] == []: - return get_unique_colors(color_info) - else: - return car_colors["data"] - - except requests.exceptions.RequestException as e: - print(f"Error fetching color information: {e}") - return None - - - def translate(content, *args, **kwargs): client = OpenAI(api_key=settings.OPENAI_API_KEY) completion = client.chat.completions.create( diff --git a/inventory/urls.py b/inventory/urls.py index ccde353e..c6bf3ac0 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -55,11 +55,20 @@ urlpatterns = [ path('cars/finance/update//', views.CarFinanceUpdateView.as_view(), name='car_finance_update'), path('cars/add/', views.CarCreateView.as_view(), name='car_add'), path('ajax/', views.AjaxHandlerView.as_view(), name='ajax_handler'), - path('cars//add-color/', views.CarColorCreateView.as_view(), name='add_color'), - path('cars//colors//update/', - views.CarColorUpdateView.as_view(), - name='color_update'), + path('cars//add-color/', views.CarColorCreate.as_view(), name='add_color'), + # path('cars//colors//update/',views.CarColorUpdateView.as_view(),name='color_update'), + path('cars/reserve//', views.reserve_car_view, name='reserve_car'), path('reservations//', views.manage_reservation, name='reservations'), path('cars//add-custom-card/', views.CustomCardCreateView.as_view(), name='add_custom_card'), -] \ No newline at end of file + + # Sales URLs + path('sales/quotations/create/', views.QuotationCreateView.as_view(), name='quotation_create'), + path('sales/quotations//', views.QuotationDetailView.as_view(), name='quotation_detail'), + path('sales/quotations/', views.QuotationListView.as_view(), name='quotation_list'), + path('sales/quotations//confirm/', views.confirm_quotation, name='confirm_quotation'), + path('sales/orders/detail//', views.SalesOrderDetailView.as_view(), name='order_detail'), + +] + + diff --git a/inventory/views.py b/inventory/views.py index 8145835c..f84de195 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -15,7 +15,7 @@ from django.views.generic import ( CreateView, UpdateView, DeleteView, - TemplateView, + TemplateView ) from django.utils import timezone, translation from django.conf import settings @@ -24,8 +24,8 @@ from django.forms import ChoiceField, ModelForm, RadioSelect from django.urls import reverse, reverse_lazy from django.contrib import messages from django.db.models import Sum, F, Count -from .services import elm, fetch_colors, translate, decode_vin_pyvin, normalize_name -from . import models, tables, forms +from .services import elm, decode_vin_pyvin +from . import models, forms from django_tables2.export.views import ExportMixin @@ -34,24 +34,17 @@ logging.basicConfig(level=logging.INFO) def switch_language(request): - language = request.GET.get("language", "en") - referer = request.META.get("HTTP_REFERER", "/") + language = request.GET.get('language', 'en') + referer = request.META.get('HTTP_REFERER', '/') parsed_url = urlparse(referer) - path_parts = parsed_url.path.split("/") + path_parts = parsed_url.path.split('/') if path_parts[1] in dict(settings.LANGUAGES): path_parts.pop(1) - new_path = "/".join(path_parts) + new_path = '/'.join(path_parts) new_url = urlunparse( - ( - parsed_url.scheme, - parsed_url.netloc, - new_path, - parsed_url.params, - parsed_url.query, - parsed_url.fragment, - ) + (parsed_url.scheme, parsed_url.netloc, new_path, parsed_url.params, parsed_url.query, parsed_url.fragment) ) if language in dict(settings.LANGUAGES): @@ -60,232 +53,203 @@ def switch_language(request): response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language) translation.activate(language) request.session[settings.LANGUAGE_COOKIE_NAME] = language - logger.debug( - f"Language switched to: {language}, Session: {request.session[settings.LANGUAGE_COOKIE_NAME]}" - ) + logger.debug(f"Language switched to: {language}, Session: {request.session[settings.LANGUAGE_COOKIE_NAME]}") return response else: logger.warning(f"Invalid language code: {language}") - return redirect("/") + return redirect('/') class HomeView(LoginRequiredMixin, TemplateView): - template_name = "index.html" + template_name = 'index.html' def dispatch(self, request, *args, **kwargs): - if not hasattr(request.user, "dealer") or not request.user.is_authenticated: - messages.error(request, _("You are not associated with any dealer.")) - return redirect("welcome") + if not hasattr(request.user, 'dealer') or not request.user.is_authenticated: + messages.error(request, _('You are not associated with any dealer.')) + return redirect('welcome') return super().dispatch(request, *args, **kwargs) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) total_cars = models.Car.objects.count() - total_reservations = models.CarReservation.objects.filter( - reserved_until__gte=timezone.now() - ).count() + total_reservations = models.CarReservation.objects.filter(reserved_until__gte=timezone.now()).count() stats = models.CarFinance.objects.aggregate( - total_cost_price=Sum("cost_price"), - total_selling_price=Sum("selling_price"), + total_cost_price=Sum('cost_price'), + total_selling_price=Sum('selling_price'), ) - total_cost_price = stats["total_cost_price"] or 0 - total_selling_price = stats["total_selling_price"] or 0 + total_cost_price = stats['total_cost_price'] or 0 + total_selling_price = stats['total_selling_price'] or 0 total_profit = total_selling_price - total_cost_price - context["total_cars"] = total_cars - context["total_reservations"] = total_reservations - context["total_cost_price"] = total_cost_price - context["total_selling_price"] = total_selling_price - context["total_profit"] = total_profit + context['total_cars'] = total_cars + context['total_reservations'] = total_reservations + context['total_cost_price'] = total_cost_price + context['total_selling_price'] = total_selling_price + context['total_profit'] = total_profit return context class WelcomeView(TemplateView): template_name = "welcome.html" - - def dispatch(self, request, *args, **kwargs): - if hasattr(request.user, "dealer") and request.user.is_authenticated: - return redirect("landing_page") - return super().dispatch(request, *args, **kwargs) class CarCreateView(LoginRequiredMixin, CreateView): model = models.Car form_class = forms.CarForm - template_name = "inventory/car_form.html" - success_url = reverse_lazy("inventory_stats") + template_name = 'inventory/car_form.html' + # success_url = reverse_lazy('inventory_stats') + + def get_success_url(self): + """Determine the redirect URL based on user choice.""" + if self.request.POST.get('add_another'): + return reverse('car_add') + return reverse('inventory_stats') def form_valid(self, form): form.instance.dealer = self.request.user.dealer form.save() - messages.success(self.request, "Car saved successfully.") + messages.success(self.request, 'Car saved successfully.') return super().form_valid(form) class AjaxHandlerView(LoginRequiredMixin, View): def get(self, request, *args, **kwargs): - action = request.GET.get("action") + action = request.GET.get('action') handlers = { - "decode_vin": self.decode_vin, - "get_models": self.get_models, - "get_series": self.get_series, - "get_trims": self.get_trims, - "get_specifications": self.get_specifications, + 'decode_vin': self.decode_vin, + 'get_models': self.get_models, + 'get_series': self.get_series, + 'get_trims': self.get_trims, + 'get_specifications': self.get_specifications, } handler = handlers.get(action) if handler: return handler(request) else: - return JsonResponse({"error": "Invalid action"}, status=400) + return JsonResponse({'error': 'Invalid action'}, status=400) def decode_vin(self, request): - vin_no = request.GET.get("vin_no") + vin_no = request.GET.get('vin_no') if not vin_no or len(vin_no.strip()) != 17: - return JsonResponse( - {"success": False, "error": "Invalid VIN number provided."}, status=400 - ) + return JsonResponse({'success': False, 'error': 'Invalid VIN number provided.'}, status=400) vin_no = vin_no.strip() vin_data = {} - decoding_method = "" + decoding_method = '' - decoding_methods = [("PYVIN", decode_vin_pyvin), ("VIN", VIN), ("ELM", elm)] + decoding_methods = [ + ('PYVIN', decode_vin_pyvin), + ('VIN', VIN), + ('ELM', elm) + ] - manufacturer_name = model_name_before = model_name = year_model = None + manufacturer_name = model_name = year_model = None for method_name, decode_function in decoding_methods: try: vin_info = decode_function(vin_no) if vin_info: - if method_name == "PYVIN": + if method_name == 'PYVIN': manufacturer_name = vin_info.Make.strip() - model_name_before = vin_info.Model.strip() + model_name = vin_info.Model.strip() year_model = vin_info.ModelYear if not manufacturer_name or not year_model: - raise ValueError("PYVIN returned incomplete data.") - elif method_name == "VIN": + raise ValueError('PYVIN returned incomplete data.') + elif method_name == 'VIN': manufacturer_name = vin_info.make.strip() - model_name_before = vin_info.model.strip() + model_name = vin_info.model.strip() year_model = vin_info.model_year - if ( - not manufacturer_name - or not model_name_before - or not year_model - ): - raise ValueError("VIN returned incomplete data.") - elif method_name == "ELM": - elm_data = vin_info.get("data", {}) - manufacturer_name = elm_data.get("maker", "").strip() - model_name_before = elm_data.get("model", "").strip() - year_model = elm_data.get("modelYear", "").strip() - if ( - not manufacturer_name - or not model_name_before - or not year_model - ): - raise ValueError("ELM returned incomplete data.") - model_name = normalize_name(model_name_before) - decoding_method = method_name - print(f"decoded by {method_name}") - break + if not manufacturer_name or not model_name or not year_model: + raise ValueError('VIN returned incomplete data.') + elif method_name == 'ELM': + elm_data = vin_info.get('data', {}) + manufacturer_name = elm_data.get('maker', '').strip() + print(manufacturer_name) + model_name = elm_data.get('model', '').strip() + print(model_name) + year_model = elm_data.get('modelYear', '').strip() + print(year_model) + if not manufacturer_name or not model_name or not year_model: + raise ValueError('ELM returned incomplete data.') + + # model_name = normalize_name(model_name_before) + decoding_method = method_name + print(f"decoded by {method_name}") + break else: logger.warning(f"{method_name} returned no data for {vin_no}.") except Exception as e: - logger.warning( - f"VIN decoding with {method_name} failed for {vin_no}: {e}" - ) + logger.warning(f"VIN decoding with {method_name} failed for {vin_no}: {e}") if not manufacturer_name or not model_name or not year_model: - return JsonResponse( - {"success": False, "error": "VIN not found in all sources."}, status=404 - ) + return JsonResponse({'success': False, 'error': 'VIN not found in all sources.'}, status=404) logger.info( f"VIN decoded using {decoding_method}: Make={manufacturer_name}, Model={model_name}, Year={year_model}" ) - - car_make = models.CarMake.objects.filter( - name__icontains=manufacturer_name - ).first() + regex_make= manufacturer_name.replace(" ", "[- ]?") + car_make = models.CarMake.objects.filter(name__iregex=regex_make).first() if not car_make: - return JsonResponse( - {"success": False, "error": "Manufacturer not found in the database."}, - status=404, - ) - vin_data["make_id"] = car_make.id_car_make - vin_data["name"] = car_make.name - vin_data["arabic_name"] = car_make.arabic_name + return JsonResponse({'success': False, 'error': 'Manufacturer not found in the database.'}, status=404) + vin_data['make_id'] = car_make.id_car_make + vin_data['name'] = car_make.name + vin_data['arabic_name'] = car_make.arabic_name - car_model = models.CarModel.objects.filter( - id_car_make=car_make.id_car_make, name__icontains=model_name - ).first() + # car_model = models.CarModel.objects.filter(id_car_make=car_make.id_car_make, name__contains=model_name).first() + regex_pattern = model_name.replace(" ", "[- ]?") + car_model = models.CarModel.objects.filter(id_car_make=car_make.id_car_make, name__iregex=regex_pattern).first() if not car_model: - return JsonResponse( - { - "success": False, - "error": "Model not found for the given manufacturer.", - }, - status=404, - ) + return JsonResponse({'success': False, 'error': 'Model not found for the given manufacturer.'}, status=404) - vin_data["model_id"] = car_model.id_car_model - vin_data["year"] = year_model - return JsonResponse({"success": True, "data": vin_data}) + vin_data['model_id'] = car_model.id_car_model + vin_data['year'] = year_model + return JsonResponse({'success': True, 'data': vin_data}) def get_models(self, request): - make_id = request.GET.get("make_id") - car_models = models.CarModel.objects.filter(id_car_make=make_id).values( - "id_car_model", "name", "arabic_name" - ) + make_id = request.GET.get('make_id') + car_models = models.CarModel.objects.filter(id_car_make=make_id).values('id_car_model', 'name', 'arabic_name') return JsonResponse(list(car_models), safe=False) def get_series(self, request): - model_id = request.GET.get("model_id") + model_id = request.GET.get('model_id') series = models.CarSerie.objects.filter( id_car_model=model_id, - ).values("id_car_serie", "name", "arabic_name") + ).values('id_car_serie', 'name', 'arabic_name') return JsonResponse(list(series), safe=False) def get_trims(self, request): - serie_id = request.GET.get("serie_id") - trims = models.CarTrim.objects.filter(id_car_serie=serie_id).values( - "id_car_trim", "name", "arabic_name" - ) + serie_id = request.GET.get('serie_id') + trims = models.CarTrim.objects.filter( + id_car_serie=serie_id + ).values('id_car_trim', 'name', 'arabic_name') return JsonResponse(list(trims), safe=False) def get_specifications(self, request): - trim_id = request.GET.get("trim_id") - car_spec_values = models.CarSpecificationValue.objects.filter( - id_car_trim=trim_id - ) + trim_id = request.GET.get('trim_id') + car_spec_values = models.CarSpecificationValue.objects.filter(id_car_trim=trim_id) lang = translation.get_language() specs_by_parent = {} for value in car_spec_values: specification = value.id_car_specification parent = specification.id_parent parent_id = parent.id_car_specification if parent else 0 - if lang == "ar": + if lang == 'ar': parent_name = parent.arabic_name if parent else "Root" else: parent_name = parent.name if parent else "Root" if parent_id not in specs_by_parent: - specs_by_parent[parent_id] = { - "parent_name": parent_name, - "specifications": [], - } + specs_by_parent[parent_id] = {'parent_name': parent_name, 'specifications': []} spec_data = { - "specification_id": specification.id_car_specification, - "s_name": specification.arabic_name - if lang == "ar" - else specification.name, - "s_value": value.value, - "s_unit": value.unit if value.unit else "", - "trim_name": value.id_car_trim.name, + 'specification_id': specification.id_car_specification, + 's_name': specification.arabic_name if lang == 'ar' else specification.name, + 's_value': value.value, + 's_unit': value.unit if value.unit else "", + 'trim_name': value.id_car_trim.name } - specs_by_parent[parent_id]["specifications"].append(spec_data) + specs_by_parent[parent_id]['specifications'].append(spec_data) serialized_specs = [ - {"parent_name": v["parent_name"], "specifications": v["specifications"]} + {'parent_name': v['parent_name'], 'specifications': v['specifications']} for v in specs_by_parent.values() ] return JsonResponse(serialized_specs, safe=False) @@ -293,23 +257,22 @@ class AjaxHandlerView(LoginRequiredMixin, View): class CarInventory(LoginRequiredMixin, ListView): model = models.Car - home_label = _("inventory") - template_name = "inventory/car_inventory.html" - context_object_name = "cars" + home_label = _('inventory') + template_name = 'inventory/car_inventory.html' + context_object_name = 'cars' paginate_by = 10 - ordering = ["receiving_date"] + ordering = ['receiving_date'] def get_queryset(self, *args, **kwargs): - query = self.request.GET.get("q") - make_id = self.kwargs["make_id"] - model_id = self.kwargs["model_id"] - trim_id = self.kwargs["trim_id"] + query = self.request.GET.get('q') + make_id = self.kwargs['make_id'] + model_id = self.kwargs['model_id'] + trim_id = self.kwargs['trim_id'] cars = models.Car.objects.filter( dealer__user=self.request.user, id_car_make=make_id, id_car_model=model_id, - id_car_trim=trim_id, - ).order_by("receiving_date") + id_car_trim=trim_id,).order_by('receiving_date') if query: cars = cars.filter(Q(vin__icontains=query)) @@ -317,10 +280,29 @@ class CarInventory(LoginRequiredMixin, ListView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context["query"] = self.request.GET.get("q", "") - context["make_id"] = self.kwargs["make_id"] - context["model_id"] = self.kwargs["model_id"] - context["trim_id"] = self.kwargs["trim_id"] + context['query'] = self.request.GET.get('q', '') + context['make_id'] = self.kwargs['make_id'] + context['model_id'] = self.kwargs['model_id'] + context['trim_id'] = self.kwargs['trim_id'] + return context + + +class CarColorCreate(LoginRequiredMixin, CreateView): + model = models.CarColors + form_class = forms.CarColorsForm + template_name = "inventory/add_colors.html" + + def form_valid(self, form): + car = get_object_or_404(models.Car, pk=self.kwargs['car_pk']) + form.instance.car = car + return super().form_valid(form) + + def get_success_url(self): + return reverse_lazy('car_detail', kwargs={'pk': self.kwargs['car_pk']}) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['car'] = get_object_or_404(models.Car, pk=self.kwargs['car_pk']) return context @@ -331,11 +313,11 @@ def inventory_stats_view(request): # Annotate total cars by make, model, and trim cars = ( models.Car.objects.filter(dealer=dealer) - .select_related("id_car_make", "id_car_model", "id_car_trim") + .select_related('id_car_make', 'id_car_model', 'id_car_trim') .annotate( - make_total=Count("id_car_make"), - model_total=Count("id_car_model"), - trim_total=Count("id_car_trim"), + make_total=Count('id_car_make'), + model_total=Count('id_car_model'), + trim_total=Count('id_car_trim') ) ) @@ -344,128 +326,123 @@ def inventory_stats_view(request): for car in cars: # Make Level make = car.id_car_make - if make.id_car_make not in inventory: inventory[make.id_car_make] = { - "make_id": make.id_car_make, - "make_name": make.get_local_name(), - "total_cars": 0, - "models": {}, + 'make_id': make.id_car_make, + 'make_name': make.get_local_name(), + 'total_cars': 0, + 'models': {} } - inventory[make.id_car_make]["total_cars"] += 1 + inventory[make.id_car_make]['total_cars'] += 1 # Model Level model = car.id_car_model - if model and model.id_car_model not in inventory[make.id_car_make]["models"]: - inventory[make.id_car_make]["models"][model.id_car_model] = { - "model_id": model.id_car_model, - "model_name": model.get_local_name(), - "total_cars": 0, - "trims": {}, + if model and model.id_car_model not in inventory[make.id_car_make]['models']: + inventory[make.id_car_make]['models'][model.id_car_model] = { + 'model_id': model.id_car_model, + 'model_name': model.get_local_name(), + 'total_cars': 0, + 'trims': {} } - inventory[make.id_car_make]["models"][model.id_car_model]["total_cars"] += 1 + inventory[make.id_car_make]['models'][model.id_car_model]['total_cars'] += 1 # Trim Level trim = car.id_car_trim - if ( - trim - and trim.id_car_trim - not in inventory[make.id_car_make]["models"][model.id_car_model]["trims"] - ): - inventory[make.id_car_make]["models"][model.id_car_model]["trims"][ - trim.id_car_trim - ] = {"trim_id": trim.id_car_trim, "trim_name": trim.name, "total_cars": 0} - inventory[make.id_car_make]["models"][model.id_car_model]["trims"][ - trim.id_car_trim - ]["total_cars"] += 1 + if trim and trim.id_car_trim not in inventory[make.id_car_make]['models'][model.id_car_model]['trims']: + inventory[make.id_car_make]['models'][model.id_car_model]['trims'][trim.id_car_trim] = { + 'trim_id': trim.id_car_trim, + 'trim_name': trim.name, + 'total_cars': 0 + } + inventory[make.id_car_make]['models'][model.id_car_model]['trims'][trim.id_car_trim]['total_cars'] += 1 # Convert to a list for easier template rendering result = { - "total_cars": cars.count(), - "makes": [ + 'total_cars': cars.count(), + 'makes': [ { - "make_id": make_data["make_id"], - "make_name": make_data["make_name"], - "total_cars": make_data["total_cars"], - "models": [ + 'make_id': make_data['make_id'], + 'make_name': make_data['make_name'], + 'total_cars': make_data['total_cars'], + 'models': [ { - "model_id": model_data["model_id"], - "model_name": model_data["model_name"], - "total_cars": model_data["total_cars"], - "trims": list(model_data["trims"].values()), + 'model_id': model_data['model_id'], + 'model_name': model_data['model_name'], + 'total_cars': model_data['total_cars'], + 'trims': list(model_data['trims'].values()) } - for model_data in make_data["models"].values() - ], + for model_data in make_data['models'].values() + ] } for make_data in inventory.values() - ], + ] } - return render(request, "inventory/inventory_stats.html", {"inventory": result}) + return render(request, 'inventory/inventory_stats.html', {'inventory': result}) class CarDetailView(LoginRequiredMixin, DetailView): model = models.Car - template_name = "inventory/car_detail.html" - context_object_name = "car" + template_name = 'inventory/car_detail.html' + context_object_name = 'car' class CarFinanceCreateView(LoginRequiredMixin, CreateView): model = models.CarFinance form_class = forms.CarFinanceForm - template_name = "inventory/car_finance_form.html" + template_name = 'inventory/car_finance_form.html' def dispatch(self, request, *args, **kwargs): - self.car = get_object_or_404(models.Car, pk=self.kwargs["car_pk"]) + self.car = get_object_or_404(models.Car, pk=self.kwargs['car_pk']) return super().dispatch(request, *args, **kwargs) def form_valid(self, form): form.instance.car = self.car - messages.success(self.request, _("Car finance details saved successfully.")) + messages.success(self.request, _('Car finance details saved successfully.')) return super().form_valid(form) def get_success_url(self): - return reverse("car_detail", kwargs={"pk": self.car.pk}) + return reverse('car_detail', kwargs={'pk': self.car.pk}) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context["car"] = self.car + context['car'] = self.car return context class CarFinanceUpdateView(LoginRequiredMixin, UpdateView): model = models.CarFinance form_class = forms.CarFinanceForm - template_name = "inventory/car_finance_form.html" + template_name = 'inventory/car_finance_form.html' def form_valid(self, form): - messages.success(self.request, _("Car finance updated successfully.")) + messages.success(self.request, _('Car finance updated successfully.')) return super().form_valid(form) def get_success_url(self): - return reverse("car_detail", kwargs={"pk": self.object.car.pk}) + return reverse('car_detail', kwargs={'pk': self.object.car.pk}) class CarUpdateView(LoginRequiredMixin, UpdateView): model = models.Car form_class = forms.CarUpdateForm - template_name = "inventory/car_edit.html" + template_name = 'inventory/car_edit.html' def form_valid(self, form): - messages.success(self.request, _("Car updated successfully.")) + messages.success(self.request, _('Car updated successfully.')) return super().form_valid(form) def get_success_url(self): - return reverse("car_detail", kwargs={"pk": self.object.pk}) + return reverse('car_detail', kwargs={'pk': self.object.pk}) class CarDeleteView(LoginRequiredMixin, DeleteView): model = models.Car - template_name = "inventory/car_confirm_delete.html" - success_url = reverse_lazy("inventory_stats") + template_name = 'inventory/car_confirm_delete.html' + success_url = reverse_lazy('inventory_stats') def delete(self, request, *args, **kwargs): - messages.success(request, _("Car deleted successfully.")) + messages.success(request, _('Car deleted successfully.')) return super().delete(request, *args, **kwargs) @@ -475,156 +452,18 @@ class CustomCardCreateView(LoginRequiredMixin, CreateView): template_name = "inventory/add_custom_card.html" def form_valid(self, form): - car = get_object_or_404(models.Car, pk=self.kwargs["car_pk"]) + car = get_object_or_404(models.Car, pk=self.kwargs['car_pk']) form.instance.car = car return super().form_valid(form) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context["car"] = get_object_or_404(models.Car, pk=self.kwargs["car_pk"]) + context['car'] = get_object_or_404(models.Car, pk=self.kwargs['car_pk']) return context def get_success_url(self): messages.success(self.request, _("Custom Card added successfully.")) - return reverse_lazy("car_detail", kwargs={"pk": self.kwargs["car_pk"]}) - - -class CarColorCreateView(LoginRequiredMixin, CreateView): - model = models.CarColors - template_name = "inventory/color_palette.html" - - def dispatch(self, request, *args, **kwargs): - self.car = get_object_or_404(models.Car, pk=self.kwargs["car_pk"]) - self.available_colors = self.fetch_available_colors() - return super().dispatch(request, *args, **kwargs) - - def get_form_class(self): - class ColorPickerForm(ModelForm): - color = ChoiceField( - choices=self.get_color_choices(), - widget=RadioSelect(attrs={"class": "color-picker"}), - label=_("Select a Color"), - ) - color_type = ChoiceField( - choices=models.CarColors.ColorType.choices, - widget=RadioSelect(attrs={"class": "color-type-picker"}), - label=_("Select Color Type"), - ) - - class Meta: - model = models.CarColors - fields = ["color", "color_type"] - - return ColorPickerForm - - def fetch_available_colors(self): - car_data = { - "make": self.car.id_car_make.name, - "model": self.car.id_car_model.name, - "year": str(self.car.year), - } - return fetch_colors(car_data) or [] - - def get_color_choices(self): - return [(color["rgb"], color["name"]) for color in self.available_colors] - - def form_valid(self, form): - selected_rgb = form.cleaned_data["color"] - selected_name = next( - ( - color["name"] - for color in self.available_colors - if color["rgb"] == selected_rgb - ), - None, - ) - - if not selected_name: - messages.error(self.request, _("Invalid color selection.")) - return self.form_invalid(form) - - # Assign the car and selected color details - form.instance.car = self.car - form.instance.rgb = selected_rgb - form.instance.name = selected_name - form.instance.arabic_name = translate(selected_name) - form.instance.color_type = form.cleaned_data["color_type"] - - messages.success(self.request, _("Color added successfully.")) - return super().form_valid(form) - - def get_success_url(self): - return reverse("car_detail", kwargs={"pk": self.car.pk}) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["car"] = self.car - return context - - -class CarColorUpdateView(LoginRequiredMixin, UpdateView): - model = forms.CarColors - template_name = "inventory/color_palette.html" - - def dispatch(self, request, *args, **kwargs): - self.car = get_object_or_404(models.Car, pk=self.kwargs["car_pk"]) - self.available_colors = self.fetch_available_colors() - return super().dispatch(request, *args, **kwargs) - - def get_form_class(self): - class ColorPickerForm(ModelForm): - color = ChoiceField( - choices=self.get_color_choices(), - widget=RadioSelect(attrs={"class": "color-picker"}), - label=_("Select a Color"), - ) - - class Meta: - model = forms.CarColors - fields = ["color"] - - return ColorPickerForm - - def fetch_available_colors(self): - car_data = { - "make": self.car.id_car_make.name, - "model": self.car.id_car_model.name, - "year": str(self.car.year), - } - return fetch_colors(car_data) or [] - - def get_color_choices(self): - return [(color["rgb"], color["name"]) for color in self.available_colors] - - def form_valid(self, form): - selected_rgb = form.cleaned_data["color"] - selected_name = next( - ( - color["name"] - for color in self.available_colors - if color["rgb"] == selected_rgb - ), - None, - ) - - if not selected_name: - messages.error(self.request, _("Invalid color selection.")) - return self.form_invalid(form) - - form.instance.rgb = selected_rgb - form.instance.name = selected_name - form.instance.arabic_name = translate(selected_name) - - messages.success(self.request, _("Exterior color updated successfully.")) - return super().form_valid(form) - - def get_success_url(self): - return reverse("car_detail", kwargs={"pk": self.car.pk}) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["car"] = self.car - return context + return reverse_lazy('car_detail', kwargs={'pk': self.kwargs['car_pk']}) @login_required() @@ -633,28 +472,26 @@ def reserve_car_view(request, car_id): car = get_object_or_404(models.Car, pk=car_id) if car.is_reserved(): messages.error(request, _("This car is already reserved.")) - return redirect("car_detail", pk=car.pk) + return redirect('car_detail', pk=car.pk) try: reserved_until = timezone.now() + timezone.timedelta(hours=24) models.CarReservation.objects.create( - car=car, reserved_by=request.user, reserved_until=reserved_until + car=car, + reserved_by=request.user, + reserved_until=reserved_until ) messages.success(request, _("Car reserved successfully.")) except Exception as e: messages.error(request, f"Error reserving car: {e}") - return redirect("car_detail", pk=car.pk) - return JsonResponse( - {"success": False, "message": "Invalid request method."}, status=400 - ) + return redirect('car_detail', pk=car.pk) + return JsonResponse({"success": False, "message": "Invalid request method."}, status=400) @login_required def manage_reservation(request, reservation_id): - reservation = get_object_or_404( - models.CarReservation, pk=reservation_id, reserved_by=request.user - ) + reservation = get_object_or_404(models.CarReservation, pk=reservation_id, reserved_by=request.user) if request.method == "POST": action = request.POST.get("action") @@ -662,109 +499,105 @@ def manage_reservation(request, reservation_id): reservation.reserved_until = timezone.now() + timezone.timedelta(hours=24) reservation.save() messages.success(request, _("Reservation renewed successfully.")) - return redirect("car_detail", pk=reservation.car.pk) + return redirect('car_detail', pk=reservation.car.pk) elif action == "cancel": reservation.delete() messages.success(request, _("Reservation canceled successfully.")) - return redirect("car_detail", pk=reservation.car.pk) + return redirect('car_detail', pk=reservation.car.pk) else: - return JsonResponse( - {"success": False, "message": _("Invalid action.")}, status=400 - ) + return JsonResponse({"success": False, "message": _("Invalid action.")}, status=400) - return JsonResponse( - {"success": False, "message": _("Invalid request method.")}, status=400 - ) + return JsonResponse({"success": False, "message": _("Invalid request method.")}, status=400) class DealerListView(LoginRequiredMixin, ListView): model = models.Dealer - template_name = "dealer_list.html" - context_object_name = "dealers" + template_name = 'dealer_list.html' + context_object_name = 'dealers' class DealerDetailView(LoginRequiredMixin, DetailView): model = models.Dealer - template_name = "dealers/dealer_detail.html" - context_object_name = "dealer" + template_name = 'dealers/dealer_detail.html' + context_object_name = 'dealer' class DealerCreateView(LoginRequiredMixin, CreateView): model = models.Dealer form_class = forms.DealerForm - template_name = "dealer_form.html" - success_url = reverse_lazy("dealer_list") + template_name = 'dealer_form.html' + success_url = reverse_lazy('dealer_list') def form_valid(self, form): - messages.success(self.request, _("Dealer created successfully.")) + messages.success(self.request, _('Dealer created successfully.')) return super().form_valid(form) class DealerUpdateView(LoginRequiredMixin, UpdateView): model = models.Dealer form_class = forms.DealerForm - template_name = "dealers/dealer_form.html" - success_url = reverse_lazy("dealer_detail") + template_name = 'dealers/dealer_form.html' + success_url = reverse_lazy('dealer_detail') def form_valid(self, form): - messages.success(self.request, _("Dealer updated successfully.")) + messages.success(self.request, _('Dealer updated successfully.')) return super().form_valid(form) class DealerDeleteView(LoginRequiredMixin, DeleteView): model = models.Dealer - template_name = "dealer_confirm_delete.html" - success_url = reverse_lazy("dealer_list") + template_name = 'dealer_confirm_delete.html' + success_url = reverse_lazy('dealer_list') def delete(self, request, *args, **kwargs): - messages.success(request, _("Dealer deleted successfully.")) + messages.success(request, _('Dealer deleted successfully.')) return super().delete(request, *args, **kwargs) class CustomerListView(LoginRequiredMixin, ListView): model = models.Customer - home_label = _("customers") - context_object_name = "customers" + home_label = _('customers') + context_object_name = 'customers' paginate_by = 10 template_name = "customers/customer_list.html" def get_queryset(self): - query = self.request.GET.get("q") + query = self.request.GET.get('q') customers = models.Customer.objects.filter(dealer__user=self.request.user) if query: customers = customers.filter( - Q(national_id__icontains=query) - | Q(first_name__icontains=query) - | Q(last_name__icontains=query) + Q(national_id__icontains=query) | + Q(first_name__icontains=query) | + Q(last_name__icontains=query) ) return customers def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context["query"] = self.request.GET.get("q", "") + context['query'] = self.request.GET.get('q', '') return context class CustomerDetailView(LoginRequiredMixin, DetailView): model = models.Customer - template_name = "customers/view_customer.html" - context_object_name = "customer" + template_name = 'customers/view_customer.html' + context_object_name = 'customer' class CustomerCreateView(LoginRequiredMixin, CreateView): model = models.Customer form_class = forms.CustomerForm - template_name = "customers/customer_form.html" - success_url = reverse_lazy("customer_list") + template_name = 'customers/customer_form.html' + success_url = reverse_lazy('customer_list') def form_valid(self, form): if form.is_valid(): form.instance.dealer = self.request.user.dealer form.save() - messages.success(self.request, _("Customer created successfully.")) + messages.success(self.request, _('Customer created successfully.')) return super().form_valid(form) else: return form.errors @@ -773,14 +606,14 @@ class CustomerCreateView(LoginRequiredMixin, CreateView): class CustomerUpdateView(LoginRequiredMixin, UpdateView): model = models.Customer form_class = forms.CustomerForm - template_name = "customers/customer_form.html" - success_url = reverse_lazy("customer_list") + template_name = 'customers/customer_form.html' + success_url = reverse_lazy('customer_list') def form_valid(self, form): if form.is_valid(): form.instance.dealer = self.request.user.dealer form.save() - messages.success(self.request, _("Customer updated successfully.")) + messages.success(self.request, _('Customer updated successfully.')) return super().form_valid(form) else: return form.errors @@ -846,4 +679,62 @@ def delete_vendor(request, pk): return redirect('vendor_list') +class QuotationCreateView(LoginRequiredMixin, CreateView): + model = models.SaleQuotation + form_class = forms.QuotationForm + template_name = 'sales/quotation_form.html' + def form_valid(self, form): + quotation = form.save() + selected_cars = form.cleaned_data.get("cars") + for car in selected_cars: + car_finance = car.finances.first() + if car_finance: + models.SaleQuotationCar.objects.create( + quotation=quotation, + car=car, + ) + + messages.success(self.request, _("Quotation created successfully.")) + return redirect('quotation_list') + + +class QuotationListView(LoginRequiredMixin, ListView): + model = models.SaleQuotation + template_name = "sales/quotation_list.html" + context_object_name = "quotations" + paginate_by = 10 + + def get_queryset(self): + status = self.request.GET.get("status") + queryset = models.SaleQuotation.objects.all() + if status: + queryset = queryset.filter(status=status) + return queryset + + +class QuotationDetailView(LoginRequiredMixin, DetailView): + model = models.SaleQuotation + template_name = "sales/quotation_detail.html" + context_object_name = "quotation" + + +@login_required +def confirm_quotation(request, pk): + quotation = get_object_or_404(models.SaleQuotation, pk=pk) + try: + quotation.confirm() + models.SalesOrder.objects.create( + quotation=quotation, + total_amount=quotation.quotation_cars.aggregate(Sum("total_amount"))["total_amount__sum"], + ) + messages.success(request, _("Quotation confirmed and sales order created.")) + except ValueError as e: + messages.error(request, str(e)) + return redirect("quotation_detail", pk=pk) + + +class SalesOrderDetailView(LoginRequiredMixin, DetailView): + model = models.SalesOrder + template_name = "sales/sales_order_detail.html" + context_object_name = "sales_order" \ No newline at end of file diff --git a/locale/ar/LC_MESSAGES/django.mo b/locale/ar/LC_MESSAGES/django.mo index 4e8479c5..7eac67fc 100644 Binary files a/locale/ar/LC_MESSAGES/django.mo and b/locale/ar/LC_MESSAGES/django.mo differ diff --git a/locale/ar/LC_MESSAGES/django.po b/locale/ar/LC_MESSAGES/django.po index 93f97514..cdce0689 100644 --- a/locale/ar/LC_MESSAGES/django.po +++ b/locale/ar/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-12-08 16:18+0300\n" +"POT-Creation-Date: 2024-12-09 09:40+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -19,9 +19,9 @@ msgstr "" "Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " "&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" -#: api/models.py:6 inventory/models.py:122 +#: api/models.py:6 inventory/models.py:123 #: templates/inventory/car_detail.html:61 templates/inventory/car_form.html:83 -#: templates/inventory/car_inventory.html:40 +#: templates/inventory/car_inventory.html:53 #: templates/inventory/car_list.html:67 templates/inventory/car_list.html:69 msgid "VIN" msgstr "رقم الهيكل" @@ -34,47 +34,51 @@ msgstr "الإنجليزية" msgid "Arabic" msgstr "العربية" -#: inventory/forms.py:147 inventory/models.py:284 +#: inventory/forms.py:102 inventory/models.py:357 #: templates/inventory/car_detail.html:135 msgid "Custom Date" msgstr "تاريخ البطاقة الجمركية" -#: inventory/models.py:30 templates/vendors/vendors_list.html:44 +#: inventory/forms.py:151 +msgid "Both exterior and interior colors must be selected." +msgstr "يجب اختيار اللونين الخارجي والداخلي." + +#: inventory/models.py:31 templates/vendors/vendors_list.html:44 msgid "logo" msgstr "الشعار" -#: inventory/models.py:110 +#: inventory/models.py:111 #: venv/lib/python3.11/site-packages/django_ledger/templates/django_ledger/invoice/tags/invoice_item_formset.html:21 msgid "Available" msgstr "متاح" -#: inventory/models.py:111 +#: inventory/models.py:112 msgid "Sold" msgstr "تم البيع" -#: inventory/models.py:112 +#: inventory/models.py:113 msgid "Hold" msgstr "في الانتظار" -#: inventory/models.py:113 +#: inventory/models.py:114 msgid "Damaged" msgstr "تالف" -#: inventory/models.py:117 +#: inventory/models.py:118 msgid "New" msgstr "جديد" -#: inventory/models.py:118 +#: inventory/models.py:119 msgid "Used" msgstr "مستعمل" -#: inventory/models.py:127 inventory/models.py:332 +#: inventory/models.py:128 inventory/models.py:405 msgid "Dealer" msgstr "المعرض" -#: inventory/models.py:136 inventory/models.py:351 +#: inventory/models.py:137 inventory/models.py:424 #: templates/inventory/car_detail.html:108 -#: templates/inventory/car_form.html:225 +#: templates/inventory/car_form.html:230 #: venv/lib/python3.11/site-packages/django_ledger/models/bill.py:359 #: venv/lib/python3.11/site-packages/django_ledger/models/vendor.py:191 #: venv/lib/python3.11/site-packages/django_ledger/templates/django_ledger/bills/tags/bill_table.html:12 @@ -82,28 +86,28 @@ msgstr "المعرض" msgid "Vendor" msgstr "المورد" -#: inventory/models.py:144 templates/inventory/car_inventory.html:42 +#: inventory/models.py:145 templates/inventory/car_inventory.html:55 msgid "Make" msgstr "الصانع" -#: inventory/models.py:152 templates/inventory/car_inventory.html:43 +#: inventory/models.py:153 templates/inventory/car_inventory.html:56 msgid "Model" msgstr "الموديل" -#: inventory/models.py:154 templates/inventory/car_form.html:117 -#: templates/inventory/car_inventory.html:41 +#: inventory/models.py:155 templates/inventory/car_form.html:118 +#: templates/inventory/car_inventory.html:54 msgid "Year" msgstr "السنة" -#: inventory/models.py:161 templates/inventory/car_form.html:177 +#: inventory/models.py:162 templates/inventory/car_form.html:182 msgid "Series" msgstr "السلسلة" -#: inventory/models.py:169 +#: inventory/models.py:170 msgid "Trim" msgstr "الفئة" -#: inventory/models.py:175 templates/inventory/car_detail.html:86 +#: inventory/models.py:176 templates/inventory/car_detail.html:86 #: templates/inventory/car_list.html:163 #: venv/lib/python3.11/site-packages/django_ledger/templates/django_ledger/bills/tags/bill_table.html:10 #: venv/lib/python3.11/site-packages/django_ledger/templates/django_ledger/estimate/includes/card_estimate.html:12 @@ -114,171 +118,201 @@ msgstr "الفئة" msgid "Status" msgstr "الحالة" -#: inventory/models.py:181 templates/inventory/car_detail.html:90 -#: templates/inventory/car_form.html:243 templates/inventory/car_list.html:177 +#: inventory/models.py:182 templates/inventory/car_detail.html:90 +#: templates/inventory/car_form.html:248 templates/inventory/car_list.html:177 msgid "Stock Type" msgstr "نوع المخزون" -#: inventory/models.py:183 templates/inventory/car_detail.html:113 -#: templates/inventory/car_form.html:296 templates/inventory/car_list.html:200 +#: inventory/models.py:184 templates/inventory/car_detail.html:113 +#: templates/inventory/car_form.html:301 templates/inventory/car_list.html:200 msgid "Remarks" msgstr "ملاحظات" -#: inventory/models.py:184 templates/inventory/car_detail.html:94 -#: templates/inventory/car_form.html:260 templates/inventory/car_list.html:191 +#: inventory/models.py:185 templates/inventory/car_detail.html:94 +#: templates/inventory/car_form.html:265 templates/inventory/car_list.html:191 #: templates/inventory/car_list.html:192 msgid "Mileage" msgstr "عدد الكيلومترات" -#: inventory/models.py:185 templates/inventory/car_detail.html:98 -#: templates/inventory/car_form.html:278 +#: inventory/models.py:186 templates/inventory/car_detail.html:98 +#: templates/inventory/car_form.html:283 msgid "Receiving Date" msgstr "تاريخ الاستلام" -#: inventory/models.py:188 inventory/models.py:282 inventory/models.py:296 +#: inventory/models.py:189 inventory/models.py:355 inventory/models.py:369 msgid "Car" msgstr "السيارة" -#: inventory/models.py:189 +#: inventory/models.py:190 msgid "Cars" msgstr "السيارات" -#: inventory/models.py:234 templates/inventory/car_detail.html:160 -#: templates/inventory/car_finance_form.html:33 +#: inventory/models.py:237 templates/inventory/car_detail.html:160 msgid "Cost Price" msgstr "سعر التكلفة" -#: inventory/models.py:235 templates/inventory/car_detail.html:164 -#: templates/inventory/car_finance_form.html:40 +#: inventory/models.py:240 msgid "Profit Margin" msgstr "هامش الربح" -#: inventory/models.py:236 templates/inventory/car_detail.html:168 -#: templates/inventory/car_finance_form.html:54 +#: inventory/models.py:244 templates/inventory/car_detail.html:165 msgid "Selling Price" msgstr "سعر البيع" -#: inventory/models.py:237 templates/inventory/car_detail.html:172 -#: templates/inventory/car_finance_form.html:47 -msgid "VAT Rate" -msgstr "نسبة ضريبة القيمة المضافة" - -#: inventory/models.py:238 templates/inventory/car_detail.html:176 -#: templates/inventory/car_finance_form.html:58 +#: inventory/models.py:247 templates/inventory/car_detail.html:189 msgid "VAT Amount" msgstr "مبلغ ضريبة القيمة المضافة" -#: inventory/models.py:239 templates/inventory/car_finance_form.html:62 +#: inventory/models.py:251 templates/inventory/car_detail.html:173 +msgid "Registration Fee" +msgstr "رسوم التسجيل" + +#: inventory/models.py:255 templates/inventory/car_detail.html:169 +msgid "Administration Fee" +msgstr "الرسوم الادارية" + +#: inventory/models.py:259 templates/inventory/car_detail.html:177 +msgid "Transportation Fee" +msgstr "رسوم النقل" + +#: inventory/models.py:263 templates/inventory/car_detail.html:181 +msgid "Custom Card Fee" +msgstr "رسوم البطاقة الجمركية" + +#: inventory/models.py:267 templates/inventory/car_detail.html:185 +msgid "Discount Amount" +msgstr "مبلغ الخصم" + +#: inventory/models.py:271 msgid "Total Amount" msgstr "المبلغ الإجمالي" -#: inventory/models.py:242 +#: inventory/models.py:275 msgid "Car Financial Details" -msgstr "التفاصيل المالية السيارة" +msgstr "تفاصيل المالية للسيارة" -#: inventory/models.py:258 -msgid "Exterior" -msgstr "الخارجي" +#: inventory/models.py:280 +msgid "Selling price cannot be negative." +msgstr "لا يمكن أن يكون سعر البيع سالبًا." -#: inventory/models.py:259 -msgid "Interior" -msgstr "الداخلي" +#: inventory/models.py:282 +msgid "Discount amount cannot be negative." +msgstr "لا يمكن أن يكون مبلغ الخصم سالبًا." -#: inventory/models.py:262 templates/dealers/dealer_detail.html:26 +#: inventory/models.py:284 +msgid "Discount amount cannot exceed selling price." +msgstr "لا يمكن أن يتجاوز مبلغ الخصم سعر البيع." + +#: inventory/models.py:289 +msgid "Fees cannot be negative." +msgstr "لا يمكن أن تكون الرسوم سلبية." + +#: inventory/models.py:304 +#, python-format +msgid "Invalid decimal operation: %s" +msgstr "عملية عشرية غير صالحة: %s" + +#: inventory/models.py:313 inventory/models.py:326 +#: templates/dealers/dealer_detail.html:26 #: templates/vendors/view_vendor.html:39 #: venv/lib/python3.11/site-packages/django_ledger/forms/coa.py:16 #: venv/lib/python3.11/site-packages/django_ledger/forms/coa.py:37 msgid "Name" msgstr "الاسم" -#: inventory/models.py:263 inventory/models.py:325 inventory/models.py:344 +#: inventory/models.py:314 inventory/models.py:327 inventory/models.py:398 +#: inventory/models.py:417 msgid "Arabic Name" msgstr "الاسم بالعربية" -#: inventory/models.py:264 +#: inventory/models.py:315 inventory/models.py:328 msgid "RGB" msgstr "آر جي بي" -#: inventory/models.py:269 templates/inventory/color_palette.html:83 -msgid "Color Type" -msgstr "نوع اللون" +#: inventory/models.py:318 inventory/models.py:319 +msgid "Exterior Colors" +msgstr "الألوان الخارجية" -#: inventory/models.py:273 +#: inventory/models.py:331 inventory/models.py:332 +msgid "Interior Colors" +msgstr "الألوان الداخلية" + +#: inventory/models.py:345 msgid "Color" msgstr "اللون" -#: inventory/models.py:274 +#: inventory/models.py:346 msgid "Colors" msgstr "الألوان" -#: inventory/models.py:283 templates/inventory/car_detail.html:131 +#: inventory/models.py:356 templates/inventory/car_detail.html:131 msgid "Custom Number" msgstr "رقم البطاقة الجمركية" -#: inventory/models.py:287 templates/inventory/car_detail.html:16 +#: inventory/models.py:360 templates/inventory/car_detail.html:16 #: templates/inventory/car_detail.html:141 msgid "Custom Card" msgstr "البطاقة الجمركية" -#: inventory/models.py:288 +#: inventory/models.py:361 msgid "Custom Cards" msgstr "البطاقات الجمركية" -#: inventory/models.py:297 +#: inventory/models.py:370 msgid "Plate Number" msgstr "رقم اللوحة" -#: inventory/models.py:298 +#: inventory/models.py:371 msgid "Text 1" msgstr "النص 1" -#: inventory/models.py:299 +#: inventory/models.py:372 msgid "Text 2" msgstr "النص 2" -#: inventory/models.py:300 +#: inventory/models.py:373 msgid "Text 3" msgstr "النص 3" -#: inventory/models.py:301 +#: inventory/models.py:374 msgid "Registration Date" msgstr "تاريخ التسجيل" -#: inventory/models.py:304 +#: inventory/models.py:377 msgid "Registration" msgstr "التسجيل" -#: inventory/models.py:305 +#: inventory/models.py:378 msgid "Registrations" msgstr "تسجيل السيارات" -#: inventory/models.py:313 inventory/models.py:368 +#: inventory/models.py:386 inventory/models.py:441 #: venv/lib/python3.11/site-packages/django_ledger/templates/django_ledger/chart_of_accounts/includes/coa_card.html:38 #: venv/lib/python3.11/site-packages/django_ledger/templates/django_ledger/closing_entry/tags/closing_entry_table.html:12 #: venv/lib/python3.11/site-packages/django_ledger/templates/django_ledger/ledger/tags/ledgers_table.html:17 msgid "Created" msgstr "تاريخ الإنشاء" -#: inventory/models.py:314 +#: inventory/models.py:387 #: venv/lib/python3.11/site-packages/django_ledger/templates/django_ledger/chart_of_accounts/includes/coa_card.html:41 msgid "Updated" msgstr "تم التحديث" -#: inventory/models.py:323 inventory/models.py:342 +#: inventory/models.py:396 inventory/models.py:415 #: templates/dealers/dealer_detail.html:30 msgid "Commercial Registration Number" msgstr "رقم السجل التجاري" -#: inventory/models.py:324 inventory/models.py:343 +#: inventory/models.py:397 inventory/models.py:416 #: templates/dealers/dealer_detail.html:34 msgid "VAT Registration Number" msgstr "رقم التسجيل في ضريبة القيمة المضافة" -#: inventory/models.py:326 inventory/models.py:345 +#: inventory/models.py:399 inventory/models.py:418 msgid "English Name" msgstr "الاسم بالإنجليزية" -#: inventory/models.py:327 inventory/models.py:347 inventory/models.py:366 +#: inventory/models.py:400 inventory/models.py:420 inventory/models.py:439 #: templates/customers/view_customer.html:53 #: templates/dealers/dealer_detail.html:38 #: templates/vendors/view_vendor.html:44 @@ -286,7 +320,7 @@ msgstr "الاسم بالإنجليزية" msgid "Phone Number" msgstr "رقم الهاتف" -#: inventory/models.py:328 inventory/models.py:348 inventory/models.py:367 +#: inventory/models.py:401 inventory/models.py:421 inventory/models.py:440 #: templates/customers/view_customer.html:54 #: templates/dealers/dealer_detail.html:42 #: templates/vendors/view_vendor.html:46 @@ -295,47 +329,47 @@ msgstr "رقم الهاتف" msgid "Address" msgstr "العنوان" -#: inventory/models.py:329 +#: inventory/models.py:402 msgid "Logo" msgstr "الشعار" -#: inventory/models.py:333 +#: inventory/models.py:406 msgid "Dealers" msgstr "المعارض" -#: inventory/models.py:346 templates/vendors/view_vendor.html:43 +#: inventory/models.py:419 templates/vendors/view_vendor.html:43 msgid "Contact Person" msgstr "الشخص المسؤول" -#: inventory/models.py:352 templates/header.html:85 templates/header.html:100 +#: inventory/models.py:425 templates/header.html:85 templates/header.html:100 #: templates/vendors/vendor_form.html:4 templates/vendors/vendors_list.html:4 #: templates/vendors/vendors_list.html:5 msgid "Vendors" msgstr "الموردين" -#: inventory/models.py:361 templates/customers/view_customer.html:46 +#: inventory/models.py:434 templates/customers/view_customer.html:46 msgid "First Name" msgstr "الاسم الأول" -#: inventory/models.py:362 templates/customers/view_customer.html:47 +#: inventory/models.py:435 templates/customers/view_customer.html:47 msgid "Middle Name" msgstr "اسم الأب" -#: inventory/models.py:363 templates/customers/view_customer.html:48 +#: inventory/models.py:436 templates/customers/view_customer.html:48 msgid "Last Name" msgstr "اسم العائلة" -#: inventory/models.py:364 templates/customers/view_customer.html:51 +#: inventory/models.py:437 templates/customers/view_customer.html:51 #: templates/vendors/view_vendor.html:45 #: venv/lib/python3.11/site-packages/django_ledger/models/mixins.py:111 msgid "Email" msgstr "البريد الإلكتروني" -#: inventory/models.py:365 templates/customers/view_customer.html:52 +#: inventory/models.py:438 templates/customers/view_customer.html:52 msgid "National ID" msgstr "رقم الهوية الوطنية" -#: inventory/models.py:371 +#: inventory/models.py:444 #: venv/lib/python3.11/site-packages/django_ledger/models/customer.py:199 #: venv/lib/python3.11/site-packages/django_ledger/models/estimate.py:252 #: venv/lib/python3.11/site-packages/django_ledger/models/invoice.py:318 @@ -344,11 +378,11 @@ msgstr "رقم الهوية الوطنية" msgid "Customer" msgstr "العميل" -#: inventory/models.py:372 +#: inventory/models.py:445 msgid "Customers" msgstr "العملاء" -#: inventory/models.py:394 +#: inventory/models.py:467 #: venv/lib/python3.11/site-packages/django_ledger/models/accounts.py:436 #: venv/lib/python3.11/site-packages/django_ledger/models/coa.py:152 msgid "Chart of Accounts" @@ -370,116 +404,96 @@ msgstr "نسيت كلمة المرور؟" msgid "You are not associated with any dealer." msgstr "أنت غير مرتبط بأي معرض." -#: inventory/views.py:248 templates/header.html:33 templates/index.html:20 +#: inventory/views.py:260 templates/header.html:33 templates/index.html:20 #: templates/inventory/car_inventory.html:5 #: templates/inventory/car_inventory.html:7 msgid "inventory" msgstr "المخزون" -#: inventory/views.py:370 +#: inventory/views.py:401 msgid "Car finance details saved successfully." msgstr "تم حفظ تفاصيل المالية للسيارة بنجاح." -#: inventory/views.py:388 +#: inventory/views.py:419 msgid "Car finance updated successfully." msgstr "تم تحديث التفاصيل المالية للسيارة بنجاح." -#: inventory/views.py:401 +#: inventory/views.py:432 msgid "Car updated successfully." msgstr "تم تحديث السيارة بنجاح" -#: inventory/views.py:414 +#: inventory/views.py:445 msgid "Car deleted successfully." msgstr "تم حذف السيارة بنجاح." -#: inventory/views.py:434 +#: inventory/views.py:465 msgid "Custom Card added successfully." msgstr "تم إضافة البطاقة الجمركية بنجاح." -#: inventory/views.py:452 inventory/views.py:521 -msgid "Select a Color" -msgstr "اختر اللون" - -#: inventory/views.py:457 -msgid "Select Color Type" -msgstr "حدد نوع اللون" - -#: inventory/views.py:485 inventory/views.py:548 -msgid "Invalid color selection." -msgstr "تحديد اللون غير صالح." - -#: inventory/views.py:495 -msgid "Color added successfully." -msgstr "تمت إضافة اللون بنجاح." - -#: inventory/views.py:555 -msgid "Exterior color updated successfully." -msgstr "تم تحديث اللون الخارجي بنجاح." - -#: inventory/views.py:572 +#: inventory/views.py:474 msgid "This car is already reserved." msgstr "هذه السيارة محجوزة بالفعل." -#: inventory/views.py:582 +#: inventory/views.py:484 msgid "Car reserved successfully." msgstr "تم حجز السيارة بنجاح." -#: inventory/views.py:599 +#: inventory/views.py:501 msgid "Reservation renewed successfully." msgstr "تم تجديد الحجز بنجاح" -#: inventory/views.py:604 +#: inventory/views.py:506 msgid "Reservation canceled successfully." msgstr "تم إلغاء الحجز بنجاح." -#: inventory/views.py:608 +#: inventory/views.py:510 msgid "Invalid action." msgstr "إجراء غير صالح." -#: inventory/views.py:610 +#: inventory/views.py:512 msgid "Invalid request method." msgstr "طريقة الطلب غير صالحة" -#: inventory/views.py:632 +#: inventory/views.py:534 msgid "Dealer created successfully." msgstr "تم إنشاء المعرض بنجاح." -#: inventory/views.py:643 +#: inventory/views.py:545 msgid "Dealer updated successfully." msgstr "تم تحديث المعرض بنجاح." -#: inventory/views.py:653 +#: inventory/views.py:555 msgid "Dealer deleted successfully." msgstr "تم حذف المعرض بنجاح." -#: inventory/views.py:659 templates/customers/customer_form.html:4 +#: inventory/views.py:561 templates/customers/customer_form.html:4 #: templates/customers/customer_list.html:5 #: templates/customers/customer_list.html:6 templates/header.html:59 #: templates/header.html:74 msgid "customers" msgstr "العملاء" -#: inventory/views.py:698 +#: inventory/views.py:600 msgid "Customer created successfully." msgstr "تم إنشاء العميل بنجاح." -#: inventory/views.py:714 +#: inventory/views.py:616 msgid "Customer updated successfully." msgstr "تم تحديث العميل بنجاح." -#: inventory/views.py:724 +#: inventory/views.py:626 msgid "Customer deleted successfully." msgstr "تم حذف العميل بنجاح." -#: inventory/views.py:750 +#: inventory/views.py:652 msgid "Vendor created successfully." msgstr "تم إنشاء المورد بنجاح." -#: inventory/views.py:766 +#: inventory/views.py:668 msgid "Vendor updated successfully." msgstr "تم تحديث المورد بنجاح" -#: inventory/views.py:776 +#: inventory/views.py:678 msgid "Vendor deleted successfully." msgstr "تم حذف المورد بنجاح." @@ -583,8 +597,10 @@ msgid "Add Customer" msgstr "إضافة عميل" #: templates/customers/customer_form.html:31 +#: templates/inventory/add_colors.html:54 #: templates/inventory/add_custom_card.html:7 -#: templates/inventory/car_edit.html:41 templates/inventory/car_form.html:320 +#: templates/inventory/car_edit.html:41 +#: templates/inventory/car_finance_form.html:40 #: templates/inventory/color_palette.html:106 #: templates/vendors/vendor_form.html:31 #: venv/lib/python3.11/site-packages/django_ledger/templates/django_ledger/bills/tags/bill_item_formset.html:81 @@ -598,10 +614,11 @@ msgid "Save" msgstr "حفظ" #: templates/customers/customer_form.html:33 +#: templates/inventory/add_colors.html:55 #: templates/inventory/add_custom_card.html:8 #: templates/inventory/car_confirm_delete.html:14 -#: templates/inventory/car_detail.html:266 -#: templates/inventory/car_finance_form.html:68 +#: templates/inventory/car_detail.html:282 +#: templates/inventory/car_finance_form.html:41 #: templates/inventory/color_palette.html:107 #: templates/inventory/reserve_car.html:30 #: templates/vendors/vendor_form.html:33 @@ -616,7 +633,7 @@ msgid "Cancel" msgstr "إلغاء" #: templates/customers/customer_list.html:20 -#: templates/inventory/car_inventory.html:29 +#: templates/inventory/car_inventory.html:31 #: templates/inventory/car_list.html:70 templates/vendors/vendors_list.html:21 msgid "search" msgstr "بحث" @@ -644,7 +661,7 @@ msgstr "الإجراءات" #: templates/customers/customer_list.html:59 #: templates/inventory/car_detail.html:124 -#: templates/inventory/car_inventory.html:62 +#: templates/inventory/car_inventory.html:75 #: templates/vendors/vendors_list.html:65 msgid "view" msgstr "عرض" @@ -674,8 +691,7 @@ msgid "Customer Details" msgstr "تفاصيل العميل" #: templates/customers/view_customer.html:61 -#: templates/inventory/car_detail.html:217 -#: templates/inventory/car_detail.html:291 +#: templates/inventory/car_detail.html:307 #: templates/vendors/view_vendor.html:48 #: venv/lib/python3.11/site-packages/django_ledger/templates/django_ledger/customer/includes/card_customer.html:28 #: venv/lib/python3.11/site-packages/django_ledger/templates/django_ledger/journal_entry/tags/je_table.html:83 @@ -717,7 +733,7 @@ msgid "Delete" msgstr "حذف" #: templates/customers/view_customer.html:72 -#: templates/inventory/car_detail.html:293 +#: templates/inventory/car_detail.html:309 msgid "Back to List" msgstr "العودة إلى القائمة" @@ -941,7 +957,7 @@ msgid "View your best-selling cars." msgstr "عرض السيارات الأكثر مبيعاً." #: templates/index.html:151 templates/inventory/car_detail.html:74 -#: templates/inventory/car_form.html:155 templates/inventory/car_list.html:97 +#: templates/inventory/car_form.html:159 templates/inventory/car_list.html:97 msgid "model" msgstr "الموديل" @@ -1000,13 +1016,31 @@ msgstr "الجمعة" msgid "Sat" msgstr "السبت" +#: templates/inventory/add_colors.html:5 +msgid "Add Colors" +msgstr "إضافة لون" + +#: templates/inventory/add_colors.html:6 +msgid "Select exterior and interior colors for" +msgstr "اختر الألوان الخارجية والداخلية لـ" + +#: templates/inventory/add_colors.html:13 +#: templates/inventory/car_detail.html:219 +msgid "Exterior" +msgstr "الخارجي" + +#: templates/inventory/add_colors.html:32 +#: templates/inventory/car_detail.html:228 +msgid "Interior" +msgstr "الداخلي" + #: templates/inventory/car_detail.html:6 templates/inventory/car_detail.html:58 msgid "Car Details" msgstr "تفاصيل السيارة" #: templates/inventory/car_detail.html:37 #: templates/inventory/car_detail.html:117 templates/inventory/car_form.html:37 -#: templates/inventory/car_form.html:316 templates/inventory/car_list.html:47 +#: templates/inventory/car_form.html:322 templates/inventory/car_list.html:47 #: templates/inventory/car_list.html:221 msgid "specifications" msgstr "المواصفات" @@ -1015,7 +1049,7 @@ msgstr "المواصفات" msgid "year" msgstr "السنة" -#: templates/inventory/car_detail.html:69 templates/inventory/car_form.html:137 +#: templates/inventory/car_detail.html:69 templates/inventory/car_form.html:140 #: templates/inventory/car_list.html:79 msgid "make" msgstr "الصانع" @@ -1024,7 +1058,7 @@ msgstr "الصانع" msgid "series" msgstr "السلسلة" -#: templates/inventory/car_detail.html:82 templates/inventory/car_form.html:199 +#: templates/inventory/car_detail.html:82 templates/inventory/car_form.html:204 #: templates/inventory/car_list.html:141 msgid "trim" msgstr "الفئة" @@ -1041,7 +1075,7 @@ msgstr "إضافة" msgid "Financial Details" msgstr "التفاصيل المالية" -#: templates/inventory/car_detail.html:180 +#: templates/inventory/car_detail.html:193 #: templates/inventory/inventory_stats.html:61 #: venv/lib/python3.11/site-packages/django_ledger/templates/django_ledger/bills/bill_detail.html:98 #: venv/lib/python3.11/site-packages/django_ledger/templates/django_ledger/bills/bill_detail.html:127 @@ -1060,44 +1094,44 @@ msgstr "التفاصيل المالية" msgid "Total" msgstr "الإجمالي" -#: templates/inventory/car_detail.html:187 +#: templates/inventory/car_detail.html:200 msgid "Edit Finance Details" msgstr "تعديل التفاصيل المالية" -#: templates/inventory/car_detail.html:191 +#: templates/inventory/car_detail.html:204 msgid "No finance details available." msgstr "لا توجد تفاصيل مالية متاحة." -#: templates/inventory/car_detail.html:194 +#: templates/inventory/car_detail.html:207 msgid "Add Finance Details" msgstr "إضافة التفاصيل المالية" -#: templates/inventory/car_detail.html:200 +#: templates/inventory/car_detail.html:213 msgid "Colors Details" msgstr "تفاصيل الألوان" -#: templates/inventory/car_detail.html:224 +#: templates/inventory/car_detail.html:240 msgid "No colors available for this car." msgstr "لا تتوفر ألوان لهذه السيارة." -#: templates/inventory/car_detail.html:231 +#: templates/inventory/car_detail.html:247 msgid "Get Colors" msgstr "الحصول على الألوان" -#: templates/inventory/car_detail.html:238 +#: templates/inventory/car_detail.html:254 msgid "Reservations Details" msgstr "تفاصيل الحجز" -#: templates/inventory/car_detail.html:243 +#: templates/inventory/car_detail.html:259 msgid "Reserved By" msgstr "محجوز بواسطة" -#: templates/inventory/car_detail.html:244 +#: templates/inventory/car_detail.html:260 msgid "Expires At" msgstr "ينتهي في" -#: templates/inventory/car_detail.html:245 -#: templates/inventory/car_inventory.html:44 +#: templates/inventory/car_detail.html:261 +#: templates/inventory/car_inventory.html:57 #: venv/lib/python3.11/site-packages/django_ledger/templates/django_ledger/account/tags/account_txs_table.html:29 #: venv/lib/python3.11/site-packages/django_ledger/templates/django_ledger/account/tags/accounts_table.html:29 #: venv/lib/python3.11/site-packages/django_ledger/templates/django_ledger/account/tags/accounts_table.html:92 @@ -1124,27 +1158,27 @@ msgstr "ينتهي في" msgid "Actions" msgstr "الإجراءات" -#: templates/inventory/car_detail.html:260 +#: templates/inventory/car_detail.html:276 msgid "renew" msgstr "تجديد" -#: templates/inventory/car_detail.html:277 +#: templates/inventory/car_detail.html:293 #: templates/inventory/reserve_car.html:29 msgid "Reserve" msgstr "حجز" -#: templates/inventory/car_detail.html:289 +#: templates/inventory/car_detail.html:305 #: templates/inventory/transfer_car.html:23 msgid "transfer" msgstr "نقل" -#: templates/inventory/car_detail.html:378 +#: templates/inventory/car_detail.html:394 #: templates/inventory/car_list.html:542 #: templates/partials/specifications_modal.html:11 msgid "No specifications available." msgstr "لا توجد مواصفات متاحة." -#: templates/inventory/car_detail.html:382 +#: templates/inventory/car_detail.html:398 #: templates/inventory/car_list.html:546 msgid "Error loading specifications." msgstr "حدث خطأ أثناء تحميل المواصفات." @@ -1153,30 +1187,14 @@ msgstr "حدث خطأ أثناء تحميل المواصفات." msgid "Edit Car" msgstr "تعديل السيارة" -#: templates/inventory/car_finance_form.html:5 +#: templates/inventory/car_finance_form.html:6 msgid "Car Finance Details" msgstr "التفاصيل المالية السيارة" -#: templates/inventory/car_finance_form.html:9 +#: templates/inventory/car_finance_form.html:10 msgid "Finance Details for" msgstr "التفاصيل المالية لـ" -#: templates/inventory/car_finance_form.html:36 -msgid "Please provide a valid cost price." -msgstr "يرجى تقديم سعر تكلفة صالح." - -#: templates/inventory/car_finance_form.html:43 -msgid "Please provide a profit margin between 0 and 100." -msgstr "يجب أن يكون هامش الربح بين 0 و 100" - -#: templates/inventory/car_finance_form.html:50 -msgid "Please provide a valid VAT rate." -msgstr "يرجى تقديم معدل صالح لضريبة القيمة المضافة." - -#: templates/inventory/car_finance_form.html:67 -msgid "Save Finance Details" -msgstr "حفظ التفاصيل المالية" - #: templates/inventory/car_form.html:42 templates/inventory/car_form.html:58 #: templates/partials/scanner_modal.html:6 #: templates/partials/specifications_modal.html:8 @@ -1190,7 +1208,7 @@ msgstr "الماسح الضوئي" #: templates/inventory/car_form.html:62 #: templates/partials/scanner_modal.html:10 msgid "VIN will appear here." -msgstr "" +msgstr "رقم الهيكل سيظهر هنا." #: templates/inventory/car_form.html:63 #: templates/partials/scanner_modal.html:11 @@ -1201,34 +1219,42 @@ msgstr "التعرف الآلي على الحروف" msgid "Search" msgstr "بحث" -#: templates/inventory/car_form.html:160 templates/inventory/car_form.html:182 -#: templates/inventory/car_form.html:204 templates/inventory/car_form.html:498 -#: templates/inventory/car_form.html:515 templates/inventory/car_form.html:516 -#: templates/inventory/car_form.html:534 +#: templates/inventory/car_form.html:165 templates/inventory/car_form.html:187 +#: templates/inventory/car_form.html:209 templates/inventory/car_form.html:519 +#: templates/inventory/car_form.html:536 templates/inventory/car_form.html:537 +#: templates/inventory/car_form.html:555 msgid "Select" msgstr "اختيار" -#: templates/inventory/car_form.html:381 +#: templates/inventory/car_form.html:329 +msgid "Save and Add Another" +msgstr "حفظ وإضافة آخر" + +#: templates/inventory/car_form.html:335 +msgid "Save and Go to Inventory" +msgstr "حفظ والانتقال إلى المخزون" + +#: templates/inventory/car_form.html:399 msgid "Please Wait" msgstr "الرجاء الإنتظار" -#: templates/inventory/car_form.html:382 +#: templates/inventory/car_form.html:400 msgid "Loading" msgstr "تحميل" -#: templates/inventory/car_form.html:413 +#: templates/inventory/car_form.html:431 msgid "Please enter a valid VIN." msgstr "الرجاء إدخال رقم هيكل صالح مكون من 17 حرفًا." -#: templates/inventory/car_form.html:429 +#: templates/inventory/car_form.html:447 msgid "Failed to decode VIN." msgstr "فشل في فك تشفير رقم الهيكل" -#: templates/inventory/car_form.html:434 +#: templates/inventory/car_form.html:452 msgid "An error occurred while decoding the VIN." msgstr "حدث خطأ أثناء فك تشفير الهيكل" -#: templates/inventory/car_inventory.html:67 +#: templates/inventory/car_inventory.html:80 msgid "No cars available." msgstr "لا توجد سيارات متاحة." @@ -1294,6 +1320,10 @@ msgstr "تحديث اللون" msgid "Add Color for" msgstr "إضافة لون الى" +#: templates/inventory/color_palette.html:83 +msgid "Color Type" +msgstr "نوع اللون" + #: templates/inventory/inventory_stats.html:5 msgid "Inventory Statistics" msgstr "إحصائيات المخزون" diff --git a/static/.DS_Store b/static/.DS_Store index 7acca17a..804c8de6 100644 Binary files a/static/.DS_Store and b/static/.DS_Store differ diff --git a/static/images/.DS_Store b/static/images/.DS_Store index 496b795d..5884e201 100644 Binary files a/static/images/.DS_Store and b/static/images/.DS_Store differ diff --git a/static/images/logos/.DS_Store b/static/images/logos/.DS_Store index 15f777ee..4ba9080d 100644 Binary files a/static/images/logos/.DS_Store and b/static/images/logos/.DS_Store differ diff --git a/static/images/logos/Alamjdouie-Hyundai-Auto.png b/static/images/logos/Alamjdouie-Hyundai-Auto.png new file mode 100644 index 00000000..acaec6fc Binary files /dev/null and b/static/images/logos/Alamjdouie-Hyundai-Auto.png differ diff --git a/static/images/logos/suppliers/.DS_Store b/static/images/logos/suppliers/.DS_Store new file mode 100644 index 00000000..3c7e5847 Binary files /dev/null and b/static/images/logos/suppliers/.DS_Store differ diff --git a/static/images/logos/suppliers/Alamjdouie-Hyundai-01.png b/static/images/logos/suppliers/Alamjdouie-Hyundai-01.png new file mode 100644 index 00000000..67e8deb9 Binary files /dev/null and b/static/images/logos/suppliers/Alamjdouie-Hyundai-01.png differ diff --git a/static/images/logos/suppliers/Alamjdouie-Hyundai-Auto_١.png b/static/images/logos/suppliers/Alamjdouie-Hyundai-Auto_١.png new file mode 100644 index 00000000..02e88367 Binary files /dev/null and b/static/images/logos/suppliers/Alamjdouie-Hyundai-Auto_١.png differ diff --git a/static/images/logos/suppliers/Aljomaih-Automotive-Company-2.png b/static/images/logos/suppliers/Aljomaih-Automotive-Company-2.png new file mode 100644 index 00000000..ea22bb09 Binary files /dev/null and b/static/images/logos/suppliers/Aljomaih-Automotive-Company-2.png differ diff --git a/static/images/logos/suppliers/Aljomaih-Automotive-Company-3.png b/static/images/logos/suppliers/Aljomaih-Automotive-Company-3.png new file mode 100644 index 00000000..26e8ac35 Binary files /dev/null and b/static/images/logos/suppliers/Aljomaih-Automotive-Company-3.png differ diff --git a/static/images/logos/users/marwan-company_b9ATueB.png b/static/images/logos/users/marwan-company_b9ATueB.png new file mode 100644 index 00000000..b8addbe8 Binary files /dev/null and b/static/images/logos/users/marwan-company_b9ATueB.png differ diff --git a/static/images/logos/vendors/Alamjdouie-Hyundai-01.png b/static/images/logos/vendors/Alamjdouie-Hyundai-01.png new file mode 100644 index 00000000..67e8deb9 Binary files /dev/null and b/static/images/logos/vendors/Alamjdouie-Hyundai-01.png differ diff --git a/static/images/logos/vendors/Alamjdouie-Hyundai-Auto.png b/static/images/logos/vendors/Alamjdouie-Hyundai-Auto.png new file mode 100644 index 00000000..2e2317be Binary files /dev/null and b/static/images/logos/vendors/Alamjdouie-Hyundai-Auto.png differ diff --git a/static/images/logos/vendors/Alamjdouie-Hyundai-Auto_3Z7TlPQ.png b/static/images/logos/vendors/Alamjdouie-Hyundai-Auto_3Z7TlPQ.png new file mode 100644 index 00000000..2e2317be Binary files /dev/null and b/static/images/logos/vendors/Alamjdouie-Hyundai-Auto_3Z7TlPQ.png differ diff --git a/static/images/logos/vendors/Alamjdouie-Hyundai-Auto_IybmIRQ.png b/static/images/logos/vendors/Alamjdouie-Hyundai-Auto_IybmIRQ.png new file mode 100644 index 00000000..13c38e0c Binary files /dev/null and b/static/images/logos/vendors/Alamjdouie-Hyundai-Auto_IybmIRQ.png differ diff --git a/static/images/logos/vendors/Aljomaih-Automotive-Company-2.png b/static/images/logos/vendors/Aljomaih-Automotive-Company-2.png new file mode 100644 index 00000000..ea22bb09 Binary files /dev/null and b/static/images/logos/vendors/Aljomaih-Automotive-Company-2.png differ diff --git a/static/images/logos/vendors/Aljomaih-Automotive-Company-3.png b/static/images/logos/vendors/Aljomaih-Automotive-Company-3.png new file mode 100644 index 00000000..26e8ac35 Binary files /dev/null and b/static/images/logos/vendors/Aljomaih-Automotive-Company-3.png differ diff --git a/static/images/logos/vendors/Aljomaih-Automotive-Company.png b/static/images/logos/vendors/Aljomaih-Automotive-Company.png new file mode 100644 index 00000000..748b8f79 Binary files /dev/null and b/static/images/logos/vendors/Aljomaih-Automotive-Company.png differ diff --git a/static/images/logos/vendors/Aljomaih-Automotive-Company_PrrrHZs.png b/static/images/logos/vendors/Aljomaih-Automotive-Company_PrrrHZs.png new file mode 100644 index 00000000..748b8f79 Binary files /dev/null and b/static/images/logos/vendors/Aljomaih-Automotive-Company_PrrrHZs.png differ diff --git a/templates/.DS_Store b/templates/.DS_Store index 5efa45d0..71fbc5fa 100644 Binary files a/templates/.DS_Store and b/templates/.DS_Store differ diff --git a/templates/base.html b/templates/base.html index d6b62c0a..35106123 100644 --- a/templates/base.html +++ b/templates/base.html @@ -30,7 +30,10 @@ min-height: 100vh; text-transform: uppercase; } - +small, .small { + font-size: 0.9rem; + line-height: 0; +} .btn { text-transform: uppercase; } @@ -45,7 +48,7 @@ {% endblock %} - {% include 'footer.html' %} + diff --git a/templates/footer.html b/templates/footer.html deleted file mode 100644 index 5339c4b5..00000000 --- a/templates/footer.html +++ /dev/null @@ -1,9 +0,0 @@ -{% load i18n %} - - diff --git a/templates/inventory/add_colors.html b/templates/inventory/add_colors.html new file mode 100644 index 00000000..269f7525 --- /dev/null +++ b/templates/inventory/add_colors.html @@ -0,0 +1,61 @@ +{% extends "base.html" %} +{% load i18n %} +{% block content %} +
+
{% trans "Add Colors" %}
+

{% trans "Select exterior and interior colors for" %} {{ car.id_car_make.get_local_name }} {{ car.id_car_model.get_local_name }}

+ +
+ {% csrf_token %} + + +
+

{% trans 'Exterior Colors' %}

+ {% for color in form.fields.exterior.queryset %} +
+
+
+
+ + +
+
+
+
+ {% endfor %} + +

{% trans 'Interior Colors' %}

+ {% for color in form.fields.interior.queryset %} +
+
+
+
+ + +
+
+
+
+ {% endfor %} +
+ + +
+
+ + {% trans "Cancel" %} +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/inventory/car_detail.html b/templates/inventory/car_detail.html index 94e9dc91..cf8ae4a2 100644 --- a/templates/inventory/car_detail.html +++ b/templates/inventory/car_detail.html @@ -31,7 +31,7 @@ aria-hidden="true">