from django.db import models class BillOfMaterial(models.Model): product = models.ForeignKey('inventory.Product', on_delete=models.CASCADE, related_name='boms') bom_code = models.CharField(max_length=50, unique=True, blank=True) version = models.CharField(max_length=20, default='1.0') is_active = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) def save(self, *args, **kwargs): if not self.bom_code: self.bom_code = self.generate_bom_code() super().save(*args, **kwargs) def generate_bom_code(self): """Generate a unique BOM code""" last_bom = BillOfMaterial.objects.order_by('-id').first() if last_bom and last_bom.bom_code and last_bom.bom_code.startswith('BOM'): try: last_number = int(last_bom.bom_code[3:]) # Remove 'BOM' prefix new_number = last_number + 1 except ValueError: new_number = 1 else: new_number = 1 return f'BOM{new_number:04d}' def __str__(self): return f"{self.bom_code} - {self.product.name}" @property def total_cost(self): """Calculate total cost of all BOM items""" return sum(item.total_cost for item in self.items.all()) class BOMItem(models.Model): bom = models.ForeignKey(BillOfMaterial, on_delete=models.CASCADE, related_name='items') component = models.ForeignKey('inventory.Product', on_delete=models.CASCADE, related_name='bom_components') quantity = models.DecimalField(max_digits=10, decimal_places=2) unit_of_measure = models.ForeignKey('inventory.UnitOfMeasure', on_delete=models.SET_NULL, null=True) class Meta: unique_together = ('bom', 'component') @property def total_cost(self): """Calculate total cost for this BOM item""" return self.quantity * self.component.cost class ManufacturingOrder(models.Model): STATUS_CHOICES = [ ('scheduled', 'Scheduled'), ('in_progress', 'In Progress'), ('completed', 'Completed'), ('cancelled', 'Cancelled'), ] mo_number = models.CharField(max_length=50, unique=True) bom = models.ForeignKey(BillOfMaterial, on_delete=models.CASCADE) quantity_to_produce = models.DecimalField(max_digits=10, decimal_places=2) scheduled_start_date = models.DateField() scheduled_end_date = models.DateField() actual_start_date = models.DateField(null=True, blank=True) actual_end_date = models.DateField(null=True, blank=True) status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='in_progress') created_by = models.ForeignKey('accounts.User', on_delete=models.SET_NULL, null=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) def __str__(self): return self.mo_number @property def total_cost(self): """Calculate total cost of all components""" return sum(component.total_cost for component in self.mocomponents.all()) @property def is_overdue(self): """Check if the manufacturing order is overdue""" from django.utils import timezone if self.status in ['scheduled', 'in_progress']: return timezone.now().date() > self.scheduled_end_date return False def can_start(self): """Check if the manufacturing order can be started""" return self.status == 'scheduled' def can_complete(self): """Check if the manufacturing order can be completed""" return self.status == 'in_progress' def can_cancel(self): """Check if the manufacturing order can be cancelled""" return self.status in ['scheduled', 'in_progress'] class MOComponent(models.Model): manufacturing_order = models.ForeignKey(ManufacturingOrder, on_delete=models.CASCADE, related_name='mocomponents', null=True, blank=True) component = models.ForeignKey('inventory.Product', on_delete=models.CASCADE) required_quantity = models.DecimalField(max_digits=10, decimal_places=2) consumed_quantity = models.DecimalField(max_digits=10, decimal_places=2, default=0) class Meta: unique_together = ('manufacturing_order', 'component') @property def total_cost(self): """Calculate total cost for this component""" return self.required_quantity * self.component.cost