2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-05-03 22:08:49 +00:00

Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters 2019-05-02 21:30:34 +10:00
commit b7ac86fab4
21 changed files with 302 additions and 112 deletions

View File

@ -3,6 +3,8 @@ Provides helper functions used throughout the InvenTree project
""" """
import io import io
import json
from datetime import datetime
from wsgiref.util import FileWrapper from wsgiref.util import FileWrapper
from django.http import StreamingHttpResponse from django.http import StreamingHttpResponse
@ -44,6 +46,29 @@ def WrapWithQuotes(text, quote='"'):
return text return text
def MakeBarcode(object_type, object_id, object_url, data={}):
""" Generate a string for a barcode. Adds some global InvenTree parameters.
Args:
object_type: string describing the object type e.g. 'StockItem'
object_id: ID (Primary Key) of the object in the database
object_url: url for JSON API detail view of the object
data: Python dict object containing extra datawhich will be rendered to string (must only contain stringable values)
Returns:
json string of the supplied data plus some other data
"""
# Add in some generic InvenTree data
data['type'] = object_type
data['id'] = object_id
data['url'] = object_url
data['tool'] = 'InvenTree'
data['generated'] = str(datetime.now().date())
return json.dumps(data, sort_keys=True)
def DownloadFile(data, filename, content_type='application/text'): def DownloadFile(data, filename, content_type='application/text'):
""" Create a dynamic file for the user to download. """ Create a dynamic file for the user to download.

View File

@ -15,6 +15,8 @@ from django.views import View
from django.views.generic import UpdateView, CreateView, DeleteView from django.views.generic import UpdateView, CreateView, DeleteView
from django.views.generic.base import TemplateView from django.views.generic.base import TemplateView
from part.models import Part
from rest_framework import views from rest_framework import views
@ -287,6 +289,21 @@ class IndexView(TemplateView):
template_name = 'InvenTree/index.html' template_name = 'InvenTree/index.html'
def get_context_data(self, **kwargs):
context = super(TemplateView, self).get_context_data(**kwargs)
# Generate a list of orderable parts which have stock below their minimum values
context['to_order'] = [part for part in Part.objects.filter(purchaseable=True) if part.need_to_restock()]
# Generate a list of buildable parts which have stock below their minimum values
context['to_build'] = [part for part in Part.objects.filter(buildable=True) if part.need_to_restock()]
print("order:", len(context['to_order']))
print("build:", len(context['to_build']))
return context
class SearchView(TemplateView): class SearchView(TemplateView):
""" View for InvenTree search page. """ View for InvenTree search page.

View File

@ -11,12 +11,9 @@
<hr> <hr>
{% for bom_item in bom_items.all %} {% for bom_item in bom_items.all %}
{% include "build/allocation_item.html" with item=bom_item build=build %} {% include "build/allocation_item.html" with item=bom_item build=build collapse_id=bom_item.id %}
{% endfor %} {% endfor %}
<table class='table table-striped' id='build-table'>
</table>
<div> <div>
<button class='btn btn-warning' type='button' id='complete-build'>Complete Build</button> <button class='btn btn-warning' type='button' id='complete-build'>Complete Build</button>
</div> </div>

View File

@ -1,14 +1,12 @@
{% extends "collapse.html" %}
{% load inventree_extras %} {% load inventree_extras %}
<div class='panel-group'> {% block collapse_title %}
<div class='panel pane-default'> {{ item.sub_part.name }}
<div class='panel panel-heading'> {% endblock %}
<div class='row'>
<div class='col-sm-6'> {% block collapse_heading %}
<div class='panel-title'>
<a data-toggle='collapse' href='#collapse-item-{{ item.id }}'>{{ item.sub_part.name }}</a>
</div>
</div>
<div class='col-sm-1' align='right'> <div class='col-sm-1' align='right'>
Required: Required:
</div> </div>
@ -26,13 +24,9 @@
<button class='btn btn-success btn-sm' id='new-item-{{ item.sub_part.id }}' url="{% url 'build-item-create' %}?part={{ item.sub_part.id }}&build={{ build.id }}">Allocate Parts</button> <button class='btn btn-success btn-sm' id='new-item-{{ item.sub_part.id }}' url="{% url 'build-item-create' %}?part={{ item.sub_part.id }}&build={{ build.id }}">Allocate Parts</button>
</div> </div>
</div> </div>
</div> {% endblock %}
</div>
<div id='collapse-item-{{ item.id }}' class='panel-collapse collapse'> {% block collapse_content %}
<div class='panel-body'>
<table class='table table-striped table-condensed' id='allocate-table-id-{{ item.sub_part.id }}'> <table class='table table-striped table-condensed' id='allocate-table-id-{{ item.sub_part.id }}'>
</table> </table>
</div> {% endblock %}
</div>
</div>
</div>

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2 on 2019-05-02 10:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('part', '0013_auto_20190429_2229'),
]
operations = [
migrations.AlterField(
model_name='part',
name='URL',
field=models.URLField(blank=True, help_text='Link to extenal URL'),
),
]

