38086-vm/core/models.py
2026-02-06 03:17:50 +00:00

439 lines
23 KiB
Python

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.ImageField(_("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")
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 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")
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)
# New field to link back to LOP
lop = models.OneToOneField('LOP', on_delete=models.SET_NULL, null=True, blank=True, related_name="purchase")
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")
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"Return #{self.id} for Sale #{self.sale.id if self.sale else 'N/A'}"
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"Return #{self.id} for Purchase #{self.purchase.id if self.purchase 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)
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 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=50, blank=True)
email = models.EmailField(_("Email"), blank=True)
vat_number = models.CharField(_("VAT Number"), max_length=50, blank=True)
logo = models.ImageField(_("Company Logo"), upload_to="company_logos/", blank=True, null=True)
currency = models.CharField(_("Currency"), max_length=10, default="SAR")
vat_percentage = models.DecimalField(_("Default VAT %"), max_digits=5, decimal_places=2, default=15.00)
footer_text = models.TextField(_("Invoice Footer Text"), blank=True)
decimal_places = models.IntegerField(_("Decimal Places"), default=2)
# Wablas (WhatsApp) Integration
wablas_enabled = models.BooleanField(_("Enable Wablas WhatsApp"), default=False)
wablas_server_url = models.URLField(_("Wablas Server URL"), blank=True, help_text=_("e.g., https://tegal.wablas.com"))
wablas_api_token = models.CharField(_("Wablas API Token"), max_length=255, blank=True)
wablas_secret_key = models.CharField(_("Wablas Secret Key"), max_length=255, blank=True, help_text=_("Used for verifying webhooks"))
def __str__(self):
return "System Settings"
class HeldSale(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="held_sales", null=True, blank=True) # Changed to match migration
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="held_sales_created") # Added to match migration potentially
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_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"Held Sale {self.id} - {self.customer.name if self.customer else 'Guest'}"
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
image = models.ImageField(_("Profile Picture"), upload_to='profile_pics/', null=True, blank=True)
phone = models.CharField(_("Phone Number"), max_length=20, blank=True)
bio = models.TextField(_("Bio"), blank=True)
role = models.CharField(max_length=20, choices=[('admin', 'Admin'), ('manager', 'Manager'), ('cashier', 'Cashier')], default='cashier')
def __str__(self):
return self.user.username
class Device(models.Model):
DEVICE_TYPES = [
('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 self.name
class LOP(models.Model):
STATUS_CHOICES = [
('draft', _('Draft')),
('converted', _('Converted to Purchase')),
('cancelled', _('Cancelled')),
]
supplier = models.ForeignKey(Supplier, on_delete=models.SET_NULL, null=True, related_name="lops")
lop_number = models.CharField(_("LOP 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')
date = models.DateField(_("Date"), default=timezone.now)
notes = models.TextField(_("Notes"), blank=True)
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="lops")
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"LOP #{self.id} - {self.supplier.name if self.supplier else 'N/A'}"
class LOPItem(models.Model):
lop = models.ForeignKey(LOP, 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}"