from django.db import models from shimatta_modules import RandomFileName from django.db.models import F, Sum from django.contrib.auth import get_user_model 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) passive = models.BooleanField() possible_parameter = models.ManyToManyField(ComponentParameterType, blank=True) def __str__(self): return '[' + self.class_name + ']' 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]) verbose_name = models.CharField(max_length=100, null=True, blank=True) parent_storage = models.ForeignKey('self', on_delete=models.CASCADE, blank=True, null=True) responsible = models.ForeignKey(get_user_model(), on_delete=models.SET_NULL, blank=True, null=True) # allow storages to be templates which can be selected when adding new storages is_template = models.BooleanField(default=False) template = models.ForeignKey('self', on_delete=models.SET_NULL, blank=True, null=True, related_name='template_of') 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 @classmethod def from_path(cls, path, root_storage=None): ''' Get the storage object described by its complete path or the sub-path from the passed root_storage uuid ''' parts = path.split('/') # assemble filter query filter_dict = {} layer = 0 for part in parts[::-1]: filter_dict[f'{"parent_storage__" * layer}name'] = part layer += 1 if root_storage: filter_dict[f'{"parent_storage__" * layer}id'] = root_storage else: filter_dict[f'{"parent_storage__" * layer}isnull'] = True obj = cls.objects.get(**filter_dict) return obj 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 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 class AbstractParameter(models.Model): class Meta: abstract = True id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True) parameter_type = models.ForeignKey(ComponentParameterType, on_delete=models.CASCADE) value = models.FloatField(default=0) text_value = models.TextField(null=False, blank=True) def _get_object_of_param(self): return None def __str__(self): if self.parameter_type.parameter_type == 'F': value = self.text_value else: value = str(self.value) return str(self._get_object_of_param())+ ': '+ 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 ComponentParameter(AbstractParameter): class Meta: unique_together = ('component', 'parameter_type') ordering = ['id'] component = models.ForeignKey(Component, on_delete=models.CASCADE) # A target component is required! def _get_object_of_param(self): return self.component class PackageParameter(AbstractParameter): class Meta: unique_together = ('package', 'parameter_type') ordering = ['id'] package = models.ForeignKey(Package, on_delete=models.CASCADE) def _get_object_of_param(self): return self.package 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(get_user_model(), 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) @receiver(models.signals.post_save, sender=Storage) def auto_apply_template_structure(sender, instance, created, **kwargs): """ Add the sub-storages from the template. If there are nested sub-storages these will be added when the sub-storages are created automatically. """ if created: if instance.template: for sub_storage in instance.template.storage_set.all(): Storage.objects.create(name=sub_storage.name, parent_storage=instance, responsible=instance.responsible, is_template=False, template=sub_storage)