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/models.py b/InvenTree/build/models.py index 3908816454..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 @@ -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 MakeBarcode( + "buildorder", + self.pk, + { + "reference": self.title, + "url": self.get_absolute_url(), + } + ) + @staticmethod def filterByDate(queryset, min_date, max_date): """ diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 76cc9bcfde..3c4b94c43d 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -675,6 +675,13 @@ 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 +691,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 8ba7ba799d..0257caee0c 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 @@ -884,20 +884,135 @@ 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 + + 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 + + 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 @@ -979,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 eb347fc29e..1221c271b6 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -119,36 +119,35 @@

{% 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 not part.is_template %} - {% 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 %} + {% if on_order > 0 %} {% trans "On Order" %} - {% decimal part.on_order %} + {% decimal 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 %} @@ -162,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 %} + {% trans "Building" %} + {% decimal quantity_being_built %} {% endif %} {% endif %} 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 8f1c86fdfc..745650b55d 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -792,6 +792,22 @@ 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['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'] + return context 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) 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);