diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css
index 50a24aa095..ac3d402c3b 100644
--- a/InvenTree/InvenTree/static/css/inventree.css
+++ b/InvenTree/InvenTree/static/css/inventree.css
@@ -304,11 +304,11 @@
.rowinvalid {
color: #A00;
- font-style: italic;
}
.rowinherited {
- background-color: #dde;
+ background-color: #eee;
+ font-style: italic;
}
.dropdown {
diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py
index ae7ea14ece..2872fecb55 100644
--- a/InvenTree/build/models.py
+++ b/InvenTree/build/models.py
@@ -379,24 +379,32 @@ class Build(MPTTModel):
if cls.objects.count() == 0:
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
if not ref:
return None
- tries = set()
+ tries = set(ref)
+
+ new_ref = ref
while 1:
- new_ref = increment(ref)
+ new_ref = increment(new_ref)
if new_ref in tries:
# We are potentially stuck in a loop - simply return the original reference
return ref
+ # Check if the existing build reference exists
if cls.objects.filter(reference=new_ref).exists():
tries.add(new_ref)
- new_ref = increment(new_ref)
else:
break
diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html
index 40dc772c4f..5e56990c3e 100644
--- a/InvenTree/build/templates/build/detail.html
+++ b/InvenTree/build/templates/build/detail.html
@@ -1,9 +1,10 @@
{% extends "build/build_base.html" %}
{% load static %}
{% load i18n %}
-{% block details %}
{% load status_codes %}
+{% block details %}
+
{% include "build/tabs.html" with tab='details' %}
{% trans "Build Details" %}
diff --git a/InvenTree/build/templates/build/parts.html b/InvenTree/build/templates/build/parts.html
new file mode 100644
index 0000000000..3b80cf551b
--- /dev/null
+++ b/InvenTree/build/templates/build/parts.html
@@ -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' %}
+
+{% trans "Build Parts" %}
+
+
+
+
+{% 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 %}
\ No newline at end of file
diff --git a/InvenTree/build/templates/build/tabs.html b/InvenTree/build/templates/build/tabs.html
index 8bdb2a3013..c6d2893620 100644
--- a/InvenTree/build/templates/build/tabs.html
+++ b/InvenTree/build/templates/build/tabs.html
@@ -5,16 +5,22 @@
{% trans "Details" %}
{% if build.active %}
+
+
+ {% trans "Required Parts" %}
+ {{ build.part.bom_count }}
+
+
- {% trans "Incomplete" %}
+ {% trans "In Progress" %}
{{ build.incomplete_outputs.count }}
{% endif %}
- {% trans "Build Outputs" %}
+ {% trans "Completed Outputs" %}
{{ build.output_count }}
diff --git a/InvenTree/build/urls.py b/InvenTree/build/urls.py
index 08142e6939..6f681f5488 100644
--- a/InvenTree/build/urls.py
+++ b/InvenTree/build/urls.py
@@ -20,6 +20,7 @@ build_detail_urls = [
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'^output/', views.BuildDetail.as_view(template_name='build/build_output.html'), name='build-output'),
diff --git a/InvenTree/order/templates/order/purchase_orders.html b/InvenTree/order/templates/order/purchase_orders.html
index 52314df833..262f083074 100644
--- a/InvenTree/order/templates/order/purchase_orders.html
+++ b/InvenTree/order/templates/order/purchase_orders.html
@@ -158,6 +158,14 @@ $("#po-create").click(function() {
launchModalForm("{% url 'po-create' %}",
{
follow: true,
+ secondary: [
+ {
+ field: 'supplier',
+ label: '{% trans "New Supplier" %}',
+ title: '{% trans "Create new Supplier" %}',
+ url: '{% url "supplier-create" %}',
+ }
+ ]
}
);
});
diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py
index 1fd5092f14..04a37e5fff 100644
--- a/InvenTree/part/api.py
+++ b/InvenTree/part/api.py
@@ -465,6 +465,18 @@ class PartList(generics.ListCreateAPIView):
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'?
ancestor = params.get('ancestor', None)
@@ -839,12 +851,6 @@ class BomList(generics.ListCreateAPIView):
except (ValueError, Part.DoesNotExist):
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
part_active = params.get('part_active', None)
diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py
index 911a2cdac4..c1dbb454b4 100644
--- a/InvenTree/part/models.py
+++ b/InvenTree/part/models.py
@@ -643,7 +643,7 @@ class Part(MPTTModel):
super().clean()
if self.trackable:
- for item in self.used_in.all():
+ for item in self.get_used_in().all():
parent_part = item.part
if not parent_part.trackable:
parent_part.trackable = True
@@ -891,10 +891,10 @@ class Part(MPTTModel):
Return list of outstanding build orders which require this part
"""
- # List of BOM that this part is required for
- boms = BomItem.objects.filter(sub_part=self)
+ # List parts that this part is required for
+ 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
builds = BuildModels.Build.objects.filter(
@@ -909,36 +909,24 @@ class Part(MPTTModel):
Return the quantity of this part required for active build orders
"""
- # List of BOM that this part is required for
- boms = BomItem.objects.filter(sub_part=self)
-
- 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
- )
+ # List active build orders which reference this part
+ builds = self.requiring_build_orders()
quantity = 0
for build in builds:
-
+
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
- for bom in boms:
- if bom.part == build.part:
- bom_item = bom
- break
+ for bom_item in bom_items:
- if bom_item is None:
- logger.warning("Found null BomItem when calculating required quantity")
- continue
+ build_quantity = build.quantity * bom_item.quantity
- build_quantity = build.quantity * bom_item.quantity
-
- quantity += build_quantity
+ quantity += build_quantity
return quantity
@@ -1240,6 +1228,54 @@ class Part(MPTTModel):
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
def has_bom(self):
return self.get_bom_items().count() > 0
@@ -1265,7 +1301,7 @@ class Part(MPTTModel):
@property
def used_in_count(self):
""" 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):
""" Return a checksum hash for the BOM for this part.
@@ -1364,7 +1400,7 @@ class Part(MPTTModel):
parts = parts.exclude(id=self.id)
# 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])
@@ -1524,7 +1560,7 @@ class Part(MPTTModel):
# Copy existing BOM items from another part
# 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,
# delete that BomItem from this part first!
@@ -2094,6 +2130,8 @@ class BomItem(models.Model):
- Quantity
- Reference 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.note).encode())
hash.update(str(self.reference).encode())
+ hash.update(str(self.optional).encode())
+ hash.update(str(self.inherited).encode())
return str(hash.digest())
diff --git a/InvenTree/part/templates/part/bom.html b/InvenTree/part/templates/part/bom.html
index ca0446378c..322d637089 100644
--- a/InvenTree/part/templates/part/bom.html
+++ b/InvenTree/part/templates/part/bom.html
@@ -77,19 +77,6 @@
{% endblock %}
-{% block js_load %}
-{{ block.super }}
-
-
-
-
-
-
-
-
-{% endblock %}
-
-
{% block js_ready %}
{{ block.super }}
diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html
index 1221c271b6..6b0163089d 100644
--- a/InvenTree/part/templates/part/part_base.html
+++ b/InvenTree/part/templates/part/part_base.html
@@ -133,11 +133,18 @@
{% decimal on_order %} |
{% endif %}
- {% if required > 0 %}
+ {% if required_build_order_quantity > 0 %}
|
- {% trans "Required for Orders" %} |
- {% decimal required %}
+ | {% trans "Required for Build Orders" %} |
+ {% decimal required_build_order_quantity %}
+ |
+ {% endif %}
+ {% if required_sales_order_quantity > 0 %}
+
+ |
+ {% trans "Required for Sales Orders" %} |
+ {% decimal required_sales_order_quantity %}
|
{% endif %}
{% if allocated > 0 %}
diff --git a/InvenTree/part/templates/part/used_in.html b/InvenTree/part/templates/part/used_in.html
index 25e858b5f0..686578a93a 100644
--- a/InvenTree/part/templates/part/used_in.html
+++ b/InvenTree/part/templates/part/used_in.html
@@ -22,10 +22,14 @@
{% block js_ready %}
{{ block.super }}
- loadUsedInTable('#used-table', {
- part_detail: true,
- part_id: {{ part.pk }}
- });
+ loadSimplePartTable('#used-table',
+ '{% url "api-part-list" %}',
+ {
+ params: {
+ uses: {{ part.pk }},
+ }
+ }
+ );
{% endblock %}
\ No newline at end of file
diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html
index c1640892de..bb78e4a45d 100644
--- a/InvenTree/templates/base.html
+++ b/InvenTree/templates/base.html
@@ -107,6 +107,13 @@ InvenTree
+
+
+
+
+
+
+
diff --git a/InvenTree/templates/js/bom.js b/InvenTree/templates/js/bom.js
index 76c65d44dd..beb4110df6 100644
--- a/InvenTree/templates/js/bom.js
+++ b/InvenTree/templates/js/bom.js
@@ -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 += "{% trans 'INACTIVE' %}";
- }
-
- 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,
- });
-}
diff --git a/InvenTree/templates/js/build.js b/InvenTree/templates/js/build.js
index 965207de66..cb7e27486b 100644
--- a/InvenTree/templates/js/build.js
+++ b/InvenTree/templates/js/build.js
@@ -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 = ``;
+
+ 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 = ``;
+
+ 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 += `
`;
+
+ 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,
+ });
}
\ No newline at end of file
diff --git a/InvenTree/templates/modals.html b/InvenTree/templates/modals.html
index a162a3f167..11166751f8 100644
--- a/InvenTree/templates/modals.html
+++ b/InvenTree/templates/modals.html
@@ -34,10 +34,12 @@
Form Title Here
-
- {% trans "Form errors exist" %}
-
-
+
+
+ {% trans "Form errors exist" %}
+
+
+