View File

@ -21,6 +21,7 @@ from django.core.validators import MinValueValidator
from django.db.models.signals import pre_delete from django.db.models.signals import pre_delete
from django.dispatch import receiver from django.dispatch import receiver
from InvenTree import helpers
from InvenTree.models import InvenTreeTree from InvenTree.models import InvenTreeTree
from company.models import Company from company.models import Company
@ -179,6 +180,16 @@ class Part(models.Model):
def __str__(self): def __str__(self):
return "{n} - {d}".format(n=self.name, d=self.description) return "{n} - {d}".format(n=self.name, d=self.description)
@property
def format_barcode(self):
""" Return a JSON string for formatting a barcode for this Part object """
return helpers.MakeBarcode(
"Part",
self.id,
reverse('api-part-detail', kwargs={'pk': self.id}),
)
class Meta: class Meta:
verbose_name = "Part" verbose_name = "Part"
verbose_name_plural = "Parts" verbose_name_plural = "Parts"
@ -193,14 +204,25 @@ class Part(models.Model):
def available_stock(self): def available_stock(self):
""" """
Return the total available stock. Return the total available stock.
This subtracts stock which is already allocated
- This subtracts stock which is already allocated to builds
""" """
total = self.total_stock total = self.total_stock
total -= self.allocation_count total -= self.allocation_count
return max(total, 0) return total
def need_to_restock(self):
""" Return True if this part needs to be restocked
(either by purchasing or building).
If the allocated_stock exceeds the total_stock,
then we need to restock.
"""
return (self.total_stock - self.allocation_count) < self.minimum_stock
@property @property
def can_build(self): def can_build(self):
@ -307,6 +329,12 @@ class Part(models.Model):
def used_in_count(self): def used_in_count(self):
return self.used_in.count() return self.used_in.count()
def required_parts(self):
parts = []
for bom in self.bom_items.all():
parts.append(bom.sub_part)
return parts
@property @property
def supplier_count(self): def supplier_count(self):
# Return the number of supplier parts available for this part # Return the number of supplier parts available for this part

View File

@ -33,9 +33,9 @@
</div> </div>
</div> </div>
{% if category %} {% if category and category.children.all|length > 0 %}
{% include "part/subcategories.html" with children=category.children.all %} {% include "part/subcategories.html" with children=category.children.all collapse_id="children"%}
{% else %} {% elif children|length > 0 %}
{% include "part/subcategories.html" with children=children %} {% include "part/subcategories.html" with children=children %}
{% endif %} {% endif %}
<hr> <hr>

View File

@ -1,5 +1,6 @@
{% extends "part/part_base.html" %} {% extends "part/part_base.html" %}
{% load static %} {% load static %}
{% load qr_code %}
{% block details %} {% block details %}
{% include 'part/tabs.html' with tab='detail' %} {% include 'part/tabs.html' with tab='detail' %}
@ -115,6 +116,8 @@
</div> </div>
{% endif %} {% endif %}
{% qr_from_text part.format_barcode size="s" image_format="png" error_correction="L" %}
{% endblock %} {% endblock %}
{% block js_load %} {% block js_load %}

View File

@ -1,14 +1,10 @@
{% if children|length > 0 %} {% extends "collapse.html" %}
<hr>
<div class="panel-group"> {% block collapse_title %}
<div class="panel panel-default"> {{ children | length }} Child Categories
<div class="panel-heading"> {% endblock %}
<h4 class="panel-title">
<a data-toggle="collapse" href="#collapse1">{{ children | length }} Child Categories</a> {% block collapse_content %}
</h4>
</div>
<div id="collapse1" class="panel-collapse collapse">
<div class="panel-body">
<ul class="list-group"> <ul class="list-group">
{% for child in children %} {% for child in children %}
<li class="list-group-item"> <li class="list-group-item">
@ -20,8 +16,4 @@
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> {% endblock %}
</div>
</div>
</div>
{% endif %}

View File

