Django_Basic_Manufacturing/inventory/models.py
2025-08-17 21:42:40 +07:00

199 lines
7.0 KiB
Python

from django.db import models
from django.core.validators import MinValueValidator
from django.utils.translation import gettext_lazy as _
from decimal import Decimal
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)