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
5d4ec2154b
@ -7,9 +7,56 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
|
|
||||||
|
|
||||||
def validate_part_name(value):
|
def validate_part_name(value):
|
||||||
# Prevent some illegal characters in part names
|
""" Prevent some illegal characters in part names.
|
||||||
for c in ['|', '#', '$']:
|
"""
|
||||||
|
|
||||||
|
for c in ['|', '#', '$', '{', '}']:
|
||||||
if c in str(value):
|
if c in str(value):
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_('Invalid character in part name')
|
_('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")
|
||||||
|
)
|
||||||
|
@ -59,7 +59,7 @@ class Build(models.Model):
|
|||||||
take_from = models.ForeignKey('stock.StockLocation', on_delete=models.SET_NULL,
|
take_from = models.ForeignKey('stock.StockLocation', on_delete=models.SET_NULL,
|
||||||
related_name='sourcing_builds',
|
related_name='sourcing_builds',
|
||||||
null=True, blank=True,
|
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(
|
quantity = models.PositiveIntegerField(
|
||||||
@ -261,7 +261,7 @@ class Build(models.Model):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
item = BomItem.objects.get(part=self.part.id, sub_part=part.id)
|
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:
|
except BomItem.DoesNotExist:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -111,6 +112,18 @@ class Company(models.Model):
|
|||||||
""" Return True if this company supplies any parts """
|
""" Return True if this company supplies any parts """
|
||||||
return self.part_count > 0
|
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):
|
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.
|
||||||
|
@ -25,7 +25,25 @@ class CompanySerializer(serializers.ModelSerializer):
|
|||||||
""" Serializer for Company object (full detail) """
|
""" Serializer for Company object (full detail) """
|
||||||
|
|
||||||
url = serializers.CharField(source='get_absolute_url', read_only=True)
|
url = serializers.CharField(source='get_absolute_url', read_only=True)
|
||||||
|
part_count = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Company
|
model = Company
|
||||||
fields = '__all__'
|
fields = [
|
||||||
|
'id',
|
||||||
|
'url',
|
||||||
|
'name',
|
||||||
|
'description',
|
||||||
|
'website',
|
||||||
|
'name',
|
||||||
|
'phone',
|
||||||
|
'address',
|
||||||
|
'email',
|
||||||
|
'contact',
|
||||||
|
'URL',
|
||||||
|
'image',
|
||||||
|
'notes',
|
||||||
|
'is_customer',
|
||||||
|
'is_supplier',
|
||||||
|
'part_count'
|
||||||
|
]
|
||||||
|
@ -72,6 +72,11 @@ InvenTree | Company - {{ company.name }}
|
|||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js_load %}
|
||||||
|
{{ block.super }}
|
||||||
|
<script type='text/javascript' src="{% static 'script/inventree/stock.js' %}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block js_ready %}
|
{% block js_ready %}
|
||||||
|
|
||||||
enableDragAndDrop(
|
enableDragAndDrop(
|
||||||
|
@ -33,6 +33,14 @@
|
|||||||
supplier: {{ company.id }}
|
supplier: {{ company.id }}
|
||||||
},
|
},
|
||||||
reload: true,
|
reload: true,
|
||||||
|
secondary: [
|
||||||
|
{
|
||||||
|
field: 'part',
|
||||||
|
label: 'New Part',
|
||||||
|
title: 'Create New Part',
|
||||||
|
url: "{% url 'part-create' %}"
|
||||||
|
},
|
||||||
|
]
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
26
InvenTree/company/templates/company/detail_stock.html
Normal file
26
InvenTree/company/templates/company/detail_stock.html
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{% extends "company/company_base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block details %}
|
||||||
|
|
||||||
|
{% include "company/tabs.html" with tab='stock' %}
|
||||||
|
|
||||||
|
<h3>Supplier Stock</h3>
|
||||||
|
|
||||||
|
{% 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 %}
|
@ -3,19 +3,19 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block page_title %}
|
{% block page_title %}
|
||||||
InvenTree | Company List
|
InvenTree | Supplier List
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class='row'>
|
<div class='row'>
|
||||||
<div class='col-sm-6'>
|
<div class='col-sm-6'>
|
||||||
<h3>Company List</h3>
|
<h3>Supplier List</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class='col-sm-6'>
|
<div class='col-sm-6'>
|
||||||
<div class='container' id='active-build-toolbar' style='float: right;'>
|
<div class='container' id='active-build-toolbar' style='float: right;'>
|
||||||
<div class='btn-group' style='float: right;'>
|
<div class='btn-group' style='float: right;'>
|
||||||
<button type='button' class="btn btn-success" id='new-company'>New Company</button>
|
<button type='button' class="btn btn-success" id='new-company' title='Add new supplier'>New Supplier</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -54,7 +54,7 @@ InvenTree | Company List
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'name',
|
field: 'name',
|
||||||
title: 'Company',
|
title: 'Supplier',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
formatter: function(value, row, index, field) {
|
formatter: function(value, row, index, field) {
|
||||||
return imageHoverIcon(row.image) + renderLink(value, row.url);
|
return imageHoverIcon(row.image) + renderLink(value, row.url);
|
||||||
@ -73,7 +73,14 @@ InvenTree | Company List
|
|||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'part_count',
|
||||||
|
title: 'Parts',
|
||||||
|
formatter: function(value, row, index, field) {
|
||||||
|
return renderLink(value, row.url + 'parts/');
|
||||||
}
|
}
|
||||||
|
},
|
||||||
],
|
],
|
||||||
url: "{% url 'api-company-list' %}"
|
url: "{% url 'api-company-list' %}"
|
||||||
});
|
});
|
||||||
|
@ -6,6 +6,9 @@
|
|||||||
<li{% if tab == 'parts' %} class='active'{% endif %}>
|
<li{% if tab == 'parts' %} class='active'{% endif %}>
|
||||||
<a href="{% url 'company-detail-parts' company.id %}">Supplier Parts <span class='badge'>{{ company.part_count }}</span></a>
|
<a href="{% url 'company-detail-parts' company.id %}">Supplier Parts <span class='badge'>{{ company.part_count }}</span></a>
|
||||||
</li>
|
</li>
|
||||||
|
<li{% if tab == 'stock' %} class='active'{% endif %}>
|
||||||
|
<a href="{% url 'company-detail-stock' company.id %}">Stock <span class='badge'>{{ company.stock_count }}</a>
|
||||||
|
</li>
|
||||||
{% if 0 %}
|
{% if 0 %}
|
||||||
<li{% if tab == 'po' %} class='active'{% endif %}>
|
<li{% if tab == 'po' %} class='active'{% endif %}>
|
||||||
<a href="#">Purchase Orders</a>
|
<a href="#">Purchase Orders</a>
|
||||||
|
@ -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'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'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'),
|
url(r'thumbnail/?', views.CompanyImage.as_view(), name='company-image'),
|
||||||
|
|
||||||
|
@ -12,7 +12,6 @@ from rest_framework.response import Response
|
|||||||
from rest_framework import filters
|
from rest_framework import filters
|
||||||
from rest_framework import generics, permissions
|
from rest_framework import generics, permissions
|
||||||
|
|
||||||
from django.db.models import Q
|
|
||||||
from django.conf.urls import url, include
|
from django.conf.urls import url, include
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
@ -109,20 +108,7 @@ class PartList(generics.ListCreateAPIView):
|
|||||||
if cat_id:
|
if cat_id:
|
||||||
try:
|
try:
|
||||||
category = PartCategory.objects.get(pk=cat_id)
|
category = PartCategory.objects.get(pk=cat_id)
|
||||||
|
parts_list = parts_list.filter(category__in=category.getUniqueChildren())
|
||||||
# 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)
|
|
||||||
|
|
||||||
except PartCategory.DoesNotExist:
|
except PartCategory.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -133,6 +133,7 @@ class EditBomItemForm(HelperForm):
|
|||||||
'part',
|
'part',
|
||||||
'sub_part',
|
'sub_part',
|
||||||
'quantity',
|
'quantity',
|
||||||
|
'overage',
|
||||||
'note'
|
'note'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
46
InvenTree/part/migrations/0025_auto_20190515_0012.py
Normal file
46
InvenTree/part/migrations/0025_auto_20190515_0012.py
Normal file
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
@ -300,6 +300,23 @@ class Part(models.Model):
|
|||||||
# Default case - no default category found
|
# Default case - no default category found
|
||||||
return None
|
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',
|
default_supplier = models.ForeignKey('part.SupplierPart',
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
blank=True, null=True,
|
blank=True, null=True,
|
||||||
@ -557,6 +574,7 @@ class Part(models.Model):
|
|||||||
|
|
||||||
# Copy the part image
|
# Copy the part image
|
||||||
if kwargs.get('image', True):
|
if kwargs.get('image', True):
|
||||||
|
if other.image:
|
||||||
image_file = ContentFile(other.image.read())
|
image_file = ContentFile(other.image.read())
|
||||||
image_file.name = rename_part_image(self, 'test.png')
|
image_file.name = rename_part_image(self, 'test.png')
|
||||||
|
|
||||||
@ -661,6 +679,7 @@ class BomItem(models.Model):
|
|||||||
part: Link to the parent part (the part that will be produced)
|
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)
|
sub_part: Link to the child part (the part that will be consumed)
|
||||||
quantity: Number of 'sub_parts' consumed to produce one 'part'
|
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
|
note: Note field for this BOM item
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -688,6 +707,10 @@ class BomItem(models.Model):
|
|||||||
# Quantity required
|
# Quantity required
|
||||||
quantity = models.PositiveIntegerField(default=1, validators=[MinValueValidator(0)], help_text='BOM quantity for this BOM item')
|
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 attached to this BOM line item
|
||||||
note = models.CharField(max_length=100, blank=True, help_text='BOM item notes')
|
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,
|
child=self.sub_part.full_name,
|
||||||
n=self.quantity)
|
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):
|
class SupplierPart(models.Model):
|
||||||
""" Represents a unique part as provided by a Supplier
|
""" Represents a unique part as provided by a Supplier
|
||||||
|
@ -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')
|
|
@ -104,8 +104,6 @@ class PartStarSerializer(InvenTreeModelSerializer):
|
|||||||
class BomItemSerializer(InvenTreeModelSerializer):
|
class BomItemSerializer(InvenTreeModelSerializer):
|
||||||
""" Serializer for BomItem object """
|
""" Serializer for BomItem object """
|
||||||
|
|
||||||
# url = serializers.CharField(source='get_absolute_url', read_only=True)
|
|
||||||
|
|
||||||
part_detail = PartBriefSerializer(source='part', many=False, 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)
|
sub_part_detail = PartBriefSerializer(source='sub_part', many=False, read_only=True)
|
||||||
|
|
||||||
@ -113,12 +111,12 @@ class BomItemSerializer(InvenTreeModelSerializer):
|
|||||||
model = BomItem
|
model = BomItem
|
||||||
fields = [
|
fields = [
|
||||||
'pk',
|
'pk',
|
||||||
# 'url',
|
|
||||||
'part',
|
'part',
|
||||||
'part_detail',
|
'part_detail',
|
||||||
'sub_part',
|
'sub_part',
|
||||||
'sub_part_detail',
|
'sub_part_detail',
|
||||||
'quantity',
|
'quantity',
|
||||||
|
'overage',
|
||||||
'note',
|
'note',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -73,8 +73,22 @@
|
|||||||
{% if category %}
|
{% if category %}
|
||||||
data: {
|
data: {
|
||||||
category: {{ category.id }}
|
category: {{ category.id }}
|
||||||
}
|
},
|
||||||
{% endif %}
|
{% 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: {
|
query: {
|
||||||
{% if category %}
|
{% if category %}
|
||||||
category: {{ category.id }},
|
category: {{ category.id }},
|
||||||
include_child_categories: true,
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
},
|
},
|
||||||
buttons: ['#part-options'],
|
buttons: ['#part-options'],
|
||||||
|
@ -13,25 +13,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<div id='button-toolbar'>
|
{% include "stock_table.html" %}
|
||||||
{% if part.active %}
|
|
||||||
<button class='btn btn-success' id='add-stock-item'>New Stock Item</button>
|
|
||||||
{% endif %}
|
|
||||||
<div id='opt-dropdown' class="dropdown" style='float: right;'>
|
|
||||||
<button id='stock-options' class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">Options
|
|
||||||
<span class="caret"></span></button>
|
|
||||||
<ul class="dropdown-menu">
|
|
||||||
<li><a href='#' id='multi-item-take' title='Take items from stock'>Take items</a></li>
|
|
||||||
<li><a href='#' id='multi-item-give' title='Give items to stock'>Add items</a></li>
|
|
||||||
<li><a href="#" id='multi-item-stocktake' title='Stocktake selected stock items'>Stocktake</a></li>
|
|
||||||
<li><a href='#' id='multi-item-move' title='Move selected stock items'>Move items</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<table class='table table-striped table-condensed' data-toolbar='#button-toolbar' id='stock-table'>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@ -62,43 +44,4 @@
|
|||||||
url: "{% url 'api-stock-list' %}",
|
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 %}
|
{% endblock %}
|
@ -105,13 +105,6 @@ class PartAPITest(APITestCase):
|
|||||||
url = reverse('api-part-list')
|
url = reverse('api-part-list')
|
||||||
data = {'category': 1}
|
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
|
# Now request to include child categories
|
||||||
response = self.client.get(url, data, format='json')
|
response = self.client.get(url, data, format='json')
|
||||||
|
|
||||||
|
@ -113,6 +113,15 @@ function loadBomTable(table, options) {
|
|||||||
title: 'Required',
|
title: 'Required',
|
||||||
searchable: false,
|
searchable: false,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
|
formatter: function(value, row, index, field) {
|
||||||
|
var text = value;
|
||||||
|
|
||||||
|
if (row.overage) {
|
||||||
|
text += "<small> (+" + row.overage + ") </small>";
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -43,6 +43,7 @@ function updateStock(items, options={}) {
|
|||||||
html += '<th>Item</th>';
|
html += '<th>Item</th>';
|
||||||
html += '<th>Location</th>';
|
html += '<th>Location</th>';
|
||||||
html += '<th>Quantity</th>';
|
html += '<th>Quantity</th>';
|
||||||
|
html += '<th>' + options.action + '</th>';
|
||||||
|
|
||||||
html += '</thead><tbody>';
|
html += '</thead><tbody>';
|
||||||
|
|
||||||
@ -71,6 +72,9 @@ function updateStock(items, options={}) {
|
|||||||
} else {
|
} else {
|
||||||
html += '<td><i>No location set</i></td>';
|
html += '<td><i>No location set</i></td>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html += '<td>' + item.quantity + '</td>';
|
||||||
|
|
||||||
html += "<td><input class='form-control' ";
|
html += "<td><input class='form-control' ";
|
||||||
html += "value='" + vCur + "' ";
|
html += "value='" + vCur + "' ";
|
||||||
html += "min='" + vMin + "' ";
|
html += "min='" + vMin + "' ";
|
||||||
@ -87,8 +91,18 @@ function updateStock(items, options={}) {
|
|||||||
html += '</tbody></table>';
|
html += '</tbody></table>';
|
||||||
|
|
||||||
html += "<hr><input type='text' id='stocktake-notes' placeholder='Notes'/>";
|
html += "<hr><input type='text' id='stocktake-notes' placeholder='Notes'/>";
|
||||||
|
html += "<p class='help-inline' id='note-warning'><strong>Note field must be filled</strong></p>";
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<hr>
|
||||||
|
<div class='control-group'>
|
||||||
|
<label class='checkbox'>
|
||||||
|
<input type='checkbox' id='stocktake-confirm' placeholder='Confirm'/>
|
||||||
|
Confirm Stocktake
|
||||||
|
</label>
|
||||||
|
<p class='help-inline' id='confirm-warning'><strong>Confirm stock count</strong></p>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
html += "<p class='warning-msg' id='note-warning'><i>Note field must be filled</i></p>";
|
|
||||||
|
|
||||||
var title = '';
|
var title = '';
|
||||||
|
|
||||||
@ -109,6 +123,7 @@ function updateStock(items, options={}) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$(modal).find('#note-warning').hide();
|
$(modal).find('#note-warning').hide();
|
||||||
|
$(modal).find('#confirm-warning').hide();
|
||||||
|
|
||||||
modalEnable(modal, true);
|
modalEnable(modal, true);
|
||||||
|
|
||||||
@ -116,13 +131,23 @@ function updateStock(items, options={}) {
|
|||||||
|
|
||||||
var stocktake = [];
|
var stocktake = [];
|
||||||
var notes = $(modal).find('#stocktake-notes').val();
|
var notes = $(modal).find('#stocktake-notes').val();
|
||||||
|
var confirm = $(modal).find('#stocktake-confirm').is(':checked');
|
||||||
|
|
||||||
|
var valid = true;
|
||||||
|
|
||||||
if (!notes) {
|
if (!notes) {
|
||||||
$(modal).find('#note-warning').show();
|
$(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
|
// Form stocktake data
|
||||||
for (idx = 0; idx < items.length; idx++) {
|
for (idx = 0; idx < items.length; idx++) {
|
||||||
@ -413,6 +438,42 @@ function loadStockTable(table, options) {
|
|||||||
if (options.buttons) {
|
if (options.buttons) {
|
||||||
linkButtonsToSelection(table, 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;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,11 +7,12 @@ from django_filters import NumberFilter
|
|||||||
|
|
||||||
from django.conf.urls import url, include
|
from django.conf.urls import url, include
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.db.models import Q
|
|
||||||
|
|
||||||
from .models import StockLocation, StockItem
|
from .models import StockLocation, StockItem
|
||||||
from .models import StockItemTracking
|
from .models import StockItemTracking
|
||||||
|
|
||||||
|
from part.models import PartCategory
|
||||||
|
|
||||||
from .serializers import StockItemSerializer, StockQuantitySerializer
|
from .serializers import StockItemSerializer, StockQuantitySerializer
|
||||||
from .serializers import LocationSerializer
|
from .serializers import LocationSerializer
|
||||||
from .serializers import StockTrackingSerializer
|
from .serializers import StockTrackingSerializer
|
||||||
@ -237,16 +238,20 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
- GET: Return a list of all StockItem objects (with optional query filters)
|
- GET: Return a list of all StockItem objects (with optional query filters)
|
||||||
- POST: Create a new StockItem
|
- 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):
|
def get_queryset(self):
|
||||||
"""
|
"""
|
||||||
If the query includes a particular location,
|
If the query includes a particular location,
|
||||||
we may wish to also request stock items from all child locations.
|
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)
|
loc_id = self.request.query_params.get('location', None)
|
||||||
|
|
||||||
# Start with all objects
|
# Start with all objects
|
||||||
@ -255,23 +260,28 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
if loc_id:
|
if loc_id:
|
||||||
try:
|
try:
|
||||||
location = StockLocation.objects.get(pk=loc_id)
|
location = StockLocation.objects.get(pk=loc_id)
|
||||||
|
stock_list = stock_list.filter(location__in=location.getUniqueChildren())
|
||||||
# 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)
|
|
||||||
|
|
||||||
except StockLocation.DoesNotExist:
|
except StockLocation.DoesNotExist:
|
||||||
pass
|
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
|
return stock_list
|
||||||
|
|
||||||
serializer_class = StockItemSerializer
|
serializer_class = StockItemSerializer
|
||||||
|
@ -44,24 +44,7 @@
|
|||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<div id='button-toolbar'>
|
{% include "stock_table.html" %}
|
||||||
<div class='button-toolbar container-fluid' style='float: right;'>
|
|
||||||
<button class="btn btn-success" id='item-create'>New Stock Item</button>
|
|
||||||
<div class="dropdown" style='float: right;'>
|
|
||||||
<button id='stock-options' class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">Options<span class="caret"></span></button>
|
|
||||||
<ul class="dropdown-menu">
|
|
||||||
<li><a href="#" id='multi-item-add' title='Add to selected stock items'>Add stock</a></li>
|
|
||||||
<li><a href="#" id='multi-item-remove' title='Remove from selected stock items'>Remove stock</a></li>
|
|
||||||
<li><a href="#" id='multi-item-stocktake' title='Stocktake selected stock items'>Stocktake</a></li>
|
|
||||||
<li><a href='#' id='multi-item-move' title='Move selected stock items'>Move</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<table class='table table-striped table-condensed' data-toolbar='#button-toolbar' id='stock-table'>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
|
|
||||||
{% include 'modals.html' %}
|
{% include 'modals.html' %}
|
||||||
|
|
||||||
@ -80,7 +63,15 @@
|
|||||||
location: {{ location.id }}
|
location: {{ location.id }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
},
|
},
|
||||||
follow: true
|
follow: true,
|
||||||
|
secondary: [
|
||||||
|
{
|
||||||
|
field: 'parent',
|
||||||
|
label: 'New Location',
|
||||||
|
title: 'Create new location',
|
||||||
|
url: "{% url 'stock-location-create' %}",
|
||||||
|
},
|
||||||
|
]
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
@ -142,45 +133,6 @@
|
|||||||
return false;
|
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"), {
|
loadStockTable($("#stock-table"), {
|
||||||
buttons: [
|
buttons: [
|
||||||
'#stock-options',
|
'#stock-options',
|
||||||
@ -188,7 +140,6 @@
|
|||||||
params: {
|
params: {
|
||||||
{% if location %}
|
{% if location %}
|
||||||
location: {{ location.id }},
|
location: {{ location.id }},
|
||||||
include_child_locations: true,
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
},
|
},
|
||||||
url: "{% url 'api-stock-list' %}",
|
url: "{% url 'api-stock-list' %}",
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
{% block page_title %}
|
{% block page_title %}
|
||||||
InvenTree | Search Results
|
InvenTree | Search Results
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -19,6 +21,11 @@ InvenTree | Search Results
|
|||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js_load %}
|
||||||
|
{{ block.super }}
|
||||||
|
<script type='text/javascript' src="{% static 'script/inventree/part.js' %}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block js_ready %}
|
{% block js_ready %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<li><a href="{% url 'part-index' %}">Parts</a></li>
|
<li><a href="{% url 'part-index' %}">Parts</a></li>
|
||||||
<li><a href="{% url 'stock-index' %}">Stock</a></li>
|
<li><a href="{% url 'stock-index' %}">Stock</a></li>
|
||||||
<li><a href="{% url 'build-index' %}">Build</a></li>
|
<li><a href="{% url 'build-index' %}">Build</a></li>
|
||||||
<li><a href="{% url 'company-index' %}">Companies</a></li>
|
<li><a href="{% url 'company-index' %}">Suppliers</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="nav navbar-nav navbar-right">
|
<ul class="nav navbar-nav navbar-right">
|
||||||
{% include "search_form.html" %}
|
{% include "search_form.html" %}
|
||||||
|
17
InvenTree/templates/stock_table.html
Normal file
17
InvenTree/templates/stock_table.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<div id='button-toolbar'>
|
||||||
|
<div class='button-toolbar container-fluid' style='float: right;'>
|
||||||
|
<button class="btn btn-success" id='item-create'>New Stock Item</button>
|
||||||
|
<div class="dropdown" style='float: right;'>
|
||||||
|
<button id='stock-options' class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">Options<span class="caret"></span></button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a href="#" id='multi-item-add' title='Add to selected stock items'>Add stock</a></li>
|
||||||
|
<li><a href="#" id='multi-item-remove' title='Remove from selected stock items'>Remove stock</a></li>
|
||||||
|
<li><a href="#" id='multi-item-stocktake' title='Stocktake selected stock items'>Stocktake</a></li>
|
||||||
|
<li><a href='#' id='multi-item-move' title='Move selected stock items'>Move</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class='table table-striped table-condensed' data-toolbar='#button-toolbar' id='stock-table'>
|
||||||
|
</table>
|
Loading…
Reference in New Issue
Block a user