@ -272,7 +272,6 @@ class StockList(generics.ListCreateAPIView):
filter_fields = [ filter_fields = [
'part', 'part',
'uuid',
'supplier_part', 'supplier_part',
'customer', 'customer',
'belongs_to', 'belongs_to',
@ -346,11 +345,11 @@ class LocationDetail(generics.RetrieveUpdateDestroyAPIView):
stock_endpoints = [ stock_endpoints = [
url(r'^$', StockDetail.as_view(), name='stockitem-detail'), url(r'^$', StockDetail.as_view(), name='api-stock-detail'),
] ]
location_endpoints = [ location_endpoints = [
url(r'^$', LocationDetail.as_view(), name='stocklocation-detail'), url(r'^$', LocationDetail.as_view(), name='api-location-detail'),
] ]
stock_api_urls = [ stock_api_urls = [

View File

@ -0,0 +1,17 @@
# Generated by Django 2.2 on 2019-05-02 10:39
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('stock', '0012_auto_20190502_0058'),
]
operations = [
migrations.RemoveField(
model_name='stockitem',
name='uuid',
),
]

View File

@ -17,7 +17,7 @@ from django.db.models.signals import pre_delete
from django.dispatch import receiver from django.dispatch import receiver
from datetime import datetime from datetime import datetime
import uuid from InvenTree import helpers
from InvenTree.models import InvenTreeTree from InvenTree.models import InvenTreeTree
@ -36,6 +36,19 @@ class StockLocation(InvenTreeTree):
def has_items(self): def has_items(self):
return self.stock_items.count() > 0 return self.stock_items.count() > 0
@property
def format_barcode(self):
""" Return a JSON string for formatting a barcode for this StockLocation object """
return helpers.MakeBarcode(
'StockLocation',
self.id,
reverse('api-location-detail', kwargs={'pk': self.id}),
{
'name': self.name,
}
)
@receiver(pre_delete, sender=StockLocation, dispatch_uid='stocklocation_delete_log') @receiver(pre_delete, sender=StockLocation, dispatch_uid='stocklocation_delete_log')
def before_delete_stock_location(sender, instance, using, **kwargs): def before_delete_stock_location(sender, instance, using, **kwargs):
@ -126,8 +139,27 @@ class StockItem(models.Model):
('part', 'serial'), ('part', 'serial'),
] ]
# UUID for generating QR codes @property
uuid = models.UUIDField(default=uuid.uuid4, blank=True, editable=False, help_text='Unique ID for the StockItem') def format_barcode(self):
""" Return a JSON string for formatting a barcode for this StockItem.
Can be used to perform lookup of a stockitem using barcode
Contains the following data:
{ type: 'StockItem', stock_id: <pk>, part_id: <part_pk> }
Voltagile data (e.g. stock quantity) should be looked up using the InvenTree API (as it may change)
"""
return helpers.MakeBarcode(
'StockItem',
self.id,
reverse('api-stock-detail', kwargs={'pk': self.id}),
{
'part_id': self.part.id,
'part_name': self.part.name
}
)
# The 'master' copy of the part of which this stock item is an instance # The 'master' copy of the part of which this stock item is an instance
part = models.ForeignKey('part.Part', on_delete=models.CASCADE, related_name='locations', help_text='Base part') part = models.ForeignKey('part.Part', on_delete=models.CASCADE, related_name='locations', help_text='Base part')

View File

@ -38,7 +38,6 @@ class StockItemSerializerBrief(serializers.ModelSerializer):
model = StockItem model = StockItem
fields = [ fields = [
'pk', 'pk',
'uuid',
'part', 'part',
'part_name', 'part_name',
'supplier_part', 'supplier_part',
@ -65,7 +64,6 @@ class StockItemSerializer(serializers.ModelSerializer):
model = StockItem model = StockItem
fields = [ fields = [
'pk', 'pk',
'uuid',
'url', 'url',
'part', 'part',
'supplier_part', 'supplier_part',

View File

@ -39,10 +39,6 @@
<td>Part</td> <td>Part</td>
<td><a href="{% url 'part-stock' item.part.id %}">{{ item.part.name }}</td> <td><a href="{% url 'part-stock' item.part.id %}">{{ item.part.name }}</td>
</tr> </tr>
<tr>
<td>UUID</td>
<td>{{ item.uuid }}</td>
</tr>
{% if item.belongs_to %} {% if item.belongs_to %}
<tr> <tr>
<td>Belongs To</td> <td>Belongs To</td>
@ -114,7 +110,7 @@
</table> </table>
</div> </div>
<div class='col-sm-6'> <div class='col-sm-6'>
{% qr_from_text item.uuid size="s" image_format="png" error_correction="L" %} {% qr_from_text item.format_barcode size="s" image_format="png" error_correction="L" %}
</div> </div>
</div> </div>

View File

@ -1,5 +1,6 @@
{% extends "stock/stock_app_base.html" %} {% extends "stock/stock_app_base.html" %}
{% load static %} {% load static %}
{% load qr_code %}
{% block content %} {% block content %}
<div class='row'> <div class='row'>
@ -25,16 +26,17 @@
<li><a href="#" id='location-delete' title='Delete stock location'>Delete</a></li> <li><a href="#" id='location-delete' title='Delete stock location'>Delete</a></li>
</ul> </ul>
</div> </div>
{% qr_from_text location.format_barcode size="s" image_format="png" error_correction="L" %}
{% endif %} {% endif %}
</div> </div>
</h3> </h3>
</div> </div>
</div> </div>
{% if location %} {% if location and location.children.all|length > 0 %}
{% include 'stock/location_list.html' with children=location.children.all %} {% include 'stock/location_list.html' with children=location.children.all collapse_id="locations" %}
{% else %} {% elif locations|length > 0 %}
{% include 'stock/location_list.html' with children=locations %} {% include 'stock/location_list.html' with children=locations collapse_id="locations" %}
{% endif %} {% endif %}
<hr> <hr>

View File

@ -1,14 +1,10 @@
{% if children|length > 0 %} {% extends "collapse.html" %}
<hr>
<div class="panel-group"> {% block collapse_title %}
<div class="panel panel-default"> Sub-Locations<span class='badge'>{{ children|length }}</span>
<div class="panel-heading"> {% endblock %}
<h4 class="panel-title">
<a data-toggle="collapse" href="#collapse1">Sub-Locations</a><span class='badge'>{{ children|length }}</span> {% block collapse_content %}
</h4>
</div>
<div id="collapse1" class="panel-collapse collapse">
<div class="panel-body">
<ul class="list-group"> <ul class="list-group">
{% for child in children %} {% 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> <li class="list-group-item"><a href="{% url 'stock-location-detail' child.id %}">{{ child.name }}</a> - <i>{{ child.description }}</i></li>
@ -16,8 +12,4 @@
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> {% endblock %}
</div>
</div>
</div>
{% endif %}

View File

@ -3,9 +3,20 @@
{% block content %} {% block content %}
<h3>InvenTree</h3> <h3>InvenTree</h3>
<p>Index!</p> {% if to_order %}
{% include "InvenTree/parts_to_order.html" with collapse_id="order" %}
{% endif %}
{% if to_build %}
{% include "InvenTree/parts_to_build.html" with collapse_id="build" %}
{% endif %}
{% endblock %} {% endblock %}
{% block js_load %} {% block js_load %}
{{ block.super }}
{% endblock %}
{% block js_ready %}
{{ block.super }}
{% endblock %} {% endblock %}

View File

@ -0,0 +1,14 @@
{% extends "collapse.html" %}
{% block collapse_title %}
Parts to Build<span class='badge'>{{ to_build | length }}</span>
{% endblock %}
{% block collapse_heading %}
There are {{ to_build | length }} parts which need building.
{% endblock %}
{% block collapse_content %}
{% include "required_part_table.html" with parts=to_build table_id="to-build-table" %}
{% endblock %}

View File

@ -0,0 +1,14 @@
{% extends "collapse.html" %}
{% block collapse_title %}
Parts to Order<span class='badge'>{{ to_order | length }}</span>
{% endblock %}
{% block collapse_heading %}
There are {{ to_order | length }} parts which need to be ordered.
{% endblock %}
{% block collapse_content %}
{% include "required_part_table.html" with parts=to_order table_id="to-order-table" %}
{% endblock %}

View File

@ -0,0 +1,23 @@
{% block collapse_preamble %}
{% endblock %}
<div class='panel-group'>
<div class='panel panel-default'>
<div class='panel panel-heading'>
<div class='row'>
<div class='col-sm-6'>
<div class='panel-title'>
<a data-toggle='collapse' href="#collapse-item-{{ collapse_id }}">{% block collapse_title %}Title{% endblock %}</a>
</div>
</div>
{% block collapse_heading %}
{% endblock %}
</div>
</div>
<div class='panel-collapse collapse' id='collapse-item-{{ collapse_id }}'>
<div class='panel-body'>
{% block collapse_content %}
{% endblock %}
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,18 @@
<table class='table table-striped table-condensed' id='{{ table_id }}'>
<tr>
<th>Part</th>
<th>Description</th>
<th>In Stock</th>
<th>Allocated</th>
<th>Net Stock</th>
</tr>
{% for part in parts %}
<tr>
<td><a href="{% url 'part-detail' part.id %}">{{ part.name }}</a></td>
<td>{{ part.description }}</td>
<td>{{ part.total_stock }}</td>
<td>{{ part.allocation_count }}</td>
<td>{{ part.available_stock }}</td>
</tr>
{% endfor %}
</table>