shimatta-kenkyusho/shimatta_kenkyusho/parts/models.py

401 lines
14 KiB
Python

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)
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.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])
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)