mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
commit
ef83480f65
@ -97,6 +97,10 @@ class Build(models.Model):
|
|||||||
|
|
||||||
notes = MarkdownxField(blank=True, help_text=_('Extra build notes'))
|
notes = MarkdownxField(blank=True, help_text=_('Extra build notes'))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def output_count(self):
|
||||||
|
return self.build_outputs.count()
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def cancelBuild(self, user):
|
def cancelBuild(self, user):
|
||||||
""" Mark the Build as CANCELLED
|
""" Mark the Build as CANCELLED
|
||||||
@ -235,7 +239,7 @@ class Build(models.Model):
|
|||||||
now=str(datetime.now().date())
|
now=str(datetime.now().date())
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.part.trackable:
|
if self.part.trackable and serial_numbers:
|
||||||
# Add new serial numbers
|
# Add new serial numbers
|
||||||
for serial in serial_numbers:
|
for serial in serial_numbers:
|
||||||
item = StockItem.objects.create(
|
item = StockItem.objects.create(
|
||||||
|
@ -90,6 +90,10 @@ InvenTree | Build - {{ build }}
|
|||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js_load %}
|
||||||
|
<script type='text/javascript' src="{% static 'script/inventree/stock.js' %}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block js_ready %}
|
{% block js_ready %}
|
||||||
|
|
||||||
$("#build-edit").click(function () {
|
$("#build-edit").click(function () {
|
||||||
|
32
InvenTree/build/templates/build/build_output.html
Normal file
32
InvenTree/build/templates/build/build_output.html
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{% extends "build/build_base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block details %}
|
||||||
|
|
||||||
|
{% include "build/tabs.html" with tab='output' %}
|
||||||
|
|
||||||
|
<h4>{% trans "Build Outputs" %}</h4>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
{% include "stock_table.html" %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js_ready %}
|
||||||
|
{{ block.super }}
|
||||||
|
|
||||||
|
loadStockTable($("#stock-table"), {
|
||||||
|
params: {
|
||||||
|
location_detail: true,
|
||||||
|
part_details: true,
|
||||||
|
build: {{ build.id }},
|
||||||
|
},
|
||||||
|
groupByField: 'location',
|
||||||
|
buttons: [
|
||||||
|
'#stock-options',
|
||||||
|
],
|
||||||
|
url: "{% url 'api-stock-list' %}",
|
||||||
|
});
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -4,6 +4,9 @@
|
|||||||
<li{% if tab == 'details' %} class='active'{% endif %}>
|
<li{% if tab == 'details' %} class='active'{% endif %}>
|
||||||
<a href="{% url 'build-detail' build.id %}">{% trans "Details" %}</a>
|
<a href="{% url 'build-detail' build.id %}">{% trans "Details" %}</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li{% if tab == 'output' %} class='active'{% endif %}>
|
||||||
|
<a href="{% url 'build-output' build.id %}">{% trans "Outputs" %}{% if build.output_count > 0%}<span class='badge'>{{ build.output_count }}</span>{% endif %}</a>
|
||||||
|
</li>
|
||||||
<li{% if tab == 'notes' %} class='active'{% endif %}>
|
<li{% if tab == 'notes' %} class='active'{% endif %}>
|
||||||
<a href="{% url 'build-notes' build.id %}">{% trans "Notes" %}{% if build.notes %} <span class='glyphicon glyphicon-small glyphicon-info-sign'></span>{% endif %}</a>
|
<a href="{% url 'build-notes' build.id %}">{% trans "Notes" %}{% if build.notes %} <span class='glyphicon glyphicon-small glyphicon-info-sign'></span>{% endif %}</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -26,6 +26,9 @@ build_detail_urls = [
|
|||||||
url(r'^unallocate/', views.BuildUnallocate.as_view(), name='build-unallocate'),
|
url(r'^unallocate/', views.BuildUnallocate.as_view(), name='build-unallocate'),
|
||||||
|
|
||||||
url(r'^notes/', views.BuildNotes.as_view(), name='build-notes'),
|
url(r'^notes/', views.BuildNotes.as_view(), name='build-notes'),
|
||||||
|
|
||||||
|
url(r'^output/', views.BuildDetail.as_view(template_name='build/build_output.html'), name='build-output'),
|
||||||
|
|
||||||
url(r'^.*$', views.BuildDetail.as_view(), name='build-detail'),
|
url(r'^.*$', views.BuildDetail.as_view(), name='build-detail'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.db.utils import OperationalError
|
from django.db.utils import OperationalError, ProgrammingError
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@ -43,6 +43,6 @@ class CommonConfig(AppConfig):
|
|||||||
setting.save()
|
setting.save()
|
||||||
|
|
||||||
print("Creating new key: '{k}' = '{v}'".format(k=key, v=default))
|
print("Creating new key: '{k}' = '{v}'".format(k=key, v=default))
|
||||||
except OperationalError:
|
except (OperationalError, ProgrammingError):
|
||||||
# Migrations have not yet been applied - table does not exist
|
# Migrations have not yet been applied - table does not exist
|
||||||
break
|
break
|
||||||
|
@ -85,6 +85,13 @@ class StockItemResource(ModelResource):
|
|||||||
|
|
||||||
stocktake_date = Field(attribute='stocktake_date', widget=widgets.DateWidget())
|
stocktake_date = Field(attribute='stocktake_date', widget=widgets.DateWidget())
|
||||||
|
|
||||||
|
def after_import(self, dataset, result, using_transactions, dry_run, **kwargs):
|
||||||
|
|
||||||
|
super().after_import(dataset, result, using_transactions, dry_run, **kwargs)
|
||||||
|
|
||||||
|
# Rebuild the StockItem tree(s)
|
||||||
|
StockItem.objects.rebuild()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = StockItem
|
model = StockItem
|
||||||
skip_unchanged = True
|
skip_unchanged = True
|
||||||
|
@ -257,8 +257,12 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
- location: Filter stock by location
|
- location: Filter stock by location
|
||||||
- category: Filter by parts belonging to a certain category
|
- category: Filter by parts belonging to a certain category
|
||||||
- supplier: Filter by supplier
|
- supplier: Filter by supplier
|
||||||
|
- ancestor: Filter by an 'ancestor' StockItem
|
||||||
|
- status: Filter by the StockItem status
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
queryset = StockItem.objects.all()
|
||||||
|
|
||||||
def get_serializer(self, *args, **kwargs):
|
def get_serializer(self, *args, **kwargs):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -284,6 +288,7 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
data = queryset.values(
|
data = queryset.values(
|
||||||
'pk',
|
'pk',
|
||||||
|
'parent',
|
||||||
'quantity',
|
'quantity',
|
||||||
'serial',
|
'serial',
|
||||||
'batch',
|
'batch',
|
||||||
@ -332,7 +337,9 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Start with all objects
|
# Start with all objects
|
||||||
stock_list = StockItem.objects.filter(customer=None, belongs_to=None)
|
stock_list = super(StockList, self).get_queryset()
|
||||||
|
|
||||||
|
stock_list = stock_list.filter(customer=None, belongs_to=None)
|
||||||
|
|
||||||
# Does the client wish to filter by the Part ID?
|
# Does the client wish to filter by the Part ID?
|
||||||
part_id = self.request.query_params.get('part', None)
|
part_id = self.request.query_params.get('part', None)
|
||||||
@ -347,7 +354,20 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
else:
|
else:
|
||||||
stock_list = stock_list.filter(part=part_id)
|
stock_list = stock_list.filter(part=part_id)
|
||||||
|
|
||||||
except Part.DoesNotExist:
|
except (ValueError, Part.DoesNotExist):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Does the client wish to filter by the 'ancestor'?
|
||||||
|
anc_id = self.request.query_params.get('ancestor', None)
|
||||||
|
|
||||||
|
if anc_id:
|
||||||
|
try:
|
||||||
|
ancestor = StockItem.objects.get(pk=anc_id)
|
||||||
|
|
||||||
|
# Only allow items which are descendants of the specified StockItem
|
||||||
|
stock_list = stock_list.filter(id__in=[item.pk for item in ancestor.children.all()])
|
||||||
|
|
||||||
|
except (ValueError, Part.DoesNotExist):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Does the client wish to filter by stock location?
|
# Does the client wish to filter by stock location?
|
||||||
@ -358,7 +378,7 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
location = StockLocation.objects.get(pk=loc_id)
|
location = StockLocation.objects.get(pk=loc_id)
|
||||||
stock_list = stock_list.filter(location__in=location.getUniqueChildren())
|
stock_list = stock_list.filter(location__in=location.getUniqueChildren())
|
||||||
|
|
||||||
except StockLocation.DoesNotExist:
|
except (ValueError, StockLocation.DoesNotExist):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Does the client wish to filter by part category?
|
# Does the client wish to filter by part category?
|
||||||
@ -369,9 +389,15 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
category = PartCategory.objects.get(pk=cat_id)
|
category = PartCategory.objects.get(pk=cat_id)
|
||||||
stock_list = stock_list.filter(part__category__in=category.getUniqueChildren())
|
stock_list = stock_list.filter(part__category__in=category.getUniqueChildren())
|
||||||
|
|
||||||
except PartCategory.DoesNotExist:
|
except (ValueError, PartCategory.DoesNotExist):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Filter by StockItem status
|
||||||
|
status = self.request.query_params.get('status', None)
|
||||||
|
|
||||||
|
if status:
|
||||||
|
stock_list = stock_list.filter(status=status)
|
||||||
|
|
||||||
# Filter by supplier_part ID
|
# Filter by supplier_part ID
|
||||||
supplier_part_id = self.request.query_params.get('supplier_part', None)
|
supplier_part_id = self.request.query_params.get('supplier_part', None)
|
||||||
|
|
||||||
@ -411,7 +437,7 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
'supplier_part',
|
'supplier_part',
|
||||||
'customer',
|
'customer',
|
||||||
'belongs_to',
|
'belongs_to',
|
||||||
# 'status' TODO - There are some issues filtering based on an enumeration field
|
'build'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,6 +7,10 @@
|
|||||||
location: 3
|
location: 3
|
||||||
batch: 'B123'
|
batch: 'B123'
|
||||||
quantity: 4000
|
quantity: 4000
|
||||||
|
level: 0
|
||||||
|
tree_id: 0
|
||||||
|
lft: 0
|
||||||
|
rght: 0
|
||||||
|
|
||||||
# 5,000 screws in the bathroom
|
# 5,000 screws in the bathroom
|
||||||
- model: stock.stockitem
|
- model: stock.stockitem
|
||||||
@ -14,6 +18,10 @@
|
|||||||
part: 1
|
part: 1
|
||||||
location: 2
|
location: 2
|
||||||
quantity: 5000
|
quantity: 5000
|
||||||
|
level: 0
|
||||||
|
tree_id: 0
|
||||||
|
lft: 0
|
||||||
|
rght: 0
|
||||||
|
|
||||||
# 1234 2K2 resistors in 'Drawer_1'
|
# 1234 2K2 resistors in 'Drawer_1'
|
||||||
- model: stock.stockitem
|
- model: stock.stockitem
|
||||||
@ -22,6 +30,10 @@
|
|||||||
part: 3
|
part: 3
|
||||||
location: 5
|
location: 5
|
||||||
quantity: 1234
|
quantity: 1234
|
||||||
|
level: 0
|
||||||
|
tree_id: 0
|
||||||
|
lft: 0
|
||||||
|
rght: 0
|
||||||
|
|
||||||
# Some widgets in drawer 3
|
# Some widgets in drawer 3
|
||||||
- model: stock.stockitem
|
- model: stock.stockitem
|
||||||
@ -31,6 +43,10 @@
|
|||||||
location: 7
|
location: 7
|
||||||
quantity: 10
|
quantity: 10
|
||||||
delete_on_deplete: False
|
delete_on_deplete: False
|
||||||
|
level: 0
|
||||||
|
tree_id: 0
|
||||||
|
lft: 0
|
||||||
|
rght: 0
|
||||||
|
|
||||||
- model: stock.stockitem
|
- model: stock.stockitem
|
||||||
pk: 101
|
pk: 101
|
||||||
@ -38,6 +54,10 @@
|
|||||||
part: 25
|
part: 25
|
||||||
location: 7
|
location: 7
|
||||||
quantity: 5
|
quantity: 5
|
||||||
|
level: 0
|
||||||
|
tree_id: 0
|
||||||
|
lft: 0
|
||||||
|
rght: 0
|
||||||
|
|
||||||
- model: stock.stockitem
|
- model: stock.stockitem
|
||||||
pk: 102
|
pk: 102
|
||||||
@ -45,3 +65,7 @@
|
|||||||
part: 25
|
part: 25
|
||||||
location: 7
|
location: 7
|
||||||
quantity: 3
|
quantity: 3
|
||||||
|
level: 0
|
||||||
|
tree_id: 0
|
||||||
|
lft: 0
|
||||||
|
rght: 0
|
44
InvenTree/stock/migrations/0021_auto_20200215_2232.py
Normal file
44
InvenTree/stock/migrations/0021_auto_20200215_2232.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# Generated by Django 2.2.9 on 2020-02-15 22:32
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import mptt.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('stock', '0020_auto_20200206_1213'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='stockitem',
|
||||||
|
name='level',
|
||||||
|
field=models.PositiveIntegerField(default=0, editable=False),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='stockitem',
|
||||||
|
name='lft',
|
||||||
|
field=models.PositiveIntegerField(default=0, editable=False),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='stockitem',
|
||||||
|
name='parent',
|
||||||
|
field=mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='children', to='stock.StockItem'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='stockitem',
|
||||||
|
name='rght',
|
||||||
|
field=models.PositiveIntegerField(default=0, editable=False),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='stockitem',
|
||||||
|
name='tree_id',
|
||||||
|
field=models.PositiveIntegerField(db_index=True, default=0, editable=False),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
21
InvenTree/stock/migrations/0022_auto_20200217_1109.py
Normal file
21
InvenTree/stock/migrations/0022_auto_20200217_1109.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Generated by Django 2.2.9 on 2020-02-17 11:09
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
from stock import models
|
||||||
|
|
||||||
|
|
||||||
|
def update_stock_item_tree(apps, schema_editor):
|
||||||
|
# Update the StockItem MPTT model
|
||||||
|
|
||||||
|
models.StockItem.objects.rebuild()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('stock', '0021_auto_20200215_2232'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(update_stock_item_tree)
|
||||||
|
]
|
@ -18,7 +18,7 @@ from django.dispatch import receiver
|
|||||||
|
|
||||||
from markdownx.models import MarkdownxField
|
from markdownx.models import MarkdownxField
|
||||||
|
|
||||||
from mptt.models import TreeForeignKey
|
from mptt.models import MPTTModel, TreeForeignKey
|
||||||
|
|
||||||
from decimal import Decimal, InvalidOperation
|
from decimal import Decimal, InvalidOperation
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@ -102,11 +102,12 @@ def before_delete_stock_location(sender, instance, using, **kwargs):
|
|||||||
child.save()
|
child.save()
|
||||||
|
|
||||||
|
|
||||||
class StockItem(models.Model):
|
class StockItem(MPTTModel):
|
||||||
"""
|
"""
|
||||||
A StockItem object represents a quantity of physical instances of a part.
|
A StockItem object represents a quantity of physical instances of a part.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
|
parent: Link to another StockItem from which this StockItem was created
|
||||||
part: Link to the master abstract part that this StockItem is an instance of
|
part: Link to the master abstract part that this StockItem is an instance of
|
||||||
supplier_part: Link to a specific SupplierPart (optional)
|
supplier_part: Link to a specific SupplierPart (optional)
|
||||||
location: Where this StockItem is located
|
location: Where this StockItem is located
|
||||||
@ -296,6 +297,11 @@ class StockItem(models.Model):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
parent = TreeForeignKey('self',
|
||||||
|
on_delete=models.DO_NOTHING,
|
||||||
|
blank=True, null=True,
|
||||||
|
related_name='children')
|
||||||
|
|
||||||
part = models.ForeignKey('part.Part', on_delete=models.CASCADE,
|
part = models.ForeignKey('part.Part', on_delete=models.CASCADE,
|
||||||
related_name='stock_items', help_text=_('Base part'),
|
related_name='stock_items', help_text=_('Base part'),
|
||||||
limit_choices_to={
|
limit_choices_to={
|
||||||
@ -370,15 +376,31 @@ class StockItem(models.Model):
|
|||||||
def can_delete(self):
|
def can_delete(self):
|
||||||
""" Can this stock item be deleted? It can NOT be deleted under the following circumstances:
|
""" Can this stock item be deleted? It can NOT be deleted under the following circumstances:
|
||||||
|
|
||||||
|
- Has child StockItems
|
||||||
- Has a serial number and is tracked
|
- Has a serial number and is tracked
|
||||||
- Is installed inside another StockItem
|
- Is installed inside another StockItem
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if self.child_count > 0:
|
||||||
|
return False
|
||||||
|
|
||||||
if self.part.trackable and self.serial is not None:
|
if self.part.trackable and self.serial is not None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def children(self):
|
||||||
|
""" Return a list of the child items which have been split from this stock item """
|
||||||
|
return self.get_descendants(include_self=False)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def child_count(self):
|
||||||
|
""" Return the number of 'child' items associated with this StockItem.
|
||||||
|
A child item is one which has been split from this one.
|
||||||
|
"""
|
||||||
|
return self.children.count()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def in_stock(self):
|
def in_stock(self):
|
||||||
|
|
||||||
@ -469,6 +491,7 @@ class StockItem(models.Model):
|
|||||||
new_item.quantity = 1
|
new_item.quantity = 1
|
||||||
new_item.serial = serial
|
new_item.serial = serial
|
||||||
new_item.pk = None
|
new_item.pk = None
|
||||||
|
new_item.parent = self
|
||||||
|
|
||||||
if location:
|
if location:
|
||||||
new_item.location = location
|
new_item.location = location
|
||||||
@ -496,13 +519,14 @@ class StockItem(models.Model):
|
|||||||
item.save()
|
item.save()
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def splitStock(self, quantity, user):
|
def splitStock(self, quantity, location, user):
|
||||||
""" Split this stock item into two items, in the same location.
|
""" Split this stock item into two items, in the same location.
|
||||||
Stock tracking notes for this StockItem will be duplicated,
|
Stock tracking notes for this StockItem will be duplicated,
|
||||||
and added to the new StockItem.
|
and added to the new StockItem.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
quantity: Number of stock items to remove from this entity, and pass to the next
|
quantity: Number of stock items to remove from this entity, and pass to the next
|
||||||
|
location: Where to move the new StockItem to
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
The provided quantity will be subtracted from this item and given to the new one.
|
The provided quantity will be subtracted from this item and given to the new one.
|
||||||
@ -530,7 +554,15 @@ class StockItem(models.Model):
|
|||||||
# Nullify the PK so a new record is created
|
# Nullify the PK so a new record is created
|
||||||
new_stock = StockItem.objects.get(pk=self.pk)
|
new_stock = StockItem.objects.get(pk=self.pk)
|
||||||
new_stock.pk = None
|
new_stock.pk = None
|
||||||
|
new_stock.parent = self
|
||||||
new_stock.quantity = quantity
|
new_stock.quantity = quantity
|
||||||
|
|
||||||
|
# Move to the new location if specified, otherwise use current location
|
||||||
|
if location:
|
||||||
|
new_stock.location = location
|
||||||
|
else:
|
||||||
|
new_stock.location = self.location
|
||||||
|
|
||||||
new_stock.save()
|
new_stock.save()
|
||||||
|
|
||||||
# Copy the transaction history of this part into the new one
|
# Copy the transaction history of this part into the new one
|
||||||
@ -549,6 +581,11 @@ class StockItem(models.Model):
|
|||||||
def move(self, location, notes, user, **kwargs):
|
def move(self, location, notes, user, **kwargs):
|
||||||
""" Move part to a new location.
|
""" Move part to a new location.
|
||||||
|
|
||||||
|
If less than the available quantity is to be moved,
|
||||||
|
a new StockItem is created, with the defined quantity,
|
||||||
|
and that new StockItem is moved.
|
||||||
|
The quantity is also subtracted from the existing StockItem.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
location: Destination location (cannot be null)
|
location: Destination location (cannot be null)
|
||||||
notes: User notes
|
notes: User notes
|
||||||
@ -576,8 +613,10 @@ class StockItem(models.Model):
|
|||||||
if quantity < self.quantity:
|
if quantity < self.quantity:
|
||||||
# We need to split the stock!
|
# We need to split the stock!
|
||||||
|
|
||||||
# Leave behind certain quantity
|
# Split the existing StockItem in two
|
||||||
self.splitStock(self.quantity - quantity, user)
|
self.splitStock(quantity, location, user)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
msg = "Moved to {loc}".format(loc=str(location))
|
msg = "Moved to {loc}".format(loc=str(location))
|
||||||
|
|
||||||
@ -586,10 +625,11 @@ class StockItem(models.Model):
|
|||||||
|
|
||||||
self.location = location
|
self.location = location
|
||||||
|
|
||||||
self.addTransactionNote(msg,
|
self.addTransactionNote(
|
||||||
user,
|
msg,
|
||||||
notes=notes,
|
user,
|
||||||
system=True)
|
notes=notes,
|
||||||
|
system=True)
|
||||||
|
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
@ -727,6 +767,23 @@ class StockItem(models.Model):
|
|||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(pre_delete, sender=StockItem, dispatch_uid='stock_item_pre_delete_log')
|
||||||
|
def before_delete_stock_item(sender, instance, using, **kwargs):
|
||||||
|
""" Receives pre_delete signal from StockItem object.
|
||||||
|
|
||||||
|
Before a StockItem is deleted, ensure that each child object is updated,
|
||||||
|
to point to the new parent item.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Update each StockItem parent field
|
||||||
|
for child in instance.children.all():
|
||||||
|
child.parent = instance.parent
|
||||||
|
child.save()
|
||||||
|
|
||||||
|
# Rebuild the MPTT tree
|
||||||
|
StockItem.objects.rebuild()
|
||||||
|
|
||||||
|
|
||||||
class StockItemTracking(models.Model):
|
class StockItemTracking(models.Model):
|
||||||
""" Stock tracking entry - breacrumb for keeping track of automated stock transactions
|
""" Stock tracking entry - breacrumb for keeping track of automated stock transactions
|
||||||
|
|
||||||
|
@ -43,21 +43,32 @@
|
|||||||
<button type='button' class='btn btn-default btn-glyph' id='stock-edit' title='Edit stock item'>
|
<button type='button' class='btn btn-default btn-glyph' id='stock-edit' title='Edit stock item'>
|
||||||
<span class='glyphicon glyphicon-edit'/>
|
<span class='glyphicon glyphicon-edit'/>
|
||||||
</button>
|
</button>
|
||||||
|
{% if item.can_delete %}
|
||||||
<button type='button' class='btn btn-default btn-glyph' id='stock-delete' title='Edit stock item'>
|
<button type='button' class='btn btn-default btn-glyph' id='stock-delete' title='Edit stock item'>
|
||||||
<span class='glyphicon glyphicon-trash'/>
|
<span class='glyphicon glyphicon-trash'/>
|
||||||
</button>
|
</button>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</p>
|
</p>
|
||||||
{% if item.serialized %}
|
{% if item.serialized %}
|
||||||
<div class='alert alert-block alert-info'>
|
<div class='alert alert-block alert-info'>
|
||||||
{% trans "This stock item is serialized - it has a unique serial number and the quantity cannot be adjusted." %}
|
{% trans "This stock item is serialized - it has a unique serial number and the quantity cannot be adjusted." %}
|
||||||
</div>
|
</div>
|
||||||
|
{% elif item.child_count > 0 %}
|
||||||
|
<div class='alert alert-block alert-warning'>
|
||||||
|
{% trans "This stock item cannot be deleted as it has child items" %}
|
||||||
|
</div>
|
||||||
{% elif item.delete_on_deplete %}
|
{% elif item.delete_on_deplete %}
|
||||||
<div class='alert alert-block alert-warning'>
|
<div class='alert alert-block alert-warning'>
|
||||||
{% trans "This stock item will be automatically deleted when all stock is depleted." %}
|
{% trans "This stock item will be automatically deleted when all stock is depleted." %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
{% if item.parent %}
|
||||||
|
<div class='alert alert-block alert-info'>
|
||||||
|
{% trans "This stock item was split from " %}<a href="{% url 'stock-item-detail' item.parent.id %}">{{ item.parent }}</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class='row'>
|
<div class='row'>
|
||||||
<div class='col-sm-6'>
|
<div class='col-sm-6'>
|
||||||
|
42
InvenTree/stock/templates/stock/item_childs.html
Normal file
42
InvenTree/stock/templates/stock/item_childs.html
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
{% extends "stock/item_base.html" %}
|
||||||
|
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block details %}
|
||||||
|
|
||||||
|
{% include "stock/tabs.html" with tab='children' %}
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h4>{% trans "Child Stock Items" %}</h4>
|
||||||
|
|
||||||
|
{% if item.child_count > 0 %}
|
||||||
|
{% include "stock_table.html" %}
|
||||||
|
{% else %}
|
||||||
|
<div class='alert alert-block alert-info'>
|
||||||
|
{% trans "This stock item does not have any child items" %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js_ready %}
|
||||||
|
{{ block.super }}
|
||||||
|
|
||||||
|
{% if item.child_count > 0 %}
|
||||||
|
loadStockTable($("#stock-table"), {
|
||||||
|
params: {
|
||||||
|
location_detail: true,
|
||||||
|
part_details: true,
|
||||||
|
ancestor: {{ item.id }},
|
||||||
|
},
|
||||||
|
groupByField: 'location',
|
||||||
|
buttons: [
|
||||||
|
'#stock-options',
|
||||||
|
],
|
||||||
|
url: "{% url 'api-stock-list' %}",
|
||||||
|
});
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -4,6 +4,9 @@
|
|||||||
<li{% ifequal tab 'tracking' %} class='active'{% endifequal %}>
|
<li{% ifequal tab 'tracking' %} class='active'{% endifequal %}>
|
||||||
<a href="{% url 'stock-item-detail' item.id %}">{% trans "Tracking" %}</a>
|
<a href="{% url 'stock-item-detail' item.id %}">{% trans "Tracking" %}</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li{% ifequal tab 'children' %} class='active'{% endifequal %}>
|
||||||
|
<a href="{% url 'stock-item-children' item.id %}">{% trans "Children" %}{% if item.child_count > 0 %}<span class='badge'>{{ item.child_count }}</span>{% endif %}</a>
|
||||||
|
</li>
|
||||||
{% if 0 %}
|
{% if 0 %}
|
||||||
<!-- These tabs are to be implemented in the future -->
|
<!-- These tabs are to be implemented in the future -->
|
||||||
<li{% ifequal tab 'builds' %} class='active'{% endifequal %}>
|
<li{% ifequal tab 'builds' %} class='active'{% endifequal %}>
|
||||||
|
@ -156,7 +156,9 @@ class StockTest(TestCase):
|
|||||||
|
|
||||||
# Move 6 of the units
|
# Move 6 of the units
|
||||||
self.assertTrue(w1.move(self.diningroom, 'Moved', None, quantity=6))
|
self.assertTrue(w1.move(self.diningroom, 'Moved', None, quantity=6))
|
||||||
self.assertEqual(w1.quantity, 6)
|
|
||||||
|
# There should be 4 remaining
|
||||||
|
self.assertEqual(w1.quantity, 4)
|
||||||
|
|
||||||
# There should also be a new object still in drawer3
|
# There should also be a new object still in drawer3
|
||||||
self.assertEqual(StockItem.objects.filter(part=25).count(), 4)
|
self.assertEqual(StockItem.objects.filter(part=25).count(), 4)
|
||||||
@ -175,17 +177,17 @@ class StockTest(TestCase):
|
|||||||
N = StockItem.objects.filter(part=3).count()
|
N = StockItem.objects.filter(part=3).count()
|
||||||
|
|
||||||
stock = StockItem.objects.get(id=1234)
|
stock = StockItem.objects.get(id=1234)
|
||||||
stock.splitStock(1000, None)
|
stock.splitStock(1000, None, self.user)
|
||||||
self.assertEqual(stock.quantity, 234)
|
self.assertEqual(stock.quantity, 234)
|
||||||
|
|
||||||
# There should be a new stock item too!
|
# There should be a new stock item too!
|
||||||
self.assertEqual(StockItem.objects.filter(part=3).count(), N + 1)
|
self.assertEqual(StockItem.objects.filter(part=3).count(), N + 1)
|
||||||
|
|
||||||
# Try to split a negative quantity
|
# Try to split a negative quantity
|
||||||
stock.splitStock(-10, None)
|
stock.splitStock(-10, None, self.user)
|
||||||
self.assertEqual(StockItem.objects.filter(part=3).count(), N + 1)
|
self.assertEqual(StockItem.objects.filter(part=3).count(), N + 1)
|
||||||
|
|
||||||
stock.splitStock(stock.quantity, None)
|
stock.splitStock(stock.quantity, None, self.user)
|
||||||
self.assertEqual(StockItem.objects.filter(part=3).count(), N + 1)
|
self.assertEqual(StockItem.objects.filter(part=3).count(), N + 1)
|
||||||
|
|
||||||
def test_stocktake(self):
|
def test_stocktake(self):
|
||||||
@ -325,6 +327,3 @@ class StockTest(TestCase):
|
|||||||
|
|
||||||
# Serialize the remainder of the stock
|
# Serialize the remainder of the stock
|
||||||
item.serializeStock(2, [99, 100], self.user)
|
item.serializeStock(2, [99, 100], self.user)
|
||||||
|
|
||||||
# Two more items but the original has been deleted
|
|
||||||
self.assertEqual(StockItem.objects.filter(part=25).count(), n + 9)
|
|
||||||
|
@ -24,6 +24,7 @@ stock_item_detail_urls = [
|
|||||||
|
|
||||||
url(r'^add_tracking/', views.StockItemTrackingCreate.as_view(), name='stock-tracking-create'),
|
url(r'^add_tracking/', views.StockItemTrackingCreate.as_view(), name='stock-tracking-create'),
|
||||||
|
|
||||||
|
url(r'^children/', views.StockItemDetail.as_view(template_name='stock/item_childs.html'), name='stock-item-children'),
|
||||||
url(r'^notes/', views.StockItemNotes.as_view(), name='stock-item-notes'),
|
url(r'^notes/', views.StockItemNotes.as_view(), name='stock-item-notes'),
|
||||||
|
|
||||||
url('^.*$', views.StockItemDetail.as_view(), name='stock-item-detail'),
|
url('^.*$', views.StockItemDetail.as_view(), name='stock-item-detail'),
|
||||||
|
@ -35,7 +35,7 @@ To configure Inventree inside a virtual environment, ``cd`` into the inventree b
|
|||||||
|
|
||||||
``source inventree-env/bin/activate``
|
``source inventree-env/bin/activate``
|
||||||
|
|
||||||
This will place the current shell session inside a virtual environment - the terminal should display the ``(inventree)`` prefix.
|
This will place the current shell session inside a virtual environment - the terminal should display the ``(inventree-env)`` prefix.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
Remember to run ``source inventree-env/bin/activate`` when starting each shell session, before running Inventree commands. This will ensure that the correct environment is being used.
|
Remember to run ``source inventree-env/bin/activate`` when starting each shell session, before running Inventree commands. This will ensure that the correct environment is being used.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
Django==2.2.10 # Django package
|
Django==2.2.9 # Django package
|
||||||
pillow==6.2.0 # Image manipulation
|
pillow==6.2.0 # Image manipulation
|
||||||
djangorestframework==3.10.3 # DRF framework
|
djangorestframework==3.10.3 # DRF framework
|
||||||
django-cors-headers==3.2.0 # CORS headers extension for DRF
|
django-cors-headers==3.2.0 # CORS headers extension for DRF
|
||||||
|
Loading…
Reference in New Issue
Block a user