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
d944e7916e
@ -304,11 +304,11 @@
|
|||||||
|
|
||||||
.rowinvalid {
|
.rowinvalid {
|
||||||
color: #A00;
|
color: #A00;
|
||||||
font-style: italic;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.rowinherited {
|
.rowinherited {
|
||||||
background-color: #dde;
|
background-color: #eee;
|
||||||
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown {
|
.dropdown {
|
||||||
|
@ -379,24 +379,32 @@ class Build(MPTTModel):
|
|||||||
if cls.objects.count() == 0:
|
if cls.objects.count() == 0:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
build = cls.objects.last()
|
# Extract the "most recent" build order reference
|
||||||
|
builds = cls.objects.exclude(reference=None)
|
||||||
|
|
||||||
|
if not builds.exists():
|
||||||
|
return None
|
||||||
|
|
||||||
|
build = builds.last()
|
||||||
ref = build.reference
|
ref = build.reference
|
||||||
|
|
||||||
if not ref:
|
if not ref:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
tries = set()
|
tries = set(ref)
|
||||||
|
|
||||||
|
new_ref = ref
|
||||||
|
|
||||||
while 1:
|
while 1:
|
||||||
new_ref = increment(ref)
|
new_ref = increment(new_ref)
|
||||||
|
|
||||||
if new_ref in tries:
|
if new_ref in tries:
|
||||||
# We are potentially stuck in a loop - simply return the original reference
|
# We are potentially stuck in a loop - simply return the original reference
|
||||||
return ref
|
return ref
|
||||||
|
|
||||||
|
# Check if the existing build reference exists
|
||||||
if cls.objects.filter(reference=new_ref).exists():
|
if cls.objects.filter(reference=new_ref).exists():
|
||||||
tries.add(new_ref)
|
tries.add(new_ref)
|
||||||
new_ref = increment(new_ref)
|
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
{% extends "build/build_base.html" %}
|
{% extends "build/build_base.html" %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% block details %}
|
|
||||||
{% load status_codes %}
|
{% load status_codes %}
|
||||||
|
|
||||||
|
{% block details %}
|
||||||
|
|
||||||
{% include "build/tabs.html" with tab='details' %}
|
{% include "build/tabs.html" with tab='details' %}
|
||||||
|
|
||||||
<h4>{% trans "Build Details" %}</h4>
|
<h4>{% trans "Build Details" %}</h4>
|
||||||
|
28
InvenTree/build/templates/build/parts.html
Normal file
28
InvenTree/build/templates/build/parts.html
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{% extends "build/build_base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load status_codes %}
|
||||||
|
|
||||||
|
{% block details %}
|
||||||
|
|
||||||
|
{% include "build/tabs.html" with tab='parts' %}
|
||||||
|
|
||||||
|
<h4>{% trans "Build Parts" %}</h4>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<table class='table table-striped table-condensed' id='parts-table'></table>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js_ready %}
|
||||||
|
|
||||||
|
{{ block.super }}
|
||||||
|
|
||||||
|
loadBuildPartsTable($('#parts-table'), {
|
||||||
|
part: {{ build.part.pk }},
|
||||||
|
build: {{ build.pk }},
|
||||||
|
build_quantity: {{ build.quantity }},
|
||||||
|
build_remaining: {{ build.remaining }},
|
||||||
|
});
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -5,16 +5,22 @@
|
|||||||
<a href="{% url 'build-detail' build.id %}">{% trans "Details" %}</a>
|
<a href="{% url 'build-detail' build.id %}">{% trans "Details" %}</a>
|
||||||
</li>
|
</li>
|
||||||
{% if build.active %}
|
{% if build.active %}
|
||||||
|
<li {% if tab == 'parts' %} class='active'{% endif %}>
|
||||||
|
<a href='{% url "build-parts" build.id %}'>
|
||||||
|
{% trans "Required Parts" %}
|
||||||
|
<span class='badge'>{{ build.part.bom_count }}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li{% if tab == 'allocate' %} class='active'{% endif %}>
|
<li{% if tab == 'allocate' %} class='active'{% endif %}>
|
||||||
<a href="{% url 'build-allocate' build.id %}">
|
<a href="{% url 'build-allocate' build.id %}">
|
||||||
{% trans "Incomplete" %}
|
{% trans "In Progress" %}
|
||||||
<span class='badge'>{{ build.incomplete_outputs.count }}</span>
|
<span class='badge'>{{ build.incomplete_outputs.count }}</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li{% if tab == 'output' %} class='active'{% endif %}>
|
<li{% if tab == 'output' %} class='active'{% endif %}>
|
||||||
<a href="{% url 'build-output' build.id %}">
|
<a href="{% url 'build-output' build.id %}">
|
||||||
{% trans "Build Outputs" %}
|
{% trans "Completed Outputs" %}
|
||||||
<span class='badge'>{{ build.output_count }}</span>
|
<span class='badge'>{{ build.output_count }}</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -20,6 +20,7 @@ build_detail_urls = [
|
|||||||
|
|
||||||
url(r'^notes/', views.BuildNotes.as_view(), name='build-notes'),
|
url(r'^notes/', views.BuildNotes.as_view(), name='build-notes'),
|
||||||
|
|
||||||
|
url(r'^parts/', views.BuildDetail.as_view(template_name='build/parts.html'), name='build-parts'),
|
||||||
url(r'^attachments/', views.BuildDetail.as_view(template_name='build/attachments.html'), name='build-attachments'),
|
url(r'^attachments/', views.BuildDetail.as_view(template_name='build/attachments.html'), name='build-attachments'),
|
||||||
url(r'^output/', views.BuildDetail.as_view(template_name='build/build_output.html'), name='build-output'),
|
url(r'^output/', views.BuildDetail.as_view(template_name='build/build_output.html'), name='build-output'),
|
||||||
|
|
||||||
|
@ -158,6 +158,14 @@ $("#po-create").click(function() {
|
|||||||
launchModalForm("{% url 'po-create' %}",
|
launchModalForm("{% url 'po-create' %}",
|
||||||
{
|
{
|
||||||
follow: true,
|
follow: true,
|
||||||
|
secondary: [
|
||||||
|
{
|
||||||
|
field: 'supplier',
|
||||||
|
label: '{% trans "New Supplier" %}',
|
||||||
|
title: '{% trans "Create new Supplier" %}',
|
||||||
|
url: '{% url "supplier-create" %}',
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -465,6 +465,18 @@ class PartList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
queryset = super().filter_queryset(queryset)
|
queryset = super().filter_queryset(queryset)
|
||||||
|
|
||||||
|
# Filter by "uses" query - Limit to parts which use the provided part
|
||||||
|
uses = params.get('uses', None)
|
||||||
|
|
||||||
|
if uses:
|
||||||
|
try:
|
||||||
|
uses = Part.objects.get(pk=uses)
|
||||||
|
|
||||||
|
queryset = queryset.filter(uses.get_used_in_filter())
|
||||||
|
|
||||||
|
except (ValueError, Part.DoesNotExist):
|
||||||
|
pass
|
||||||
|
|
||||||
# Filter by 'ancestor'?
|
# Filter by 'ancestor'?
|
||||||
ancestor = params.get('ancestor', None)
|
ancestor = params.get('ancestor', None)
|
||||||
|
|
||||||
@ -839,12 +851,6 @@ class BomList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
except (ValueError, Part.DoesNotExist):
|
except (ValueError, Part.DoesNotExist):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Filter by sub-part?
|
|
||||||
sub_part = params.get('sub_part', None)
|
|
||||||
|
|
||||||
if sub_part is not None:
|
|
||||||
queryset = queryset.filter(sub_part=sub_part)
|
|
||||||
|
|
||||||
# Filter by "active" status of the part
|
# Filter by "active" status of the part
|
||||||
part_active = params.get('part_active', None)
|
part_active = params.get('part_active', None)
|
||||||
|
@ -643,7 +643,7 @@ class Part(MPTTModel):
|
|||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
if self.trackable:
|
if self.trackable:
|
||||||
for item in self.used_in.all():
|
for item in self.get_used_in().all():
|
||||||
parent_part = item.part
|
parent_part = item.part
|
||||||
if not parent_part.trackable:
|
if not parent_part.trackable:
|
||||||
parent_part.trackable = True
|
parent_part.trackable = True
|
||||||
@ -891,10 +891,10 @@ class Part(MPTTModel):
|
|||||||
Return list of outstanding build orders which require this part
|
Return list of outstanding build orders which require this part
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# List of BOM that this part is required for
|
# List parts that this part is required for
|
||||||
boms = BomItem.objects.filter(sub_part=self)
|
parts = self.get_used_in().all()
|
||||||
|
|
||||||
part_ids = [bom.part.pk for bom in boms]
|
part_ids = [part.pk for part in parts]
|
||||||
|
|
||||||
# Now, get a list of outstanding build orders which require this part
|
# Now, get a list of outstanding build orders which require this part
|
||||||
builds = BuildModels.Build.objects.filter(
|
builds = BuildModels.Build.objects.filter(
|
||||||
@ -909,36 +909,24 @@ class Part(MPTTModel):
|
|||||||
Return the quantity of this part required for active build orders
|
Return the quantity of this part required for active build orders
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# List of BOM that this part is required for
|
# List active build orders which reference this part
|
||||||
boms = BomItem.objects.filter(sub_part=self)
|
builds = self.requiring_build_orders()
|
||||||
|
|
||||||
part_ids = [bom.part.pk for bom in boms]
|
|
||||||
|
|
||||||
# Now, get a list of outstanding build orders which require this part
|
|
||||||
builds = BuildModels.Build.objects.filter(
|
|
||||||
part__in=part_ids,
|
|
||||||
status__in=BuildStatus.ACTIVE_CODES
|
|
||||||
)
|
|
||||||
|
|
||||||
quantity = 0
|
quantity = 0
|
||||||
|
|
||||||
for build in builds:
|
for build in builds:
|
||||||
|
|
||||||
bom_item = None
|
bom_item = None
|
||||||
|
|
||||||
|
# List the bom lines required to make the build (including inherited ones!)
|
||||||
|
bom_items = build.part.get_bom_items().filter(sub_part=self)
|
||||||
|
|
||||||
# Match BOM item to build
|
# Match BOM item to build
|
||||||
for bom in boms:
|
for bom_item in bom_items:
|
||||||
if bom.part == build.part:
|
|
||||||
bom_item = bom
|
|
||||||
break
|
|
||||||
|
|
||||||
if bom_item is None:
|
build_quantity = build.quantity * bom_item.quantity
|
||||||
logger.warning("Found null BomItem when calculating required quantity")
|
|
||||||
continue
|
|
||||||
|
|
||||||
build_quantity = build.quantity * bom_item.quantity
|
quantity += build_quantity
|
||||||
|
|
||||||
quantity += build_quantity
|
|
||||||
|
|
||||||
return quantity
|
return quantity
|
||||||
|
|
||||||
@ -1240,6 +1228,54 @@ class Part(MPTTModel):
|
|||||||
|
|
||||||
return BomItem.objects.filter(self.get_bom_item_filter(include_inherited=include_inherited))
|
return BomItem.objects.filter(self.get_bom_item_filter(include_inherited=include_inherited))
|
||||||
|
|
||||||
|
def get_used_in_filter(self, include_inherited=True):
|
||||||
|
"""
|
||||||
|
Return a query filter for all parts that this part is used in.
|
||||||
|
|
||||||
|
There are some considerations:
|
||||||
|
|
||||||
|
a) This part may be directly specified against a BOM for a part
|
||||||
|
b) This part may be specifed in a BOM which is then inherited by another part
|
||||||
|
|
||||||
|
Note: This function returns a Q object, not an actual queryset.
|
||||||
|
The Q object is used to filter against a list of Part objects
|
||||||
|
"""
|
||||||
|
|
||||||
|
# This is pretty expensive - we need to traverse multiple variant lists!
|
||||||
|
# TODO - In the future, could this be improved somehow?
|
||||||
|
|
||||||
|
# Keep a set of Part ID values
|
||||||
|
parts = set()
|
||||||
|
|
||||||
|
# First, grab a list of all BomItem objects which "require" this part
|
||||||
|
bom_items = BomItem.objects.filter(sub_part=self)
|
||||||
|
|
||||||
|
for bom_item in bom_items:
|
||||||
|
|
||||||
|
# Add the directly referenced part
|
||||||
|
parts.add(bom_item.part)
|
||||||
|
|
||||||
|
# Traverse down the variant tree?
|
||||||
|
if include_inherited and bom_item.inherited:
|
||||||
|
|
||||||
|
part_variants = bom_item.part.get_descendants(include_self=False)
|
||||||
|
|
||||||
|
for variant in part_variants:
|
||||||
|
parts.add(variant)
|
||||||
|
|
||||||
|
# Turn into a list of valid IDs (for matching against a Part query)
|
||||||
|
part_ids = [part.pk for part in parts]
|
||||||
|
|
||||||
|
return Q(id__in=part_ids)
|
||||||
|
|
||||||
|
def get_used_in(self, include_inherited=True):
|
||||||
|
"""
|
||||||
|
Return a queryset containing all parts this part is used in.
|
||||||
|
|
||||||
|
Includes consideration of inherited BOMs
|
||||||
|
"""
|
||||||
|
return Part.objects.filter(self.get_used_in_filter(include_inherited=include_inherited))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_bom(self):
|
def has_bom(self):
|
||||||
return self.get_bom_items().count() > 0
|
return self.get_bom_items().count() > 0
|
||||||
@ -1265,7 +1301,7 @@ class Part(MPTTModel):
|
|||||||
@property
|
@property
|
||||||
def used_in_count(self):
|
def used_in_count(self):
|
||||||
""" Return the number of part BOMs that this part appears in """
|
""" Return the number of part BOMs that this part appears in """
|
||||||
return self.used_in.count()
|
return self.get_used_in().count()
|
||||||
|
|
||||||
def get_bom_hash(self):
|
def get_bom_hash(self):
|
||||||
""" Return a checksum hash for the BOM for this part.
|
""" Return a checksum hash for the BOM for this part.
|
||||||
@ -1364,7 +1400,7 @@ class Part(MPTTModel):
|
|||||||
parts = parts.exclude(id=self.id)
|
parts = parts.exclude(id=self.id)
|
||||||
|
|
||||||
# Exclude any parts that this part is used *in* (to prevent recursive BOMs)
|
# Exclude any parts that this part is used *in* (to prevent recursive BOMs)
|
||||||
used_in = self.used_in.all()
|
used_in = self.get_used_in().all()
|
||||||
|
|
||||||
parts = parts.exclude(id__in=[item.part.id for item in used_in])
|
parts = parts.exclude(id__in=[item.part.id for item in used_in])
|
||||||
|
|
||||||
@ -1524,7 +1560,7 @@ class Part(MPTTModel):
|
|||||||
|
|
||||||
# Copy existing BOM items from another part
|
# Copy existing BOM items from another part
|
||||||
# Note: Inherited BOM Items will *not* be duplicated!!
|
# Note: Inherited BOM Items will *not* be duplicated!!
|
||||||
for bom_item in other.bom_items.all():
|
for bom_item in other.get_bom_items(include_inherited=False).all():
|
||||||
# If this part already has a BomItem pointing to the same sub-part,
|
# If this part already has a BomItem pointing to the same sub-part,
|
||||||
# delete that BomItem from this part first!
|
# delete that BomItem from this part first!
|
||||||
|
|
||||||
@ -2094,6 +2130,8 @@ class BomItem(models.Model):
|
|||||||
- Quantity
|
- Quantity
|
||||||
- Reference field
|
- Reference field
|
||||||
- Note field
|
- Note field
|
||||||
|
- Optional field
|
||||||
|
- Inherited field
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -2106,6 +2144,8 @@ class BomItem(models.Model):
|
|||||||
hash.update(str(self.quantity).encode())
|
hash.update(str(self.quantity).encode())
|
||||||
hash.update(str(self.note).encode())
|
hash.update(str(self.note).encode())
|
||||||
hash.update(str(self.reference).encode())
|
hash.update(str(self.reference).encode())
|
||||||
|
hash.update(str(self.optional).encode())
|
||||||
|
hash.update(str(self.inherited).encode())
|
||||||
|
|
||||||
return str(hash.digest())
|
return str(hash.digest())
|
||||||
|
|
||||||
|
@ -77,19 +77,6 @@
|
|||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js_load %}
|
|
||||||
{{ block.super }}
|
|
||||||
|
|
||||||
<!-- jquery-treegrid -->
|
|
||||||
<script type='text/javascript' src='{% static "treegrid/js/jquery.treegrid.js" %}'></script>
|
|
||||||
<script type='text/javascript' src='{% static "treegrid/js/jquery.treegrid.bootstrap3.js" %}'></script>
|
|
||||||
|
|
||||||
<!-- boostrap-table-treegrid -->
|
|
||||||
<script type='text/javascript' src='{% static "bootstrap-table/extensions/treegrid/bootstrap-table-treegrid.js" %}'></script>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
|
|
||||||
{% block js_ready %}
|
{% block js_ready %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
|
||||||
|
@ -133,11 +133,18 @@
|
|||||||
<td>{% decimal on_order %}</td>
|
<td>{% decimal on_order %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if required > 0 %}
|
{% if required_build_order_quantity > 0 %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-clipboard-list'></span></td>
|
<td><span class='fas fa-clipboard-list'></span></td>
|
||||||
<td>{% trans "Required for Orders" %}</td>
|
<td>{% trans "Required for Build Orders" %}</td>
|
||||||
<td>{% decimal required %}
|
<td>{% decimal required_build_order_quantity %}
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% if required_sales_order_quantity > 0 %}
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-clipboard-list'></span></td>
|
||||||
|
<td>{% trans "Required for Sales Orders" %}</td>
|
||||||
|
<td>{% decimal required_sales_order_quantity %}
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if allocated > 0 %}
|
{% if allocated > 0 %}
|
||||||
|
@ -22,10 +22,14 @@
|
|||||||
{% block js_ready %}
|
{% block js_ready %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
|
||||||
loadUsedInTable('#used-table', {
|
loadSimplePartTable('#used-table',
|
||||||
part_detail: true,
|
'{% url "api-part-list" %}',
|
||||||
part_id: {{ part.pk }}
|
{
|
||||||
});
|
params: {
|
||||||
|
uses: {{ part.pk }},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -107,6 +107,13 @@ InvenTree
|
|||||||
<script type='text/javascript' src="{% static 'script/bootstrap/bootstrap-table-filter-control.js' %}"></script>
|
<script type='text/javascript' src="{% static 'script/bootstrap/bootstrap-table-filter-control.js' %}"></script>
|
||||||
<!-- <script type='text/javascript' src="{% static 'script/bootstrap/filter-control-utils.js' %}"></script> -->
|
<!-- <script type='text/javascript' src="{% static 'script/bootstrap/filter-control-utils.js' %}"></script> -->
|
||||||
|
|
||||||
|
<!-- jquery-treegrid -->
|
||||||
|
<script type='text/javascript' src='{% static "treegrid/js/jquery.treegrid.js" %}'></script>
|
||||||
|
<script type='text/javascript' src='{% static "treegrid/js/jquery.treegrid.bootstrap3.js" %}'></script>
|
||||||
|
|
||||||
|
<!-- boostrap-table-treegrid -->
|
||||||
|
<script type='text/javascript' src='{% static "bootstrap-table/extensions/treegrid/bootstrap-table-treegrid.js" %}'></script>
|
||||||
|
|
||||||
<script type="text/javascript" src="{% static 'fullcalendar/main.js' %}"></script>
|
<script type="text/javascript" src="{% static 'fullcalendar/main.js' %}"></script>
|
||||||
<script type="text/javascript" src="{% static 'script/select2/select2.js' %}"></script>
|
<script type="text/javascript" src="{% static 'script/select2/select2.js' %}"></script>
|
||||||
<script type='text/javascript' src="{% static 'script/moment.js' %}"></script>
|
<script type='text/javascript' src="{% static 'script/moment.js' %}"></script>
|
||||||
|
@ -539,85 +539,3 @@ function loadBomTable(table, options) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadUsedInTable(table, options) {
|
|
||||||
/* Load a table which displays all the parts that the given part is used in.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var params = {
|
|
||||||
sub_part: options.part_id,
|
|
||||||
ordering: 'name',
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.part_detail) {
|
|
||||||
params.part_detail = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.sub_part_detail) {
|
|
||||||
params.sub_part_detail = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var filters = {};
|
|
||||||
|
|
||||||
if (!options.disableFilters) {
|
|
||||||
filters = loadTableFilters("usedin");
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var key in params) {
|
|
||||||
filters[key] = params[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
setupFilterList("usedin", $(table));
|
|
||||||
|
|
||||||
// Columns to display in the table
|
|
||||||
var cols = [
|
|
||||||
{
|
|
||||||
field: 'pk',
|
|
||||||
title: 'ID',
|
|
||||||
visible: false,
|
|
||||||
switchable: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'part_detail.full_name',
|
|
||||||
title: '{% trans "Part" %}',
|
|
||||||
sortable: true,
|
|
||||||
formatter: function(value, row, index, field) {
|
|
||||||
var link = `/part/${row.part}/bom/`;
|
|
||||||
var html = imageHoverIcon(row.part_detail.thumbnail) + renderLink(row.part_detail.full_name, link);
|
|
||||||
|
|
||||||
if (!row.part_detail.active) {
|
|
||||||
html += "<span class='label label-warning' style='float: right;'>{% trans 'INACTIVE' %}</span>";
|
|
||||||
}
|
|
||||||
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'part_detail.description',
|
|
||||||
title: '{% trans "Description" %}',
|
|
||||||
sortable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sortable: true,
|
|
||||||
field: 'quantity',
|
|
||||||
title: '{% trans "Uses" %}',
|
|
||||||
formatter: function(value, row, index, field) {
|
|
||||||
return parseFloat(value);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// Load the table
|
|
||||||
$(table).inventreeTable({
|
|
||||||
url: "{% url 'api-bom-list' %}",
|
|
||||||
formatNoMatches: function() {
|
|
||||||
return '{% trans "No matching parts found" %}';
|
|
||||||
},
|
|
||||||
columns: cols,
|
|
||||||
showColumns: true,
|
|
||||||
sortable: true,
|
|
||||||
serach: true,
|
|
||||||
queryParams: filters,
|
|
||||||
original: params,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
@ -833,4 +833,200 @@ function loadAllocationTable(table, part_id, part, url, required, button) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function loadBuildPartsTable(table, options={}) {
|
||||||
|
/**
|
||||||
|
* Display a "required parts" table for build view.
|
||||||
|
*
|
||||||
|
* This is a simplified BOM view:
|
||||||
|
* - Does not display sub-bom items
|
||||||
|
* - Does not allow editing of BOM items
|
||||||
|
*
|
||||||
|
* Options:
|
||||||
|
*
|
||||||
|
* part: Part ID
|
||||||
|
* build: Build ID
|
||||||
|
* build_quantity: Total build quantity
|
||||||
|
* build_remaining: Number of items remaining
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Query params
|
||||||
|
var params = {
|
||||||
|
sub_part_detail: true,
|
||||||
|
part: options.part,
|
||||||
|
};
|
||||||
|
|
||||||
|
var filters = {};
|
||||||
|
|
||||||
|
if (!options.disableFilters) {
|
||||||
|
filters = loadTableFilters('bom');
|
||||||
|
}
|
||||||
|
|
||||||
|
setupFilterList('bom', $(table));
|
||||||
|
|
||||||
|
for (var key in params) {
|
||||||
|
filters[key] = params[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupTableCallbacks() {
|
||||||
|
// Register button callbacks once the table data are loaded
|
||||||
|
|
||||||
|
// Callback for 'buy' button
|
||||||
|
$(table).find('.button-buy').click(function() {
|
||||||
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
|
var idx = $(this).closest('tr').attr('data-index');
|
||||||
|
var row = $(table).bootstrapTable('getData')[idx];
|
||||||
|
|
||||||
|
launchModalForm('{% url "order-parts" %}', {
|
||||||
|
data: {
|
||||||
|
parts: [
|
||||||
|
pk,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Callback for 'build' button
|
||||||
|
$(table).find('.button-build').click(function() {
|
||||||
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
|
// Extract row data from the table
|
||||||
|
var idx = $(this).closest('tr').attr('data-index');
|
||||||
|
var row = $(table).bootstrapTable('getData')[idx];
|
||||||
|
|
||||||
|
// Launch form to create a new build order
|
||||||
|
launchModalForm('{% url "build-create" %}', {
|
||||||
|
follow: true,
|
||||||
|
data: {
|
||||||
|
part: pk,
|
||||||
|
parent: options.build,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var columns = [
|
||||||
|
{
|
||||||
|
field: 'sub_part',
|
||||||
|
title: '{% trans "Part" %}',
|
||||||
|
switchable: false,
|
||||||
|
sortable: true,
|
||||||
|
formatter: function(value, row, index, field) {
|
||||||
|
var url = `/part/${row.sub_part}/`;
|
||||||
|
var html = imageHoverIcon(row.sub_part_detail.thumbnail) + renderLink(row.sub_part_detail.full_name, url);
|
||||||
|
|
||||||
|
var sub_part = row.sub_part_detail;
|
||||||
|
|
||||||
|
html += makePartIcons(row.sub_part_detail);
|
||||||
|
|
||||||
|
// Display an extra icon if this part is an assembly
|
||||||
|
if (sub_part.assembly) {
|
||||||
|
var text = `<span title='{% trans "Open subassembly" %}' class='fas fa-stream label-right'></span>`;
|
||||||
|
|
||||||
|
html += renderLink(text, `/part/${row.sub_part}/bom/`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'sub_part_detail.description',
|
||||||
|
title: '{% trans "Description" %}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'reference',
|
||||||
|
title: '{% trans "Reference" %}',
|
||||||
|
searchable: true,
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'quantity',
|
||||||
|
title: '{% trans "Quantity" %}',
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sortable: true,
|
||||||
|
switchable: false,
|
||||||
|
field: 'sub_part_detail.stock',
|
||||||
|
title: '{% trans "Available" %}',
|
||||||
|
formatter: function(value, row, index, field) {
|
||||||
|
return makeProgressBar(
|
||||||
|
value,
|
||||||
|
row.quantity * options.build_remaining,
|
||||||
|
{
|
||||||
|
id: `part-progress-${row.part}`
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
sorter: function(valA, valB, rowA, rowB) {
|
||||||
|
if (rowA.received == 0 && rowB.received == 0) {
|
||||||
|
return (rowA.quantity > rowB.quantity) ? 1 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var progressA = parseFloat(rowA.sub_part_detail.stock) / (rowA.quantity * options.build_remaining);
|
||||||
|
var progressB = parseFloat(rowB.sub_part_detail.stock) / (rowB.quantity * options.build_remaining);
|
||||||
|
|
||||||
|
return (progressA < progressB) ? 1 : -1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'actions',
|
||||||
|
title: '{% trans "Actions" %}',
|
||||||
|
switchable: false,
|
||||||
|
formatter: function(value, row, index, field) {
|
||||||
|
|
||||||
|
// Generate action buttons against the part
|
||||||
|
var html = `<div class='btn-group float-right' role='group'>`;
|
||||||
|
|
||||||
|
if (row.sub_part_detail.assembly) {
|
||||||
|
html += makeIconButton('fa-tools icon-blue', 'button-build', row.sub_part, '{% trans "Build stock" %}');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row.sub_part_detail.purchaseable) {
|
||||||
|
html += makeIconButton('fa-shopping-cart icon-blue', 'button-buy', row.sub_part, '{% trans "Order stock" %}');
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `</div>`;
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
table.inventreeTable({
|
||||||
|
url: '{% url "api-bom-list" %}',
|
||||||
|
showColumns: true,
|
||||||
|
name: 'build-parts',
|
||||||
|
sortable: true,
|
||||||
|
search: true,
|
||||||
|
onPostBody: setupTableCallbacks,
|
||||||
|
rowStyle: function(row, index) {
|
||||||
|
var classes = [];
|
||||||
|
|
||||||
|
// Shade rows differently if they are for different parent parts
|
||||||
|
if (row.part != options.part) {
|
||||||
|
classes.push('rowinherited');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row.validated) {
|
||||||
|
classes.push('rowvalid');
|
||||||
|
} else {
|
||||||
|
classes.push('rowinvalid');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
classes: classes.join(' '),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
formatNoMatches: function() {
|
||||||
|
return '{% trans "No BOM items found" %}';
|
||||||
|
},
|
||||||
|
clickToSelect: true,
|
||||||
|
queryParams: filters,
|
||||||
|
original: params,
|
||||||
|
columns: columns,
|
||||||
|
});
|
||||||
}
|
}
|
@ -34,10 +34,12 @@
|
|||||||
</button>
|
</button>
|
||||||
<h3 id='modal-title'><i>Form Title Here</i></h3>
|
<h3 id='modal-title'><i>Form Title Here</i></h3>
|
||||||
</div>
|
</div>
|
||||||
<div class='alert alert-block alert-danger' id='form-validation-warning' style="display: none;">
|
<div class='modal-form-content-wrapper'>
|
||||||
{% trans "Form errors exist" %}
|
<div class='alert alert-block alert-danger' id='form-validation-warning' style="display: none;">
|
||||||
</div>
|
{% trans "Form errors exist" %}
|
||||||
<div class='modal-form-content'>
|
</div>
|
||||||
|
<div class='modal-form-content'>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class='modal-footer'>
|
<div class='modal-footer'>
|
||||||
<button type='button' class='btn btn-default' id='modal-form-close' data-dismiss='modal'>{% trans "Close" %}</button>
|
<button type='button' class='btn btn-default' id='modal-form-close' data-dismiss='modal'>{% trans "Close" %}</button>
|
||||||
|
Loading…
Reference in New Issue
Block a user