From 85b906f49dc32d417fa4a731ea40b6454cf6de0c Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 10 Nov 2021 09:48:52 +1100 Subject: [PATCH 01/13] Adds "stock" column sorting for stock table --- InvenTree/stock/api.py | 2 ++ InvenTree/templates/js/translated/stock.js | 2 ++ 2 files changed, 4 insertions(+) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index e287441382..e01ad51b55 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'], } ordering_fields = [ @@ -887,6 +888,7 @@ class StockList(generics.ListCreateAPIView): 'stocktake_date', 'expiry_date', 'quantity', + 'stock', 'status', 'SKU', ] 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); From 257be18d7962c64e85be903e0a0f2daea9cc0ad8 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 10 Nov 2021 10:47:17 +1100 Subject: [PATCH 02/13] Add 'serial_int' field to stockitem model - Allows for integer sorting of stock items --- .../migrations/0068_stockitem_serial_int.py | 18 ++++++++++++++++++ InvenTree/stock/models.py | 2 ++ 2 files changed, 20 insertions(+) create mode 100644 InvenTree/stock/migrations/0068_stockitem_serial_int.py 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/models.py b/InvenTree/stock/models.py index 320807e0c1..8601e96f36 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -504,6 +504,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, From 738ba4eddfa05f7a20e78b0c011bd88095bd774f Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 10 Nov 2021 10:59:48 +1100 Subject: [PATCH 03/13] Data migration to initially update serial number fields for all stock items - Also automatically updates serial_int field when saving StockItem --- .../migrations/0069_auto_20211109_2347.py | 55 +++++++++++++++++++ InvenTree/stock/models.py | 26 +++++++++ 2 files changed, 81 insertions(+) create mode 100644 InvenTree/stock/migrations/0069_auto_20211109_2347.py 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..3eaa8c784d --- /dev/null +++ b/InvenTree/stock/migrations/0069_auto_20211109_2347.py @@ -0,0 +1,55 @@ +# 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+)", item.serial or "") + + if result and len(result.groups()) == 1: + try: + serial = int(result.groups()[0]) + except: + serial = 0 + + + item.serial_int = serial + print(item, item.serial, '->', item.serial_int) + 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 8601e96f36..5bc56f4bc6 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,29 @@ 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 + + # Look at the start of the string - can it be "integerized"? + result = re.match(r'^(\d+)', serial or "") + + 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 +258,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 From 25ce043337a26e2d417febdd5d446ed62b31076b Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 10 Nov 2021 11:00:43 +1100 Subject: [PATCH 04/13] Adjust ordering sequence for StockItem --- InvenTree/stock/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index e01ad51b55..2ffc2e8d69 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -876,7 +876,7 @@ class StockList(generics.ListCreateAPIView): ordering_field_aliases = { 'SKU': 'supplier_part__SKU', - 'stock': ['quantity', 'serial'], + 'stock': ['quantity', 'serial_int', 'serial'], } ordering_fields = [ From 067f12d5eb19014c31a95511189301da8ce52a6d Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 10 Nov 2021 11:01:12 +1100 Subject: [PATCH 05/13] PEP fixes --- InvenTree/stock/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 5bc56f4bc6..84942b215e 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -246,7 +246,6 @@ class StockItem(MPTTModel): self.serial_int = serial_int - def save(self, *args, **kwargs): """ Save this StockItem to the database. Performs a number of checks: From 6a534e0322e618694beb7f486f5d504a2f134e5d Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 10 Nov 2021 11:11:52 +1100 Subject: [PATCH 06/13] Improve options for installing a part into another part - Allow "variant" parts when the BOM specifies that variants are allowed for a particular BOM item --- InvenTree/part/models.py | 22 ++++++++++++++++++++++ InvenTree/stock/views.py | 5 ++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 3ab616d96d..ef53e8bb95 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -1392,6 +1392,28 @@ 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.all(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/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) From bd81eba380935fd8f66796b72d9c0c52ccde86e6 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 10 Nov 2021 11:13:30 +1100 Subject: [PATCH 07/13] PEP style fixes --- InvenTree/part/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index ef53e8bb95..26bc3538e5 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -1413,7 +1413,6 @@ class Part(MPTTModel): return parts - def get_used_in_filter(self, include_inherited=True): """ Return a query filter for all parts that this part is used in. From ba11a73f91e73c0e97dfdfddc232d60408a3a9a8 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 10 Nov 2021 11:22:58 +1100 Subject: [PATCH 08/13] Fix for saving stock item objects --- InvenTree/stock/models.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 84942b215e..8e07074a76 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -235,14 +235,18 @@ class StockItem(MPTTModel): # Default value if we cannot convert to an integer serial_int = 0 - # Look at the start of the string - can it be "integerized"? - result = re.match(r'^(\d+)', serial or "") + if serial is not None: - if result and len(result.groups()) == 1: - try: - serial_int = int(result.groups()[0]) - except: - serial_int = 0 + 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 From 919f182c25d06cff2ac98ab2140cc2b9f1630989 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 10 Nov 2021 11:30:11 +1100 Subject: [PATCH 09/13] Remove debug messages --- InvenTree/stock/migrations/0069_auto_20211109_2347.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/InvenTree/stock/migrations/0069_auto_20211109_2347.py b/InvenTree/stock/migrations/0069_auto_20211109_2347.py index 3eaa8c784d..f4cdde7794 100644 --- a/InvenTree/stock/migrations/0069_auto_20211109_2347.py +++ b/InvenTree/stock/migrations/0069_auto_20211109_2347.py @@ -20,7 +20,7 @@ def update_serials(apps, schema_editor): serial = 0 - result = re.match(r"^(\d+)", item.serial or "") + result = re.match(r"^(\d+)", str(item.serial)) if result and len(result.groups()) == 1: try: @@ -30,7 +30,6 @@ def update_serials(apps, schema_editor): item.serial_int = serial - print(item, item.serial, '->', item.serial_int) item.save() From 986a13864ae2c38b9858749ce5cbedfa67640b46 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 10 Nov 2021 11:37:30 +1100 Subject: [PATCH 10/13] Exclude 'serial_int' when exporting stock data --- InvenTree/stock/admin.py | 2 ++ 1 file changed, 2 insertions(+) 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', ] From 1bb51a73093d13832e19ca993db0e3ef45ddb118 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 10 Nov 2021 13:27:59 +1100 Subject: [PATCH 11/13] Normalize "available stock" quantity before exporting --- InvenTree/part/bom.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) 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: From 4a26f274af1f159af394d2bc7e31e223d58ed97a Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 10 Nov 2021 14:00:30 +1100 Subject: [PATCH 12/13] fixing silly bug I introduced --- 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 26bc3538e5..e77008b076 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -1403,7 +1403,7 @@ class Part(MPTTModel): parts = set() - for bom_item in self.get_bom_items.all(include_inherited=include_inherited): + 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): From 4d6189653ea455d599ea718ed83303ac3736789e Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 11 Nov 2021 08:39:28 +1100 Subject: [PATCH 13/13] Update README.md --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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)