from django.db import models from shimatta_modules import RandomFileName from django.db.models import F, Sum from django.contrib.auth.models import User as AuthUser from django.core.exceptions import ValidationError from django.core.validators import RegexValidator from django.dispatch import receiver from django.core.validators import MinValueValidator, MaxValueValidator import os import uuid from shimatta_modules.EngineeringNumberConverter import EngineeringNumberConverter as NumConv storage_name_validator = RegexValidator(r'^[^/]*$', 'Slashes are not allowed in storage names') # Create your models here. class ComponentParameterType(models.Model): class Meta: ordering = ['parameter_name'] TYPE_CHOICES = ( ('F', 'Free Text'), ('N', 'Standard float number'), ('E', 'Engineering / SI Unit'), ('I', 'IT Type'), ) parameter_name = models.CharField(max_length=50, unique=True) parameter_description = models.TextField(null=False, blank=True) unit = models.CharField(max_length=10, null=False, blank=True) parameter_type = models.CharField(max_length=1, choices=TYPE_CHOICES, default='N') def __str__(self): unit = '' if self.unit: unit = ' in ' + self.unit return self.parameter_name + unit + ' | ' + self.get_parameter_type_display() def descriptive_name(self): if self.unit: return f'{self.parameter_name} [{self.unit}]' else: return self.parameter_name class ComponentType(models.Model): class Meta: ordering = ['class_name'] class_name = models.CharField(max_length=50, unique=True) parent_class = models.ForeignKey('self', on_delete=models.PROTECT, related_name='child_classes', null=True, blank=True) passive = models.BooleanField() possible_parameter = models.ManyToManyField(ComponentParameterType, blank=True) key_parameter1 = models.ForeignKey(ComponentParameterType, on_delete=models.CASCADE, blank=True, null=True, related_name="type_param1") key_parameter2 = models.ForeignKey(ComponentParameterType, on_delete=models.CASCADE, blank=True, null=True, related_name="type_param2") key_parameter3 = models.ForeignKey(ComponentParameterType, on_delete=models.CASCADE, blank=True, null=True, related_name="type_param3") def __str__(self): return self.get_full_path() def get_path_components(self): chain = [] iterator = self chain.append(self) while iterator.parent_class is not None: chain.append(iterator.parent_class) iterator = iterator.parent_class return chain def get_full_path(self): output = '' chain = self.get_path_components() for i in range(len(chain) - 1, -1, -1): output = output + ' / ' + chain[i].class_name return output class Storage(models.Model): class Meta: # permissions = () # Allow only one child with the same name unique_together = ('name', 'parent_storage') ordering = ['name'] id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True) name = models.CharField(max_length=100, validators=[storage_name_validator]) parent_storage = models.ForeignKey('self', on_delete=models.CASCADE, blank=True, null=True) responsible = models.ForeignKey(AuthUser, on_delete=models.SET_NULL, blank=True, null=True) def get_path_components(self): chain = [] iterator = self chain.append(self) while iterator.parent_storage is not None: chain.append(iterator.parent_storage) iterator = iterator.parent_storage return chain def get_full_path(self): output = '' chain = self.get_path_components() for i in range(len(chain) - 1, -1, -1): output = output + '/' + chain[i].name return output def get_qr_code(self): qrdata = '[stor_uuid]' + str(self.id) return qrdata def __str__(self): return self.get_full_path() def get_children(self): if self is None: # Root node return Storage.objects.filter(parent_storage=None) else: return self.storage_set.all() def validate_unique(self, exclude=None): if Storage.objects.exclude(id=self.id).filter(name=self.name, parent_storage__isnull=True).exists(): if self.parent_storage is None: raise ValidationError('The fields name, parent_storage must make a unique set') try: super(Storage, self).validate_unique(exclude) except: raise def get_total_stock_amount(self): stocks = Stock.objects.filter(storage=self) sum = stocks.aggregate(Sum('amount'))['amount__sum'] if sum is None: sum = 0 return sum def save(self, *args, **kwargs): self.validate_unique() super(Storage, self).save(*args, **kwargs) class Distributor(models.Model): class Meta: ordering = ['name'] id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True) name = models.CharField(max_length=100, unique=True) website = models.CharField(max_length=200, null=False, blank=True) image = models.ImageField(upload_to=RandomFileName.RandomFileName('distributor-logos'), null=True, blank=True) component_link_pattern = models.CharField(max_length=255, blank=True, null=False) def __str__(self): return self.name class Package(models.Model): class Meta: ordering = ['name'] id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True) name = models.CharField(max_length=50, unique=True) pin_count = models.PositiveIntegerField() smd = models.BooleanField() image = models.ImageField(upload_to=RandomFileName.RandomFileName('package-images'), blank=True, null=True) def __str__(self): return self.name class PackageParameter(models.Model): class Meta: unique_together = ('package', 'parameter_type') ordering = ['id'] id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True) package = models.ForeignKey(Package, on_delete=models.CASCADE) # A target package is required! parameter_type = models.ForeignKey(ComponentParameterType, on_delete=models.CASCADE) value = models.FloatField(default=0) text_value = models.TextField(null=False, blank=True) def __str__(self): if self.parameter_type.parameter_type == 'F': value = self.text_value else: value = str(self.value) return str(self.package)+ ': '+ str(self.parameter_type) + ': ' + value def resolved_value_as_string(self): my_type = self.parameter_type.parameter_type if my_type == 'E' or my_type == 'I': # Engineering float number (num, prefix) = NumConv.number_to_engineering(self.value, it_unit=(True if my_type=='I' else False)) return f'{num:.3f} {prefix}{self.parameter_type.unit}' elif my_type == 'N': # Standard float number return f'{self.value:.3f} {self.parameter_type.unit}' elif my_type == 'F': return self.text_value class Manufacturer(models.Model): class Meta: ordering = ['name'] id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True) name = models.CharField(max_length=100, unique=True) website = models.CharField(max_length=200, null=False, blank=True) image = models.ImageField(upload_to=RandomFileName.RandomFileName('manufacturer-images'), blank=True, null=True) def __str__(self): return str(self.name) class Component(models.Model): class Meta: unique_together = ('name', 'manufacturer', 'package') ordering = ['name', 'manufacturer'] id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True) name = models.CharField(max_length=100) manufacturer = models.ForeignKey(Manufacturer, on_delete=models.PROTECT, blank=True, null=True) component_type = models.ForeignKey(ComponentType, on_delete=models.SET_NULL, blank=True, null=True) pref_distri = models.ForeignKey(Distributor, on_delete=models.PROTECT, blank=True, null=True) description = models.TextField(null=False, blank=True) datasheet_link = models.CharField(max_length=300, null=False, blank=True) package = models.ForeignKey(Package, on_delete=models.PROTECT, blank=True, null=True) image = models.ImageField(upload_to=RandomFileName.RandomFileName('component-images'), blank=True, null=True) def __str__(self): pack_name = '' man_name = '' if self.package: pack_name = ' in ' + self.package.name if self.manufacturer: man_name = ' by ' + self.manufacturer.name return self.name + pack_name + man_name def get_resolved_image(self): media_url = '' url = None if self.image: url = '%s%s' % (media_url, self.image.url) else: if self.package: if self.package.image: url = '%s%s' % (media_url, self.package.image.url) return url def get_qr_code(self): qrdata = '[comp_uuid]' + str(self.id) return qrdata def get_total_amount(self): stocks = Stock.objects.filter(component=self) sum = stocks.aggregate(Sum('amount'))['amount__sum'] if sum is None: sum = 0 return sum def get_key_parameters(self): """ Get the key parameters of a component defined by its component type. Returns a tuple of 3 elements. All three might be None """ p1 = None p2 = None p3 = None if self.component_type: t = (self.component_type.key_parameter1, self.component_type.key_parameter2, self.component_type.key_parameter3) if t[0]: p1 = ComponentParameter.objects.filter(component=self, parameter_type=t[0]).first() if t[1]: p2 = ComponentParameter.objects.filter(component=self, parameter_type=t[1]).first() if t[2]: p3 = ComponentParameter.objects.filter(component=self, parameter_type=t[2]).first() return (p1, p2, p3) def get_key_parameters_as_text(self): params = self.get_key_parameters() ret_strings = [] for p in params: if p: ret_strings.append(p.resolved_value_as_string()) return ret_strings class ComponentParameter(models.Model): class Meta: unique_together = ('component', 'parameter_type') ordering = ['id'] id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True) component = models.ForeignKey(Component, on_delete=models.CASCADE) # A target component is required! parameter_type = models.ForeignKey(ComponentParameterType, on_delete=models.CASCADE) value = models.FloatField(default=0) text_value = models.TextField(null=False, blank=True) def __str__(self): if self.parameter_type.parameter_type == 'F': value = self.text_value else: value = str(self.value) return str(self.component)+ ': '+ str(self.parameter_type) + ': ' + value def resolved_value_as_string(self): my_type = self.parameter_type.parameter_type if my_type == 'E' or my_type == 'I': # Engineering float number (num, prefix) = NumConv.number_to_engineering(self.value, it_unit=(True if my_type=='I' else False)) num = round(num, 3) return f'{num} {prefix}{self.parameter_type.unit}' elif my_type == 'N': # Standard float number num = round(self.value, 3) return f'{num} {self.parameter_type.unit}' elif my_type == 'F': return self.text_value class Stock(models.Model): class Meta: unique_together = ('component', 'storage') ordering = ['id'] id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True) component = models.ForeignKey(Component, on_delete=models.PROTECT, blank=True, null=True) storage = models.ForeignKey(Storage, on_delete=models.PROTECT, blank=True, null=True) amount = models.PositiveIntegerField() watermark = models.IntegerField() # negative is no watermark lot = models.CharField(max_length=255, null=False, blank=True) def atomic_increment(self, increment): if self.amount + increment < 0: return False self.amount = F('amount') + increment self.save(update_fields=['amount']) return True def get_qr_code(self): qr_data = '[stock]'+str(self.id) return qr_data def __str__(self): return str(self.component) + ' @ ' + str(self.amount) + ' in ' + str( self.storage) @staticmethod def get_under_watermark(user_filter = None, invert: bool = False): query = Stock.objects.filter(amount__lt = F('watermark')) if user_filter is not None: if invert: query = query.exclude(storage__responsible=user_filter) else: query = query.filter(storage__responsible=user_filter) return query class DistributorNum(models.Model): class Meta: unique_together = ('component', 'distributor') ordering = ['distributor__name'] id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True) distributor_part_number = models.CharField(max_length=100) distributor = models.ForeignKey(Distributor, on_delete=models.CASCADE) component = models.ForeignKey(Component, on_delete=models.CASCADE) def __str__(self): return self.component.name + '@' + self.distributor.name + ': ' + self.distributor_part_number class QrPrintJob(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True) user = models.ForeignKey(AuthUser, on_delete=models.CASCADE, null=False, blank=False) qrdata = models.CharField(max_length=256, blank=True, null=False) text = models.TextField(max_length=512, blank=True, null=False) print_count = models.PositiveIntegerField(default=1, validators=[MinValueValidator(0), MaxValueValidator(32)]) def clean(self): super().clean() if self.qrdata == '' and self.text == '': raise ValidationError('Either QR Data or text must be set') # These functions ensure that the uploaded images are deleted if the model is deleted or the image file is changed. @receiver(models.signals.post_delete, sender=Component) @receiver(models.signals.post_delete, sender=Distributor) @receiver(models.signals.post_delete, sender=Package) @receiver(models.signals.post_delete, sender=Manufacturer) def auto_delete_file_on_delete(sender, instance, **kwargs): """ Deletes file from filesystem when corresponding `MediaFile` object is deleted. """ if instance.image: if os.path.isfile(instance.image.path): os.remove(instance.image.path) @receiver(models.signals.pre_save, sender=Component) @receiver(models.signals.pre_save, sender=Distributor) @receiver(models.signals.pre_save, sender=Package) @receiver(models.signals.pre_save, sender=Manufacturer) def auto_delete_file_on_change(sender, instance, **kwargs): """ Deletes old file from filesystem when corresponding `MediaFile` object is updated with new file. """ if not instance.pk: return False try: old_file = sender.objects.get(pk=instance.pk).image except sender.DoesNotExist: return False new_file = instance.image if not old_file == new_file: if not old_file: return True if os.path.isfile(old_file.path): os.remove(old_file.path)