2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-01 11:10:54 +00:00

Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters
2019-09-09 00:09:58 +10:00
41 changed files with 779 additions and 128 deletions

View File

@ -5,6 +5,10 @@
fields:
name: 'Home'
description: 'My house'
level: 0
tree_id: 1
lft: 1
rght: 6
- model: stock.stocklocation
pk: 2
@ -12,6 +16,10 @@
name: 'Bathroom'
description: 'Where I keep my bath'
parent: 1
level: 1
tree_id: 1
lft: 2
rght: 3
- model: stock.stocklocation
pk: 3
@ -19,12 +27,20 @@
name: 'Dining Room'
description: 'A table lives here'
parent: 1
level: 0
tree_id: 1
lft: 4
rght: 5
- model: stock.stocklocation
pk: 4
fields:
name: 'Office'
description: 'Place of work'
level: 0
tree_id: 2
lft: 1
rght: 8
- model: stock.stocklocation
pk: 5
@ -32,6 +48,10 @@
name: 'Drawer_1'
description: 'In my desk'
parent: 4
level: 0
tree_id: 2
lft: 2
rght: 3
- model: stock.stocklocation
pk: 6
@ -39,10 +59,18 @@
name: 'Drawer_2'
description: 'Also in my desk'
parent: 4
level: 0
tree_id: 2
lft: 4
rght: 5
- model: stock.stocklocation
pk: 7
fields:
name: 'Drawer_3'
description: 'Again, in my desk'
parent: 4
parent: 4
level: 0
tree_id: 2
lft: 6
rght: 7

View File

