diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 62041f756b..e9d246fd46 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -1,5 +1,5 @@ from __future__ import unicode_literals - +from django.utils.translation import ugettext as _ from django.db import models from django.db.models import Sum from django.core.exceptions import ObjectDoesNotExist, ValidationError @@ -10,15 +10,15 @@ from InvenTree.models import InvenTreeTree class PartCategory(InvenTreeTree): """ PartCategory provides hierarchical organization of Part objects. """ - + class Meta: verbose_name = "Part Category" verbose_name_plural = "Part Categories" - - + + class Part(models.Model): """ Represents a """ - + name = models.CharField(max_length=100) description = models.CharField(max_length=250, blank=True) IPN = models.CharField(max_length=100, blank=True) @@ -26,7 +26,7 @@ class Part(models.Model): minimum_stock = models.IntegerField(default=0) units = models.CharField(max_length=20, default="pcs", blank=True) trackable = models.BooleanField(default=False) - + def __str__(self): if self.IPN: return "{name} ({ipn})".format( @@ -34,39 +34,39 @@ class Part(models.Model): name=self.name) else: return self.name - + class Meta: verbose_name = "Part" verbose_name_plural = "Parts" - + @property def stock(self): """ Return the total stock quantity for this part. Part may be stored in multiple locations """ - + stocks = self.locations.all() if len(stocks) == 0: return 0 - + result = stocks.aggregate(total=Sum('quantity')) return result['total'] - + @property def projects(self): """ Return a list of unique projects that this part is associated with """ - + project_ids = set() project_parts = self.projectpart_set.all() - + projects = [] - + for pp in project_parts: if pp.project.id not in project_ids: project_ids.add(pp.project.id) projects.append(pp.project) - + return projects @@ -78,28 +78,31 @@ class PartParameterTemplate(models.Model): name = models.CharField(max_length=20) description = models.CharField(max_length=100, blank=True) units = models.CharField(max_length=10, blank=True) - + default_value = models.CharField(max_length=50, blank=True) default_min = models.CharField(max_length=50, blank=True) default_max = models.CharField(max_length=50, blank=True) - + # Parameter format PARAM_NUMERIC = 10 PARAM_TEXT = 20 PARAM_BOOL = 30 - + + PARAM_TYPE_CODES = { + PARAM_NUMERIC: _("Numeric"), + PARAM_TEXT: _("Text"), + PARAM_BOOL: _("Bool") + } + format = models.IntegerField( default=PARAM_NUMERIC, - choices=[ - (PARAM_NUMERIC, "Numeric"), - (PARAM_TEXT, "Text"), - (PARAM_BOOL, "Boolean")]) - + choices=PARAM_TYPE_CODES.items()) + def __str__(self): return "{name} ({units})".format( name=self.name, units=self.units) - + class Meta: verbose_name = "Parameter Template" verbose_name_plural = "Parameter Templates" @@ -110,7 +113,7 @@ class CategoryParameterLink(models.Model): """ category = models.ForeignKey(PartCategory, on_delete=models.CASCADE) template = models.ForeignKey(PartParameterTemplate, on_delete=models.CASCADE) - + def __str__(self): return "{name} - {cat}".format( name=self.template.name, @@ -119,20 +122,20 @@ class CategoryParameterLink(models.Model): class Meta: verbose_name = "Category Parameter" verbose_name_plural = "Category Parameters" - + class PartParameter(models.Model): """ PartParameter is associated with a single part """ - + part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='parameters') template = models.ForeignKey(PartParameterTemplate) - + # Value data value = models.CharField(max_length=50, blank=True) min_value = models.CharField(max_length=50, blank=True) max_value = models.CharField(max_length=50, blank=True) - + # Prevent multiple parameters of the same template # from being added to the same part def save(self, *args, **kwargs): @@ -141,27 +144,27 @@ class PartParameter(models.Model): return if len(params) == 1 and params[0].id != self.id: return - + super(PartParameter, self).save(*args, **kwargs) - + def __str__(self): return "{name} : {val}{units}".format( name=self.template.name, val=self.value, units=self.template.units) - + @property def units(self): return self.template.units - + @property def name(self): return self.template.name - + class Meta: verbose_name = "Part Parameter" verbose_name_plural = "Part Parameters" - + class PartRevision(models.Model): """ A PartRevision represents a change-notification to a Part @@ -169,12 +172,12 @@ class PartRevision(models.Model): which should be tracked. UniqueParts can have a single associated PartRevision """ - + part = models.ForeignKey(Part, on_delete=models.CASCADE) - + name = models.CharField(max_length=100) description = models.CharField(max_length=500) revision_date = models.DateField(auto_now_add=True) - + def __str__(self): return self.name diff --git a/InvenTree/project/admin.py b/InvenTree/project/admin.py index b37fa8435c..0f1ad27b11 100644 --- a/InvenTree/project/admin.py +++ b/InvenTree/project/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from .models import ProjectCategory, Project, ProjectPart +from .models import ProjectCategory, Project, ProjectPart, ProjectRun class ProjectCategoryAdmin(admin.ModelAdmin): @@ -14,6 +14,11 @@ class ProjectAdmin(admin.ModelAdmin): class ProjectPartAdmin(admin.ModelAdmin): list_display = ('part', 'project', 'quantity') + +class ProjectRunAdmin(admin.ModelAdmin): + list_display = ('project', 'quantity', 'run_date') + admin.site.register(ProjectCategory, ProjectCategoryAdmin) admin.site.register(Project, ProjectAdmin) admin.site.register(ProjectPart, ProjectPartAdmin) +admin.site.register(ProjectRun, ProjectRunAdmin) diff --git a/InvenTree/project/models.py b/InvenTree/project/models.py index a46b641d39..15f87ae006 100644 --- a/InvenTree/project/models.py +++ b/InvenTree/project/models.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals +from django.utils.translation import ugettext as _ from django.db import models @@ -11,7 +12,7 @@ class ProjectCategory(InvenTreeTree): Each ProjectCategory can contain zero-or-more child categories, and in turn can have zero-or-one parent category. """ - + class Meta: verbose_name = "Project Category" verbose_name_plural = "Project Categories" @@ -21,14 +22,14 @@ class Project(models.Model): """ A Project takes multiple Part objects. A project can output zero-or-more Part objects """ - + name = models.CharField(max_length=100) description = models.CharField(max_length=500, blank=True) category = models.ForeignKey(ProjectCategory, on_delete=models.CASCADE) - + def __str__(self): return self.name - + @property def projectParts(self): """ Return a list of all project parts associated with this project @@ -41,23 +42,40 @@ class ProjectPart(models.Model): The quantity of parts required for a single-run of that project is stored. The overage is the number of extra parts that are generally used for a single run. """ - + # Overage types OVERAGE_PERCENT = 0 OVERAGE_ABSOLUTE = 1 - + + OVARAGE_CODES = { + OVERAGE_PERCENT: _("Percent"), + OVERAGE_ABSOLUTE: _("Absolute") + } + part = models.ForeignKey(Part, on_delete=models.CASCADE) project = models.ForeignKey(Project, on_delete=models.CASCADE) - quantity = models.IntegerField(default=1) + quantity = models.PositiveIntegerField(default=1) overage = models.FloatField(default=0) - overage_type = models.IntegerField( + overage_type = models.PositiveIntegerField( default=1, - choices=[ - (OVERAGE_PERCENT, "Percent"), - (OVERAGE_ABSOLUTE, "Absolute") - ]) - + choices=OVARAGE_CODES.items()) + def __str__(self): return "{quan} x {name}".format( name=self.part.name, quan=self.quantity) + + +class ProjectRun(models.Model): + """ A single run of a particular project. + Tracks the number of 'units' made in the project. + Provides functionality to update stock, + based on both: + a) Parts used (project inputs) + b) Parts produced (project outputs) + """ + + project = models.ForeignKey(Project, on_delete=models.CASCADE) + quantity = models.PositiveIntegerField(default=1) + + run_date = models.DateField(auto_now_add=True) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 0e6baeb840..40d95a5676 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -1,5 +1,5 @@ from __future__ import unicode_literals - +from django.utils.translation import ugettext as _ from django.db import models from part.models import Part @@ -8,32 +8,47 @@ from InvenTree.models import InvenTreeTree class Warehouse(InvenTreeTree): pass - + class StockItem(models.Model): part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='locations') location = models.ForeignKey(Warehouse, on_delete=models.CASCADE) - quantity = models.IntegerField() + quantity = models.PositiveIntegerField() updated = models.DateField(auto_now=True) - + + # last time the stock was checked / counted + last_checked = models.DateField(blank=True, null=True) + + review_needed = models.BooleanField(default=False) + # Stock status types - ITEM_IN_PROGRESS = 0 - ITEM_INCOMING = 5 - ITEM_DAMAGED = 10 - ITEM_ATTENTION = 20 - ITEM_COMPLETE = 50 - - status = models.IntegerField(default=ITEM_IN_PROGRESS, - choices=[ - (ITEM_IN_PROGRESS, "In progress"), - (ITEM_INCOMING, "Incoming"), - (ITEM_DAMAGED, "Damaged"), - (ITEM_ATTENTION, "Requires attention"), - (ITEM_COMPLETE, "Complete") - ]) - + ITEM_IN_STOCK = 10 + ITEM_INCOMING = 15 + ITEM_IN_PROGRESS = 20 + ITEM_COMPLETE = 25 + ITEM_ATTENTION = 50 + ITEM_DAMAGED = 55 + ITEM_DESTROYED = 60 + + ITEM_STATUS_CODES = { + ITEM_IN_STOCK: _("In stock"), + ITEM_INCOMING: _("Incoming"), + ITEM_IN_PROGRESS: _("In progress"), + ITEM_COMPLETE: _("Complete"), + ITEM_ATTENTION: _("Attention needed"), + ITEM_DAMAGED: _("Damaged"), + ITEM_DESTROYED: _("Destroyed") + } + + status = models.PositiveIntegerField( + default=ITEM_IN_STOCK, + choices=ITEM_STATUS_CODES.items()) + + # If stock item is incoming, an (optional) ETA field + expected_arrival = models.DateField(null=True, blank=True) + def __str__(self): return "{n} x {part} @ {loc}".format( n=self.quantity, diff --git a/InvenTree/supplier/models.py b/InvenTree/supplier/models.py index db4f8a6659..b288be7c2c 100644 --- a/InvenTree/supplier/models.py +++ b/InvenTree/supplier/models.py @@ -9,9 +9,8 @@ from part.models import Part class Supplier(Company): """ Represents a manufacturer or supplier """ - pass - + class Manufacturer(Company): """ Represents a manfufacturer @@ -32,21 +31,32 @@ class SupplierPart(models.Model): - A Part may be available from multiple suppliers """ - part = models.ForeignKey(Part, - on_delete=models.CASCADE) - supplier = models.ForeignKey(Supplier, - on_delete=models.CASCADE) + part = models.ForeignKey(Part, null=True, blank=True, on_delete=models.CASCADE, related_name='supplier_parts') + supplier = models.ForeignKey(Supplier, on_delete=models.CASCADE) SKU = models.CharField(max_length=100) - + manufacturer = models.ForeignKey(Manufacturer, blank=True, null=True, on_delete=models.CASCADE) MPN = models.CharField(max_length=100, blank=True) - + URL = models.URLField(blank=True) description = models.CharField(max_length=250, blank=True) + single_price = models.DecimalField(max_digits=10, + decimal_places=3, + default=0) + + # packaging that the part is supplied in, e.g. "Reel" + packaging = models.CharField(max_length=50, blank=True) + + # multiple that the part is provided in + multiple = models.PositiveIntegerField(default=1) + + # lead time for parts that cannot be delivered immediately + lead_time = models.DurationField(blank=True, null=True) + def __str__(self): - return "{mpn} - {supplier}".format( - mpn=self.MPN, + return "{sku} - {supplier}".format( + sku=self.SKU, supplier=self.supplier.name) @@ -56,12 +66,9 @@ class SupplierPriceBreak(models.Model): - SupplierPart(s) may have zero-or-more associated SupplierPriceBreak(s) """ - part = models.ForeignKey(SupplierPart, - on_delete=models.CASCADE) - quantity = models.IntegerField() + part = models.ForeignKey(SupplierPart, on_delete=models.CASCADE, related_name='price_breaks') + quantity = models.PositiveIntegerField() cost = models.DecimalField(max_digits=10, decimal_places=3) - currency = models.CharField(max_length=10, - blank=True) def __str__(self): return "{mpn} - {cost}{currency} @ {quan}".format(