Source code for inventory.models

import datetime
# Create your models here.
import logging

from django.conf import settings
from django.db import models
from django.db.models import Q
from django.urls.base import reverse
from six import python_2_unicode_compatible
from django.utils.functional import cached_property
from mptt.fields import TreeForeignKey
from mptt.managers import TreeManager
from mptt.models import MPTTModel

from events.models import Location

GLOBAL_LOC_DEFAULT = {'name': "Office", "building__name": "Campus Center"}
GLOBAL_STATUS_DEFAULT = {'name': "Available"}

logger = logging.getLogger(__name__)


[docs]class EquipmentCategory(MPTTModel): name = models.CharField(max_length=64, blank=False, null=False) usual_place = models.ForeignKey(Location, on_delete=models.PROTECT, blank=True, null=True, help_text="Default place for items of this category. " "Inherits from parent categories.") parent = TreeForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='children', db_index=True, help_text="If this is a subcategory, the parent is what this is a subcategory of. " "Choose '---' if not.") # for templates @cached_property def get_ancestors_inclusive(self, ascending=False): return self.get_ancestors(ascending=ascending, include_self=True) @cached_property def get_descendants_inclusive(self): return self.get_descendants(include_self=True)
[docs] @classmethod def possible_locations(cls): return Location.objects.complex_filter( Q(holds_equipment=True) | # The usual culprits Q(equipmentcategory__isnull=False) | # or is the default place of a category Q(equipmentitem__isnull=False) # or has at least one item in it. ).distinct()
@cached_property def default_location(self): if self.usual_place: return self.usual_place parent_places = self.get_ancestors(ascending=True).filter(usual_place__isnull=False) if parent_places: return parent_places.first().usual_place try: return Location.objects.get(**GLOBAL_LOC_DEFAULT) except (Location.MultipleObjectsReturned, Location.DoesNotExist): logging.warn("Unable to load default location for %s" % self.name) return None @cached_property def breadcrumbs(self): out = [('Inventory', reverse('inventory:view_all'))] out.extend([ (cat.name, reverse('inventory:cat', args=[cat.pk])) for cat in self.get_ancestors_inclusive ]) return out def __str__(self): return self.name # noinspection PyClassHasNoInit class MPTTMeta: order_insertion_by = ['name']
[docs]class EquipmentItemManager(TreeManager):
[docs] def bulk_add_helper(self, item_type, num_to_add, put_into=None): # items = [] # loc is usually automatic, but not in bulk queries default_loc = item_type.category.default_location if put_into is None else None # with self.delay_mptt_updates(): # for i in xrange(0, num_to_add): # items.append(EquipmentItem(item_type=item_type, home=default_loc, # purchase_date=datetime.date.today(), # case=put_into, # level=0, rght=0, lft=0, tree_id=0)) # self.bulk_create(items) # ^^^ works well for large values, but rather hacky. for i in xrange(0, num_to_add): self.create(item_type=item_type, home=default_loc, purchase_date=datetime.date.today(), case=put_into)
[docs]class EquipmentItem(MPTTModel): objects = EquipmentItemManager() item_type = models.ForeignKey('EquipmentClass', on_delete=models.CASCADE, related_name="items", null=False, blank=False) serial_number = models.CharField(max_length=190, null=True, blank=True) case = TreeForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='contents', db_index=True, help_text="Case or item that contains this item") barcode = models.BigIntegerField(null=True, blank=True, unique=True) purchase_date = models.DateField(null=False, blank=True) home = models.ForeignKey(Location, on_delete=models.PROTECT, null=True, blank=True, help_text="Place where this item typically resides.") features = models.CharField(max_length=128, null=True, blank=True, verbose_name='Identifying Features')
[docs] def save(self, *args, **kwargs): if self.purchase_date is None: self.purchase_date = datetime.date.today() if self.pk is None and not self.home and not self.case: self.home = self.item_type.category.default_location if self.case and self.home: self.home = None # location inherits from parent super(EquipmentItem, self).save(*args, **kwargs)
@cached_property def breadcrumbs(self): out = self.item_type.breadcrumbs out.append( ("Item %s" % (self.barcode or self.pk), reverse('inventory:item_detail', args=[self.pk])) ) return out @property def location(self): if self.case: return self.get_root().location elif self.home: return self.home elif self.case: return self.case.home else: return None @cached_property def unsafe_to_delete(self): return self.get_children().exists() @cached_property def status(self): try: return self.maintenance.latest('date').status except: try: return EquipmentStatus.objects.get(**GLOBAL_STATUS_DEFAULT) except Exception as e: logging.warn("Unable to load default status for item %d [%s]" % (self.pk, e.message)) return None def __str__(self): return "%s (%d)" % (str(self.item_type), self.barcode or self.pk) # noinspection PyClassHasNoInit class MPTTMeta: parent_attr = 'case'
[docs]@python_2_unicode_compatible class EquipmentClass(models.Model): name = models.CharField(max_length=190) category = TreeForeignKey(EquipmentCategory, on_delete=models.CASCADE, null=False, blank=False) description = models.TextField(help_text="Function, appearance, and included acessories", null=True, blank=True) value = models.DecimalField(help_text="Estimated purchase value", max_digits=9, decimal_places=2, null=True, blank=True) model_number = models.CharField(max_length=190, null=True, blank=True) manufacturer = models.CharField(max_length=128, null=True, blank=True) url = models.URLField(null=True, blank=True) holds_items = models.BooleanField(default=False, help_text="Can hold other items") length = models.DecimalField(help_text="Length in inches", max_digits=6, decimal_places=2, null=True, blank=True) width = models.DecimalField(help_text="Width in inches", max_digits=6, decimal_places=2, null=True, blank=True) height = models.DecimalField(help_text="Height in inches", max_digits=6, decimal_places=2, null=True, blank=True) weight = models.DecimalField(help_text="Weight in lbs.", max_digits=6, decimal_places=2, null=True, blank=True) wiki_text = models.TextField(help_text="How to use this item", null=True, blank=True) def __str__(self): return self.name @cached_property def breadcrumbs(self): out = self.category.breadcrumbs out.append( (self.name, reverse('inventory:type_detail', args=[self.pk])) ) return out
[docs] def size(self): dims = filter(lambda dim: dim is not None, [self.length, self.width, self.height]) return "x".join((str(dim) for dim in dims))
class Meta: permissions = ( ("edit_equipment_wiki", "Edit the wiki of an equipment"), ("view_equipment_value", "View estimated value of an equipment"), ("view_equipment", "View equipment") )
# Eg. 'In Repair', 'Out on rental', 'In service'
[docs]@python_2_unicode_compatible class EquipmentStatus(models.Model): name = models.CharField(max_length=32) glyphicon = models.CharField(max_length=32) def __str__(self): return self.name
[docs]@python_2_unicode_compatible class EquipmentMaintEntry(models.Model): date = models.DateTimeField(auto_now_add=True) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, null=False, blank=False) title = models.CharField(max_length=32, null=False, blank=False) entry = models.TextField(null=True, blank=True) equipment = models.ForeignKey(EquipmentItem, on_delete=models.CASCADE, related_name='maintenance', null=False, blank=False) status = models.ForeignKey(EquipmentStatus, on_delete=models.PROTECT, null=False, blank=False) def __str__(self): return str(self.date) class Meta: get_latest_by = "date" ordering = ['-date']