From aafa8781d72bae941406c173f15190e316831a48 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 29 Mar 2017 22:44:59 +1100 Subject: [PATCH 1/8] Added "ProjectRun" model --- InvenTree/project/models.py | 32 +++++++++++++++++++++++--------- InvenTree/supplier/models.py | 15 ++++++++++----- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/InvenTree/project/models.py b/InvenTree/project/models.py index a46b641d39..2e228314ae 100644 --- a/InvenTree/project/models.py +++ b/InvenTree/project/models.py @@ -11,7 +11,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 +21,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 +41,37 @@ 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 - + 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") ]) - + 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/supplier/models.py b/InvenTree/supplier/models.py index db4f8a6659..968f2d011c 100644 --- a/InvenTree/supplier/models.py +++ b/InvenTree/supplier/models.py @@ -9,9 +9,9 @@ from part.models import Part class Supplier(Company): """ Represents a manufacturer or supplier """ - + pass - + class Manufacturer(Company): """ Represents a manfufacturer @@ -37,13 +37,18 @@ class SupplierPart(models.Model): 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) + + # packaging that the part is supplied in, e.g. "Reel" + packaging = models.CharField(max_length=50, blank=True) + def __str__(self): return "{mpn} - {supplier}".format( mpn=self.MPN, @@ -58,7 +63,7 @@ class SupplierPriceBreak(models.Model): part = models.ForeignKey(SupplierPart, on_delete=models.CASCADE) - quantity = models.IntegerField() + quantity = models.PositiveIntegerField() cost = models.DecimalField(max_digits=10, decimal_places=3) currency = models.CharField(max_length=10, blank=True) From 591ae5dc5c2937049fa15102af6a40b5dd06e279 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 29 Mar 2017 22:47:17 +1100 Subject: [PATCH 2/8] Single price field for supplier part --- InvenTree/supplier/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/InvenTree/supplier/models.py b/InvenTree/supplier/models.py index 968f2d011c..cb5859f432 100644 --- a/InvenTree/supplier/models.py +++ b/InvenTree/supplier/models.py @@ -44,7 +44,9 @@ class SupplierPart(models.Model): URL = models.URLField(blank=True) description = models.CharField(max_length=250, blank=True) - single_price = models.DecimalField(max_digits=10, decimal_places=3) + 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) From 2355adb0441f470b7b74dd7354c6088c069c4f47 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 29 Mar 2017 22:55:16 +1100 Subject: [PATCH 3/8] Added lead-time for supplier part --- InvenTree/supplier/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/InvenTree/supplier/models.py b/InvenTree/supplier/models.py index cb5859f432..8a7d00d2c0 100644 --- a/InvenTree/supplier/models.py +++ b/InvenTree/supplier/models.py @@ -51,6 +51,9 @@ class SupplierPart(models.Model): # packaging that the part is supplied in, e.g. "Reel" packaging = models.CharField(max_length=50, blank=True) + # 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, From b2eca2aa48cab10d2b80131ae0421097272d153d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 29 Mar 2017 22:55:28 +1100 Subject: [PATCH 4/8] Added ETA for part stock --- InvenTree/project/models.py | 1 + InvenTree/stock/models.py | 30 +++++++++++++++++------------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/InvenTree/project/models.py b/InvenTree/project/models.py index 2e228314ae..2878c87497 100644 --- a/InvenTree/project/models.py +++ b/InvenTree/project/models.py @@ -62,6 +62,7 @@ class ProjectPart(models.Model): 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. diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 0e6baeb840..51e3ca2b3b 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -8,32 +8,36 @@ 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) - + # 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") - ]) - + + status = models.PositiveIntegerField( + default=ITEM_IN_PROGRESS, + choices=[ + (ITEM_IN_PROGRESS, "In progress"), + (ITEM_INCOMING, "Incoming"), + (ITEM_DAMAGED, "Damaged"), + (ITEM_ATTENTION, "Requires attention"), + (ITEM_COMPLETE, "Complete") + ]) + + # 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, From 8ba4ea344e515451d7cdcbb01fba869044bd3745 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 29 Mar 2017 23:19:53 +1100 Subject: [PATCH 5/8] updates --- InvenTree/part/models.py | 79 +++++++++++++++++++----------------- InvenTree/project/admin.py | 7 +++- InvenTree/project/models.py | 11 +++-- InvenTree/stock/models.py | 34 +++++++++------- InvenTree/supplier/models.py | 6 +-- 5 files changed, 77 insertions(+), 60 deletions(-) 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 2878c87497..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 @@ -46,16 +47,18 @@ class ProjectPart(models.Model): 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.PositiveIntegerField(default=1) overage = models.FloatField(default=0) 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( diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 51e3ca2b3b..e4b8f3bcfa 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 @@ -19,21 +19,27 @@ class StockItem(models.Model): updated = models.DateField(auto_now=True) # Stock status types - ITEM_IN_PROGRESS = 0 - ITEM_INCOMING = 5 - ITEM_DAMAGED = 10 - ITEM_ATTENTION = 20 - ITEM_COMPLETE = 50 + 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_PROGRESS, - choices=[ - (ITEM_IN_PROGRESS, "In progress"), - (ITEM_INCOMING, "Incoming"), - (ITEM_DAMAGED, "Damaged"), - (ITEM_ATTENTION, "Requires attention"), - (ITEM_COMPLETE, "Complete") - ]) + 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) diff --git a/InvenTree/supplier/models.py b/InvenTree/supplier/models.py index 8a7d00d2c0..36c164afbc 100644 --- a/InvenTree/supplier/models.py +++ b/InvenTree/supplier/models.py @@ -32,7 +32,7 @@ class SupplierPart(models.Model): - A Part may be available from multiple suppliers """ - part = models.ForeignKey(Part, + part = models.ForeignKey(Part,null=True,blank=True, on_delete=models.CASCADE) supplier = models.ForeignKey(Supplier, on_delete=models.CASCADE) @@ -55,8 +55,8 @@ class SupplierPart(models.Model): 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) From d0489d692aef75a405172df35da4e773d885bfe6 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 29 Mar 2017 23:21:54 +1100 Subject: [PATCH 6/8] PEP fixes --- InvenTree/supplier/models.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/InvenTree/supplier/models.py b/InvenTree/supplier/models.py index 36c164afbc..0da61c150e 100644 --- a/InvenTree/supplier/models.py +++ b/InvenTree/supplier/models.py @@ -32,10 +32,8 @@ class SupplierPart(models.Model): - A Part may be available from multiple suppliers """ - part = models.ForeignKey(Part,null=True,blank=True, - on_delete=models.CASCADE) - supplier = models.ForeignKey(Supplier, - on_delete=models.CASCADE) + part = models.ForeignKey(Part, null=True, blank=True, on_delete=models.CASCADE) + 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) From 9db1f99e26fcada5fa4d2c1716361ef6ff4cd7a3 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 29 Mar 2017 23:32:21 +1100 Subject: [PATCH 7/8] updated supplier model --- InvenTree/supplier/models.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/InvenTree/supplier/models.py b/InvenTree/supplier/models.py index 0da61c150e..b288be7c2c 100644 --- a/InvenTree/supplier/models.py +++ b/InvenTree/supplier/models.py @@ -9,7 +9,6 @@ from part.models import Part class Supplier(Company): """ Represents a manufacturer or supplier """ - pass @@ -32,7 +31,7 @@ class SupplierPart(models.Model): - A Part may be available from multiple suppliers """ - part = models.ForeignKey(Part, null=True, blank=True, 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) @@ -49,6 +48,9 @@ class SupplierPart(models.Model): # 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) @@ -64,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) + 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( From 6e0a02a885b1f494ed399a7c85c50f38d53e5807 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 29 Mar 2017 23:36:06 +1100 Subject: [PATCH 8/8] Added extra fields to stock item --- InvenTree/stock/models.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index e4b8f3bcfa..40d95a5676 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -18,6 +18,11 @@ class StockItem(models.Model): 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_STOCK = 10 ITEM_INCOMING = 15