from django.db import models from django.contrib.auth import get_user_model from django.core.validators import MinValueValidator, MaxValueValidator from django.utils.translation import gettext_lazy as _ from decimal import Decimal User = get_user_model() class Category(models.Model): """Product categories""" name = models.CharField(max_length=100, unique=True) description = models.TextField(blank=True) parent = models.ForeignKey('self', on_delete=models.CASCADE, blank=True, null=True, related_name='children') is_active = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: verbose_name = _('Category') verbose_name_plural = _('Categories') ordering = ['name'] def __str__(self): return self.name def get_full_path(self): """Get full category path including parent categories""" if self.parent: return f"{self.parent.get_full_path()} > {self.name}" return self.name class Product(models.Model): """Product model for inventory management""" UNIT_CHOICES = [ ('pcs', 'Pieces'), ('kg', 'Kilograms'), ('m', 'Meters'), ('l', 'Liters'), ('box', 'Boxes'), ('set', 'Sets'), ] name = models.CharField(max_length=200) code = models.CharField(max_length=50, unique=True, help_text='Unique product code/SKU') description = models.TextField(blank=True) category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True) # Stock information current_stock = models.DecimalField( max_digits=10, decimal_places=2, default=0, validators=[MinValueValidator(Decimal('0'))] ) min_stock_level = models.DecimalField( max_digits=10, decimal_places=2, default=0, validators=[MinValueValidator(Decimal('0'))] ) max_stock_level = models.DecimalField( max_digits=10, decimal_places=2, default=1000, validators=[MinValueValidator(Decimal('0'))] ) # Pricing cost_price = models.DecimalField( max_digits=10, decimal_places=2, default=0, validators=[MinValueValidator(Decimal('0'))] ) selling_price = models.DecimalField( max_digits=10, decimal_places=2, default=0, validators=[MinValueValidator(Decimal('0'))] ) # Product details unit = models.CharField(max_length=10, choices=UNIT_CHOICES, default='pcs') weight = models.DecimalField(max_digits=8, decimal_places=2, blank=True, null=True) dimensions = models.CharField(max_length=100, blank=True, help_text='Format: LxWxH in cm') # Status is_active = models.BooleanField(default=True) is_manufactured = models.BooleanField(default=False, help_text='Is this a manufactured product?') # Timestamps created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: ordering = ['name'] verbose_name = _('Product') verbose_name_plural = _('Products') def __str__(self): return f"{self.name} ({self.code})" def get_stock_status(self): """Get stock status for the product""" if self.current_stock <= self.min_stock_level: return 'low' elif self.current_stock >= self.max_stock_level: return 'high' else: return 'normal' def get_stock_value(self): """Calculate total stock value""" return self.current_stock * self.cost_price def is_low_stock(self): """Check if product has low stock""" return self.current_stock <= self.min_stock_level def is_overstocked(self): """Check if product is overstocked""" return self.current_stock >= self.max_stock_level class StockMovement(models.Model): """Track all stock movements (in/out)""" MOVEMENT_TYPES = [ ('in', 'Stock In'), ('out', 'Stock Out'), ('adjustment', 'Stock Adjustment'), ('manufacturing', 'Manufacturing Output'), ('sale', 'Sale'), ('purchase', 'Purchase'), ] product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='stock_movements') movement_type = models.CharField(max_length=20, choices=MOVEMENT_TYPES) quantity = models.DecimalField(max_digits=10, decimal_places=2) unit_price = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True) total_value = models.DecimalField(max_digits=12, decimal_places=2, blank=True, null=True) # Reference fields for different movement types reference_type = models.CharField(max_length=50, blank=True, help_text='Type of reference document') reference_id = models.IntegerField(blank=True, null=True, help_text='ID of reference document') reference_note = models.CharField(max_length=200, blank=True) # Movement details date = models.DateField() notes = models.TextField(blank=True) # User tracking created_by = models.ForeignKey('users.CustomUser', on_delete=models.SET_NULL, null=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ['-date', '-created_at'] verbose_name = _('Stock Movement') verbose_name_plural = _('Stock Movements') def __str__(self): return f"{self.product.name} - {self.get_movement_type_display()} ({self.quantity})" def save(self, *args, **kwargs): """Override save to calculate total value and update product stock""" if self.unit_price and self.quantity: self.total_value = self.unit_price * self.quantity # Update product stock if self.pk: # Existing movement - revert old quantity old_movement = StockMovement.objects.get(pk=self.pk) if old_movement.movement_type == 'in': self.product.current_stock -= old_movement.quantity elif old_movement.movement_type == 'out': self.product.current_stock += old_movement.quantity # Apply new movement if self.movement_type == 'in': self.product.current_stock += self.quantity elif self.movement_type == 'out': self.product.current_stock -= self.quantity elif self.movement_type == 'adjustment': # For adjustments, quantity is the new stock level self.product.current_stock = self.quantity # Ensure stock doesn't go negative if self.product.current_stock < 0: self.product.current_stock = 0 self.product.save() super().save(*args, **kwargs) def delete(self, *args, **kwargs): """Override delete to revert stock changes""" if self.movement_type == 'in': self.product.current_stock -= self.quantity elif self.movement_type == 'out': self.product.current_stock += self.quantity self.product.save() super().delete(*args, **kwargs) class Customer(models.Model): """Customer model""" CUSTOMER_TYPE_CHOICES = [ ('retail', 'Retail'), ('wholesale', 'Wholesale'), ('corporate', 'Corporate'), ] code = models.CharField(max_length=50, unique=True) name = models.CharField(max_length=200) customer_type = models.CharField(max_length=20, choices=CUSTOMER_TYPE_CHOICES, default='retail') contact_person = models.CharField(max_length=100, blank=True) email = models.EmailField(blank=True) phone = models.CharField(max_length=20, blank=True) address = models.TextField(blank=True) credit_limit = models.DecimalField(max_digits=12, decimal_places=2, default=0) is_active = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: ordering = ['name'] def __str__(self): return f"{self.code} - {self.name}" class Supplier(models.Model): """Supplier model""" code = models.CharField(max_length=50, unique=True) name = models.CharField(max_length=200) contact_person = models.CharField(max_length=100, blank=True) email = models.EmailField(blank=True) phone = models.CharField(max_length=20, blank=True) address = models.TextField(blank=True) rating = models.PositiveSmallIntegerField( validators=[MinValueValidator(1), MaxValueValidator(5)], default=3 ) credit_limit = models.DecimalField(max_digits=12, decimal_places=2, default=0) is_active = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: ordering = ['name'] def __str__(self): return f"{self.code} - {self.name}"