From cda97829ab7280b36b27df08823f7061f8bab054 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 17 Feb 2021 10:27:36 +1100 Subject: [PATCH 01/10] Add function for required build order quantity --- InvenTree/part/models.py | 56 ++++++++++++++++++++ InvenTree/part/templates/part/part_base.html | 7 +++ 2 files changed, 63 insertions(+) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 8ba7ba799d..85e7c051bf 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -884,6 +884,62 @@ class Part(MPTTModel): return max(total, 0) + def requiring_build_orders(self): + """ + 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) + + 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 + ) + + return builds + + def required_build_order_quantity(self): + """ + 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 + ) + + quantity = 0 + + for build in builds: + + bom_item = None + + # Match BOM item to build + for bom in boms: + if bom.part == build.part: + bom_item = bom + break + + if bom_item is None: + logger.warning("Found null BomItem when calculating required quantity") + continue + + build_quantity = build.quantity * bom_item.quantity + + quantity += build_quantity + + return quantity + @property def quantity_to_order(self): """ Return the quantity needing to be ordered for this part. """ diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index eb347fc29e..0ddb5d2180 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -127,6 +127,13 @@ {% include "part/stock_count.html" %} {% if not part.is_template %} + {% if part.required_build_order_quantity > 0 %} + + + {% trans "Required for Build Orders" %} + {% decimal part.required_build_order_quantity %} + + {% endif %} {% if part.build_order_allocation_count > 0 %} From 28c9c80f540d0c05bd49f383798e58fd81f16cd8 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 17 Feb 2021 10:57:17 +1100 Subject: [PATCH 02/10] Calculate quantity required for sales orders - Cache data going to part detail view --- InvenTree/part/models.py | 38 ++++++++++++++++++- InvenTree/part/templates/part/part_base.html | 40 ++++++++------------ InvenTree/part/views.py | 10 +++++ 3 files changed, 63 insertions(+), 25 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 85e7c051bf..6041c2827e 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -41,7 +41,7 @@ from InvenTree.models import InvenTreeTree, InvenTreeAttachment from InvenTree.fields import InvenTreeURLField from InvenTree.helpers import decimal2string, normalize -from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus +from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus, SalesOrderStatus from build import models as BuildModels from order import models as OrderModels @@ -940,6 +940,42 @@ class Part(MPTTModel): return quantity + def requiring_sales_orders(self): + """ + Return a list of sales orders which require this part + """ + + orders = set() + + # Get a list of line items for open orders which match this part + open_lines = OrderModels.SalesOrderLineItem.objects.filter( + order__status__in=SalesOrderStatus.OPEN, + part=self + ) + + for line in open_lines: + orders.add(line.order) + + return orders + + def required_sales_order_quantity(self): + """ + Return the quantity of this part required for active sales orders + """ + + # Get a list of line items for open orders which match this part + open_lines = OrderModels.SalesOrderLineItem.objects.filter( + order__status__in=SalesOrderStatus.OPEN, + part=self + ) + + quantity = 0 + + for line in open_lines: + quantity += line.quantity + + return quantity + @property def quantity_to_order(self): """ Return the quantity needing to be ordered for this part. """ diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index 0ddb5d2180..24c0c0b234 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -126,28 +126,6 @@ {% trans "In Stock" %} {% include "part/stock_count.html" %} - {% if not part.is_template %} - {% if part.required_build_order_quantity > 0 %} - - - {% trans "Required for Build Orders" %} - {% decimal part.required_build_order_quantity %} - - {% endif %} - {% if part.build_order_allocation_count > 0 %} - - - {% trans "Allocated to Build Orders" %} - {% decimal part.build_order_allocation_count %} - - {% endif %} - {% if part.sales_order_allocation_count > 0 %} - - - {% trans "Allocated to Sales Orders" %} - {% decimal part.sales_order_allocation_count %} - - {% endif %} {% if part.on_order > 0 %} @@ -155,7 +133,21 @@ {% decimal part.on_order %} {% endif %} + {% if required > 0 %} + + + {% trans "Required for Orders" %} + {% decimal required %} + {% endif %} + {% if allocated > 0 %} + + + {% trans "Allocated to Orders" %} + {% decimal allocated %} + + {% endif %} + {% if not part.is_template %} {% if part.assembly %} @@ -169,11 +161,11 @@ {% trans "Can Build" %} {% decimal part.can_build %} - {% if part.quantity_being_built > 0 %} + {% if quantity_being_built > 0 %} {% trans "Underway" %} - {% decimal part.quantity_being_built %} + {% decimal quantity_being_built %} {% endif %} {% endif %} diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 8f1c86fdfc..42c48c8cab 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -792,6 +792,16 @@ class PartDetail(InvenTreeRoleMixin, DetailView): context['starred'] = part.isStarredBy(self.request.user) context['disabled'] = not part.active + # Pre-calculate complex queries so they only need to be performed once + context['required_build_order_quantity'] = part.required_build_order_quantity() + context['allocated_build_order_quantity'] = part.build_order_allocation_count() + + context['required_sales_order_quantity'] = part.required_sales_order_quantity() + context['allocated_sales_order_quantity'] = part.sales_order_allocation_count() + + context['required'] = context['required_build_order_quantity'] + context['required_sales_order_quantity'] + context['allocated'] = context['allocated_build_order_quantity'] + context['allocated_sales_order_quantity'] + return context From 34df19242cc8afcd4229dbe1dede7322242914de Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 17 Feb 2021 11:08:11 +1100 Subject: [PATCH 03/10] Adds more context data --- InvenTree/part/templates/part/part_base.html | 6 +++--- InvenTree/part/templates/part/stock_count.html | 6 +++--- InvenTree/part/templates/part/tabs.html | 4 ++-- InvenTree/part/views.py | 6 ++++++ 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index 24c0c0b234..57821b4a89 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -119,18 +119,18 @@

