# from datetime import timezone from django.utils import timezone import itertools from uuid import uuid4 from django.conf import settings from django.db import models, transaction from django.db.models import Sum, F, Count from django.contrib.auth.models import User, UserManager from django.db.models.signals import pre_save, post_save from django.dispatch import receiver from django.utils.translation import gettext_lazy as _ from django_ledger.models import ( VendorModel, EntityModel, EntityUnitModel, ItemModel, AccountModel, ItemModelAbstract, UnitOfMeasureModel, CustomerModel, ItemModelQuerySet, ) from django_ledger.io.io_core import get_localdate from django.db.models import Sum from decimal import Decimal, InvalidOperation from django.core.exceptions import ValidationError from phonenumber_field.modelfields import PhoneNumberField from django.utils.timezone import now from sqlalchemy.orm.base import object_state from .utilities.financials import get_financial_value, get_total, get_total_financials from django.db.models import FloatField from .mixins import LocalizedNameMixin from django_ledger.models import EntityModel, ItemModel,EstimateModel,InvoiceModel from django_countries.fields import CountryField from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType class DealerUserManager(UserManager): def create_user_with_dealer( self, email, password, dealer_name, arabic_name, crn, vrn, address, **extra_fields, ): user = self.create_user( username=email, email=email, password=password, **extra_fields ) Dealer.objects.create( user=user, name=dealer_name, arabic_name=arabic_name, crn=crn, vrn=vrn, address=address, **extra_fields, ) return user class StaffUserManager(UserManager): def create_user_with_staff( self, email, password, name, arabic_name, phone_number, staff_type, **extra_fields, ): user = self.create_user( username=email, email=email, password=password, **extra_fields ) Staff.objects.create( user=user, name=name, arabic_name=arabic_name, phone_number=phone_number, staff_type=staff_type, **extra_fields, ) return user class UnitOfMeasure(models.TextChoices): EACH = "EA", "Each" PAIR = "PR", "Pair" SET = "SET", "Set" GALLON = "GAL", "Gallon" LITER = "L", "Liter" METER = "M", "Meter" KILOGRAM = "KG", "Kilogram" HOUR = "HR", "Hour" BOX = "BX", "Box" ROLL = "RL", "Roll" PACKAGE = "PKG", "Package" DOZEN = "DZ", "Dozen" SQUARE_METER = "SQ_M", "Square Meter" PIECE = "PC", "Piece" BUNDLE = "BDL", "Bundle" class VatRate(models.Model): rate = models.DecimalField(max_digits=5, decimal_places=2, default=Decimal("0.15")) is_active = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"Rate: {self.rate}%" class CarType(models.IntegerChoices): CAR = 1, _("Car") LIGHT_COMMERCIAL = 2, _("Light Commercial") HEAVY_DUTY_TRACTORS = 3, _("Heavy-Duty Tractors") TRAILERS = 4, _("Trailers") MEDIUM_TRUCKS = 5, _("Medium Trucks") BUSES = 6, _("Buses") MOTORCYCLES = 20, _("Motorcycles") BUGGY = 21, _("Buggy") MOTO_ATV = 22, _("Moto ATV") SCOOTERS = 23, _("Scooters") KARTING = 24, _("Karting") ATV = 25, _("ATV") SNOWMOBILES = 26, _("Snowmobiles") class CarMake(models.Model, LocalizedNameMixin): id_car_make = models.AutoField(primary_key=True) name = models.CharField(max_length=255, blank=True, null=True) arabic_name = models.CharField(max_length=255, blank=True, null=True) logo = models.ImageField(_("logo"), upload_to="car_make", blank=True, null=True) is_sa_import = models.BooleanField(default=False) car_type = models.SmallIntegerField(choices=CarType.choices, blank=True, null=True) def __str__(self): return self.name class Meta: verbose_name = "Make" 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") name = models.CharField(max_length=255, blank=True, null=True) arabic_name = models.CharField(max_length=255, blank=True, null=True) def __str__(self): return self.name class Meta: verbose_name = "Model" 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" ) name = models.CharField(max_length=255, blank=True, null=True) arabic_name = models.CharField(max_length=255, blank=True, null=True) year_begin = models.IntegerField(blank=True, null=True) year_end = models.IntegerField(blank=True, null=True) generation_name = models.CharField(max_length=255, blank=True, null=True) def __str__(self): return self.name class Meta: verbose_name = "Series" 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" ) name = models.CharField(max_length=255, blank=True, null=True) arabic_name = models.CharField(max_length=255, blank=True, null=True) start_production_year = models.IntegerField(blank=True, null=True) end_production_year = models.IntegerField(blank=True, null=True) def __str__(self): return self.name class Meta: verbose_name = "Trim" class CarEquipment(models.Model, LocalizedNameMixin): id_car_equipment = models.AutoField(primary_key=True) id_car_trim = models.ForeignKey(CarTrim, models.DO_NOTHING, db_column="id_car_trim") name = models.CharField(max_length=255, blank=True, null=True) arabic_name = models.CharField(max_length=255, blank=True, null=True) year_begin = models.IntegerField(blank=True, null=True) def __str__(self): return self.name class Meta: verbose_name = "Equipment" 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 ) def __str__(self): return self.name class Meta: verbose_name = "Specification" 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" ) value = models.CharField(max_length=500) unit = models.CharField(max_length=255, blank=True, null=True) def __str__(self): return f"{self.id_car_specification.name}: {self.value} {self.unit}" class Meta: verbose_name = "Specification Value" class CarOption(models.Model, LocalizedNameMixin): id_car_option = models.AutoField(primary_key=True) name = models.CharField(max_length=255, blank=True, null=True) arabic_name = models.CharField(max_length=255, 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 class Meta: verbose_name = "Option" class CarOptionValue(models.Model): id_car_option_value = models.AutoField(primary_key=True) id_car_option = models.ForeignKey( CarOption, models.DO_NOTHING, db_column="id_car_option" ) id_car_equipment = models.ForeignKey( CarEquipment, models.DO_NOTHING, db_column="id_car_equipment" ) value = models.CharField(max_length=500) unit = models.CharField(max_length=255, blank=True, null=True) is_base = models.IntegerField() def __str__(self): return f"{self.id_car_option.name}: {self.value} {self.unit}" class Meta: verbose_name = "Option Value" class CarTransferStatusChoices(models.TextChoices): draft = "draft", _("Draft") approved = "approved", _("Approved") pending = "pending", _("Pending") accepted = "accepted", _("Accepted") success = "success", _("Success") reject = "reject", _("Reject") cancelled = "cancelled", _("Cancelled") class CarStatusChoices(models.TextChoices): AVAILABLE = "available", _("Available") SOLD = "sold", _("Sold") HOLD = "hold", _("Hold") DAMAGED = "damaged", _("Damaged") RESERVED = "reserved", _("Reserved") TRANSFER = "transfer", _("Transfer") class CarStockTypeChoices(models.TextChoices): NEW = "new", _("New") USED = "used", _("Used") class AdditionalServices(models.Model, LocalizedNameMixin): name = models.CharField(max_length=255, verbose_name=_("Name")) arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name")) description = models.TextField(verbose_name=_("Description")) price = models.DecimalField( max_digits=14, decimal_places=2, verbose_name=_("Price") ) taxable = models.BooleanField(default=False, verbose_name=_("taxable")) uom = models.CharField( max_length=10, choices=UnitOfMeasure.choices, verbose_name=_("Unit of Measurement"), ) dealer = models.ForeignKey( "Dealer", on_delete=models.CASCADE, verbose_name=_("Dealer") ) item = models.OneToOneField( ItemModel, on_delete=models.CASCADE, verbose_name=_("Item"), null=True, blank=True, ) def to_dict(self): return { "name": self.name, "price": str(self.price), "price_": str(self.price_), "taxable": self.taxable, "uom": self.uom, } @property def price_(self): vat = VatRate.objects.filter(is_active=True).first() return Decimal(self.price + (self.price * vat.rate)) if self.taxable else self.price class Meta: verbose_name = _("Additional Services") verbose_name_plural = _("Additional Services") def __str__(self): return self.name + " - " + str(self.price) 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") ) vendor = models.ForeignKey( VendorModel, models.DO_NOTHING, null=True, blank=True, related_name="cars", verbose_name=_("Vendor"), ) id_car_make = models.ForeignKey( CarMake, models.DO_NOTHING, db_column="id_car_make", null=True, blank=True, verbose_name=_("Make"), ) id_car_model = models.ForeignKey( CarModel, models.DO_NOTHING, db_column="id_car_model", null=True, blank=True, verbose_name=_("Model"), ) year = models.IntegerField(verbose_name=_("Year")) id_car_serie = models.ForeignKey( CarSerie, models.DO_NOTHING, db_column="id_car_serie", null=True, blank=True, verbose_name=_("Series"), ) id_car_trim = models.ForeignKey( CarTrim, models.DO_NOTHING, db_column="id_car_trim", null=True, blank=True, verbose_name=_("Trim"), ) status = models.CharField( max_length=10, choices=CarStatusChoices.choices, default=CarStatusChoices.AVAILABLE, verbose_name=_("Status"), ) stock_type = models.CharField( max_length=10, choices=CarStockTypeChoices.choices, default=CarStockTypeChoices.NEW, verbose_name=_("Stock Type"), ) remarks = models.TextField(blank=True, null=True, verbose_name=_("Remarks")) mileage = models.IntegerField(blank=True, null=True, verbose_name=_("Mileage")) receiving_date = models.DateTimeField(verbose_name=_("Receiving Date")) class Meta: verbose_name = _("Car") verbose_name_plural = _("Cars") def __str__(self): make = self.id_car_make.name if self.id_car_make else "Unknown Make" model = self.id_car_model.name if self.id_car_model else "Unknown Model" trim = self.id_car_trim.name if self.id_car_trim else "Unknown Trim" return f"{self.year} - {make} - {model} - {trim}" def is_reserved(self): active_reservations = self.reservations.filter(reserved_until__gt=now()) return active_reservations.exists() def get_transfer(self): return self.transfer_logs.filter(active=True).first() @property def get_car_group(self): return f"{self.id_car_make.get_local_name} {self.id_car_model.get_local_name}" def to_dict(self): return { "vin": self.vin, "make": self.id_car_make.name if self.id_car_make else "Unknown Make", "model": self.id_car_model.name if self.id_car_model else "Unknown Model", "trim": self.id_car_trim.name if self.id_car_trim else "Unknown Trim", "year": self.year, "display_name": self.get_car_group, "status": self.status, "stock_type": self.stock_type, "remarks": self.remarks, "mileage": self.mileage, "receiving_date": self.receiving_date.strftime('%Y-%m-%d %H:%M:%S'), "id": self.id, } class CarTransfer(models.Model): car = models.ForeignKey( "Car", on_delete=models.CASCADE, related_name="transfer_logs", verbose_name=_("Car"), ) from_dealer = models.ForeignKey( "Dealer", on_delete=models.CASCADE, related_name="transfers_out", verbose_name=_("From Dealer"), ) to_dealer = models.ForeignKey( "Dealer", on_delete=models.CASCADE, related_name="transfers_in", verbose_name=_("To Dealer"), ) transfer_date = models.DateTimeField( auto_now_add=True, verbose_name=_("Transfer Date") ) quantity = models.IntegerField(verbose_name=_("Quantity"),default=1) remarks = models.TextField(blank=True, null=True, verbose_name=_("Remarks")) status = models.CharField( CarTransferStatusChoices.choices, max_length=10, default=CarTransferStatusChoices.draft, ) is_approved = models.BooleanField(default=False) active = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At")) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At")) @property def total_price(self): return self.quantity * self.car.finances.total_vat class Meta: verbose_name = _("Car Transfer Log") verbose_name_plural = _("Car Transfer Logs") def __str__(self): return f"{self.car} Transfer car from {self.from_dealer} to {self.to_dealer}" class CarReservation(models.Model): car = models.ForeignKey( "Car", on_delete=models.CASCADE, related_name="reservations", verbose_name=_("Car"), ) reserved_by = models.ForeignKey( User, on_delete=models.CASCADE, related_name="reservations", verbose_name=_("Reserved By"), ) reserved_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Reserved At")) reserved_until = models.DateTimeField(verbose_name=_("Reserved Until")) @property def is_active(self): return self.reserved_until > now() class Meta: unique_together = ("car", "reserved_until") ordering = ["-reserved_at"] verbose_name = _("Car Reservation") verbose_name_plural = _("Car Reservations") # Car Finance Model class CarFinance(models.Model): additional_services = models.ManyToManyField( AdditionalServices, related_name="additional_finances", blank=True ) car = models.OneToOneField(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") ) discount_amount = models.DecimalField( max_digits=14, decimal_places=2, verbose_name=_("Discount Amount"), default=Decimal("0.00"), ) @property def total(self): return self.selling_price @property def total_additionals(self): return sum(x.price_ for x in self.additional_services.all()) @property def total_discount(self): if self.discount_amount > 0: return self.selling_price - self.discount_amount return self.selling_price @property def total_vat(self): return round(self.total_discount + self.vat_amount + self.total_additionals,2) @property def vat_amount(self): vat = VatRate.objects.filter(is_active=True).first() if vat: return (self.total_discount * Decimal(vat.rate)).quantize(Decimal("0.01")) return Decimal("0.00") @property def revenue(self): return self.selling_price - self.cost_price def to_dict(self): return { "cost_price": str(self.cost_price), "selling_price": str(self.selling_price), "discount_amount": str(self.discount_amount), "total": str(self.total), "total_discount": str(self.total_discount), "total_vat": str(self.total_vat), "vat_amount": str(self.vat_amount), } def __str__(self): return f"Car: {self.car}, Selling Price: {self.selling_price}" # def save(self, *args, **kwargs): # self.full_clean() # try: # price_after_discount = self.selling_price - self.discount_amount # self.profit_margin = price_after_discount - self.cost_price # self.vat_amount = settings.VAT_RATE # except InvalidOperation as e: # raise ValidationError(_("Invalid decimal operation: %s") % str(e)) # super().save(*args, **kwargs) class Meta: verbose_name = _("Car Financial Details") verbose_name_plural = _("Car Financial Details") 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"{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})" class CarColors(models.Model): 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.car} ({self.exterior.name}) ({self.interior.name})" class CustomCard(models.Model): car = models.OneToOneField( 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")) class Meta: verbose_name = _("Custom Card") verbose_name_plural = _("Custom Cards") def __str__(self): return f"{self.car} - {self.custom_number}" class CarLocation(models.Model): car = models.OneToOneField( Car, on_delete=models.CASCADE, related_name="location", verbose_name=_("Car") ) owner = models.ForeignKey( "Dealer", on_delete=models.CASCADE, related_name="owned_cars", verbose_name=_("Owner"), help_text=_("Dealer who owns the car."), ) showroom = models.ForeignKey( "Dealer", on_delete=models.CASCADE, related_name="showroom_cars", verbose_name=_("Showroom"), help_text=_("Dealer where the car is displayed (can be the owner)."), ) description = models.TextField( blank=True, null=True, verbose_name=_("Description"), help_text=_("Optional description about the showroom placement."), ) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At")) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Last Updated")) class Meta: verbose_name = _("Car Location") verbose_name_plural = _("Car Locations") def __str__(self): return f"Car: {self.car}, Showroom: {self.showroom}, Owner: {self.owner}" def is_owner_showroom(self): """ Returns True if the showroom is the same as the owner. """ return self.owner == self.showroom class CarRegistration(models.Model): car = models.OneToOneField( 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"), null=True, blank=True) text3 = models.CharField(max_length=1, verbose_name=_("Text 3"), null=True, blank=True) registration_date = models.DateTimeField(verbose_name=_("Registration Date")) class Meta: verbose_name = _("Registration") verbose_name_plural = _("Registrations") def __str__(self): return f"{self.plate_number}" # TimestampedModel Abstract Class class TimestampedModel(models.Model): created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created")) updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) class Meta: abstract = True class Subscription(models.Model): plan = models.ForeignKey( "SubscriptionPlan", on_delete=models.CASCADE, related_name="subscriptions" ) start_date = models.DateField(help_text="Date when the subscription starts") end_date = models.DateField(help_text="Date when the subscription ends") users = models.ManyToManyField( User, through="SubscriptionUser" ) # many-to-many relationship with User model is_active = models.BooleanField(default=True) billing_cycle = models.CharField( max_length=10, choices=[("monthly", "Monthly"), ("annual", "Annual")], default="monthly", help_text="Billing cycle for the subscription", ) last_payment_date = models.DateField( null=True, blank=True, help_text="Date of the last payment made" ) next_payment_date = models.DateField( null=True, blank=True, help_text="Date of the next payment due" ) class Meta: verbose_name = _("Subscription") verbose_name_plural = _("Subscriptions") def __str__(self): return self.plan.name @property def total_subscribers(self): return self.users.count() class SubscriptionUser(models.Model): subscription = models.ForeignKey(Subscription, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE) class Meta: verbose_name = _("Subscription User") verbose_name_plural = _("Subscription Users") def __str__(self): return f"{self.subscription} - {self.user}" class SubscriptionPlan(models.Model): name = models.CharField( max_length=100, unique=True, help_text=_("Name of the subscription plan") ) description = models.TextField() price = models.DecimalField(max_digits=10, decimal_places=2) max_users = models.PositiveIntegerField( help_text=_("Maximum number of users allowed"), default=1 ) max_inventory_size = models.PositiveIntegerField( help_text=_("Maximum number of cars in inventory"), default=50 ) support_level = models.CharField( max_length=50, choices=[ ("basic", "Basic Support"), ("priority", "Priority Support"), ("dedicated", "Dedicated Support"), ], default="basic", help_text="Level of support provided", ) custom_features = models.JSONField( blank=True, null=True, help_text=_("Additional features specific to this plan") ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: verbose_name = _("Subscription Plan") verbose_name_plural = _("Subscription Plans") def __str__(self): return f"{self.name} - {self.price}" 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"), null=True, blank=True, ) vrn = models.CharField( max_length=15, verbose_name=_("VAT Registration Number"), null=True, blank=True ) 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") ) entity = models.ForeignKey( EntityModel, on_delete=models.SET_NULL, null=True, blank=True ) joined_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Joined At")) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At")) objects = DealerUserManager() @property def get_active_plan(self): try: return self.user.subscription_set.filter(is_active=True).first() except SubscriptionPlan.DoesNotExist: return None @property def get_plan(self): active_plan = self.get_active_plan if active_plan: subscription_plan = SubscriptionPlan.objects.filter( pk=active_plan.pk ).first() if subscription_plan: return subscription_plan return None class Meta: verbose_name = _("Dealer") verbose_name_plural = _("Dealers") # permissions = [ # ('change_dealer_type', 'Can change dealer type'), # ] def __str__(self): return self.name # @property # def get_sub_dealers(self): # if self.dealer_type == "OWNER": # return self.sub_dealers.all() # return None # # @property # def is_parent(self): # return self.dealer_type == "OWNER" # @property # def get_root_dealer(self): # return self.parent_dealer if self.parent_dealer else self ############################## # Additional staff types for later # COORDINATOR = "coordinator", _("Coordinator") # RECEPTIONIST = "receptionist", _("Receptionist") # AGENT = "agent", _("Agent") # TECHNICIAN = "technician", _("Technician") # DRIVER = "driver", _("Driver") ############################## class StaffTypes(models.TextChoices): MANAGER = "manager", _("Manager") INVENTORY = "inventory", _("Inventory") ACCOUNTANT = "accountant", _("Accountant") SALES = "sales", _("Sales") COORDINATOR = "coordinator", _("Coordinator") RECEPTIONIST = "receptionist", _("Receptionist") AGENT = "agent", _("Agent") class Staff(models.Model, LocalizedNameMixin): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="staff") dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="staff") name = models.CharField(max_length=255, verbose_name=_("Name")) arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name")) phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number")) staff_type = models.CharField( choices=StaffTypes.choices, max_length=255, verbose_name=_("Staff Type") ) created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created")) updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) objects = StaffUserManager() class Meta: verbose_name = _("Staff") verbose_name_plural = _("Staff") permissions = [] def __str__(self): return f"{self.name}" class Sources(models.TextChoices): REFERRALS = "referrals", _("Referrals") WHATSAPP = "whatsapp", _("WhatsApp") SHOWROOM = "showroom", _("Showroom") TIKTOK = "tiktok", _("TikTok") INSTAGRAM = "instagram", _("Instagram") X = "x", _("X") FACEBOOK = "facebook", _("Facebook") MOTORY = "motory", _("Motory") INFLUENCERS = "influencers", _("Influencers") YOUTUBE = "youtube", _("Youtube") CAMPAIGN = "campaign", _("Campaign") class Channel(models.TextChoices): WALK_IN = "walk_in", _("Walk In") TOLL_FREE = "toll_free", _("Toll Free") WEBSITE = "website", _("Website") EMAIL = "email", _("Email") FORM = "form", _("Form") class Status(models.TextChoices): NEW = "new", _("New") PENDING = "pending", _("Pending") IN_PROGRESS = "in_progress", _("In Progress") QUALIFIED = "qualified", _("Qualified") CANCELED = "canceled", _("Canceled") class Title(models.TextChoices): MR = "mr", _("Mr") MRS = "mrs", _("Mrs") MS = "ms", _("Ms") MISS = "miss", _("Miss") DR = "dr", _("Dr") PROF = "prof", _("Prof") PRINCE = "prince", _("Prince") PRINCESS = "princess", _("Princess") COMPANY = "company", _("Company") NA = "na", _("N/A") class ActionChoices(models.TextChoices): CALL = "call", _("Call") SMS = "sms", _("SMS") EMAIL = "email", _("Email") WHATSAPP = "whatsapp", _("WhatsApp") VISIT = "visit", _("Visit") ADD_CAR = "add_car", _("Add Car") RESERVE_CAR = "reserve_car", _("Reserve Car") REMOVE_CAR = "remove_car", _("Remove Car") CREATE_QUOTATION = "create_quotation", _("Create Quotation") CANCEL_QUOTATION = "cancel_quotation", _("Cancel Quotation") CREATE_ORDER = "create_order", _("Create Order") CANCEL_ORDER = "cancel_order", _("Cancel Order") CREATE_INVOICE = "create_invoice", _("Create Invoice") CANCEL_INVOICE = "cancel_invoice", _("Cancel Invoice") class Stage(models.TextChoices): PROSPECT = "prospect", _("Prospect") PROPOSAL = "proposal", _("Proposal") NEGOTIATION = "negotiation", _("Negotiation") CLOSED_WON = "closed_won", _("Closed Won") CLOSED_LOST = "closed_lost", _("Closed Lost") class Priority(models.TextChoices): LOW = "low", _("Low") MEDIUM = "medium", _("Medium") HIGH = "high", _("High") class Customer(models.Model): dealer = models.ForeignKey( Dealer, on_delete=models.CASCADE, related_name="customers" ) title = models.CharField( choices=Title.choices, default=Title.NA, max_length=10, verbose_name=_("Title") ) 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") ) last_name = models.CharField(max_length=50, verbose_name=_("Last Name")) gender = models.CharField( choices=[("m", _("Male")), ("f", _("Female"))], max_length=1, verbose_name=_("Gender"), ) dob = models.DateField(verbose_name=_("Date of Birth")) 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") ) created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created")) updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) class Meta: verbose_name = _("Customer") verbose_name_plural = _("Customers") def __str__(self): middle = f" {self.middle_name}" if self.middle_name else "" return f"{self.first_name}{middle} {self.last_name}" @property def get_full_name(self): return f"{self.first_name} {self.middle_name} {self.last_name}" class Organization(models.Model, LocalizedNameMixin): dealer = models.ForeignKey( Dealer, on_delete=models.CASCADE, related_name="organizations" ) name = models.CharField(max_length=255, verbose_name=_("Name")) arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name")) crn = models.CharField( max_length=15, verbose_name=_("Commercial Registration Number") ) vrn = models.CharField(max_length=15, verbose_name=_("VAT Registration Number")) 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", blank=True, null=True, verbose_name=_("Logo") ) created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created")) updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) class Meta: verbose_name = _("Organization") verbose_name_plural = _("Organizations") def __str__(self): return self.name class Representative(models.Model, LocalizedNameMixin): dealer = models.ForeignKey( Dealer, on_delete=models.CASCADE, related_name="representatives" ) name = models.CharField(max_length=255, verbose_name=_("Name")) arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name")) id_number = models.CharField( max_length=10, unique=True, verbose_name=_("ID Number") ) phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number")) email = models.EmailField(max_length=255, verbose_name=_("Email Address")) address = models.CharField( max_length=200, blank=True, null=True, verbose_name=_("Address") ) organization = models.ManyToManyField(Organization, related_name="representatives") class Meta: verbose_name = _("Representative") verbose_name_plural = _("Representatives") def __str__(self): return self.name class Lead(models.Model): dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="leads") first_name = models.CharField(max_length=50, verbose_name=_("First Name")) last_name = models.CharField(max_length=50, verbose_name=_("Last Name")) email = models.EmailField(verbose_name=_("Email")) phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number")) customer = models.ForeignKey( CustomerModel, on_delete=models.CASCADE, related_name="leads", null=True,blank=True ) car = models.ForeignKey( Car, on_delete=models.DO_NOTHING, blank=True, null=True, verbose_name=_("Car") ) # id_car_make = models.ForeignKey( # CarMake, # on_delete=models.DO_NOTHING, # blank=True, # null=True, # verbose_name=_("Make"), # ) # id_car_model = models.ForeignKey( # CarModel, # on_delete=models.DO_NOTHING, # blank=True, # null=True, # verbose_name=_("Model"), # ) # year = models.PositiveSmallIntegerField( # verbose_name=_("Year"), blank=True, null=True # ) source = models.CharField( max_length=50, choices=Sources.choices, verbose_name=_("Source") ) channel = models.CharField( max_length=50, choices=Channel.choices, verbose_name=_("Channel") ) address = models.CharField(max_length=50, verbose_name=_("address")) staff = models.ForeignKey( Staff, on_delete=models.SET_NULL, blank=True, null=True, related_name="assigned", verbose_name=_("Assigned"), ) priority = models.CharField( max_length=10, choices=Priority.choices, default=Priority.MEDIUM, verbose_name=_("Priority"), ) status = models.CharField( max_length=50, choices=Status.choices, verbose_name=_("Status"), db_index=True, default=Status.NEW, ) created = models.DateTimeField( auto_now_add=True, verbose_name=_("Created"), db_index=True ) updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) class Meta: verbose_name = _("Lead") verbose_name_plural = _("Leads") def __str__(self): return f"{self.first_name} {self.last_name}" @property def is_converted(self): return bool(self.customer) def to_dict(self): return { "first_name": str(self.first_name), "last_name": str(self.last_name), "email": str(self.email), "address": str(self.address), "phone_number": str(self.phone_number), "car": self.car.to_dict(), "created_at": str(self.created.strftime("%Y-%m-%d")), } @property def full_name(self): return f"{self.first_name} {self.last_name}" def convert_to_customer(self,entity): if entity and not entity.get_customers().filter(email=self.email).exists(): customer = entity.create_customer( customer_model_kwargs={ "customer_name": self.full_name, "address_1": self.address, "phone": self.phone_number, "email": self.email, } ) customer.additional_info = {} customer.additional_info.update({"info":self.to_dict()}) self.customer = customer customer.save() self.save() def get_latest_schedule(self): return self.schedules.order_by('-scheduled_at').first() class Schedule(models.Model): PURPOSE_CHOICES = [ ('Product Demo', 'Product Demo'), ('Follow-Up Call', 'Follow-Up Call'), ('Contract Discussion', 'Contract Discussion'), ('Sales Meeting', 'Sales Meeting'), ('Support Call', 'Support Call'), ('Other', 'Other'), ] ScheduledType = [ ('Call', 'Call'), ('Meeting', 'Meeting'), ('Email', 'Email'), ] ScheduleStatusChoices = [ ('Scheduled', 'Scheduled'), ('Completed', 'Completed'), ('Canceled', 'Canceled'), ] lead = models.ForeignKey(Lead, on_delete=models.CASCADE, related_name='schedules') customer = models.ForeignKey(CustomerModel, on_delete=models.CASCADE, related_name='schedules',null=True,blank=True) scheduled_by = models.ForeignKey(Staff, on_delete=models.CASCADE) purpose = models.CharField(max_length=200, choices=PURPOSE_CHOICES) scheduled_at = models.DateTimeField() scheduled_type = models.CharField(max_length=200, choices=ScheduledType,default='Call') notes = models.TextField(blank=True, null=True) status = models.CharField(max_length=200, choices=ScheduleStatusChoices, default='Scheduled') created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) def __str__(self): return f"Scheduled {self.purpose} with {self.customer.customer_name} on {self.scheduled_at}" def schedule_past_date(self): if self.scheduled_at < timezone.now(): return True return False class Meta: ordering = ['-scheduled_at'] class LeadStatusHistory(models.Model): lead = models.ForeignKey( Lead, on_delete=models.CASCADE, related_name="status_history" ) old_status = models.CharField( max_length=50, choices=Status.choices, verbose_name=_("Old Status") ) new_status = models.CharField( max_length=50, choices=Status.choices, verbose_name=_("New Status") ) changed_by = models.ForeignKey( Staff, on_delete=models.DO_NOTHING, related_name="status_changes" ) changed_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Changed At")) class Meta: verbose_name = _("Lead Status History") verbose_name_plural = _("Lead Status Histories") def __str__(self): return f"{self.lead}: {self.old_status} → {self.new_status}" def validate_probability(value): if value < 0 or value > 100: raise ValidationError(_("Probability must be between 0 and 100.")) class Opportunity(models.Model): dealer = models.ForeignKey( Dealer, on_delete=models.CASCADE, related_name="opportunities" ) customer = models.ForeignKey( CustomerModel, on_delete=models.CASCADE, related_name="opportunities" ) car = models.ForeignKey( Car, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_("Car") ) stage = models.CharField( max_length=20, choices=Stage.choices, verbose_name=_("Stage") ) status = models.CharField( max_length=20, choices=Status.choices, verbose_name=_("Status"), default=Status.NEW, ) staff = models.ForeignKey( Staff, on_delete=models.SET_NULL, null=True, related_name="owner", verbose_name=_("Owner"), ) probability = models.PositiveIntegerField(validators=[validate_probability]) closing_date = models.DateField(verbose_name=_("Closing Date")) created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created")) updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) closed = models.BooleanField(default=False, verbose_name=_("Closed")) class Meta: verbose_name = _("Opportunity") verbose_name_plural = _("Opportunities") def __str__(self): return f"{self.car.id_car_make.name} - {self.car.id_car_model.name} : {self.customer}" class Notes(models.Model): content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) object_id = models.PositiveIntegerField() content_object = GenericForeignKey("content_type", "object_id") note = models.TextField(verbose_name=_("Note")) created_by = models.ForeignKey( User, on_delete=models.DO_NOTHING, related_name="notes_created" ) created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created")) updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) class Meta: verbose_name = _("Note") verbose_name_plural = _("Notes") def __str__(self): return f"Note by {self.created_by.first_name} on {self.content_object}" class Activity(models.Model): content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) object_id = models.PositiveIntegerField() content_object = GenericForeignKey("content_type", "object_id") activity_type = models.CharField( max_length=50, choices=ActionChoices.choices, verbose_name=_("Activity Type") ) notes = models.TextField(blank=True, null=True, verbose_name=_("Notes")) created_by = models.ForeignKey( User, on_delete=models.DO_NOTHING, related_name="activities_created" ) created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created")) updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) class Meta: verbose_name = _("Activity") verbose_name_plural = _("Activities") def __str__(self): return f"{self.get_activity_type_display()} by {self.created_by.get_full_name} on {self.content_object}" class Notification(models.Model): user = models.ForeignKey( User, on_delete=models.CASCADE, related_name="notifications" ) message = models.CharField(max_length=255, verbose_name=_("Message")) is_read = models.BooleanField(default=False, verbose_name=_("Is Read")) created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created")) class Meta: verbose_name = _("Notification") verbose_name_plural = _("Notifications") ordering = ["-created"] def __str__(self): return self.message 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") ) 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")) email = models.EmailField(max_length=255, verbose_name=_("Email Address")) 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") ) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At")) class Meta: verbose_name = _("Vendor") verbose_name_plural = _("Vendors") def __str__(self): return self.name # class SaleQuotation(models.Model): # quotation_number = models.CharField(max_length=10, unique=True) # # STATUS_CHOICES = [ # ("Draft", _("Draft")), # ("Approved", _("Approved")), # ("In Review", _("In Review")), # ("Paid", _("Paid")), # ] # dealer = models.ForeignKey( # Dealer, on_delete=models.CASCADE, related_name="sales", null=True # ) # customer = models.ForeignKey( # Customer, # on_delete=models.CASCADE, # related_name="quotations", # verbose_name=_("Customer"), # ) # amount = models.DecimalField( # decimal_places=2, # default=Decimal("0.00"), # max_digits=10, # verbose_name=_("Amount"), # ) # remarks = models.TextField(blank=True, null=True, verbose_name=_("Remarks")) # is_approved = models.BooleanField(default=False) # 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")) # # posted = models.BooleanField(default=False) # payment_id = models.CharField( # max_length=255, null=True, blank=True, verbose_name=_("Payment ID") # ) # is_paid = models.BooleanField(default=False) # date_draft = models.DateTimeField( # null=True, blank=True, verbose_name=_("Draft Date") # ) # date_in_review = models.DateTimeField( # null=True, blank=True, verbose_name=_("In Review Date") # ) # date_approved = models.DateTimeField( # null=True, blank=True, verbose_name=_("Approved Date") # ) # date_paid = models.DateTimeField(null=True, blank=True, verbose_name=_("Paid Date")) # date_void = models.DateTimeField(null=True, blank=True, verbose_name=_("Void Date")) # date_canceled = models.DateTimeField( # null=True, blank=True, verbose_name=_("Canceled Date") # ) # # @property # def total_quantity(self): # total_quantity = self.quotation_cars.aggregate(total=Sum("quantity"))["total"] # return total_quantity or 0 # # @property # def total(self): # total = self.quotation_cars.aggregate( # total_price=Sum(F("car__finances__selling_price") * F("quantity")) # ) # if not total: # return 0 # return total["total_price"] # # @property # def total_vat(self): # if self.total: # return float(self.total) * 0.15 + float(self.total) # return 0 # 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.quotation_number} for {self.customer}" # # @property # def display_quotation_number(self): # return f"QN-{self.quotation_number}" # # def save(self, *args, **kwargs): # if not self.quotation_number: # self.quotation_number = str(next(self._get_quotation_number())).zfill(6) # super().save(*args, **kwargs) # # @classmethod # def _get_quotation_number(cls): # last_quotation = cls.objects.all().order_by("id").last() # if last_quotation: # last_quotation_number = int(last_quotation.quotation_number) # else: # last_quotation_number = 0 # return itertools.count(last_quotation_number + 1) # # 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")) # quantity = models.PositiveIntegerField(default=1, verbose_name=_("Quantity")) # # @property # def finance(self): # return self.car.finances # # @property # def financial_details(self): # """ # Retrieve financial details dynamically from CarFinance. # Returns a dictionary with all financial fields for better access. # """ # car_finance = self.car.finances # 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_amount": car_finance.vat_amount, # # "total_amount": car_finance.total, # } # # @property # def total(self): # """ # Calculate total price dynamically based on quantity and selling price. # """ # if not self.car.finances: # return Decimal("0.00") # return self.car.finances.selling_price * self.quantity # # @property # def total_vat(self): # """ # Calculate total price dynamically based on quantity and selling price. # """ # if not self.car.finances: # return Decimal("0.00") # price = float(self.car.finances.selling_price * self.quantity) # return (price * 0.15) + price # # 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}" class Payment(models.Model): METHOD_CHOICES = [ ("cash", _("cash")), ("credit", _("credit")), ("transfer", _("transfer")), ("debit", _("debit")), ("SADAD", _("SADAD")), ] # quotation = models.ForeignKey( # SaleQuotation, on_delete=models.CASCADE, related_name="payments" # ) amount = models.DecimalField( max_digits=10, decimal_places=2, verbose_name=_("amount") ) payment_method = models.CharField( choices=METHOD_CHOICES, max_length=50, verbose_name=_("method") ) reference_number = models.CharField( max_length=100, null=True, blank=True, verbose_name=_("reference number") ) payment_date = models.DateField(auto_now_add=True, verbose_name=_("date")) # def save(self, *args, **kwargs): # super().save(*args, **kwargs) # self.quotation.remaining_balance -= self.amount # if self.quotation.remaining_balance <= 0: # self.quotation.is_paid = True # self.quotation.save() class Meta: verbose_name = _("payment") verbose_name_plural = _("payments") def __str__(self): return f"Payment of {self.amount} on {self.payment_date} for {self.quotation}" class Refund(models.Model): payment = models.OneToOneField( Payment, on_delete=models.CASCADE, related_name="refund" ) amount = models.DecimalField( max_digits=10, decimal_places=2, verbose_name=_("amount") ) reason = models.TextField(blank=True, verbose_name=_("reason")) refund_date = models.DateField(auto_now_add=True, verbose_name=_("refund date")) class Meta: verbose_name = _("refund") verbose_name_plural = _("refunds") def __str__(self): return f"Refund of {self.amount} on {self.refund_date}" class UserActivityLog(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) action = models.TextField() timestamp = models.DateTimeField(auto_now_add=True) class Meta: verbose_name = "User Activity Log" verbose_name_plural = "User Activity Logs" ordering = ["-timestamp"] def __str__(self): return f"{self.user.email} - {self.action} - {self.timestamp}" class SaleOrder(models.Model): estimate = models.ForeignKey( EstimateModel, on_delete=models.CASCADE, related_name="sale_orders", verbose_name=_("Estimate") ) invoice = models.ForeignKey( InvoiceModel, on_delete=models.CASCADE, related_name="sale_orders", verbose_name=_("Invoice"), null=True, blank=True ) payment_method = models.CharField(max_length=20, choices=[ ('cash', 'Cash'), ('finance', 'Finance'), ('lease', 'Lease'), ]) comments = models.TextField(blank=True, null=True) formatted_order_id = models.CharField(max_length=10, unique=True, editable=False) created = models.DateTimeField(auto_now_add=True) class Meta: ordering = ['-created'] def save(self, *args, **kwargs): if not self.formatted_order_id: last_order = SaleOrder.objects.order_by('-id').first() if last_order: next_id = last_order.id + 1 else: next_id = 1 year = get_localdate().year self.formatted_order_id = f"O-{year}-{next_id:09d}" super().save(*args, **kwargs) def __str__(self): return f"Sale Order for {self.full_name}" @property def full_name(self): return f"{self.customer.customer_name}" @property def price(self): return self.car.finances.selling_price @property def items(self): if self.estimate.get_itemtxs_data(): return self.estimate.get_itemtxs_data()[0] return [] @property def customer(self): return self.estimate.customer