Django_Basic_Manufacturing/inventory/models.py
2025-08-19 12:28:49 +07:00

247 lines
8.8 KiB
Python

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)
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}"