{% trans "Available Stock" %}

-

{% decimal part.available_stock %}{% if part.units %} {{ part.units }}{% endif %}

+

{% decimal available %}{% if part.units %} {{ part.units }}{% endif %}

{% trans "In Stock" %} {% include "part/stock_count.html" %} - {% if part.on_order > 0 %} + {% if on_order > 0 %} {% trans "On Order" %} - {% decimal part.on_order %} + {% decimal on_order %} {% endif %} {% if required > 0 %} diff --git a/InvenTree/part/templates/part/stock_count.html b/InvenTree/part/templates/part/stock_count.html index 58e447b051..5b70995a22 100644 --- a/InvenTree/part/templates/part/stock_count.html +++ b/InvenTree/part/templates/part/stock_count.html @@ -1,10 +1,10 @@ {% load inventree_extras %} {% load i18n %} -{% decimal part.total_stock %} +{% decimal total_stock %} -{% if part.total_stock == 0 %} +{% if total_stock == 0 %} {% trans "No Stock" %} -{% elif part.total_stock < part.minimum_stock %} +{% elif total_stock < part.minimum_stock %} {% trans "Low Stock" %} {% endif %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/tabs.html b/InvenTree/part/templates/part/tabs.html index 8bfaba4d89..32a62f94e9 100644 --- a/InvenTree/part/templates/part/tabs.html +++ b/InvenTree/part/templates/part/tabs.html @@ -15,12 +15,12 @@ {% endif %} {% if not part.virtual %} - {% trans "Stock" %} {% decimal part.total_stock %} + {% trans "Stock" %} {% decimal total_stock %} {% endif %} {% if part.component or part.salable or part.used_in_count > 0 %} - {% trans "Allocated" %} {% decimal part.allocation_count %} + {% trans "Allocated" %} {% decimal allocated %} {% endif %} {% if part.assembly %} diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 42c48c8cab..745650b55d 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -793,12 +793,18 @@ class PartDetail(InvenTreeRoleMixin, DetailView): context['disabled'] = not part.active # Pre-calculate complex queries so they only need to be performed once + context['total_stock'] = part.total_stock + + context['quantity_being_built'] = part.quantity_being_built + context['required_build_order_quantity'] = part.required_build_order_quantity() context['allocated_build_order_quantity'] = part.build_order_allocation_count() context['required_sales_order_quantity'] = part.required_sales_order_quantity() context['allocated_sales_order_quantity'] = part.sales_order_allocation_count() + context['available'] = part.available_stock + context['on_order'] = part.on_order context['required'] = context['required_build_order_quantity'] + context['required_sales_order_quantity'] context['allocated'] = context['allocated_build_order_quantity'] + context['allocated_sales_order_quantity'] From 8780b8435a0d4084926f13456ba63649aea7ba4c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 17 Feb 2021 12:46:05 +1100 Subject: [PATCH 04/10] style fix --- InvenTree/part/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 6041c2827e..c5e562ec80 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -934,7 +934,7 @@ class Part(MPTTModel): 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 From c8650ce34cfca85d16cc789a6cfa425d4a328c80 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 17 Feb 2021 13:05:58 +1100 Subject: [PATCH 05/10] Bug fix for tables --- InvenTree/templates/js/tables.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/InvenTree/templates/js/tables.js b/InvenTree/templates/js/tables.js index f2107d8fc9..ceb38690a8 100644 --- a/InvenTree/templates/js/tables.js +++ b/InvenTree/templates/js/tables.js @@ -45,6 +45,10 @@ function linkButtonsToSelection(table, buttons) { * The buttons will only be enabled if there is at least one row selected */ + if (typeof table === 'string') { + table = $(table); + } + // Initially set the enable state of the buttons enableButtons(buttons, table.bootstrapTable('getSelections').length > 0); From ba542dcbdb6a4fdc4c5599d515003090ed47fc91 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 17 Feb 2021 13:06:18 +1100 Subject: [PATCH 06/10] Auto-fill build quantity --- InvenTree/build/forms.py | 5 +++++ InvenTree/build/views.py | 22 +++++++++++++++------- InvenTree/part/models.py | 39 +++++++++++++++++++++++++++++++-------- 3 files changed, 51 insertions(+), 15 deletions(-) diff --git a/InvenTree/build/forms.py b/InvenTree/build/forms.py index f5764b9d93..061ae0d16d 100644 --- a/InvenTree/build/forms.py +++ b/InvenTree/build/forms.py @@ -39,6 +39,11 @@ class EditBuildForm(HelperForm): help_text=_('Target date for build completion. Build will be overdue after this date.') ) + quantity = RoundingDecimalFormField( + max_digits=10, decimal_places=5, + help_text=_('Number of items to build') + ) + class Meta: model = Build fields = [ diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 76cc9bcfde..18971d74a6 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -675,6 +675,14 @@ class BuildCreate(AjaxCreateView): initials = super(BuildCreate, self).get_initial().copy() + + initials['parent'] = self.request.GET.get('parent', None) + + # User has provided a SalesOrder ID + initials['sales_order'] = self.request.GET.get('sales_order', None) + + initials['quantity'] = self.request.GET.get('quantity', 1) + part = self.request.GET.get('part', None) if part: @@ -684,18 +692,18 @@ class BuildCreate(AjaxCreateView): # User has provided a Part ID initials['part'] = part initials['destination'] = part.get_default_location() + + to_order = part.quantity_to_order + + if to_order < 1: + to_order = 1 + + initials['quantity'] = to_order except (ValueError, Part.DoesNotExist): pass initials['reference'] = Build.getNextBuildNumber() - initials['parent'] = self.request.GET.get('parent', None) - - # User has provided a SalesOrder ID - initials['sales_order'] = self.request.GET.get('sales_order', None) - - initials['quantity'] = self.request.GET.get('quantity', 1) - # Pre-fill the issued_by user initials['issued_by'] = self.request.user diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index c5e562ec80..82a7073612 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -976,20 +976,43 @@ class Part(MPTTModel): return quantity + def required_order_quantity(self): + """ + Return total required to fulfil orders + """ + + return self.required_build_order_quantity() + self.required_sales_order_quantity() + @property def quantity_to_order(self): - """ Return the quantity needing to be ordered for this part. """ + """ + Return the quantity needing to be ordered for this part. + + Here, an "order" could be one of: + - Build Order + - Sales Order - # How many do we need to have "on hand" at any point? - required = self.net_stock - self.minimum_stock + To work out how many we need to order: - if required < 0: - return abs(required) + Stock on hand = self.total_stock + Required for orders = self.required_order_quantity() + Currently on order = self.on_order + Currently building = self.quantity_being_built + + """ - # Do not need to order any - return 0 + # Total requirement + required = self.required_order_quantity() + + # Subtract stock levels + required -= max(self.total_stock, self.minimum_stock) + + # Subtract quantity on order + required -= self.on_order + + # Subtract quantity being built + required -= self.quantity_being_built - required = self.net_stock return max(required, 0) @property From fcc35f226030589bded2c5c51e645aa73f3369ca Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 17 Feb 2021 13:14:27 +1100 Subject: [PATCH 07/10] Fix display of parts currently being built --- InvenTree/part/models.py | 18 ++++++++++++------ InvenTree/part/templates/part/part_base.html | 2 +- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 82a7073612..0257caee0c 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -1094,16 +1094,22 @@ class Part(MPTTModel): @property def quantity_being_built(self): - """ Return the current number of parts currently being built + """ + Return the current number of parts currently being built. + + Note: This is the total quantity of Build orders, *not* the number of build outputs. + In this fashion, it is the "projected" quantity of builds """ - stock_items = self.stock_items.filter(is_building=True) + builds = self.active_builds - query = stock_items.aggregate( - quantity=Coalesce(Sum('quantity'), Decimal(0)) - ) + quantity = 0 - return query['quantity'] + for build in builds: + # The remaining items in the build + quantity += build.remaining + + return quantity def build_order_allocations(self): """ diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index 57821b4a89..1221c271b6 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -164,7 +164,7 @@ {% if quantity_being_built > 0 %} - {% trans "Underway" %} + {% trans "Building" %} {% decimal quantity_being_built %} {% endif %} From 98bd7dfa9a62234bef6d6cb17cd79c90d7e630bd Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 17 Feb 2021 13:16:15 +1100 Subject: [PATCH 08/10] Style fixes --- InvenTree/build/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 18971d74a6..3c4b94c43d 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -675,7 +675,6 @@ class BuildCreate(AjaxCreateView): initials = super(BuildCreate, self).get_initial().copy() - initials['parent'] = self.request.GET.get('parent', None) # User has provided a SalesOrder ID From 08cc866e744fb4b619deb857d2a6371a5ebe1ea8 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 17 Feb 2021 13:47:14 +1100 Subject: [PATCH 09/10] Add function to make barcode for build order --- InvenTree/build/models.py | 14 ++++++++++++++ .../report/inventree_build_order_base.html | 1 + InvenTree/stock/tests.py | 2 +- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 3908816454..0d42ae77c7 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -65,6 +65,20 @@ class Build(MPTTModel): verbose_name = _("Build Order") verbose_name_plural = _("Build Orders") + def format_barcode(self, **kwargs): + """ + Return a JSON string to represent this build as a barcode + """ + + return helpers.MakeBarcode( + "buildorder", + self.pk, + { + "reference": self.title, + "url": self.get_absolute_url(), + } + ) + @staticmethod def filterByDate(queryset, min_date, max_date): """ diff --git a/InvenTree/report/templates/report/inventree_build_order_base.html b/InvenTree/report/templates/report/inventree_build_order_base.html index 1e2dd26c07..0a4f8b3bb6 100644 --- a/InvenTree/report/templates/report/inventree_build_order_base.html +++ b/InvenTree/report/templates/report/inventree_build_order_base.html @@ -4,6 +4,7 @@ {% load report %} {% load inventree_extras %} {% load markdownify %} +{% load qr_code %} {% block page_margin %} margin: 2cm; diff --git a/InvenTree/stock/tests.py b/InvenTree/stock/tests.py index 55fbdef826..b54411b0d2 100644 --- a/InvenTree/stock/tests.py +++ b/InvenTree/stock/tests.py @@ -110,7 +110,7 @@ class StockTest(TestCase): # The "is_building" quantity should not be counted here self.assertEqual(part.total_stock, n + 5) - self.assertEqual(part.quantity_being_built, 100) + self.assertEqual(part.quantity_being_built, 1) def test_loc_count(self): self.assertEqual(StockLocation.objects.count(), 7) From afc33c59eabbd62f09ba7c5b103786d008c5b240 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 17 Feb 2021 14:13:56 +1100 Subject: [PATCH 10/10] bug fix --- InvenTree/build/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 0d42ae77c7..ae7ea14ece 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -23,7 +23,7 @@ from markdownx.models import MarkdownxField from mptt.models import MPTTModel, TreeForeignKey from InvenTree.status_codes import BuildStatus -from InvenTree.helpers import increment, getSetting, normalize +from InvenTree.helpers import increment, getSetting, normalize, MakeBarcode from InvenTree.validators import validate_build_order_reference from InvenTree.models import InvenTreeAttachment @@ -70,7 +70,7 @@ class Build(MPTTModel): Return a JSON string to represent this build as a barcode """ - return helpers.MakeBarcode( + return MakeBarcode( "buildorder", self.pk, {