diff --git a/InvenTree/part/bom.py b/InvenTree/part/bom.py index d09cc5130a..fd31aba339 100644 --- a/InvenTree/part/bom.py +++ b/InvenTree/part/bom.py @@ -7,7 +7,7 @@ from collections import OrderedDict from django.utils.translation import gettext as _ -from InvenTree.helpers import DownloadFile, GetExportFormats +from InvenTree.helpers import DownloadFile, GetExportFormats, normalize from .admin import BomItemResource from .models import BomItem @@ -59,7 +59,7 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa uids = [] - def add_items(items, level): + def add_items(items, level, cascade): # Add items at a given layer for item in items: @@ -71,21 +71,13 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa bom_items.append(item) - if item.sub_part.assembly: + if cascade and item.sub_part.assembly: if max_levels is None or level < max_levels: add_items(item.sub_part.bom_items.all().order_by('id'), level + 1) - if cascade: - # Cascading (multi-level) BOM + top_level_items = part.get_bom_items().order_by('id') - # Start with the top level - items_to_process = part.bom_items.all().order_by('id') - - add_items(items_to_process, 1) - - else: - # No cascading needed - just the top-level items - bom_items = [item for item in part.bom_items.all().order_by('id')] + add_items(top_level_items, 1, cascade) dataset = BomItemResource().export(queryset=bom_items, cascade=cascade) @@ -148,8 +140,9 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa stock_data.append('') except AttributeError: stock_data.append('') + # Get part current stock - stock_data.append(str(bom_item.sub_part.available_stock)) + stock_data.append(str(normalize(bom_item.sub_part.available_stock))) for s_idx, header in enumerate(stock_headers): try: diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 3ab616d96d..e77008b076 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -1392,6 +1392,27 @@ class Part(MPTTModel): return BomItem.objects.filter(self.get_bom_item_filter(include_inherited=include_inherited)) + def get_installed_part_options(self, include_inherited=True, include_variants=True): + """ + Return a set of all Parts which can be "installed" into this part, based on the BOM. + + arguments: + include_inherited - If set, include BomItem entries defined for parent parts + include_variants - If set, include variant parts for BomItems which allow variants + """ + + parts = set() + + for bom_item in self.get_bom_items(include_inherited=include_inherited): + + if include_variants and bom_item.allow_variants: + for part in bom_item.sub_part.get_descendants(include_self=True): + parts.add(part) + else: + parts.add(bom_item.sub_part) + + return parts + def get_used_in_filter(self, include_inherited=True): """ Return a query filter for all parts that this part is used in. diff --git a/InvenTree/stock/admin.py b/InvenTree/stock/admin.py index 5f1b134966..abd3db20cb 100644 --- a/InvenTree/stock/admin.py +++ b/InvenTree/stock/admin.py @@ -117,6 +117,8 @@ class StockItemResource(ModelResource): exclude = [ # Exclude MPTT internal model fields 'lft', 'rght', 'tree_id', 'level', + # Exclude internal fields + 'serial_int', ] diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index e287441382..2ffc2e8d69 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -876,6 +876,7 @@ class StockList(generics.ListCreateAPIView): ordering_field_aliases = { 'SKU': 'supplier_part__SKU', + 'stock': ['quantity', 'serial_int', 'serial'], } ordering_fields = [ @@ -887,6 +888,7 @@ class StockList(generics.ListCreateAPIView): 'stocktake_date', 'expiry_date', 'quantity', + 'stock', 'status', 'SKU', ] diff --git a/InvenTree/stock/migrations/0068_stockitem_serial_int.py b/InvenTree/stock/migrations/0068_stockitem_serial_int.py new file mode 100644 index 0000000000..874978dc61 --- /dev/null +++ b/InvenTree/stock/migrations/0068_stockitem_serial_int.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.5 on 2021-11-09 23:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0067_alter_stockitem_part'), + ] + + operations = [ + migrations.AddField( + model_name='stockitem', + name='serial_int', + field=models.IntegerField(default=0), + ), + ] diff --git a/InvenTree/stock/migrations/0069_auto_20211109_2347.py b/InvenTree/stock/migrations/0069_auto_20211109_2347.py new file mode 100644 index 0000000000..f4cdde7794 --- /dev/null +++ b/InvenTree/stock/migrations/0069_auto_20211109_2347.py @@ -0,0 +1,54 @@ +# Generated by Django 3.2.5 on 2021-11-09 23:47 + +import re + +from django.db import migrations + + +def update_serials(apps, schema_editor): + """ + Rebuild the integer serial number field for existing StockItem objects + """ + + StockItem = apps.get_model('stock', 'stockitem') + + for item in StockItem.objects.all(): + + if item.serial is None: + # Skip items without existing serial numbers + continue + + serial = 0 + + result = re.match(r"^(\d+)", str(item.serial)) + + if result and len(result.groups()) == 1: + try: + serial = int(result.groups()[0]) + except: + serial = 0 + + + item.serial_int = serial + item.save() + + +def nupdate_serials(apps, schema_editor): + """ + Provided only for reverse migration compatibility + """ + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0068_stockitem_serial_int'), + ] + + operations = [ + migrations.RunPython( + update_serials, + reverse_code=nupdate_serials, + ) + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 320807e0c1..8e07074a76 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -7,6 +7,7 @@ Stock database model definitions from __future__ import unicode_literals import os +import re from django.utils.translation import gettext_lazy as _ from django.core.exceptions import ValidationError, FieldError @@ -223,6 +224,32 @@ class StockItem(MPTTModel): self.scheduled_for_deletion = True self.save() + def update_serial_number(self): + """ + Update the 'serial_int' field, to be an integer representation of the serial number. + This is used for efficient numerical sorting + """ + + serial = getattr(self, 'serial', '') + + # Default value if we cannot convert to an integer + serial_int = 0 + + if serial is not None: + + serial = str(serial) + + # Look at the start of the string - can it be "integerized"? + result = re.match(r'^(\d+)', serial) + + if result and len(result.groups()) == 1: + try: + serial_int = int(result.groups()[0]) + except: + serial_int = 0 + + self.serial_int = serial_int + def save(self, *args, **kwargs): """ Save this StockItem to the database. Performs a number of checks: @@ -234,6 +261,8 @@ class StockItem(MPTTModel): self.validate_unique() self.clean() + self.update_serial_number() + user = kwargs.pop('user', None) # If 'add_note = False' specified, then no tracking note will be added for item creation @@ -504,6 +533,8 @@ class StockItem(MPTTModel): help_text=_('Serial number for this item') ) + serial_int = models.IntegerField(default=0) + link = InvenTreeURLField( verbose_name=_('External Link'), max_length=125, blank=True, diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index ba45314dcb..647c123130 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -560,9 +560,8 @@ class StockItemInstall(AjaxUpdateView): # Filter for parts to install in this item if self.install_item: - # Get parts used in this part's BOM - bom_items = self.part.get_bom_items() - allowed_parts = [item.sub_part for item in bom_items] + # Get all parts which can be installed into this part + allowed_parts = self.part.get_installed_part_options() # Filter items = items.filter(part__in=allowed_parts) diff --git a/InvenTree/templates/js/translated/stock.js b/InvenTree/templates/js/translated/stock.js index ec785969cd..ba4238e6f7 100644 --- a/InvenTree/templates/js/translated/stock.js +++ b/InvenTree/templates/js/translated/stock.js @@ -1128,7 +1128,9 @@ function loadStockTable(table, options) { col = { field: 'quantity', + sortName: 'stock', title: '{% trans "Stock" %}', + sortable: true, formatter: function(value, row) { var val = parseFloat(value); diff --git a/README.md b/README.md index f0ccdb88b9..1f64f81050 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,6 @@ ![SQLite](https://github.com/inventree/inventree/actions/workflows/coverage.yaml/badge.svg) ![MySQL](https://github.com/inventree/inventree/actions/workflows/mysql.yaml/badge.svg) ![PostgreSQL](https://github.com/inventree/inventree/actions/workflows/postgresql.yaml/badge.svg) -[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/InvenTree/InvenTree) InvenTree is an open-source Inventory Management System which provides powerful low-level stock control and part tracking. The core of the InvenTree system is a Python/Django database backend which provides an admin interface (web-based) and a JSON API for interaction with external interfaces and applications. @@ -17,6 +16,10 @@ InvenTree is designed to be lightweight and easy to use for SME or hobbyist appl However, powerful business logic works in the background to ensure that stock tracking history is maintained, and users have ready access to stock level information. +# Demo + +A demo instance of InvenTree is provided to allow users to explore the functionality of the software. [Read more here](https://inventree.readthedocs.io/en/latest/demo/) + # Docker [![Docker Pulls](https://img.shields.io/docker/pulls/inventree/inventree)](https://hub.docker.com/r/inventree/inventree)