diff --git a/InvenTree/InvenTree/validators.py b/InvenTree/InvenTree/validators.py
index f3fd9ef306..0e1622a49a 100644
--- a/InvenTree/InvenTree/validators.py
+++ b/InvenTree/InvenTree/validators.py
@@ -7,9 +7,56 @@ from django.utils.translation import gettext_lazy as _
def validate_part_name(value):
- # Prevent some illegal characters in part names
- for c in ['|', '#', '$']:
+ """ Prevent some illegal characters in part names.
+ """
+
+ for c in ['|', '#', '$', '{', '}']:
if c in str(value):
raise ValidationError(
_('Invalid character in part name')
)
+
+
+def validate_overage(value):
+ """ Validate that a BOM overage string is properly formatted.
+
+ An overage string can look like:
+
+ - An integer number ('1' / 3 / 4)
+ - A percentage ('5%' / '10 %')
+ """
+
+ value = str(value).lower().strip()
+
+ # First look for a simple integer value
+ try:
+ i = int(value)
+
+ if i < 0:
+ raise ValidationError(_("Overage value must not be negative"))
+
+ # Looks like an integer!
+ return True
+ except ValueError:
+ pass
+
+ # Now look for a percentage value
+ if value.endswith('%'):
+ v = value[:-1].strip()
+
+ # Does it look like a number?
+ try:
+ f = float(v)
+
+ if f < 0:
+ raise ValidationError(_("Overage value must not be negative"))
+ elif f > 100:
+ raise ValidationError(_("Overage must not exceed 100%"))
+
+ return True
+ except ValueError:
+ pass
+
+ raise ValidationError(
+ _("Overage must be an integer value or a percentage")
+ )
diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py
index 823a19ba61..c69857fda3 100644
--- a/InvenTree/build/models.py
+++ b/InvenTree/build/models.py
@@ -59,7 +59,7 @@ class Build(models.Model):
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'
+ help_text='Select location to take stock from for this build (leave blank to take from any stock location)'
)
quantity = models.PositiveIntegerField(
@@ -261,7 +261,7 @@ class Build(models.Model):
try:
item = BomItem.objects.get(part=self.part.id, sub_part=part.id)
- return item.quantity * self.quantity
+ return item.get_required_quantity(self.quantity)
except BomItem.DoesNotExist:
return 0
diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py
index 2e6d3c694c..63fcf4add8 100644
--- a/InvenTree/company/models.py
+++ b/InvenTree/company/models.py
@@ -7,6 +7,7 @@ from __future__ import unicode_literals
import os
+from django.apps import apps
from django.db import models
from django.urls import reverse
from django.conf import settings
@@ -111,6 +112,18 @@ class Company(models.Model):
""" Return True if this company supplies any parts """
return self.part_count > 0
+ @property
+ def stock_items(self):
+ """ Return a list of all stock items supplied by this company """
+ stock = apps.get_model('stock', 'StockItem')
+ return stock.objects.filter(supplier_part__supplier=self.id).all()
+
+ @property
+ def stock_count(self):
+ """ Return the number of stock items supplied by this company """
+ stock = apps.get_model('stock', 'StockItem')
+ return stock.objects.filter(supplier_part__supplier=self.id).count()
+
class Contact(models.Model):
""" A Contact represents a person who works at a particular company.
diff --git a/InvenTree/company/serializers.py b/InvenTree/company/serializers.py
index 88792f7f64..2967dbebd5 100644
--- a/InvenTree/company/serializers.py
+++ b/InvenTree/company/serializers.py
@@ -25,7 +25,25 @@ class CompanySerializer(serializers.ModelSerializer):
""" Serializer for Company object (full detail) """
url = serializers.CharField(source='get_absolute_url', read_only=True)
+ part_count = serializers.CharField(read_only=True)
class Meta:
model = Company
- fields = '__all__'
+ fields = [
+ 'id',
+ 'url',
+ 'name',
+ 'description',
+ 'website',
+ 'name',
+ 'phone',
+ 'address',
+ 'email',
+ 'contact',
+ 'URL',
+ 'image',
+ 'notes',
+ 'is_customer',
+ 'is_supplier',
+ 'part_count'
+ ]
diff --git a/InvenTree/company/templates/company/company_base.html b/InvenTree/company/templates/company/company_base.html
index 56983784ee..8c64190865 100644
--- a/InvenTree/company/templates/company/company_base.html
+++ b/InvenTree/company/templates/company/company_base.html
@@ -72,6 +72,11 @@ InvenTree | Company - {{ company.name }}
{% endblock %}
+{% block js_load %}
+{{ block.super }}
+
+{% endblock %}
+
{% block js_ready %}
enableDragAndDrop(
diff --git a/InvenTree/company/templates/company/detail_part.html b/InvenTree/company/templates/company/detail_part.html
index 5d01f1b10e..b3213d5993 100644
--- a/InvenTree/company/templates/company/detail_part.html
+++ b/InvenTree/company/templates/company/detail_part.html
@@ -33,6 +33,14 @@
supplier: {{ company.id }}
},
reload: true,
+ secondary: [
+ {
+ field: 'part',
+ label: 'New Part',
+ title: 'Create New Part',
+ url: "{% url 'part-create' %}"
+ },
+ ]
});
});
diff --git a/InvenTree/company/templates/company/detail_stock.html b/InvenTree/company/templates/company/detail_stock.html
new file mode 100644
index 0000000000..5bc7b9598a
--- /dev/null
+++ b/InvenTree/company/templates/company/detail_stock.html
@@ -0,0 +1,26 @@
+{% extends "company/company_base.html" %}
+{% load static %}
+
+{% block details %}
+
+{% include "company/tabs.html" with tab='stock' %}
+
+
Supplier Stock
+
+{% include "stock_table.html" %}
+
+{% endblock %}
+{% block js_ready %}
+{{ block.super }}
+
+ loadStockTable($('#stock-table'), {
+ url: "{% url 'api-stock-list' %}",
+ params: {
+ supplier: {{ company.id }},
+ },
+ buttons: [
+ '#stock-options',
+ ]
+ });
+
+{% endblock %}
\ No newline at end of file
diff --git a/InvenTree/company/templates/company/index.html b/InvenTree/company/templates/company/index.html
index 3f0dc7c489..cea452fdc1 100644
--- a/InvenTree/company/templates/company/index.html
+++ b/InvenTree/company/templates/company/index.html
@@ -3,19 +3,19 @@
{% load static %}
{% block page_title %}
-InvenTree | Company List
+InvenTree | Supplier List
{% endblock %}
{% block content %}
-
Company List
+ Supplier List
@@ -54,7 +54,7 @@ InvenTree | Company List
},
{
field: 'name',
- title: 'Company',
+ title: 'Supplier',
sortable: true,
formatter: function(value, row, index, field) {
return imageHoverIcon(row.image) + renderLink(value, row.url);
@@ -73,7 +73,14 @@ InvenTree | Company List
}
return '';
}
- }
+ },
+ {
+ field: 'part_count',
+ title: 'Parts',
+ formatter: function(value, row, index, field) {
+ return renderLink(value, row.url + 'parts/');
+ }
+ },
],
url: "{% url 'api-company-list' %}"
});
diff --git a/InvenTree/company/templates/company/tabs.html b/InvenTree/company/templates/company/tabs.html
index df8b9a4fc9..211b56e1d1 100644
--- a/InvenTree/company/templates/company/tabs.html
+++ b/InvenTree/company/templates/company/tabs.html
@@ -6,6 +6,9 @@
Supplier Parts {{ company.part_count }}
+
+ Stock {{ company.stock_count }}
+
{% if 0 %}
Purchase Orders
diff --git a/InvenTree/company/urls.py b/InvenTree/company/urls.py
index 2c5018ebec..0f8719fe01 100644
--- a/InvenTree/company/urls.py
+++ b/InvenTree/company/urls.py
@@ -16,6 +16,7 @@ company_detail_urls = [
# url(r'orders/?', views.CompanyDetail.as_view(template_name='company/orders.html'), name='company-detail-orders'),
url(r'parts/?', views.CompanyDetail.as_view(template_name='company/detail_part.html'), name='company-detail-parts'),
+ url(r'stock/?', views.CompanyDetail.as_view(template_name='company/detail_stock.html'), name='company-detail-stock'),
url(r'thumbnail/?', views.CompanyImage.as_view(), name='company-image'),
diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py
index e671b49e4f..0973138b21 100644
--- a/InvenTree/part/api.py
+++ b/InvenTree/part/api.py
@@ -12,7 +12,6 @@ from rest_framework.response import Response
from rest_framework import filters
from rest_framework import generics, permissions
-from django.db.models import Q
from django.conf.urls import url, include
from django.urls import reverse
@@ -109,20 +108,7 @@ class PartList(generics.ListCreateAPIView):
if cat_id:
try:
category = PartCategory.objects.get(pk=cat_id)
-
- # Filter by the supplied category
- flt = Q(category=cat_id)
-
- if self.request.query_params.get('include_child_categories', None):
- childs = category.getUniqueChildren()
- for child in childs:
- # Ignore the top-level category (already filtered)
- if str(child) == str(cat_id):
- continue
- flt |= Q(category=child)
-
- parts_list = parts_list.filter(flt)
-
+ parts_list = parts_list.filter(category__in=category.getUniqueChildren())
except PartCategory.DoesNotExist:
pass
diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py
index 88c6c11385..d4e70ee47a 100644
--- a/InvenTree/part/forms.py
+++ b/InvenTree/part/forms.py
@@ -133,6 +133,7 @@ class EditBomItemForm(HelperForm):
'part',
'sub_part',
'quantity',
+ 'overage',
'note'
]
diff --git a/InvenTree/part/migrations/0025_auto_20190515_0012.py b/InvenTree/part/migrations/0025_auto_20190515_0012.py
new file mode 100644
index 0000000000..aaeb8ea1a3
--- /dev/null
+++ b/InvenTree/part/migrations/0025_auto_20190515_0012.py
@@ -0,0 +1,46 @@
+# Generated by Django 2.2 on 2019-05-14 14:12
+
+import InvenTree.validators
+import django.core.validators
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('part', '0024_partcategory_default_keywords'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='bomitem',
+ name='overage',
+ field=models.CharField(blank=True, help_text='Estimated build wastage quantity (absolute or percentage)', max_length=24, validators=[InvenTree.validators.validate_overage]),
+ ),
+ migrations.AlterField(
+ model_name='bomitem',
+ name='note',
+ field=models.CharField(blank=True, help_text='BOM item notes', max_length=100),
+ ),
+ migrations.AlterField(
+ model_name='bomitem',
+ name='part',
+ field=models.ForeignKey(help_text='Select parent part', limit_choices_to={'active': True, 'buildable': True}, on_delete=django.db.models.deletion.CASCADE, related_name='bom_items', to='part.Part'),
+ ),
+ migrations.AlterField(
+ model_name='bomitem',
+ name='quantity',
+ field=models.PositiveIntegerField(default=1, help_text='BOM quantity for this BOM item', validators=[django.core.validators.MinValueValidator(0)]),
+ ),
+ migrations.AlterField(
+ model_name='bomitem',
+ name='sub_part',
+ field=models.ForeignKey(help_text='Select part to be used in BOM', limit_choices_to={'active': True, 'consumable': True}, on_delete=django.db.models.deletion.CASCADE, related_name='used_in', to='part.Part'),
+ ),
+ migrations.AlterField(
+ model_name='supplierpart',
+ name='URL',
+ field=models.URLField(blank=True, help_text='URL for external supplier part link'),
+ ),
+ ]
diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py
index d255a319f3..f4673dc309 100644
--- a/InvenTree/part/models.py
+++ b/InvenTree/part/models.py
@@ -300,6 +300,23 @@ class Part(models.Model):
# Default case - no default category found
return None
+ def get_default_supplier(self):
+ """ Get the default supplier part for this part (may be None).
+
+ - If the part specifies a default_supplier, return that
+ - If there is only one supplier part available, return that
+ - Else, return None
+ """
+
+ if self.default_supplier:
+ return self.default_suppliers
+
+ if self.supplier_count == 1:
+ return self.supplier_parts.first()
+
+ # Default to None if there are multiple suppliers to choose from
+ return None
+
default_supplier = models.ForeignKey('part.SupplierPart',
on_delete=models.SET_NULL,
blank=True, null=True,
@@ -557,10 +574,11 @@ class Part(models.Model):
# Copy the part image
if kwargs.get('image', True):
- image_file = ContentFile(other.image.read())
- image_file.name = rename_part_image(self, 'test.png')
+ if other.image:
+ image_file = ContentFile(other.image.read())
+ image_file.name = rename_part_image(self, 'test.png')
- self.image = image_file
+ self.image = image_file
# Copy the BOM data
if kwargs.get('bom', False):
@@ -661,6 +679,7 @@ class BomItem(models.Model):
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'
+ overage: Estimated losses for a Build. Can be expressed as absolute value (e.g. '7') or a percentage (e.g. '2%')
note: Note field for this BOM item
"""
@@ -688,6 +707,10 @@ class BomItem(models.Model):
# Quantity required
quantity = models.PositiveIntegerField(default=1, validators=[MinValueValidator(0)], help_text='BOM quantity for this BOM item')
+ overage = models.CharField(max_length=24, blank=True, validators=[validators.validate_overage],
+ help_text='Estimated build wastage quantity (absolute or percentage)'
+ )
+
# Note attached to this BOM line item
note = models.CharField(max_length=100, blank=True, help_text='BOM item notes')
@@ -721,6 +744,62 @@ class BomItem(models.Model):
child=self.sub_part.full_name,
n=self.quantity)
+ def get_overage_quantity(self, quantity):
+ """ Calculate overage quantity
+ """
+
+ # Most of the time overage string will be empty
+ if len(self.overage) == 0:
+ return 0
+
+ overage = str(self.overage).strip()
+
+ # Is the overage an integer value?
+ try:
+ ovg = int(overage)
+
+ if ovg < 0:
+ ovg = 0
+
+ return ovg
+ except ValueError:
+ pass
+
+ # Is the overage a percentage?
+ if overage.endswith('%'):
+ overage = overage[:-1].strip()
+
+ try:
+ percent = float(overage) / 100.0
+ if percent > 1:
+ percent = 1
+ if percent < 0:
+ percent = 0
+
+ return int(percent * quantity)
+
+ except ValueError:
+ pass
+
+ # Default = No overage
+ return 0
+
+ def get_required_quantity(self, build_quantity):
+ """ Calculate the required part quantity, based on the supplier build_quantity.
+ Includes overage estimate in the returned value.
+
+ Args:
+ build_quantity: Number of parts to build
+
+ Returns:
+ Quantity required for this build (including overage)
+ """
+
+ # Base quantity requirement
+ base_quantity = self.quantity * build_quantity
+
+ return base_quantity + self.get_overage_quantity(base_quantity)
+
class SupplierPart(models.Model):
""" Represents a unique part as provided by a Supplier
diff --git a/InvenTree/part/param_todo.py_todo b/InvenTree/part/param_todo.py_todo
deleted file mode 100644
index e597bedf57..0000000000
--- a/InvenTree/part/param_todo.py_todo
+++ /dev/null
@@ -1,89 +0,0 @@
-"""
-TODO - Implement part parameters, and templates
-
-See code below
-"""
-
-
-
-class PartParameterTemplate(models.Model):
- """ A PartParameterTemplate pre-defines a parameter field,
- ready to be copied for use with a given Part.
- A PartParameterTemplate can be optionally associated with a PartCategory
- """
- name = models.CharField(max_length=20, unique=True)
- units = models.CharField(max_length=10, blank=True)
-
- # Parameter format
- PARAM_NUMERIC = 10
- PARAM_TEXT = 20
- PARAM_BOOL = 30
-
- PARAM_TYPE_CODES = {
- PARAM_NUMERIC: _("Numeric"),
- PARAM_TEXT: _("Text"),
- PARAM_BOOL: _("Bool")
- }
-
- format = models.PositiveIntegerField(
- default=PARAM_NUMERIC,
- choices=PARAM_TYPE_CODES.items(),
- validators=[MinValueValidator(0)])
-
- def __str__(self):
- return "{name} ({units})".format(
- name=self.name,
- units=self.units)
-
- class Meta:
- verbose_name = "Parameter Template"
- verbose_name_plural = "Parameter Templates"
-
-
-class CategoryParameterLink(models.Model):
- """ Links a PartParameterTemplate to a PartCategory
- """
- category = models.ForeignKey(PartCategory, on_delete=models.CASCADE)
- template = models.ForeignKey(PartParameterTemplate, on_delete=models.CASCADE)
-
- def __str__(self):
- return "{name} - {cat}".format(
- name=self.template.name,
- cat=self.category)
-
- class Meta:
- verbose_name = "Category Parameter"
- verbose_name_plural = "Category Parameters"
- unique_together = ('category', 'template')
-
-
-class PartParameter(models.Model):
- """ PartParameter is associated with a single part
- """
-
- part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='parameters')
- template = models.ForeignKey(PartParameterTemplate)
-
- # Value data
- value = models.CharField(max_length=50, blank=True)
- min_value = models.CharField(max_length=50, blank=True)
- max_value = models.CharField(max_length=50, blank=True)
-
- def __str__(self):
- return "{name} : {val}{units}".format(
- name=self.template.name,
- val=self.value,
- units=self.template.units)
-
- @property
- def units(self):
- return self.template.units
-
- @property
- def name(self):
- return self.template.name
-
- class Meta:
- verbose_name = "Part Parameter"
- verbose_name_plural = "Part Parameters"
- unique_together = ('part', 'template')
diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py
index 37ccb639a0..87ca59c13b 100644
--- a/InvenTree/part/serializers.py
+++ b/InvenTree/part/serializers.py
@@ -104,8 +104,6 @@ class PartStarSerializer(InvenTreeModelSerializer):
class BomItemSerializer(InvenTreeModelSerializer):
""" Serializer for BomItem object """
- # url = serializers.CharField(source='get_absolute_url', read_only=True)
-
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
sub_part_detail = PartBriefSerializer(source='sub_part', many=False, read_only=True)
@@ -113,12 +111,12 @@ class BomItemSerializer(InvenTreeModelSerializer):
model = BomItem
fields = [
'pk',
- # 'url',
'part',
'part_detail',
'sub_part',
'sub_part_detail',
'quantity',
+ 'overage',
'note',
]
diff --git a/InvenTree/part/templates/part/category.html b/InvenTree/part/templates/part/category.html
index 3abef26af9..fd8f20d2aa 100644
--- a/InvenTree/part/templates/part/category.html
+++ b/InvenTree/part/templates/part/category.html
@@ -73,8 +73,22 @@
{% if category %}
data: {
category: {{ category.id }}
- }
+ },
{% endif %}
+ secondary: [
+ {
+ field: 'default_location',
+ label: 'New Location',
+ title: 'Create new location',
+ url: "{% url 'stock-location-create' %}",
+ },
+ {
+ field: 'parent',
+ label: 'New Category',
+ title: 'Create new category',
+ url: "{% url 'category-create' %}",
+ },
+ ]
});
})
@@ -139,7 +153,6 @@
query: {
{% if category %}
category: {{ category.id }},
- include_child_categories: true,
{% endif %}
},
buttons: ['#part-options'],
diff --git a/InvenTree/part/templates/part/stock.html b/InvenTree/part/templates/part/stock.html
index 3311dafb8d..8b9561ea2e 100644
--- a/InvenTree/part/templates/part/stock.html
+++ b/InvenTree/part/templates/part/stock.html
@@ -13,25 +13,7 @@
-
-
-
-
+{% include "stock_table.html" %}
{% endblock %}
@@ -62,43 +44,4 @@
url: "{% url 'api-stock-list' %}",
});
- function selectedStock() {
- return $("#stock-table").bootstrapTable('getSelections');
- }
-
- $("#multi-item-move").click(function() {
-
- var items = selectedStock();
-
- moveStockItems(items,
- {
- success: function() {
- $("#stock-table").bootstrapTable('refresh');
- }
- });
-
- return false;
- });
-
- $("#multi-item-stocktake").click(function() {
- updateStockItems({
- action: 'stocktake'
- });
- return false;
- });
-
- $("#multi-item-take").click(function() {
- updateStockItems({
- action: 'remove',
- });
- return false;
- });
-
- $("#multi-item-give").click(function() {
- updateStockItems({
- action: 'add',
- });
- return false;
- })
-
{% endblock %}
\ No newline at end of file
diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py
index b8d9b7ba6d..15c6f58dcd 100644
--- a/InvenTree/part/test_api.py
+++ b/InvenTree/part/test_api.py
@@ -105,13 +105,6 @@ class PartAPITest(APITestCase):
url = reverse('api-part-list')
data = {'category': 1}
- response = self.client.get(url, data, format='json')
-
- # There should be 1 part in this category
- self.assertEqual(len(response.data), 0)
-
- data['include_child_categories'] = 1
-
# Now request to include child categories
response = self.client.get(url, data, format='json')
diff --git a/InvenTree/static/script/inventree/bom.js b/InvenTree/static/script/inventree/bom.js
index 46bdc249ef..6ff81de4fc 100644
--- a/InvenTree/static/script/inventree/bom.js
+++ b/InvenTree/static/script/inventree/bom.js
@@ -113,6 +113,15 @@ function loadBomTable(table, options) {
title: 'Required',
searchable: false,
sortable: true,
+ formatter: function(value, row, index, field) {
+ var text = value;
+
+ if (row.overage) {
+ text += " (+" + row.overage + ") ";
+ }
+
+ return text;
+ }
}
);
diff --git a/InvenTree/static/script/inventree/stock.js b/InvenTree/static/script/inventree/stock.js
index 9f9c5fd40b..d34ed9beee 100644
--- a/InvenTree/static/script/inventree/stock.js
+++ b/InvenTree/static/script/inventree/stock.js
@@ -43,6 +43,7 @@ function updateStock(items, options={}) {
html += 'Item ';
html += 'Location ';
html += 'Quantity ';
+ html += '' + options.action + ' ';
html += '';
@@ -71,6 +72,9 @@ function updateStock(items, options={}) {
} else {
html += 'No location set ';
}
+
+ html += '' + item.quantity + ' ';
+
html += " ';
html += " ";
+ html += "Note field must be filled
";
+
+ html += `
+
+
+
+
+ Confirm Stocktake
+
+
Confirm stock count
+
`;
- html += "Note field must be filled
";
var title = '';
@@ -109,6 +123,7 @@ function updateStock(items, options={}) {
});
$(modal).find('#note-warning').hide();
+ $(modal).find('#confirm-warning').hide();
modalEnable(modal, true);
@@ -116,13 +131,23 @@ function updateStock(items, options={}) {
var stocktake = [];
var notes = $(modal).find('#stocktake-notes').val();
+ var confirm = $(modal).find('#stocktake-confirm').is(':checked');
+
+ var valid = true;
if (!notes) {
$(modal).find('#note-warning').show();
- return false;
+ valid = false;
}
- var valid = true;
+ if (!confirm) {
+ $(modal).find('#confirm-warning').show();
+ valid = false;
+ }
+
+ if (!valid) {
+ return false;
+ }
// Form stocktake data
for (idx = 0; idx < items.length; idx++) {
@@ -413,6 +438,42 @@ function loadStockTable(table, options) {
if (options.buttons) {
linkButtonsToSelection(table, options.buttons);
}
+
+ // Automatically link button callbacks
+ $('#multi-item-stocktake').click(function() {
+ updateStockItems({
+ action: 'stocktake',
+ });
+ return false;
+ });
+
+ $('#multi-item-remove').click(function() {
+ updateStockItems({
+ action: 'remove',
+ });
+ return false;
+ });
+
+ $('#multi-item-add').click(function() {
+ updateStockItems({
+ action: 'add',
+ });
+ return false;
+ });
+
+ $("#multi-item-move").click(function() {
+
+ var items = $("#stock-table").bootstrapTable('getSelections');
+
+ moveStockItems(items,
+ {
+ success: function() {
+ $("#stock-table").bootstrapTable('refresh');
+ }
+ });
+
+ return false;
+ });
}
diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py
index 9f305b657a..3c08ff8822 100644
--- a/InvenTree/stock/api.py
+++ b/InvenTree/stock/api.py
@@ -7,11 +7,12 @@ from django_filters import NumberFilter
from django.conf.urls import url, include
from django.urls import reverse
-from django.db.models import Q
from .models import StockLocation, StockItem
from .models import StockItemTracking
+from part.models import PartCategory
+
from .serializers import StockItemSerializer, StockQuantitySerializer
from .serializers import LocationSerializer
from .serializers import StockTrackingSerializer
@@ -237,16 +238,20 @@ class StockList(generics.ListCreateAPIView):
- GET: Return a list of all StockItem objects (with optional query filters)
- POST: Create a new StockItem
+
+ Additional query parameters are available:
+ - location: Filter stock by location
+ - category: Filter by parts belonging to a certain category
+ - supplier: Filter by supplier
"""
def get_queryset(self):
"""
If the query includes a particular location,
we may wish to also request stock items from all child locations.
- This is set by the optional param 'include_child_categories'
"""
- # Does the client wish to filter by category?
+ # Does the client wish to filter by stock location?
loc_id = self.request.query_params.get('location', None)
# Start with all objects
@@ -255,23 +260,28 @@ class StockList(generics.ListCreateAPIView):
if loc_id:
try:
location = StockLocation.objects.get(pk=loc_id)
-
- # Filter by the supplied category
- flt = Q(location=loc_id)
-
- if self.request.query_params.get('include_child_locations', None):
- childs = location.getUniqueChildren()
- for child in childs:
- # Ignore the top-level category (already filtered!)
- if str(child) == str(loc_id):
- continue
- flt |= Q(location=child)
-
- stock_list = stock_list.filter(flt)
-
+ stock_list = stock_list.filter(location__in=location.getUniqueChildren())
+
except StockLocation.DoesNotExist:
pass
+ # Does the client wish to filter by part category?
+ cat_id = self.request.query_params.get('category', None)
+
+ if cat_id:
+ try:
+ category = PartCategory.objects.get(pk=cat_id)
+ stock_list = stock_list.filter(part__category__in=category.getUniqueChildren())
+
+ except PartCategory.DoesNotExist:
+ pass
+
+ # Filter by supplier
+ supplier_id = self.request.query_params.get('supplier', None)
+
+ if supplier_id:
+ stock_list = stock_list.filter(supplier_part__supplier=supplier_id)
+
return stock_list
serializer_class = StockItemSerializer
diff --git a/InvenTree/stock/templates/stock/location.html b/InvenTree/stock/templates/stock/location.html
index f05b14c77b..ae36078ece 100644
--- a/InvenTree/stock/templates/stock/location.html
+++ b/InvenTree/stock/templates/stock/location.html
@@ -44,24 +44,7 @@
-
-
-
-
+{% include "stock_table.html" %}
{% include 'modals.html' %}
@@ -80,7 +63,15 @@
location: {{ location.id }}
{% endif %}
},
- follow: true
+ follow: true,
+ secondary: [
+ {
+ field: 'parent',
+ label: 'New Location',
+ title: 'Create new location',
+ url: "{% url 'stock-location-create' %}",
+ },
+ ]
});
return false;
});
@@ -141,46 +132,7 @@
return false;
});
-
- function selectedStock() {
- return $("#stock-table").bootstrapTable('getSelections');
- }
-
- $("#multi-item-move").click(function() {
-
- var items = selectedStock();
-
- moveStockItems(items,
- {
- success: function() {
- $("#stock-table").bootstrapTable('refresh');
- }
- });
-
- return false;
- });
-
- $('#multi-item-stocktake').click(function() {
- updateStockItems({
- action: 'stocktake',
- });
- return false;
- });
-
- $('#multi-item-remove').click(function() {
- updateStockItems({
- action: 'remove',
- });
- return false;
- });
-
- $('#multi-item-add').click(function() {
- updateStockItems({
- action: 'add',
- });
- return false;
- });
-
+
loadStockTable($("#stock-table"), {
buttons: [
'#stock-options',
@@ -188,7 +140,6 @@
params: {
{% if location %}
location: {{ location.id }},
- include_child_locations: true,
{% endif %}
},
url: "{% url 'api-stock-list' %}",
diff --git a/InvenTree/templates/InvenTree/search.html b/InvenTree/templates/InvenTree/search.html
index 187cc1addb..77730f5ea5 100644
--- a/InvenTree/templates/InvenTree/search.html
+++ b/InvenTree/templates/InvenTree/search.html
@@ -1,5 +1,7 @@
{% extends "base.html" %}
+{% load static %}
+
{% block page_title %}
InvenTree | Search Results
{% endblock %}
@@ -19,6 +21,11 @@ InvenTree | Search Results
{% endblock %}
+{% block js_load %}
+{{ block.super }}
+
+{% endblock %}
+
{% block js_ready %}
{{ block.super }}
diff --git a/InvenTree/templates/navbar.html b/InvenTree/templates/navbar.html
index 5cb71d13ac..e7785abcd5 100644
--- a/InvenTree/templates/navbar.html
+++ b/InvenTree/templates/navbar.html
@@ -9,7 +9,7 @@
Parts
Stock
Build
- Companies
+ Suppliers
{% include "search_form.html" %}
diff --git a/InvenTree/templates/stock_table.html b/InvenTree/templates/stock_table.html
new file mode 100644
index 0000000000..f5411fabb8
--- /dev/null
+++ b/InvenTree/templates/stock_table.html
@@ -0,0 +1,17 @@
+
+
+
\ No newline at end of file