@ -9,6 +9,7 @@ from django import forms
from django.forms.utils import ErrorDict
from django.utils.translation import ugettext as _
from InvenTree.helpers import GetExportFormats
from InvenTree.forms import HelperForm
from .models import StockLocation, StockItem, StockItemTracking
@ -96,6 +97,33 @@ class SerializeStockForm(forms.ModelForm):
]
class ExportOptionsForm(HelperForm):
""" Form for selecting stock export options """
file_format = forms.ChoiceField(label=_('File Format'), help_text=_('Select output file format'))
include_sublocations = forms.BooleanField(required=False, initial=True, help_text=_("Include stock items in sub locations"))
class Meta:
model = StockLocation
fields = [
'file_format',
'include_sublocations',
]
def get_format_choices(self):
""" File format choices """
choices = [(x, x.upper()) for x in GetExportFormats()]
return choices
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['file_format'].choices = self.get_format_choices()
class AdjustStockForm(forms.ModelForm):
""" Form for performing simple stock adjustments.

View File

@ -0,0 +1,37 @@
# Generated by Django 2.2.5 on 2019-09-08 04:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stock', '0010_stockitem_build'),
]
operations = [
migrations.AddField(
model_name='stocklocation',
name='level',
field=models.PositiveIntegerField(default=0, editable=False),
preserve_default=False,
),
migrations.AddField(
model_name='stocklocation',
name='lft',
field=models.PositiveIntegerField(default=0, editable=False),
preserve_default=False,
),
migrations.AddField(
model_name='stocklocation',
name='rght',
field=models.PositiveIntegerField(default=0, editable=False),
preserve_default=False,
),
migrations.AddField(
model_name='stocklocation',
name='tree_id',
field=models.PositiveIntegerField(db_index=True, default=0, editable=False),
preserve_default=False,
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 2.2.5 on 2019-09-08 04:05
from django.db import migrations
from stock import models
def update_tree(apps, schema_editor):
# Update the StockLocation MPTT model
models.StockLocation.objects.rebuild()
class Migration(migrations.Migration):
dependencies = [
('stock', '0011_auto_20190908_0404'),
]
operations = [
migrations.RunPython(update_tree)
]

View File

@ -0,0 +1,20 @@
# Generated by Django 2.2.5 on 2019-09-08 09:16
from django.db import migrations
import django.db.models.deletion
import mptt.fields
class Migration(migrations.Migration):
dependencies = [
('stock', '0012_auto_20190908_0405'),
]
operations = [
migrations.AlterField(
model_name='stockitem',
name='location',
field=mptt.fields.TreeForeignKey(blank=True, help_text='Where is this stock item located?', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='stock_items', to='stock.StockLocation'),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 2.2.5 on 2019-09-08 09:18
from django.db import migrations
import django.db.models.deletion
import mptt.fields
class Migration(migrations.Migration):
dependencies = [
('stock', '0013_auto_20190908_0916'),
]
operations = [
migrations.AlterField(
model_name='stocklocation',
name='parent',
field=mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='children', to='stock.StockLocation'),
),
]

View File

@ -16,6 +16,8 @@ from django.contrib.auth.models import User
from django.db.models.signals import pre_delete
from django.dispatch import receiver
from mptt.models import TreeForeignKey
from datetime import datetime
from InvenTree import helpers
@ -34,9 +36,6 @@ class StockLocation(InvenTreeTree):
def get_absolute_url(self):
return reverse('stock-location-detail', kwargs={'pk': self.id})
def has_items(self):
return self.stock_items.count() > 0
def format_barcode(self):
""" Return a JSON string for formatting a barcode for this StockLocation object """
@ -49,16 +48,33 @@ class StockLocation(InvenTreeTree):
}
)
def get_stock_items(self, cascade=True):
""" Return a queryset for all stock items under this category.
Args:
cascade: If True, also look under sublocations (default = True)
"""
if cascade:
query = StockItem.objects.filter(location__in=self.getUniqueChildren(include_self=True))
else:
query = StockItem.objects.filter(location=self.pk)
return query
def stock_item_count(self, cascade=True):
""" Return the number of StockItem objects which live in or under this category
"""
if cascade:
query = StockItem.objects.filter(location__in=self.getUniqueChildren())
else:
query = StockItem.objects.filter(location=self)
return self.get_stock_items(cascade).count()
return query.count()
def has_items(self, cascade=True):
""" Return True if there are StockItems existing in this category.
Args:
cascade: If True, also search an sublocations (default = True)
"""
return self.stock_item_count(cascade) > 0
@property
def item_count(self):
@ -277,9 +293,9 @@ class StockItem(models.Model):
supplier_part = models.ForeignKey('company.SupplierPart', blank=True, null=True, on_delete=models.SET_NULL,
help_text='Select a matching supplier part for this stock item')
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?')
location = TreeForeignKey(StockLocation, on_delete=models.DO_NOTHING,
related_name='stock_items', blank=True, null=True,
help_text='Where is this stock item located?')
belongs_to = models.ForeignKey('self', on_delete=models.DO_NOTHING,
related_name='owned_parts', blank=True, null=True,

View File

@ -67,6 +67,24 @@
sessionStorage.removeItem('inventree-show-part-locations');
});
$("#stock-export").click(function() {
launchModalForm("{% url 'stock-export-options' %}", {
submit_text: "Export",
success: function(response) {
var url = "{% url 'stock-export' %}";
url += "?format=" + response.format;
url += "&cascade=" + response.cascade;
{% if location %}
url += "&location={{ location.id }}";
{% endif %}
location.href = url;
}
});
});
$('#location-create').click(function () {
launchModalForm("{% url 'stock-location-create' %}",
{

View File

@ -7,8 +7,8 @@ Sub-Locations<span class='badge'>{{ children|length }}</span>
{% block collapse_content %}
<ul class="list-group">
{% for child in children %}
<li class="list-group-item"><a href="{% url 'stock-location-detail' child.id %}">{{ child.name }}</a> - <i>{{ child.description }}</i></li>
<span class='badge'>{{ child.partcount }}</span>
<li class="list-group-item"><a href="{% url 'stock-location-detail' child.id %}">{{ child.name }}</a> - <i>{{ child.description }}</i>
<span class='badge'>{{ child.item_count }}</span>
</li>
{% endfor %}
</ul>

View File

@ -67,15 +67,18 @@ class StockTest(TestCase):
# Move one of the drawers
self.drawer3.parent = self.home
self.drawer3.save()
self.assertNotEqual(self.drawer3.parent, self.office)
self.assertEqual(self.drawer3.pathstring, 'Home/Drawer_3')
def test_children(self):
self.assertTrue(self.office.has_children)
self.assertFalse(self.drawer2.has_children)
childs = self.office.getUniqueChildren()
childs = [item.pk for item in self.office.getUniqueChildren()]
self.assertIn(self.drawer1.id, childs)
self.assertIn(self.drawer2.id, childs)

View File

@ -51,6 +51,9 @@ stock_urls = [
url(r'^adjust/?', views.StockAdjust.as_view(), name='stock-adjust'),
url(r'^export-options/?', views.StockExportOptions.as_view(), name='stock-export-options'),
url(r'^export/?', views.StockExport.as_view(), name='stock-export'),
# Individual stock items
url(r'^item/(?P<pk>\d+)/', include(stock_item_detail_urls)),

View File

@ -18,10 +18,14 @@ from InvenTree.views import AjaxView
from InvenTree.views import AjaxUpdateView, AjaxDeleteView, AjaxCreateView
from InvenTree.views import QRCodeView
from InvenTree.helpers import str2bool
from InvenTree.status_codes import StockStatus
from InvenTree.helpers import str2bool, DownloadFile, GetExportFormats
from InvenTree.helpers import ExtractSerialNumbers
from datetime import datetime
import tablib
from company.models import Company
from part.models import Part
from .models import StockItem, StockLocation, StockItemTracking
@ -31,6 +35,7 @@ from .forms import EditStockItemForm
from .forms import AdjustStockForm
from .forms import TrackingEntryForm
from .forms import SerializeStockForm
from .forms import ExportOptionsForm
class StockIndex(ListView):
@ -119,6 +124,178 @@ class StockLocationQRCode(QRCodeView):
return None
class StockExportOptions(AjaxView):
""" Form for selecting StockExport options """
model = StockLocation
ajax_form_title = 'Stock Export Options'
form_class = ExportOptionsForm
def post(self, request, *args, **kwargs):
self.request = request
fmt = request.POST.get('file_format', 'csv').lower()
cascade = str2bool(request.POST.get('include_sublocations', False))
# Format a URL to redirect to
url = reverse('stock-export')
url += '?format=' + fmt
url += '&cascade=' + str(cascade)
data = {
'form_valid': True,
'format': fmt,
'cascade': cascade
}
return self.renderJsonResponse(self.request, self.form_class(), data=data)
def get(self, request, *args, **kwargs):
return self.renderJsonResponse(request, self.form_class())
class StockExport(AjaxView):
""" Export stock data from a particular location.
Returns a file containing stock information for that location.
"""
model = StockItem
def get(self, request, *args, **kwargs):
export_format = request.GET.get('format', 'csv').lower()
# Check if a particular location was specified
loc_id = request.GET.get('location', None)
location = None
if loc_id:
try:
location = StockLocation.objects.get(pk=loc_id)
except (ValueError, StockLocation.DoesNotExist):
pass
# Check if a particular supplier was specified
sup_id = request.GET.get('supplier', None)
supplier = None
if sup_id:
try:
supplier = Company.objects.get(pk=sup_id)
except (ValueError, Company.DoesNotExist):
pass
# Check if a particular part was specified
part_id = request.GET.get('part', None)
part = None
if part_id:
try:
part = Part.objects.get(pk=part_id)
except (ValueError, Part.DoesNotExist):
pass
if export_format not in GetExportFormats():
export_format = 'csv'
filename = 'InvenTree_Stocktake_{date}.{fmt}'.format(
date=datetime.now().strftime("%d-%b-%Y"),
fmt=export_format
)
if location:
# CHeck if locations should be cascading
cascade = str2bool(request.GET.get('cascade', True))
stock_items = location.get_stock_items(cascade)
else:
cascade = True
stock_items = StockItem.objects.all()
if part:
stock_items = stock_items.filter(part=part)
if supplier:
stock_items = stock_items.filter(supplier_part__supplier=supplier)
# Filter out stock items that are not 'in stock'
stock_items = stock_items.filter(customer=None)
stock_items = stock_items.filter(belongs_to=None)
# Column headers
headers = [
_('Stock ID'),
_('Part ID'),
_('Part'),
_('Supplier Part ID'),
_('Supplier ID'),
_('Supplier'),
_('Location ID'),
_('Location'),
_('Quantity'),
_('Batch'),
_('Serial'),
_('Status'),
_('Notes'),
_('Review Needed'),
_('Last Updated'),
_('Last Stocktake'),
_('Purchase Order ID'),
_('Build ID'),
]
data = tablib.Dataset(headers=headers)
for item in stock_items:
line = []
line.append(item.pk)
line.append(item.part.pk)
line.append(item.part.full_name)
if item.supplier_part:
line.append(item.supplier_part.pk)
line.append(item.supplier_part.supplier.pk)
line.append(item.supplier_part.supplier.name)
else:
line.append('')
line.append('')
line.append('')
if item.location:
line.append(item.location.pk)
line.append(item.location.name)
else:
line.append('')
line.append('')
line.append(item.quantity)
line.append(item.batch)
line.append(item.serial)
line.append(StockStatus.label(item.status))
line.append(item.notes)
line.append(item.review_needed)
line.append(item.updated)
line.append(item.stocktake_date)
if item.purchase_order:
line.append(item.purchase_order.pk)
else:
line.append('')
if item.build:
line.append(item.build.pk)
else:
line.append('')
data.append(line)
filedata = data.export(export_format)
return DownloadFile(filedata, filename)
class StockItemQRCode(QRCodeView):
""" View for displaying a QR code for a StockItem object """