mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'part-revision'
This commit is contained in:
commit
1b28326c5e
@ -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 has one parent Category, which can be blank (for a top-level Category).
|
||||||
- Each Category can have zero-or-more child Categor(y/ies)
|
- 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:
|
class Meta:
|
||||||
|
@ -21,6 +21,7 @@ class EditBuildForm(HelperForm):
|
|||||||
'title',
|
'title',
|
||||||
'part',
|
'part',
|
||||||
'quantity',
|
'quantity',
|
||||||
|
'take_from',
|
||||||
'batch',
|
'batch',
|
||||||
'URL',
|
'URL',
|
||||||
'notes',
|
'notes',
|
||||||
|
20
InvenTree/build/migrations/0013_build_take_from.py
Normal file
20
InvenTree/build/migrations/0013_build_take_from.py
Normal file
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
@ -27,6 +27,7 @@ class Build(models.Model):
|
|||||||
part: The part to be built (from component BOM items)
|
part: The part to be built (from component BOM items)
|
||||||
title: Brief title describing the build (required)
|
title: Brief title describing the build (required)
|
||||||
quantity: Number of units to be built
|
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
|
status: Build status code
|
||||||
batch: Batch code transferred to build parts (optional)
|
batch: Batch code transferred to build parts (optional)
|
||||||
creation_date: Date the build was created (auto)
|
creation_date: Date the build was created (auto)
|
||||||
@ -41,6 +42,11 @@ class Build(models.Model):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('build-detail', kwargs={'pk': self.id})
|
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,
|
part = models.ForeignKey('part.Part', on_delete=models.CASCADE,
|
||||||
related_name='builds',
|
related_name='builds',
|
||||||
limit_choices_to={
|
limit_choices_to={
|
||||||
@ -50,10 +56,11 @@ class Build(models.Model):
|
|||||||
help_text='Select part to build',
|
help_text='Select part to build',
|
||||||
)
|
)
|
||||||
|
|
||||||
title = models.CharField(
|
take_from = models.ForeignKey('stock.StockLocation', on_delete=models.SET_NULL,
|
||||||
blank=False,
|
related_name='sourcing_builds',
|
||||||
max_length=100,
|
null=True, blank=True,
|
||||||
help_text='Brief description of the build')
|
help_text='Select location to take stock from for this build (leave blank to take from any stock location'
|
||||||
|
)
|
||||||
|
|
||||||
quantity = models.PositiveIntegerField(
|
quantity = models.PositiveIntegerField(
|
||||||
default=1,
|
default=1,
|
||||||
@ -139,6 +146,11 @@ class Build(models.Model):
|
|||||||
|
|
||||||
stock = StockItem.objects.filter(part=item.sub_part)
|
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!
|
# Only one StockItem to choose from? Default to that one!
|
||||||
if len(stock) == 1:
|
if len(stock) == 1:
|
||||||
stock_item = stock[0]
|
stock_item = stock[0]
|
||||||
@ -330,7 +342,7 @@ class BuildItem(models.Model):
|
|||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
build: Link to a Build object
|
build: Link to a Build object
|
||||||
stock: Link to a StockItem object
|
stock_item: Link to a StockItem object
|
||||||
quantity: Number of units allocated
|
quantity: Number of units allocated
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -45,6 +45,16 @@ InvenTree | Build - {{ build }}
|
|||||||
<tr>
|
<tr>
|
||||||
<td>Quantity</td><td>{{ build.quantity }}</td>
|
<td>Quantity</td><td>{{ build.quantity }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Stock Source</td>
|
||||||
|
<td>
|
||||||
|
{% if build.take_from %}
|
||||||
|
<a href="{% url 'stock-location-detail' build.take_from.id %}">{{ build.take_from }}</a>
|
||||||
|
{% else %}
|
||||||
|
Stock can be taken from any available location.
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Status</td><td>{% include "build_status.html" with build=build %}</td>
|
<td>Status</td><td>{% include "build_status.html" with build=build %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -374,6 +374,16 @@ class BuildItemCreate(AjaxCreateView):
|
|||||||
query = query.filter(part=part_id)
|
query = query.filter(part=part_id)
|
||||||
|
|
||||||
if build_id is not None:
|
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
|
# 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)])
|
query = query.exclude(id__in=[item.stock_item.id for item in BuildItem.objects.filter(build=build_id, stock_item__part=part_id)])
|
||||||
|
|
||||||
|
@ -42,6 +42,19 @@ def rename_company_image(instance, filename):
|
|||||||
class Company(models.Model):
|
class Company(models.Model):
|
||||||
""" A Company object represents an external company.
|
""" A Company object represents an external company.
|
||||||
It may be a supplier or a customer (or both).
|
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,
|
name = models.CharField(max_length=100, blank=False, unique=True,
|
||||||
@ -101,9 +114,19 @@ class Company(models.Model):
|
|||||||
|
|
||||||
class Contact(models.Model):
|
class Contact(models.Model):
|
||||||
""" A Contact represents a person who works at a particular company.
|
""" 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)
|
name = models.CharField(max_length=100)
|
||||||
|
|
||||||
phone = models.CharField(max_length=100, blank=True)
|
phone = models.CharField(max_length=100, blank=True)
|
||||||
|
@ -116,9 +116,30 @@ def rename_part_image(instance, filename):
|
|||||||
|
|
||||||
|
|
||||||
class Part(models.Model):
|
class Part(models.Model):
|
||||||
""" Represents an abstract part
|
""" The Part object represents an abstract part, the 'concept' of an actual entity.
|
||||||
Parts can be "stocked" in multiple warehouses,
|
|
||||||
and can be combined to form other parts
|
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):
|
def get_absolute_url(self):
|
||||||
@ -133,26 +154,19 @@ class Part(models.Model):
|
|||||||
else:
|
else:
|
||||||
return static('/img/blank_image.png')
|
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)')
|
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')
|
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',
|
category = models.ForeignKey(PartCategory, related_name='parts',
|
||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
on_delete=models.DO_NOTHING,
|
on_delete=models.DO_NOTHING,
|
||||||
help_text='Part category')
|
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)
|
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,
|
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
|
# Default case - no default category found
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Default supplier part
|
|
||||||
default_supplier = models.ForeignKey('part.SupplierPart',
|
default_supplier = models.ForeignKey('part.SupplierPart',
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
blank=True, null=True,
|
blank=True, null=True,
|
||||||
help_text='Default supplier part',
|
help_text='Default supplier part',
|
||||||
related_name='default_parts')
|
related_name='default_parts')
|
||||||
|
|
||||||
# Minimum "allowed" stock level
|
|
||||||
minimum_stock = models.PositiveIntegerField(default=0, validators=[MinValueValidator(0)], help_text='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')
|
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?')
|
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?')
|
consumable = models.BooleanField(default=True, help_text='Can this part be used to build other parts?')
|
||||||
|
|
||||||
# Is this part "trackable"?
|
# Is this part "trackable"?
|
||||||
@ -434,6 +443,11 @@ def attach_file(instance, filename):
|
|||||||
class PartAttachment(models.Model):
|
class PartAttachment(models.Model):
|
||||||
""" A PartAttachment links a file to a part
|
""" A PartAttachment links a file to a part
|
||||||
Parts can have multiple files such as datasheets, etc
|
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,
|
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,
|
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.
|
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')
|
part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='starred_users')
|
||||||
@ -467,7 +485,13 @@ class PartStar(models.Model):
|
|||||||
class BomItem(models.Model):
|
class BomItem(models.Model):
|
||||||
""" A BomItem links a part to its component items.
|
""" A BomItem links a part to its component items.
|
||||||
A part can have a BOM (bill of materials) which defines
|
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):
|
def get_absolute_url(self):
|
||||||
@ -530,8 +554,23 @@ class SupplierPart(models.Model):
|
|||||||
""" Represents a unique part as provided by a Supplier
|
""" Represents a unique part as provided by a Supplier
|
||||||
Each SupplierPart is identified by a MPN (Manufacturer Part Number)
|
Each SupplierPart is identified by a MPN (Manufacturer Part Number)
|
||||||
Each SupplierPart is also linked to a Part object.
|
Each SupplierPart is also linked to a Part object.
|
||||||
|
|
||||||
A Part may be available from multiple suppliers
|
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):
|
def get_absolute_url(self):
|
||||||
@ -540,8 +579,6 @@ class SupplierPart(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ('part', 'supplier', 'SKU')
|
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,
|
part = models.ForeignKey(Part, on_delete=models.CASCADE,
|
||||||
related_name='supplier_parts',
|
related_name='supplier_parts',
|
||||||
limit_choices_to={'purchaseable': True},
|
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')
|
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')
|
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')
|
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)')
|
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')
|
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')
|
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)')
|
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)
|
lead_time = models.DurationField(blank=True, null=True)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -654,9 +684,14 @@ class SupplierPart(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class SupplierPriceBreak(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
|
- Suppliers can offer discounts at larger quantities
|
||||||
- SupplierPart(s) may have zero-or-more associated SupplierPriceBreak(s)
|
- 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')
|
part = models.ForeignKey(SupplierPart, on_delete=models.CASCADE, related_name='price_breaks')
|
||||||
|
@ -77,10 +77,23 @@ def before_delete_stock_location(sender, instance, using, **kwargs):
|
|||||||
|
|
||||||
class StockItem(models.Model):
|
class StockItem(models.Model):
|
||||||
"""
|
"""
|
||||||
A 'StockItem' instance represents a quantity of physical instances of a part.
|
A StockItem object 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.
|
Attributes:
|
||||||
If a serial number is assigned, then StockItem cannot have a quantity other than 1
|
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):
|
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')
|
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,
|
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')
|
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,
|
location = models.ForeignKey(StockLocation, on_delete=models.DO_NOTHING,
|
||||||
related_name='stock_items', blank=True, null=True,
|
related_name='stock_items', blank=True, null=True,
|
||||||
help_text='Where is this stock item located?')
|
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,
|
belongs_to = models.ForeignKey('self', on_delete=models.DO_NOTHING,
|
||||||
related_name='owned_parts', blank=True, null=True,
|
related_name='owned_parts', blank=True, null=True,
|
||||||
help_text='Is this item installed in another item?')
|
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,
|
customer = models.ForeignKey('company.Company', on_delete=models.SET_NULL,
|
||||||
related_name='stockitems', blank=True, null=True,
|
related_name='stockitems', blank=True, null=True,
|
||||||
help_text='Item assigned to customer?')
|
help_text='Item assigned to customer?')
|
||||||
|
|
||||||
# Optional serial number
|
|
||||||
serial = models.PositiveIntegerField(blank=True, null=True,
|
serial = models.PositiveIntegerField(blank=True, null=True,
|
||||||
help_text='Serial number for this item')
|
help_text='Serial number for this item')
|
||||||
|
|
||||||
# Optional URL to link to external resource
|
|
||||||
URL = models.URLField(max_length=125, blank=True)
|
URL = models.URLField(max_length=125, blank=True)
|
||||||
|
|
||||||
# Optional batch information
|
|
||||||
batch = models.CharField(max_length=100, blank=True, null=True,
|
batch = models.CharField(max_length=100, blank=True, null=True,
|
||||||
help_text='Batch code for this stock item')
|
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)
|
quantity = models.PositiveIntegerField(validators=[MinValueValidator(0)], default=1)
|
||||||
|
|
||||||
# Last time this item was updated (set automagically)
|
|
||||||
updated = models.DateField(auto_now=True)
|
updated = models.DateField(auto_now=True)
|
||||||
|
|
||||||
# last time the stock was checked / counted
|
# last time the stock was checked / counted
|
||||||
@ -409,33 +409,34 @@ class StockItem(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class StockItemTracking(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):
|
def get_absolute_url(self):
|
||||||
return '/stock/track/{pk}'.format(pk=self.id)
|
return '/stock/track/{pk}'.format(pk=self.id)
|
||||||
# return reverse('stock-tracking-detail', kwargs={'pk': self.id})
|
# return reverse('stock-tracking-detail', kwargs={'pk': self.id})
|
||||||
|
|
||||||
# Stock item
|
|
||||||
item = models.ForeignKey(StockItem, on_delete=models.CASCADE,
|
item = models.ForeignKey(StockItem, on_delete=models.CASCADE,
|
||||||
related_name='tracking_info')
|
related_name='tracking_info')
|
||||||
|
|
||||||
# Date this entry was created (cannot be edited)
|
|
||||||
date = models.DateTimeField(auto_now_add=True, editable=False)
|
date = models.DateTimeField(auto_now_add=True, editable=False)
|
||||||
|
|
||||||
# Short-form title for this tracking entry
|
|
||||||
title = models.CharField(blank=False, max_length=250)
|
title = models.CharField(blank=False, max_length=250)
|
||||||
|
|
||||||
# Optional longer description
|
|
||||||
notes = models.TextField(blank=True)
|
notes = models.TextField(blank=True)
|
||||||
|
|
||||||
# Which user created this tracking entry?
|
|
||||||
user = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True)
|
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)
|
system = models.BooleanField(default=False)
|
||||||
|
|
||||||
# Keep track of the StockItem quantity throughout the tracking history
|
|
||||||
quantity = models.PositiveIntegerField(validators=[MinValueValidator(0)], default=1)
|
quantity = models.PositiveIntegerField(validators=[MinValueValidator(0)], default=1)
|
||||||
|
|
||||||
# TODO
|
# TODO
|
||||||
|
Loading…
Reference in New Issue
Block a user