from django.db import models from django.utils.translation import gettext_lazy as _ from django.utils import timezone from django.contrib.auth.models import User from django.db.models.signals import post_save from django.dispatch import receiver class Category(models.Model): name_en = models.CharField(_("Name (English)"), max_length=100) name_ar = models.CharField(_("Name (Arabic)"), max_length=100) slug = models.SlugField(unique=True) class Meta: verbose_name_plural = _("Categories") def __str__(self): return f"{self.name_en} / {self.name_ar}" class Unit(models.Model): name_en = models.CharField(_("Name (English)"), max_length=50) name_ar = models.CharField(_("Name (Arabic)"), max_length=50) short_name = models.CharField(_("Short Name"), max_length=10) def __str__(self): return f"{self.name_en} / {self.name_ar}" class Product(models.Model): category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name="products") unit = models.ForeignKey(Unit, on_delete=models.SET_NULL, null=True, blank=True, related_name="products") supplier = models.ForeignKey('Supplier', on_delete=models.SET_NULL, null=True, blank=True, related_name="products") name_en = models.CharField(_("Name (English)"), max_length=200) name_ar = models.CharField(_("Name (Arabic)"), max_length=200) sku = models.CharField(_("Barcode/SKU"), max_length=50, unique=True) description = models.TextField(_("Description"), blank=True) cost_price = models.DecimalField(_("Cost Price"), max_digits=12, decimal_places=3, default=0) price = models.DecimalField(_("Sale Price"), max_digits=12, decimal_places=3) vat = models.DecimalField(_("VAT (%)"), max_digits=5, decimal_places=2, default=0) opening_stock = models.DecimalField(_("Opening Stock"), max_digits=15, decimal_places=2, default=0) stock_quantity = models.DecimalField(_("In Stock"), max_digits=15, decimal_places=2, default=0) min_stock_level = models.DecimalField(_("Stock Level (Alert)"), max_digits=15, decimal_places=2, default=0) has_expiry = models.BooleanField(_("Has Expiry Date"), default=False) expiry_date = models.DateField(_("Expiry Date"), null=True, blank=True) image = models.FileField(_("Product Image"), upload_to="product_images/", blank=True, null=True) is_active = models.BooleanField(_("Active"), default=True) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"{self.name_en} ({self.sku})" class LoyaltyTier(models.Model): name_en = models.CharField(_("Name (English)"), max_length=50) name_ar = models.CharField(_("Name (Arabic)"), max_length=50) min_points = models.PositiveIntegerField(_("Minimum Points"), default=0) point_multiplier = models.DecimalField(_("Point Multiplier"), max_digits=4, decimal_places=2, default=1.0) discount_percentage = models.DecimalField(_("Discount Percentage"), max_digits=5, decimal_places=2, default=0) color_code = models.CharField(_("Color Code"), max_length=20, default="#6c757d") def __str__(self): return f"{self.name_en} / {self.name_ar}" class Customer(models.Model): name = models.CharField(_("Name"), max_length=200) phone = models.CharField(_("Phone"), max_length=20, blank=True) email = models.EmailField(_("Email"), blank=True) address = models.TextField(_("Address"), blank=True) loyalty_points = models.DecimalField(_("Loyalty Points"), max_digits=15, decimal_places=2, default=0) loyalty_tier = models.ForeignKey(LoyaltyTier, on_delete=models.SET_NULL, null=True, blank=True, related_name="customers") def __str__(self): return self.name def update_tier(self): tiers = LoyaltyTier.objects.filter(min_points__lte=self.loyalty_points).order_by('-min_points') if tiers.exists(): self.loyalty_tier = tiers.first() self.save() class LoyaltyTransaction(models.Model): TRANSACTION_TYPES = [ ('earned', _('Earned')), ('redeemed', _('Redeemed')), ('adjusted', _('Adjusted')), ] customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name="loyalty_transactions") sale = models.ForeignKey('Sale', on_delete=models.SET_NULL, null=True, blank=True, related_name="loyalty_transactions") transaction_type = models.CharField(_("Type"), max_length=20, choices=TRANSACTION_TYPES) points = models.DecimalField(_("Points"), max_digits=15, decimal_places=2) notes = models.TextField(_("Notes"), blank=True) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"{self.transaction_type} {self.points} for {self.customer.name}" class Supplier(models.Model): name = models.CharField(_("Name"), max_length=200) contact_person = models.CharField(_("Contact Person"), max_length=200, blank=True) phone = models.CharField(_("Phone"), max_length=20, blank=True) def __str__(self): return self.name class PaymentMethod(models.Model): name_en = models.CharField(_("Name (English)"), max_length=50) name_ar = models.CharField(_("Name (Arabic)"), max_length=50) is_active = models.BooleanField(_("Active"), default=True) def __str__(self): return f"{self.name_en} / {self.name_ar}" class ExpenseCategory(models.Model): accounting_account = models.ForeignKey('accounting.Account', on_delete=models.SET_NULL, null=True, blank=True, related_name='expense_categories') name_en = models.CharField(_("Name (English)"), max_length=100) name_ar = models.CharField(_("Name (Arabic)"), max_length=100) description = models.TextField(_("Description"), blank=True) class Meta: verbose_name_plural = _("Expense Categories") def __str__(self): return f"{self.name_en} / {self.name_ar}" class Expense(models.Model): category = models.ForeignKey(ExpenseCategory, on_delete=models.CASCADE, related_name="expenses") amount = models.DecimalField(_("Amount"), max_digits=15, decimal_places=3) date = models.DateField(_("Date"), default=timezone.now) description = models.TextField(_("Description"), blank=True) payment_method = models.ForeignKey(PaymentMethod, on_delete=models.SET_NULL, null=True, blank=True, related_name="expenses") created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="expenses") attachment = models.FileField(_("Attachment"), upload_to="expense_attachments/", blank=True, null=True) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"Expense {self.id} - {self.amount} ({self.category.name_en})" class Sale(models.Model): PAYMENT_TYPE_CHOICES = [ ('cash', _('Cash')), ('credit', _('Credit')), ('partial', _('Partial')), ] STATUS_CHOICES = [ ('paid', _('Paid')), ('partial', _('Partial')), ('unpaid', _('Unpaid')), ] customer = models.ForeignKey(Customer, on_delete=models.SET_NULL, null=True, blank=True, related_name="sales") quotation = models.ForeignKey('Quotation', on_delete=models.SET_NULL, null=True, blank=True, related_name="converted_sale") invoice_number = models.CharField(_("Invoice Number"), max_length=50, blank=True) subtotal = models.DecimalField(_("Subtotal"), max_digits=15, decimal_places=3, default=0) vat_amount = models.DecimalField(_("VAT Amount"), max_digits=15, decimal_places=3, default=0) total_amount = models.DecimalField(_("Total Amount"), max_digits=15, decimal_places=3) paid_amount = models.DecimalField(_("Paid Amount"), max_digits=15, decimal_places=3, default=0) balance_due = models.DecimalField(_("Balance Due"), max_digits=15, decimal_places=3, default=0) discount = models.DecimalField(_("Discount"), max_digits=15, decimal_places=3, default=0) loyalty_points_redeemed = models.DecimalField(_("Loyalty Points Redeemed"), max_digits=15, decimal_places=2, default=0) loyalty_discount_amount = models.DecimalField(_("Loyalty Discount"), max_digits=15, decimal_places=3, default=0) payment_type = models.CharField(_("Payment Type"), max_length=20, choices=PAYMENT_TYPE_CHOICES, default='cash') status = models.CharField(_("Status"), max_length=20, choices=STATUS_CHOICES, default='paid') due_date = models.DateField(_("Due Date"), null=True, blank=True) notes = models.TextField(_("Notes"), blank=True) created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="sales") created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"Sale #{self.id} - {self.customer.name if self.customer else 'Guest'}" def update_balance(self): payments_total = self.payments.aggregate(total=models.Sum('amount'))['total'] or 0 self.paid_amount = payments_total self.balance_due = self.total_amount - self.paid_amount if self.balance_due <= 0: self.status = 'paid' elif self.paid_amount > 0: self.status = 'partial' else: self.status = 'unpaid' self.save() class SaleItem(models.Model): sale = models.ForeignKey(Sale, on_delete=models.CASCADE, related_name="items") product = models.ForeignKey(Product, on_delete=models.CASCADE) quantity = models.DecimalField(_("Quantity"), max_digits=15, decimal_places=2) unit_price = models.DecimalField(_("Unit Price"), max_digits=12, decimal_places=3) line_total = models.DecimalField(_("Line Total"), max_digits=15, decimal_places=3) def __str__(self): return f"{self.product.name_en} x {self.quantity}" class SalePayment(models.Model): sale = models.ForeignKey(Sale, on_delete=models.CASCADE, related_name="payments") amount = models.DecimalField(_("Amount"), max_digits=15, decimal_places=3) payment_date = models.DateField(_("Payment Date"), default=timezone.now) payment_method = models.ForeignKey(PaymentMethod, on_delete=models.SET_NULL, null=True, blank=True, related_name="sale_payments") payment_method_name = models.CharField(_("Payment Method Name"), max_length=50, default="Cash") # Fallback notes = models.TextField(_("Notes"), blank=True) created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="sale_payments") created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"Payment of {self.amount} for Sale #{self.sale.id}" class Quotation(models.Model): STATUS_CHOICES = [ ('draft', _('Draft')), ('sent', _('Sent')), ('accepted', _('Accepted')), ('rejected', _('Rejected')), ('converted', _('Converted to Invoice')), ] customer = models.ForeignKey(Customer, on_delete=models.SET_NULL, null=True, blank=True, related_name="quotations") quotation_number = models.CharField(_("Quotation Number"), max_length=50, blank=True) total_amount = models.DecimalField(_("Total Amount"), max_digits=15, decimal_places=3) discount = models.DecimalField(_("Discount"), max_digits=15, decimal_places=3, default=0) status = models.CharField(_("Status"), max_length=20, choices=STATUS_CHOICES, default='draft') valid_until = models.DateField(_("Valid Until"), null=True, blank=True) terms_and_conditions = models.TextField(_("Terms and Conditions"), blank=True) notes = models.TextField(_("Notes"), blank=True) created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="quotations") created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"Quotation #{self.id} - {self.customer.name if self.customer else 'Guest'}" class QuotationItem(models.Model): quotation = models.ForeignKey(Quotation, on_delete=models.CASCADE, related_name="items") product = models.ForeignKey(Product, on_delete=models.CASCADE) quantity = models.DecimalField(_("Quantity"), max_digits=15, decimal_places=2) unit_price = models.DecimalField(_("Unit Price"), max_digits=12, decimal_places=3) line_total = models.DecimalField(_("Line Total"), max_digits=15, decimal_places=3) def __str__(self): return f"{self.product.name_en} x {self.quantity}" class PurchaseOrder(models.Model): STATUS_CHOICES = [ ('draft', _('Draft')), ('sent', _('Sent')), ('converted', _('Converted to Purchase')), ('cancelled', _('Cancelled')), ] supplier = models.ForeignKey(Supplier, on_delete=models.SET_NULL, null=True, related_name="purchase_orders") lpo_number = models.CharField(_("LPO Number"), max_length=50, blank=True) total_amount = models.DecimalField(_("Total Amount"), max_digits=15, decimal_places=3) status = models.CharField(_("Status"), max_length=20, choices=STATUS_CHOICES, default='draft') issue_date = models.DateField(_("Issue Date"), default=timezone.now) expected_date = models.DateField(_("Expected Date"), null=True, blank=True) notes = models.TextField(_("Notes"), blank=True) created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="purchase_orders") created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"LPO #{self.id} - {self.supplier.name if self.supplier else 'N/A'}" class PurchaseOrderItem(models.Model): purchase_order = models.ForeignKey(PurchaseOrder, on_delete=models.CASCADE, related_name="items") product = models.ForeignKey(Product, on_delete=models.CASCADE) quantity = models.DecimalField(_("Quantity"), max_digits=15, decimal_places=2) cost_price = models.DecimalField(_("Cost Price"), max_digits=12, decimal_places=3) line_total = models.DecimalField(_("Line Total"), max_digits=15, decimal_places=3) def __str__(self): return f"{self.product.name_en} x {self.quantity}" class Purchase(models.Model): PAYMENT_TYPE_CHOICES = [ ('cash', _('Cash')), ('credit', _('Credit')), ('partial', _('Partial')), ] STATUS_CHOICES = [ ('paid', _('Paid')), ('partial', _('Partial')), ('unpaid', _('Unpaid')), ] supplier = models.ForeignKey(Supplier, on_delete=models.SET_NULL, null=True, related_name="purchases") purchase_order = models.ForeignKey(PurchaseOrder, on_delete=models.SET_NULL, null=True, blank=True, related_name="converted_purchase") invoice_number = models.CharField(_("Invoice Number"), max_length=50, blank=True) total_amount = models.DecimalField(_("Total Amount"), max_digits=15, decimal_places=3) paid_amount = models.DecimalField(_("Paid Amount"), max_digits=15, decimal_places=3, default=0) balance_due = models.DecimalField(_("Balance Due"), max_digits=15, decimal_places=3, default=0) payment_type = models.CharField(_("Payment Type"), max_length=20, choices=PAYMENT_TYPE_CHOICES, default='cash') status = models.CharField(_("Status"), max_length=20, choices=STATUS_CHOICES, default='paid') due_date = models.DateField(_("Due Date"), null=True, blank=True) notes = models.TextField(_("Notes"), blank=True) created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="purchases") created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"Purchase #{self.id} - {self.supplier.name if self.supplier else 'N/A'}" def update_balance(self): payments_total = self.payments.aggregate(total=models.Sum('amount'))['total'] or 0 self.paid_amount = payments_total self.balance_due = self.total_amount - self.paid_amount if self.balance_due <= 0: self.status = 'paid' elif self.paid_amount > 0: self.status = 'partial' else: self.status = 'unpaid' self.save() class PurchaseItem(models.Model): purchase = models.ForeignKey(Purchase, on_delete=models.CASCADE, related_name="items") product = models.ForeignKey(Product, on_delete=models.CASCADE) quantity = models.DecimalField(_("Quantity"), max_digits=15, decimal_places=2) cost_price = models.DecimalField(_("Cost Price"), max_digits=12, decimal_places=3) expiry_date = models.DateField(_("Expiry Date"), null=True, blank=True) line_total = models.DecimalField(_("Line Total"), max_digits=15, decimal_places=3) def __str__(self): return f"{self.product.name_en} x {self.quantity}" class PurchasePayment(models.Model): purchase = models.ForeignKey(Purchase, on_delete=models.CASCADE, related_name="payments") amount = models.DecimalField(_("Amount"), max_digits=15, decimal_places=3) payment_date = models.DateField(_("Payment Date"), default=timezone.now) payment_method = models.ForeignKey(PaymentMethod, on_delete=models.SET_NULL, null=True, blank=True, related_name="purchase_payments") payment_method_name = models.CharField(_("Payment Method Name"), max_length=50, default="Cash") # Fallback notes = models.TextField(_("Notes"), blank=True) created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="purchase_payments") created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"Payment of {self.amount} for Purchase #{self.purchase.id}" class SaleReturn(models.Model): sale = models.ForeignKey(Sale, on_delete=models.SET_NULL, null=True, blank=True, related_name="returns") customer = models.ForeignKey(Customer, on_delete=models.SET_NULL, null=True, blank=True, related_name="sale_returns") return_number = models.CharField(_("Return Number"), max_length=50, blank=True) total_amount = models.DecimalField(_("Total Amount"), max_digits=15, decimal_places=3) notes = models.TextField(_("Notes"), blank=True) created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="sale_returns") created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"Sale Return #{self.id} - {self.customer.name if self.customer else 'Guest'}" class SaleReturnItem(models.Model): sale_return = models.ForeignKey(SaleReturn, on_delete=models.CASCADE, related_name="items") product = models.ForeignKey(Product, on_delete=models.CASCADE) quantity = models.DecimalField(_("Quantity"), max_digits=15, decimal_places=2) unit_price = models.DecimalField(_("Unit Price"), max_digits=12, decimal_places=3) line_total = models.DecimalField(_("Line Total"), max_digits=15, decimal_places=3) def __str__(self): return f"{self.product.name_en} x {self.quantity}" class PurchaseReturn(models.Model): purchase = models.ForeignKey(Purchase, on_delete=models.SET_NULL, null=True, blank=True, related_name="returns") supplier = models.ForeignKey(Supplier, on_delete=models.SET_NULL, null=True, blank=True, related_name="purchase_returns") return_number = models.CharField(_("Return Number"), max_length=50, blank=True) total_amount = models.DecimalField(_("Total Amount"), max_digits=15, decimal_places=3) notes = models.TextField(_("Notes"), blank=True) created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="purchase_returns") created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"Purchase Return #{self.id} - {self.supplier.name if self.supplier else 'N/A'}" class PurchaseReturnItem(models.Model): purchase_return = models.ForeignKey(PurchaseReturn, on_delete=models.CASCADE, related_name="items") product = models.ForeignKey(Product, on_delete=models.CASCADE) quantity = models.DecimalField(_("Quantity"), max_digits=15, decimal_places=2) cost_price = models.DecimalField(_("Cost Price"), max_digits=12, decimal_places=3) expiry_date = models.DateField(_("Expiry Date"), null=True, blank=True) line_total = models.DecimalField(_("Line Total"), max_digits=15, decimal_places=3) def __str__(self): return f"{self.product.name_en} x {self.quantity}" class HeldSale(models.Model): customer = models.ForeignKey(Customer, on_delete=models.SET_NULL, null=True, blank=True, related_name="held_sales") cart_data = models.JSONField(_("Cart Data")) total_amount = models.DecimalField(_("Total Amount"), max_digits=15, decimal_places=3) notes = models.TextField(_("Notes"), blank=True) created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="held_sales") created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"Held Sale #{self.id} - {self.created_at.strftime('%Y-%m-%d %H:%M')}" class SystemSetting(models.Model): business_name = models.CharField(_("Business Name"), max_length=200, default="Meezan Accounting") address = models.TextField(_("Address"), blank=True) phone = models.CharField(_("Phone"), max_length=20, blank=True) email = models.EmailField(_("Email"), blank=True) currency_symbol = models.CharField(_("Currency Symbol"), max_length=10, default="OMR") tax_rate = models.DecimalField(_("Tax Rate (%)"), max_digits=5, decimal_places=2, default=0) decimal_places = models.PositiveSmallIntegerField(_("Decimal Places"), default=3) logo = models.FileField(_("Logo"), upload_to="business_logos/", blank=True, null=True) vat_number = models.CharField(_("VAT Number"), max_length=50, blank=True) registration_number = models.CharField(_("Registration Number"), max_length=50, blank=True) # Financial Settings allow_zero_stock_sales = models.BooleanField(_("Allow selling items with 0 stock"), default=False) # Loyalty Settings loyalty_enabled = models.BooleanField(_("Enable Loyalty System"), default=False) points_per_currency = models.DecimalField(_("Points Earned per Currency Unit"), max_digits=10, decimal_places=2, default=1.0) currency_per_point = models.DecimalField(_("Currency Value per Point"), max_digits=10, decimal_places=3, default=0.010) min_points_to_redeem = models.PositiveIntegerField(_("Minimum Points to Redeem"), default=100) # WhatsApp (Wablas) Settings wablas_enabled = models.BooleanField(_("Enable WhatsApp Gateway"), default=False) wablas_token = models.CharField(_("Wablas API Token"), max_length=255, blank=True) wablas_server_url = models.URLField(_("Wablas Server URL"), blank=True, help_text="Example: https://console.wablas.com") wablas_secret_key = models.CharField(_("Wablas Secret Key"), max_length=255, blank=True) def __str__(self): return self.business_name class Device(models.Model): DEVICE_TYPES = [ ('counter', _('POS Counter')), ('printer', _('Printer')), ('scanner', _('Scanner')), ('scale', _('Weight Scale')), ('display', _('Customer Display')), ('other', _('Other')), ] CONNECTION_TYPES = [ ('network', _('Network (IP)')), ('usb', _('USB')), ('bluetooth', _('Bluetooth')), ] name = models.CharField(_("Device Name"), max_length=100) device_type = models.CharField(_("Device Type"), max_length=20, choices=DEVICE_TYPES) connection_type = models.CharField(_("Connection Type"), max_length=20, choices=CONNECTION_TYPES, default='network') ip_address = models.GenericIPAddressField(_("IP Address"), blank=True, null=True) port = models.PositiveIntegerField(_("Port"), blank=True, null=True) is_active = models.BooleanField(_("Active"), default=True) config_json = models.JSONField(_("Configuration"), blank=True, null=True, help_text=_("Additional driver configuration in JSON format")) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"{self.name} ({self.get_device_type_display()})" class UserProfile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile") image = models.FileField(_("Profile Picture"), upload_to="profile_pics/", blank=True, null=True) phone = models.CharField(_("Phone Number"), max_length=20, blank=True) bio = models.TextField(_("Bio"), blank=True) def __str__(self): return self.user.username class CashierCounterRegistry(models.Model): cashier = models.OneToOneField(User, on_delete=models.CASCADE, related_name="counter_assignment", verbose_name=_("Cashier")) counter = models.ForeignKey(Device, on_delete=models.CASCADE, related_name="assigned_cashiers", verbose_name=_("Counter"), limit_choices_to={'device_type': 'counter'}) assigned_at = models.DateTimeField(_("Assigned At"), auto_now_add=True) class Meta: verbose_name = _("Cashier Counter Registry") verbose_name_plural = _("Cashier Counter Registries") def __str__(self): return f"{self.cashier.username} - {self.counter.name}" class CashierSession(models.Model): STATUS_CHOICES = [ ('active', _('Active')), ('closed', _('Closed')), ] user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='sessions', verbose_name=_("Cashier")) counter = models.ForeignKey(Device, on_delete=models.SET_NULL, null=True, blank=True, related_name='sessions', verbose_name=_("Counter"), limit_choices_to={'device_type': 'counter'}) start_time = models.DateTimeField(_("Start Time"), auto_now_add=True) end_time = models.DateTimeField(_("End Time"), null=True, blank=True) opening_balance = models.DecimalField(_("Opening Balance"), max_digits=15, decimal_places=3, default=0) closing_balance = models.DecimalField(_("Closing Balance"), max_digits=15, decimal_places=3, null=True, blank=True) status = models.CharField(_("Status"), max_length=20, choices=STATUS_CHOICES, default='active') notes = models.TextField(_("Notes"), blank=True) def __str__(self): return f"{self.user.username} - {self.start_time.strftime('%Y-%m-%d %H:%M')}" @receiver(post_save, sender=User) def create_user_profile(sender, instance, created, **kwargs): if created: UserProfile.objects.create(user=instance) @receiver(post_save, sender=User) def save_user_profile(sender, instance, **kwargs): if not hasattr(instance, 'profile'): UserProfile.objects.create(user=instance) instance.profile.save()