diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py
index a28b27a5eb..a7fc93bf3f 100644
--- a/InvenTree/InvenTree/models.py
+++ b/InvenTree/InvenTree/models.py
@@ -17,6 +17,11 @@ class InvenTreeTree(models.Model):
- Each Category has one parent Category, which can be blank (for a top-level Category).
- Each Category can have zero-or-more child Categor(y/ies)
+
+ Attributes:
+ name: brief name
+ description: longer form description
+ parent: The item immediately above this one. An item with a null parent is a top-level item
"""
class Meta:
diff --git a/InvenTree/build/forms.py b/InvenTree/build/forms.py
index 57c31ff1db..66ec98ac77 100644
--- a/InvenTree/build/forms.py
+++ b/InvenTree/build/forms.py
@@ -21,6 +21,7 @@ class EditBuildForm(HelperForm):
'title',
'part',
'quantity',
+ 'take_from',
'batch',
'URL',
'notes',
diff --git a/InvenTree/build/migrations/0013_build_take_from.py b/InvenTree/build/migrations/0013_build_take_from.py
new file mode 100644
index 0000000000..9945da2be1
--- /dev/null
+++ b/InvenTree/build/migrations/0013_build_take_from.py
@@ -0,0 +1,20 @@
+# Generated by Django 2.2 on 2019-05-10 08:50
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('stock', '0015_stockitem_delete_on_deplete'),
+ ('build', '0012_auto_20190508_2332'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='build',
+ name='take_from',
+ field=models.ForeignKey(blank=True, help_text='Select location to take stock from for this build (leave blank to take from any stock location', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sourcing_builds', to='stock.StockLocation'),
+ ),
+ ]
diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py
index 0b658dee1a..33a63797ac 100644
--- a/InvenTree/build/models.py
+++ b/InvenTree/build/models.py
@@ -27,6 +27,7 @@ class Build(models.Model):
part: The part to be built (from component BOM items)
title: Brief title describing the build (required)
quantity: Number of units to be built
+ take_from: Location to take stock from to make this build (if blank, can take from anywhere)
status: Build status code
batch: Batch code transferred to build parts (optional)
creation_date: Date the build was created (auto)
@@ -41,6 +42,11 @@ class Build(models.Model):
def get_absolute_url(self):
return reverse('build-detail', kwargs={'pk': self.id})
+ title = models.CharField(
+ blank=False,
+ max_length=100,
+ help_text='Brief description of the build')
+
part = models.ForeignKey('part.Part', on_delete=models.CASCADE,
related_name='builds',
limit_choices_to={
@@ -50,10 +56,11 @@ class Build(models.Model):
help_text='Select part to build',
)
- title = models.CharField(
- blank=False,
- max_length=100,
- help_text='Brief description of the build')
+ take_from = models.ForeignKey('stock.StockLocation', on_delete=models.SET_NULL,
+ related_name='sourcing_builds',
+ null=True, blank=True,
+ help_text='Select location to take stock from for this build (leave blank to take from any stock location'
+ )
quantity = models.PositiveIntegerField(
default=1,
@@ -139,6 +146,11 @@ class Build(models.Model):
stock = StockItem.objects.filter(part=item.sub_part)
+ # Ensure that the available stock items are in the correct location
+ if self.take_from is not None:
+ # Filter for stock that is located downstream of the designated location
+ stock = stock.filter(location__in=[loc for loc in self.take_from.getUniqueChildren()])
+
# Only one StockItem to choose from? Default to that one!
if len(stock) == 1:
stock_item = stock[0]
@@ -330,7 +342,7 @@ class BuildItem(models.Model):
Attributes:
build: Link to a Build object
- stock: Link to a StockItem object
+ stock_item: Link to a StockItem object
quantity: Number of units allocated
"""
diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html
index fef67f77c1..b4de8787b1 100644
--- a/InvenTree/build/templates/build/detail.html
+++ b/InvenTree/build/templates/build/detail.html
@@ -45,6 +45,16 @@ InvenTree | Build - {{ build }}
Quantity | {{ build.quantity }} |
+
+ Stock Source |
+
+ {% if build.take_from %}
+ {{ build.take_from }}
+ {% else %}
+ Stock can be taken from any available location.
+ {% endif %}
+ |
+
Status | {% include "build_status.html" with build=build %} |
diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py
index 79e3819df5..85d07858fb 100644
--- a/InvenTree/build/views.py
+++ b/InvenTree/build/views.py
@@ -374,6 +374,16 @@ class BuildItemCreate(AjaxCreateView):
query = query.filter(part=part_id)
if build_id is not None:
+ try:
+ build = Build.objects.get(id=build_id)
+
+ if build.take_from is not None:
+ # Limit query to stock items that are downstream of the 'take_from' location
+ query = query.filter(location__in=[loc for loc in build.take_from.getUniqueChildren()])
+
+ except Build.DoesNotExist:
+ pass
+
# Exclude StockItem objects which are already allocated to this build and part
query = query.exclude(id__in=[item.stock_item.id for item in BuildItem.objects.filter(build=build_id, stock_item__part=part_id)])
diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py
index 9086d66f1f..2e6d3c694c 100644
--- a/InvenTree/company/models.py
+++ b/InvenTree/company/models.py
@@ -42,6 +42,19 @@ def rename_company_image(instance, filename):
class Company(models.Model):
""" A Company object represents an external company.
It may be a supplier or a customer (or both).
+
+ Attributes:
+ name: Brief name of the company
+ description: Longer form description
+ website: URL for the company website
+ address: Postal address
+ phone: contact phone number
+ email: contact email address
+ URL: Secondary URL e.g. for link to internal Wiki page
+ image: Company image / logo
+ notes: Extra notes about the company
+ is_customer: boolean value, is this company a customer
+ is_supplier: boolean value, is this company a supplier
"""
name = models.CharField(max_length=100, blank=False, unique=True,
@@ -101,9 +114,19 @@ class Company(models.Model):
class Contact(models.Model):
""" A Contact represents a person who works at a particular company.
- A Company may have zero or more associated Contact objects
+ A Company may have zero or more associated Contact objects.
+
+ Attributes:
+ company: Company link for this contact
+ name: Name of the contact
+ phone: contact phone number
+ email: contact email
+ role: position in company
"""
+ company = models.ForeignKey(Company, related_name='contacts',
+ on_delete=models.CASCADE)
+
name = models.CharField(max_length=100)
phone = models.CharField(max_length=100, blank=True)
diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py
index f91f4d3d7d..5a318c85b5 100644
--- a/InvenTree/part/models.py
+++ b/InvenTree/part/models.py
@@ -116,9 +116,30 @@ def rename_part_image(instance, filename):
class Part(models.Model):
- """ Represents an abstract part
- Parts can be "stocked" in multiple warehouses,
- and can be combined to form other parts
+ """ The Part object represents an abstract part, the 'concept' of an actual entity.
+
+ An actual physical instance of a Part is a StockItem which is treated separately.
+
+ Parts can be used to create other parts (as part of a Bill of Materials or BOM).
+
+ Attributes:
+ name: Brief name for this part
+ description: Longer form description of the part
+ category: The PartCategory to which this part belongs
+ IPN: Internal part number (optional)
+ URL: Link to an external page with more information about this part (e.g. internal Wiki)
+ image: Image of this part
+ default_location: Where the item is normally stored (may be null)
+ default_supplier: The default SupplierPart which should be used to procure and stock this part
+ minimum_stock: Minimum preferred quantity to keep in stock
+ units: Units of measure for this part (default='pcs')
+ salable: Can this part be sold to customers?
+ buildable: Can this part be build from other parts?
+ consumable: Can this part be used to make other parts?
+ purchaseable: Can this part be purchased from suppliers?
+ trackable: Trackable parts can have unique serial numbers assigned, etc, etc
+ active: Is this part active? Parts are deactivated instead of being deleted
+ notes: Additional notes field for this part
"""
def get_absolute_url(self):
@@ -133,26 +154,19 @@ class Part(models.Model):
else:
return static('/img/blank_image.png')
- # Short name of the part
name = models.CharField(max_length=100, unique=True, blank=False, help_text='Part name (must be unique)')
- # Longer description of the part (optional)
description = models.CharField(max_length=250, blank=False, help_text='Part description')
- # Internal Part Number (optional)
- # Potentially multiple parts map to the same internal IPN (variants?)
- # So this does not have to be unique
- IPN = models.CharField(max_length=100, blank=True, help_text='Internal Part Number')
-
- # Provide a URL for an external link
- URL = models.URLField(blank=True, help_text='Link to extenal URL')
-
- # Part category - all parts must be assigned to a category
category = models.ForeignKey(PartCategory, related_name='parts',
null=True, blank=True,
on_delete=models.DO_NOTHING,
help_text='Part category')
+ IPN = models.CharField(max_length=100, blank=True, help_text='Internal Part Number')
+
+ URL = models.URLField(blank=True, help_text='Link to extenal URL')
+
image = models.ImageField(upload_to=rename_part_image, max_length=255, null=True, blank=True)
default_location = models.ForeignKey('stock.StockLocation', on_delete=models.SET_NULL,
@@ -183,23 +197,18 @@ class Part(models.Model):
# Default case - no default category found
return None
- # Default supplier part
default_supplier = models.ForeignKey('part.SupplierPart',
on_delete=models.SET_NULL,
blank=True, null=True,
help_text='Default supplier part',
related_name='default_parts')
- # Minimum "allowed" stock level
minimum_stock = models.PositiveIntegerField(default=0, validators=[MinValueValidator(0)], help_text='Minimum allowed stock level')
- # Units of quantity for this part. Default is "pcs"
units = models.CharField(max_length=20, default="pcs", blank=True, help_text='Stock keeping units for this part')
- # Can this part be built from other parts?
buildable = models.BooleanField(default=False, help_text='Can this part be built from other parts?')
- # Can this part be used to make other parts?
consumable = models.BooleanField(default=True, help_text='Can this part be used to build other parts?')
# Is this part "trackable"?
@@ -434,6 +443,11 @@ def attach_file(instance, filename):
class PartAttachment(models.Model):
""" A PartAttachment links a file to a part
Parts can have multiple files such as datasheets, etc
+
+ Attributes:
+ part: Link to a Part object
+ attachment: File
+ comment: String descriptor for the attachment
"""
part = models.ForeignKey(Part, on_delete=models.CASCADE,
@@ -454,6 +468,10 @@ class PartStar(models.Model):
It is used to designate a Part as 'starred' (or favourited) for a given User,
so that the user can track a list of their favourite parts.
+
+ Attributes:
+ part: Link to a Part object
+ user: Link to a User object
"""
part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='starred_users')
@@ -467,7 +485,13 @@ class PartStar(models.Model):
class BomItem(models.Model):
""" A BomItem links a part to its component items.
A part can have a BOM (bill of materials) which defines
- which parts are required (and in what quatity) to make it
+ which parts are required (and in what quatity) to make it.
+
+ Attributes:
+ part: Link to the parent part (the part that will be produced)
+ sub_part: Link to the child part (the part that will be consumed)
+ quantity: Number of 'sub_parts' consumed to produce one 'part'
+ note: Note field for this BOM item
"""
def get_absolute_url(self):
@@ -530,8 +554,23 @@ class SupplierPart(models.Model):
""" Represents a unique part as provided by a Supplier
Each SupplierPart is identified by a MPN (Manufacturer Part Number)
Each SupplierPart is also linked to a Part object.
-
A Part may be available from multiple suppliers
+
+ Attributes:
+ part: Link to the master Part
+ supplier: Company that supplies this SupplierPart object
+ SKU: Stock keeping unit (supplier part number)
+ manufacturer: Manufacturer name
+ MPN: Manufacture part number
+ URL: Link to external website for this part
+ description: Descriptive notes field
+ note: Longer form note field
+ single_price: Default price for a single unit
+ base_cost: Base charge added to order independent of quantity e.g. "Reeling Fee"
+ multiple: Multiple that the part is provided in
+ minimum: MOQ (minimum order quantity) required for purchase
+ lead_time: Supplier lead time
+ packaging: packaging that the part is supplied in, e.g. "Reel"
"""
def get_absolute_url(self):
@@ -540,8 +579,6 @@ class SupplierPart(models.Model):
class Meta:
unique_together = ('part', 'supplier', 'SKU')
- # Link to an actual part
-# The part will have a field 'supplier_parts' which links to the supplier part options
part = models.ForeignKey(Part, on_delete=models.CASCADE,
related_name='supplier_parts',
limit_choices_to={'purchaseable': True},
@@ -564,25 +601,18 @@ class SupplierPart(models.Model):
description = models.CharField(max_length=250, blank=True, help_text='Supplier part description')
- # Note attached to this BOM line item
note = models.CharField(max_length=100, blank=True, help_text='Notes')
- # Default price for a single unit
single_price = models.DecimalField(max_digits=10, decimal_places=3, default=0, validators=[MinValueValidator(0)], help_text='Price for single quantity')
- # Base charge added to order independent of quantity e.g. "Reeling Fee"
base_cost = models.DecimalField(max_digits=10, decimal_places=3, default=0, validators=[MinValueValidator(0)], help_text='Minimum charge (e.g. stocking fee)')
- # packaging that the part is supplied in, e.g. "Reel"
packaging = models.CharField(max_length=50, blank=True, help_text='Part packaging')
-
- # multiple that the part is provided in
+
multiple = models.PositiveIntegerField(default=1, validators=[MinValueValidator(1)], help_text='Order multiple')
- # Mimumum number required to order
minimum = models.PositiveIntegerField(default=1, validators=[MinValueValidator(1)], help_text='Minimum order quantity (MOQ)')
- # lead time for parts that cannot be delivered immediately
lead_time = models.DurationField(blank=True, null=True)
@property
@@ -654,9 +684,14 @@ class SupplierPart(models.Model):
class SupplierPriceBreak(models.Model):
- """ Represents a quantity price break for a SupplierPart
+ """ Represents a quantity price break for a SupplierPart.
- Suppliers can offer discounts at larger quantities
- SupplierPart(s) may have zero-or-more associated SupplierPriceBreak(s)
+
+ Attributes:
+ part: Link to a SupplierPart object that this price break applies to
+ quantity: Quantity required for price break
+ cost: Cost at specified quantity
"""
part = models.ForeignKey(SupplierPart, on_delete=models.CASCADE, related_name='price_breaks')
diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py
index 67d92c87e7..b4216c46c1 100644
--- a/InvenTree/stock/models.py
+++ b/InvenTree/stock/models.py
@@ -77,10 +77,23 @@ def before_delete_stock_location(sender, instance, using, **kwargs):
class StockItem(models.Model):
"""
- A 'StockItem' instance represents a quantity of physical instances of a part.
- It may exist in a StockLocation, or as part of a sub-assembly installed into another StockItem
- StockItems may be tracked using batch or serial numbers.
- If a serial number is assigned, then StockItem cannot have a quantity other than 1
+ A StockItem object represents a quantity of physical instances of a part.
+
+ Attributes:
+ part: Link to the master abstract part that this StockItem is an instance of
+ supplier_part: Link to a specific SupplierPart (optional)
+ location: Where this StockItem is located
+ quantity: Number of stocked units
+ batch: Batch number for this StockItem
+ URL: Optional URL to link to external resource
+ updated: Date that this stock item was last updated (auto)
+ stocktake_date: Date of last stocktake for this item
+ stocktake_user: User that performed the most recent stocktake
+ review_needed: Flag if StockItem needs review
+ delete_on_deplete: If True, StockItem will be deleted when the stock level gets to zero
+ status: Status of this StockItem (ref: ITEM_STATUS_CODES)
+ notes: Extra notes field
+ infinite: If True this StockItem can never be exhausted
"""
def save(self, *args, **kwargs):
@@ -171,46 +184,33 @@ class StockItem(models.Model):
}
)
- # The 'master' copy of the part of which this stock item is an instance
part = models.ForeignKey('part.Part', on_delete=models.CASCADE, related_name='locations', help_text='Base part')
- # The 'supplier part' used in this instance. May be null if no supplier parts are defined the master part
supplier_part = models.ForeignKey('part.SupplierPart', blank=True, null=True, on_delete=models.SET_NULL,
help_text='Select a matching supplier part for this stock item')
- # Where the part is stored. If the part has been used to build another stock item, the location may not make sense
location = models.ForeignKey(StockLocation, on_delete=models.DO_NOTHING,
related_name='stock_items', blank=True, null=True,
help_text='Where is this stock item located?')
- # If this StockItem belongs to another StockItem (e.g. as part of a sub-assembly)
belongs_to = models.ForeignKey('self', on_delete=models.DO_NOTHING,
related_name='owned_parts', blank=True, null=True,
help_text='Is this item installed in another item?')
- # The StockItem may be assigned to a particular customer
customer = models.ForeignKey('company.Company', on_delete=models.SET_NULL,
related_name='stockitems', blank=True, null=True,
help_text='Item assigned to customer?')
- # Optional serial number
serial = models.PositiveIntegerField(blank=True, null=True,
help_text='Serial number for this item')
-
- # Optional URL to link to external resource
+
URL = models.URLField(max_length=125, blank=True)
- # Optional batch information
batch = models.CharField(max_length=100, blank=True, null=True,
help_text='Batch code for this stock item')
- # If this part was produced by a build, point to that build here
- # build = models.ForeignKey('build.Build', on_delete=models.SET_NULL, blank=True, null=True)
-
- # Quantity of this stock item. Value may be overridden by other settings
quantity = models.PositiveIntegerField(validators=[MinValueValidator(0)], default=1)
- # Last time this item was updated (set automagically)
updated = models.DateField(auto_now=True)
# last time the stock was checked / counted
@@ -409,33 +409,34 @@ class StockItem(models.Model):
class StockItemTracking(models.Model):
- """ Stock tracking entry
+ """ Stock tracking entry - breacrumb for keeping track of automated stock transactions
+
+ Attributes:
+ item: Link to StockItem
+ date: Date that this tracking info was created
+ title: Title of this tracking info (generated by system)
+ notes: Associated notes (input by user)
+ user: The user associated with this tracking info
+ quantity: The StockItem quantity at this point in time
"""
def get_absolute_url(self):
return '/stock/track/{pk}'.format(pk=self.id)
# return reverse('stock-tracking-detail', kwargs={'pk': self.id})
- # Stock item
item = models.ForeignKey(StockItem, on_delete=models.CASCADE,
related_name='tracking_info')
- # Date this entry was created (cannot be edited)
date = models.DateTimeField(auto_now_add=True, editable=False)
- # Short-form title for this tracking entry
title = models.CharField(blank=False, max_length=250)
- # Optional longer description
notes = models.TextField(blank=True)
- # Which user created this tracking entry?
user = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True)
- # Was this tracking note auto-generated by the system?
system = models.BooleanField(default=False)
- # Keep track of the StockItem quantity throughout the tracking history
quantity = models.PositiveIntegerField(validators=[MinValueValidator(0)], default=1)
# TODO