From 82c6d10c3368294d011fe1f61a275104f7c8ef12 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 11 May 2021 17:11:07 +1000 Subject: [PATCH 01/16] Add translateable status codes for StockHistory --- InvenTree/InvenTree/status_codes.py | 76 +++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/InvenTree/InvenTree/status_codes.py b/InvenTree/InvenTree/status_codes.py index c73ef10018..53b747a9ad 100644 --- a/InvenTree/InvenTree/status_codes.py +++ b/InvenTree/InvenTree/status_codes.py @@ -224,6 +224,82 @@ class StockStatus(StatusCode): ] +class StockHistoryCode(StatusCode): + + LEGACY = 0 + + CREATED = 1 + + # Manual editing operations + EDITED = 5 + ASSIGNED_SERIAL = 6 + + # Manual stock operations + STOCK_COUNT = 10 + STOCK_ADD = 11 + STOCK_REMOVE = 12 + + # Location operations + STOCK_MOVE = 20 + + # Installation operations + INSTALLED_INTO_ASSEMBLY = 30 + REMOVED_FROM_ASSEMBLY = 31 + + INSTALLED_CHILD_ITEM = 35 + REMOVED_CHILD_ITEM = 36 + + # Stock splitting operations + SPLIT_FROM_PARENT = 40 + SPLIT_CHILD_ITEM = 42 + + # Build order codes + BUILD_OUTPUT_CREATED = 50 + BUILD_OUTPUT_COMPLETED = 55 + + # Sales order codes + + # Purchase order codes + RECEIVED_AGAINST_PURCHASE_ORDER = 70 + + # Customer actions + SENT_TO_CUSTOMER = 100 + RETURNED_FROM_CUSTOMER = 105 + + options = { + LEGACY: _('Legacy stock tracking entry'), + + CREATED: _('Stock item created'), + + EDITED: _('Edited stock item'), + ASSIGNED_SERIAL: _('Assigned serial number'), + + STOCK_COUNT: _('Stock counted'), + STOCK_ADD: _('Stock manually added'), + STOCK_REMOVE: _('Stock manually removed'), + + STOCK_MOVE: _('Location changed'), + + INSTALLED_INTO_ASSEMBLY: _('Installed into assembly'), + REMOVED_FROM_ASSEMBLY: _('Removed from assembly'), + + INSTALLED_CHILD_ITEM: _('Installed component item'), + REMOVED_CHILD_ITEM: _('Removed component item'), + + SPLIT_FROM_PARENT: _('Split from parent item'), + SPLIT_CHILD_ITEM: _('Split child item'), + + SENT_TO_CUSTOMER: _('Sent to customer'), + RETURNED_FROM_CUSTOMER: _('Returned from customer'), + + BUILD_OUTPUT_CREATED: _('Build order output created'), + BUILD_OUTPUT_COMPLETED: _('Build order output completed'), + + RECEIVED_AGAINST_PURCHASE_ORDER: _('Received against purchase order') + + } + + class BuildStatus(StatusCode): # Build status codes From af53b341f002e981ee3cb9da4e9f0e0d187d45a3 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 11 May 2021 17:17:48 +1000 Subject: [PATCH 02/16] Replace "addTrasactionNote" function with "add_tracking_entry" - Does not add translated strings to the database --- InvenTree/build/models.py | 12 +- InvenTree/order/models.py | 24 +- .../migrations/0060_auto_20210511_1713.py | 28 ++ .../migrations/0061_auto_20210511_0911.py | 59 ++++ InvenTree/stock/models.py | 272 ++++++++++++------ 5 files changed, 299 insertions(+), 96 deletions(-) create mode 100644 InvenTree/stock/migrations/0060_auto_20210511_1713.py create mode 100644 InvenTree/stock/migrations/0061_auto_20210511_0911.py diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index a278b4e17c..c80c0e8523 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -22,7 +22,7 @@ from markdownx.models import MarkdownxField from mptt.models import MPTTModel, TreeForeignKey -from InvenTree.status_codes import BuildStatus, StockStatus +from InvenTree.status_codes import BuildStatus, StockStatus, StockHistoryCode from InvenTree.helpers import increment, getSetting, normalize, MakeBarcode from InvenTree.validators import validate_build_order_reference from InvenTree.models import InvenTreeAttachment @@ -811,6 +811,7 @@ class Build(MPTTModel): # Select the location for the build output location = kwargs.get('location', self.destination) status = kwargs.get('status', StockStatus.OK) + notes = kwargs.get('notes', '') # List the allocated BuildItem objects for the given output allocated_items = output.items_to_install.all() @@ -834,10 +835,13 @@ class Build(MPTTModel): output.save() - output.addTransactionNote( - _('Completed build output'), + output.add_tracking_entry( + StockHistoryCode.BUILD_OUTPUT_COMPLETED, user, - system=True + notes=notes, + deltas={ + 'status': status, + } ) # Increase the completed quantity for this build diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 534775ebaf..8572d0c634 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -28,7 +28,7 @@ from company.models import Company, SupplierPart from InvenTree.fields import RoundingDecimalField from InvenTree.helpers import decimal2string, increment, getSetting -from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockStatus +from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockStatus, StockHistoryCode from InvenTree.models import InvenTreeAttachment @@ -336,10 +336,12 @@ class PurchaseOrder(Order): return self.pending_line_items().count() == 0 @transaction.atomic - def receive_line_item(self, line, location, quantity, user, status=StockStatus.OK, purchase_price=None): + def receive_line_item(self, line, location, quantity, user, status=StockStatus.OK, purchase_price=None, **kwargs): """ Receive a line item (or partial line item) against this PO """ + notes = kwargs.get('notes', '') + if not self.status == PurchaseOrderStatus.PLACED: raise ValidationError({"status": _("Lines can only be received against an order marked as 'Placed'")}) @@ -369,8 +371,22 @@ class PurchaseOrder(Order): text = _("Received items") note = _('Received {n} items against order {name}').format(n=quantity, name=str(self)) - # Add a new transaction note to the newly created stock item - stock.addTransactionNote(text, user, note) + tracking_info = { + 'status': status, + 'purchaseorder': self.pk, + 'quantity': quantity, + } + + if location: + tracking_info['location'] = location.pk + + stock.add_tracking_entry( + StockHistoryCode.RECEIVED_AGAINST_PURCHASE_ORDER, + user, + notes=notes, + url=self.get_absolute_url(), + deltas=tracking_info + ) # Update the number of parts received against the particular line item line.received += quantity diff --git a/InvenTree/stock/migrations/0060_auto_20210511_1713.py b/InvenTree/stock/migrations/0060_auto_20210511_1713.py new file mode 100644 index 0000000000..752b070750 --- /dev/null +++ b/InvenTree/stock/migrations/0060_auto_20210511_1713.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2 on 2021-05-11 07:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0059_auto_20210404_2016'), + ] + + operations = [ + migrations.AddField( + model_name='stockitemtracking', + name='deltas', + field=models.JSONField(blank=True, null=True), + ), + migrations.AddField( + model_name='stockitemtracking', + name='tracking_type', + field=models.IntegerField(default=0), + ), + migrations.AlterField( + model_name='stockitemtracking', + name='title', + field=models.CharField(blank=True, help_text='Tracking entry title', max_length=250, verbose_name='Title'), + ), + ] diff --git a/InvenTree/stock/migrations/0061_auto_20210511_0911.py b/InvenTree/stock/migrations/0061_auto_20210511_0911.py new file mode 100644 index 0000000000..087863c348 --- /dev/null +++ b/InvenTree/stock/migrations/0061_auto_20210511_0911.py @@ -0,0 +1,59 @@ +# Generated by Django 3.2 on 2021-05-10 23:11 + +from django.db import migrations + + +def update_history(apps, schema_editor): + """ + Update each existing StockItemTracking object, + convert the recorded "quantity" to a delta + """ + + StockItem = apps.get_model('stock', 'stockitem') + StockItemTracking = apps.get_model('stock', 'stockitemtracking') + + update_count = 0 + + for item in StockItem.objects.all(): + + history = StockItemTracking.objects.filter(item=item).order_by('date') + + if history.count() == 0: + continue + + quantity = history[0].quantity + + for entry in history: + + q = entry.quantity + + if not q == quantity: + + entry.deltas = { + 'quantity': float(q), + } + + entry.save() + + update_count += 1 + + quantity = q + + print(f"Updated {update_count} StockItemHistory entries") + + +def reverse_update(apps, schema_editor): + """ + """ + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0060_auto_20210511_1713'), + ] + + operations = [ + migrations.RunPython(update_history, reverse_code=reverse_update) + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 3aecfff2c2..408c598141 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -34,7 +34,7 @@ import common.models import report.models import label.models -from InvenTree.status_codes import StockStatus +from InvenTree.status_codes import StockStatus, StockHistoryCode from InvenTree.models import InvenTreeTree, InvenTreeAttachment from InvenTree.fields import InvenTreeURLField @@ -198,14 +198,18 @@ class StockItem(MPTTModel): if add_note: - note = _('Created new stock item for {part}').format(part=str(self.part)) + tracking_info = { + 'quantity': self.quantity, + 'status': self.status, + } - # This StockItem is being saved for the first time - self.addTransactionNote( - _('Created stock item'), + if self.location: + tracking_info['location'] = self.location.pk + + self.add_tracking_entry( + StockHistoryCode.CREATED, user, - note, - system=True + deltas=tracking_info ) @property @@ -610,31 +614,45 @@ class StockItem(MPTTModel): # TODO - Remove any stock item allocations from this stock item - item.addTransactionNote( - _("Assigned to Customer"), + item.add_tracking_entry( + StockHistoryCode.SENT_TO_CUSTOMER, user, - notes=_("Manually assigned to customer {name}").format(name=customer.name), - system=True + { + 'customer': customer.id, + 'customer_name': customer.name, + }, + notes=notes, ) # Return the reference to the stock item return item - def returnFromCustomer(self, location, user=None): + def returnFromCustomer(self, location, user=None, **kwargs): """ Return stock item from customer, back into the specified location. """ - self.addTransactionNote( - _("Returned from customer {name}").format(name=self.customer.name), + notes = kwargs.get('notes', '') + + tracking_info = {} + + if location: + tracking_info['location'] = location.id + tracking_info['location_name'] = location.name + + if self.customer: + tracking_info['customer'] = customer.id + tracking_info['customer_name'] = customer.name + + self.add_tracking_entry( + StockHistoryCode.RETURNED_FROM_CUSTOMER, user, - notes=_("Returned to location {loc}").format(loc=location.name), - system=True + notes=notes, + deltas=tracking_info ) self.customer = None self.location = location - self.sales_order = None self.save() @@ -788,18 +806,25 @@ class StockItem(MPTTModel): stock_item.save() # Add a transaction note to the other item - stock_item.addTransactionNote( - _('Installed into stock item {pk}').format(str(self.pk)), + stock_item.add_tracking_entry( + StockHistoryCode.INSTALLED_INTO_ASSEMBLY, user, notes=notes, - url=self.get_absolute_url() + url=self.get_absolute_url(), + deltas={ + 'assembly': self.pk, + } ) - # Add a transaction note to this item - self.addTransactionNote( - _('Installed stock item {pk}').format(str(stock_item.pk)), - user, notes=notes, - url=stock_item.get_absolute_url() + # Add a transaction note to this item (the assembly) + self.add_tracking_entry( + StockHistoryCode.INSTALLED_CHILD_ITEM, + user, + notes=notes, + url=stock_item.get_absolute_url(), + deltas={ + 'stockitem': stock_item.pk, + } ) @transaction.atomic @@ -820,32 +845,41 @@ class StockItem(MPTTModel): # TODO - Are there any other checks that need to be performed at this stage? # Add a transaction note to the parent item - self.belongs_to.addTransactionNote( - _("Uninstalled stock item {pk}").format(pk=str(self.pk)), + self.belongs_to.add_tracking_entry( + StockHistoryCode.REMOVED_CHILD_ITEM, user, + deltas={ + 'stockitem': self.pk, + }, notes=notes, url=self.get_absolute_url(), ) + tracking_info = { + 'assembly': self.belongs_to.pk + } + + if location: + tracking_info['location'] = location.pk + tracking_info['location_name'] = location.name + url = location.get_absolute_url() + else: + url = '' + + self.add_tracking_entry( + StockHistoryCode.REMOVED_FROM_ASSEMBLY, + user, + notes=notes, + url=url, + deltas=tracking_info + ) + # Mark this stock item as *not* belonging to anyone self.belongs_to = None self.location = location self.save() - if location: - url = location.get_absolute_url() - else: - url = '' - - # Add a transaction note! - self.addTransactionNote( - _('Uninstalled into location {loc}').formaT(loc=str(location)), - user, - notes=notes, - url=url - ) - @property def children(self): """ Return a list of the child items which have been split from this stock item """ @@ -901,24 +935,30 @@ class StockItem(MPTTModel): def has_tracking_info(self): return self.tracking_info_count > 0 - def addTransactionNote(self, title, user, notes='', url='', system=True): - """ Generation a stock transaction note for this item. + def add_tracking_entry(self, entry_type, user, deltas={}, notes='', url=''): + """ + Add a history tracking entry for this StockItem - Brief automated note detailing a movement or quantity change. + Args: + entry_type - Integer code describing the "type" of historical action (see StockHistoryCode) + user - The user performing this action + deltas - A map of the changes made to the model + notes - User notes associated with this tracking entry + url - Optional URL associated with this tracking entry """ - track = StockItemTracking.objects.create( + entry = StockItemTracking.objects.create( item=self, - title=title, + tracking_type=entry_type, user=user, - quantity=self.quantity, - date=datetime.now().date(), + date=datetime.now(), notes=notes, + deltas=deltas, link=url, - system=system + system=True ) - track.save() + entry.save() @transaction.atomic def serializeStock(self, quantity, serials, user, notes='', location=None): @@ -991,10 +1031,17 @@ class StockItem(MPTTModel): new_item.copyTestResultsFrom(self) # Create a new stock tracking item - new_item.addTransactionNote(_('Add serial number'), user, notes=notes) + new_item.add_tracking_entry( + StockHistoryCode.ASSIGNED_SERIAL, + user, + notes=notes, + deltas={ + 'serial': serial, + } + ) # Remove the equivalent number of items - self.take_stock(quantity, user, notes=_('Serialized {n} items').format(n=quantity)) + self.take_stock(quantity, user, notes=notes) @transaction.atomic def copyHistoryFrom(self, other): @@ -1018,7 +1065,7 @@ class StockItem(MPTTModel): result.save() @transaction.atomic - def splitStock(self, quantity, location, user): + def splitStock(self, quantity, location, user, **kwargs): """ Split this stock item into two items, in the same location. Stock tracking notes for this StockItem will be duplicated, and added to the new StockItem. @@ -1032,6 +1079,8 @@ class StockItem(MPTTModel): The new item will have a different StockItem ID, while this will remain the same. """ + notes = kwargs.get('notes', '') + # Do not split a serialized part if self.serialized: return self @@ -1071,17 +1120,20 @@ class StockItem(MPTTModel): new_stock.copyTestResultsFrom(self) # Add a new tracking item for the new stock item - new_stock.addTransactionNote( - _("Split from existing stock"), + new_stock.add_tracking_entry( + StockHistoryCode.SPLIT_FROM_PARENT, user, - _('Split {n} items').format(n=helpers.normalize(quantity)) + notes=notes, + deltas={ + 'stockitem': self.pk, + } ) # Remove the specified quantity from THIS stock item self.take_stock( quantity, user, - f"{_('Split')} {quantity} {_('items into new stock item')}" + notes=notes ) # Return a copy of the "new" stock item @@ -1138,11 +1190,21 @@ class StockItem(MPTTModel): self.location = location - self.addTransactionNote( - msg, + tracking_info = {} + + if location: + tracking_info['location'] = location.pk + url = location.get_absolute_url() + else: + url = '' + + self.add_tracking_entry( + StockHistoryCode.STOCK_MOVE, user, notes=notes, - system=True) + deltas=tracking_info, + url=url, + ) self.save() @@ -1202,13 +1264,13 @@ class StockItem(MPTTModel): if self.updateQuantity(count): - text = _('Counted {n} items').format(n=helpers.normalize(count)) - - self.addTransactionNote( - text, + self.add_tracking_entry( + StockHistoryCode.STOCK_COUNT, user, notes=notes, - system=True + deltas={ + 'quantity': self.quantity, + } ) return True @@ -1234,13 +1296,15 @@ class StockItem(MPTTModel): return False if self.updateQuantity(self.quantity + quantity): - text = _('Added {n} items').format(n=helpers.normalize(quantity)) - self.addTransactionNote( - text, + self.add_tracking_entry( + StockHistoryCode.STOCK_ADD, user, notes=notes, - system=True + deltas={ + 'added': quantity, + 'quantity': self.quantity + } ) return True @@ -1264,12 +1328,15 @@ class StockItem(MPTTModel): if self.updateQuantity(self.quantity - quantity): - text = _('Removed {n1} items').format(n1=helpers.normalize(quantity)) - - self.addTransactionNote(text, - user, - notes=notes, - system=True) + self.add_tracking_entry( + StockHistoryCode.STOCK_REMOVE, + user, + notes=notes, + deltas={ + 'removed': quantity, + 'quantity': self.quantity, + } + ) return True @@ -1527,30 +1594,57 @@ class StockItemAttachment(InvenTreeAttachment): class StockItemTracking(models.Model): - """ Stock tracking entry - breacrumb for keeping track of automated stock transactions + """ + Stock tracking entry - used for tracking history of a particular StockItem + + Note: 2021-05-11 + The legacy StockTrackingItem model contained very litle information about the "history" of the item. + In fact, only the "quantity" of the item was recorded at each interaction. + Also, the "title" was translated at time of generation, and thus was not really translateable. + The "new" system tracks all 'delta' changes to the model, + and tracks change "type" which can then later be translated + Attributes: - item: Link to StockItem + item: ForeignKey reference to a particular StockItem date: Date that this tracking info was created - title: Title of this tracking info (generated by system) + title: Title of this tracking info (legacy, no longer used!) + tracking_type: The type of tracking information notes: Associated notes (input by user) link: Optional URL to external page user: The user associated with this tracking info + deltas: The changes associated with this history item quantity: The StockItem quantity at this point in time """ def get_absolute_url(self): return '/stock/track/{pk}'.format(pk=self.id) - # return reverse('stock-tracking-detail', kwargs={'pk': self.id}) - item = models.ForeignKey(StockItem, on_delete=models.CASCADE, - related_name='tracking_info') + tracking_type = models.IntegerField( + default=StockHistoryCode.LEGACY, + ) + + item = models.ForeignKey( + StockItem, + on_delete=models.CASCADE, + related_name='tracking_info' + ) date = models.DateTimeField(auto_now_add=True, editable=False) - title = models.CharField(blank=False, max_length=250, verbose_name=_('Title'), help_text=_('Tracking entry title')) + title = models.CharField( + blank=True, + max_length=250, + verbose_name=_('Title'), + help_text=_('Tracking entry title') + ) - notes = models.CharField(blank=True, max_length=512, verbose_name=_('Notes'), help_text=_('Entry notes')) + notes = models.CharField( + blank=True, + max_length=512, + verbose_name=_('Notes'), + help_text=_('Entry notes') + ) link = InvenTreeURLField(blank=True, verbose_name=_('Link'), help_text=_('Link to external page for further information')) @@ -1558,13 +1652,15 @@ class StockItemTracking(models.Model): system = models.BooleanField(default=False) - quantity = models.DecimalField(max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], default=1, verbose_name=_('Quantity')) + deltas = models.JSONField(null=True, blank=True) - # TODO - # image = models.ImageField(upload_to=func, max_length=255, null=True, blank=True) - - # TODO - # file = models.FileField() + quantity = models.DecimalField( + max_digits=15, + decimal_places=5, + validators=[MinValueValidator(0)], + default=1, + verbose_name=_('Quantity') + ) def rename_stock_item_test_result_attachment(instance, filename): From 70ad0ba0ded70cdaa5b11748e2c7f1be39d4d68f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 11 May 2021 17:41:17 +1000 Subject: [PATCH 03/16] Improve reporting in stock history API --- InvenTree/order/models.py | 3 --- InvenTree/stock/admin.py | 2 +- InvenTree/stock/api.py | 2 +- InvenTree/stock/models.py | 7 +++++++ InvenTree/stock/serializers.py | 11 +++++++++-- 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 8572d0c634..9a521b8044 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -368,9 +368,6 @@ class PurchaseOrder(Order): stock.save() - text = _("Received items") - note = _('Received {n} items against order {name}').format(n=quantity, name=str(self)) - tracking_info = { 'status': status, 'purchaseorder': self.pk, diff --git a/InvenTree/stock/admin.py b/InvenTree/stock/admin.py index 9fed3e53a4..f32fa008a0 100644 --- a/InvenTree/stock/admin.py +++ b/InvenTree/stock/admin.py @@ -130,7 +130,7 @@ class StockAttachmentAdmin(admin.ModelAdmin): class StockTrackingAdmin(ImportExportModelAdmin): - list_display = ('item', 'date', 'title') + list_display = ('item', 'date', 'label') class StockItemTestResultAdmin(admin.ModelAdmin): diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index b70b379e69..c7914827e7 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -965,7 +965,7 @@ class StockItemTestResultList(generics.ListCreateAPIView): test_result.save() -class StockTrackingList(generics.ListCreateAPIView): +class StockTrackingList(generics.ListAPIView): """ API endpoint for list view of StockItemTracking objects. StockItemTracking objects are read-only diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 408c598141..7d55d26f4d 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -1620,6 +1620,13 @@ class StockItemTracking(models.Model): def get_absolute_url(self): return '/stock/track/{pk}'.format(pk=self.id) + def label(self): + + if self.tracking_type in StockHistoryCode.keys(): + return StockHistoryCode.label(self.tracking_type) + else: + return self.title + tracking_type = models.IntegerField( default=StockHistoryCode.LEGACY, ) diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index 4991a44e6f..c5ad3c7100 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -351,10 +351,14 @@ class StockTrackingSerializer(InvenTreeModelSerializer): url = serializers.CharField(source='get_absolute_url', read_only=True) + label = serializers.CharField(read_only=True) + item_detail = StockItemSerializerBrief(source='item', many=False, read_only=True) user_detail = UserSerializerBrief(source='user', many=False, read_only=True) + deltas = serializers.JSONField(read_only=True) + class Meta: model = StockItemTracking fields = [ @@ -363,10 +367,13 @@ class StockTrackingSerializer(InvenTreeModelSerializer): 'item', 'item_detail', 'date', - 'title', - 'notes', + 'deltas', + 'label', 'link', + 'notes', 'quantity', + 'title', + 'tracking_type', 'user', 'user_detail', 'system', From 1126e2e1106625c7c25e9ad932e5648bc1418a13 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 11 May 2021 17:41:37 +1000 Subject: [PATCH 04/16] "Guess" stock history code from recorded title string --- .../migrations/0061_auto_20210511_0911.py | 66 +++++++++++++++++-- 1 file changed, 62 insertions(+), 4 deletions(-) diff --git a/InvenTree/stock/migrations/0061_auto_20210511_0911.py b/InvenTree/stock/migrations/0061_auto_20210511_0911.py index 087863c348..0a11fe1fae 100644 --- a/InvenTree/stock/migrations/0061_auto_20210511_0911.py +++ b/InvenTree/stock/migrations/0061_auto_20210511_0911.py @@ -2,6 +2,8 @@ from django.db import migrations +from InvenTree.status_codes import StockHistoryCode + def update_history(apps, schema_editor): """ @@ -24,6 +26,8 @@ def update_history(apps, schema_editor): quantity = history[0].quantity for entry in history: + + updated = False q = entry.quantity @@ -33,13 +37,67 @@ def update_history(apps, schema_editor): 'quantity': float(q), } - entry.save() - - update_count += 1 + updated = True quantity = q - print(f"Updated {update_count} StockItemHistory entries") + # Try to "guess" the "type" of tracking entry, based on the title + title = entry.title.lower() + + tracking_type = None + + if 'completed build' in title: + tracking_type = StockHistoryCode.BUILD_OUTPUT_COMPLETED + + elif 'removed' in title and 'item' in title: + tracking_type = StockHistoryCode.STOCK_REMOVE + + elif 'split from existing' in title: + tracking_type = StockHistoryCode.SPLIT_FROM_PARENT + + elif 'moved to' in title: + tracking_type = StockHistoryCode.STOCK_MOVE + + elif 'created stock item' in title: + tracking_type = StockHistoryCode.CREATED + + elif 'add serial number' in title: + tracking_type = StockHistoryCode.ASSIGNED_SERIAL + + elif 'returned from customer' in title: + tracking_type = StockHistoryCode.RETURNED_FROM_CUSTOMER + + elif 'counted' in title: + tracking_type = StockHistoryCode.STOCK_COUNT + + elif 'added' in title: + tracking_type = StockHistoryCode.STOCK_ADD + + elif 'assigned to customer' in title: + tracking_type = StockHistoryCode.SENT_TO_CUSTOMER + + elif 'installed into stock item' in title: + tracking_type = StockHistoryCode.INSTALLED_INTO_ASSEMBLY + + elif 'uninstalled into location' in title: + tracking_type = StockHistoryCode.REMOVED_FROM_ASSEMBLY + + elif 'installed stock item' in title: + tracking_type = StockHistoryCode.INSTALLED_CHILD_ITEM + + elif 'received items' in title: + tracking_type = StockHistoryCode.RECEIVED_AGAINST_PURCHASE_ORDER + + if tracking_type is not None: + entry.tracking_type = tracking_type + updated = True + + if updated: + entry.save() + update_count += 1 + + + print(f"\n==========================\nUpdated {update_count} StockItemHistory entries") def reverse_update(apps, schema_editor): From 725a64c29dff06950298762124e6b8e49743cecd Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 11 May 2021 18:11:27 +1000 Subject: [PATCH 05/16] Extract more information from legacy tracking data --- .../migrations/0061_auto_20210511_0911.py | 43 +++++++++++++++++-- InvenTree/stock/serializers.py | 6 +-- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/InvenTree/stock/migrations/0061_auto_20210511_0911.py b/InvenTree/stock/migrations/0061_auto_20210511_0911.py index 0a11fe1fae..b3bcb5b88e 100644 --- a/InvenTree/stock/migrations/0061_auto_20210511_0911.py +++ b/InvenTree/stock/migrations/0061_auto_20210511_0911.py @@ -1,5 +1,7 @@ # Generated by Django 3.2 on 2021-05-10 23:11 +import re + from django.db import migrations from InvenTree.status_codes import StockHistoryCode @@ -27,17 +29,19 @@ def update_history(apps, schema_editor): for entry in history: + deltas = {} updated = False q = entry.quantity if not q == quantity: - entry.deltas = { - 'quantity': float(q), - } + try: + deltas['quantity']: float(q) + updated = True + except: + print(f"WARNING: Error converting quantity '{q}'") - updated = True quantity = q @@ -51,6 +55,21 @@ def update_history(apps, schema_editor): elif 'removed' in title and 'item' in title: tracking_type = StockHistoryCode.STOCK_REMOVE + + # Extract the number of removed items + result = re.search("^removed ([\d\.]+) items$", title) + + if result: + + removed = result.groups()[0] + + try: + deltas['removed'] = float(removed) + + # Ensure that 'quantity' is stored too in this case + deltas['quantity'] = float(q) + except: + print(f"WARNING: Error converting removed quantity '{removed}'") elif 'split from existing' in title: tracking_type = StockHistoryCode.SPLIT_FROM_PARENT @@ -73,6 +92,21 @@ def update_history(apps, schema_editor): elif 'added' in title: tracking_type = StockHistoryCode.STOCK_ADD + # Extract the number of added items + result = re.search("^added ([\d\.]+) items$", title) + + if result: + + added = result.groups()[0] + + try: + deltas['added'] = float(added) + + # Ensure that 'quantity' is stored too in this case + deltas['quantity'] = float(q) + except: + print(f"WARNING: Error converting added quantity '{added}'") + elif 'assigned to customer' in title: tracking_type = StockHistoryCode.SENT_TO_CUSTOMER @@ -93,6 +127,7 @@ def update_history(apps, schema_editor): updated = True if updated: + entry.deltas = deltas entry.save() update_count += 1 diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index c5ad3c7100..d46dc7b3dc 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -349,8 +349,6 @@ class StockTrackingSerializer(InvenTreeModelSerializer): if user_detail is not True: self.fields.pop('user_detail') - url = serializers.CharField(source='get_absolute_url', read_only=True) - label = serializers.CharField(read_only=True) item_detail = StockItemSerializerBrief(source='item', many=False, read_only=True) @@ -363,7 +361,6 @@ class StockTrackingSerializer(InvenTreeModelSerializer): model = StockItemTracking fields = [ 'pk', - 'url', 'item', 'item_detail', 'date', @@ -376,7 +373,6 @@ class StockTrackingSerializer(InvenTreeModelSerializer): 'tracking_type', 'user', 'user_detail', - 'system', ] read_only_fields = [ @@ -384,4 +380,6 @@ class StockTrackingSerializer(InvenTreeModelSerializer): 'user', 'system', 'quantity', + 'label', + 'tracking_type', ] From ed4da4d3380f5fea7de037a51ef8d778f2de15d4 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 11 May 2021 18:23:29 +1000 Subject: [PATCH 06/16] Improve introspection in migration file --- .../stock/migrations/0061_auto_20210511_0911.py | 17 ++++++++++++++--- InvenTree/stock/models.py | 11 +++-------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/InvenTree/stock/migrations/0061_auto_20210511_0911.py b/InvenTree/stock/migrations/0061_auto_20210511_0911.py index b3bcb5b88e..2c264aac1d 100644 --- a/InvenTree/stock/migrations/0061_auto_20210511_0911.py +++ b/InvenTree/stock/migrations/0061_auto_20210511_0911.py @@ -54,10 +54,14 @@ def update_history(apps, schema_editor): tracking_type = StockHistoryCode.BUILD_OUTPUT_COMPLETED elif 'removed' in title and 'item' in title: - tracking_type = StockHistoryCode.STOCK_REMOVE + + if entry.notes.lower().startswith('split '): + tracking_type = StockHistoryCode.SPLIT_CHILD_ITEM + else: + tracking_type = StockHistoryCode.STOCK_REMOVE # Extract the number of removed items - result = re.search("^removed ([\d\.]+) items$", title) + result = re.search("^removed ([\d\.]+) items", title) if result: @@ -70,9 +74,13 @@ def update_history(apps, schema_editor): deltas['quantity'] = float(q) except: print(f"WARNING: Error converting removed quantity '{removed}'") + else: + print(f"Could not decode '{title}'") elif 'split from existing' in title: tracking_type = StockHistoryCode.SPLIT_FROM_PARENT + + deltas['quantity'] = float(q) elif 'moved to' in title: tracking_type = StockHistoryCode.STOCK_MOVE @@ -93,7 +101,7 @@ def update_history(apps, schema_editor): tracking_type = StockHistoryCode.STOCK_ADD # Extract the number of added items - result = re.search("^added ([\d\.]+) items$", title) + result = re.search("^added ([\d\.]+) items", title) if result: @@ -107,6 +115,9 @@ def update_history(apps, schema_editor): except: print(f"WARNING: Error converting added quantity '{added}'") + else: + print(f"Could not decode '{title}'") + elif 'assigned to customer' in title: tracking_type = StockHistoryCode.SENT_TO_CUSTOMER diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 7d55d26f4d..613882ba1a 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -641,8 +641,8 @@ class StockItem(MPTTModel): tracking_info['location_name'] = location.name if self.customer: - tracking_info['customer'] = customer.id - tracking_info['customer_name'] = customer.name + tracking_info['customer'] = self.customer.id + tracking_info['customer_name'] = self.customer.name self.add_tracking_entry( StockHistoryCode.RETURNED_FROM_CUSTOMER, @@ -849,7 +849,7 @@ class StockItem(MPTTModel): StockHistoryCode.REMOVED_CHILD_ITEM, user, deltas={ - 'stockitem': self.pk, + 'stockitem': self.pk, }, notes=notes, url=self.get_absolute_url(), @@ -1183,11 +1183,6 @@ class StockItem(MPTTModel): return True - if self.location: - msg = _("Moved to {loc_new} (from {loc_old})").format(loc_new=str(location), loc_old=str(self.location)) - else: - msg = _('Moved to {loc_new}').format(loc_new=str(location)) - self.location = location tracking_info = {} From 385131137f855b810f7200b917288cfde0a2bace Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 11 May 2021 20:55:14 +1000 Subject: [PATCH 07/16] Extract stock location from legacy stocktracking entry - Not 100% accurate - e.g. if the stock location has been altered! --- .../migrations/0061_auto_20210511_0911.py | 63 ++++++++++++++++++- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/InvenTree/stock/migrations/0061_auto_20210511_0911.py b/InvenTree/stock/migrations/0061_auto_20210511_0911.py index 2c264aac1d..32cd96d71c 100644 --- a/InvenTree/stock/migrations/0061_auto_20210511_0911.py +++ b/InvenTree/stock/migrations/0061_auto_20210511_0911.py @@ -15,9 +15,26 @@ def update_history(apps, schema_editor): StockItem = apps.get_model('stock', 'stockitem') StockItemTracking = apps.get_model('stock', 'stockitemtracking') + StockLocation = apps.get_model('stock', 'stocklocation') update_count = 0 + locations = StockLocation.objects.all() + + for location in locations: + # Pre-calculate pathstring + # Note we cannot use the 'pathstring' function here as we don't have access to model functions! + + path = [location.name] + + loc = location + + while loc.parent: + loc = loc.parent + path = [loc.name] + path + + location._path = '/'.join(path) + for item in StockItem.objects.all(): history = StockItemTracking.objects.filter(item=item).order_by('date') @@ -27,14 +44,14 @@ def update_history(apps, schema_editor): quantity = history[0].quantity - for entry in history: + for idx, entry in enumerate(history): deltas = {} updated = False q = entry.quantity - if not q == quantity: + if idx == 0 or not q == quantity: try: deltas['quantity']: float(q) @@ -43,7 +60,7 @@ def update_history(apps, schema_editor): print(f"WARNING: Error converting quantity '{q}'") - quantity = q + quantity = q # Try to "guess" the "type" of tracking entry, based on the title title = entry.title.lower() @@ -84,6 +101,46 @@ def update_history(apps, schema_editor): elif 'moved to' in title: tracking_type = StockHistoryCode.STOCK_MOVE + + result = re.search('^Moved to (.*)( - )*(.*) \(from.*$', entry.title) + + if result: + # Legacy tracking entries recorded the location in multiple ways, because.. why not? + text = result.groups()[0] + + matches = set() + + for location in locations: + + # Direct match for pathstring + if text == location._path: + matches.add(location) + + # Direct match for name + if text == location.name: + matches.add(location) + + # Match for "name - description" + compare = f"{location.name} - {location.description}" + + if text == compare: + matches.add(location) + + # Match for "pathstring - description" + compare = f"{location._path} - {location.description}" + + if text == compare: + matches.add(location) + + if len(matches) == 1: + location = list(matches)[0] + + deltas['location'] = location.pk + deltas['location_path'] = location._path + + else: + print(f"No location match: '{text}'") + break elif 'created stock item' in title: tracking_type = StockHistoryCode.CREATED From 0020e85397622aadedad91b56dc2e2b2b4cfbc5f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 11 May 2021 20:56:20 +1000 Subject: [PATCH 08/16] Refactor add_tracking_entry --- InvenTree/order/models.py | 10 +++--- InvenTree/stock/models.py | 66 +++++++++++++++++++++------------------ 2 files changed, 39 insertions(+), 37 deletions(-) diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 9a521b8044..e5a26386ac 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -370,19 +370,17 @@ class PurchaseOrder(Order): tracking_info = { 'status': status, - 'purchaseorder': self.pk, - 'quantity': quantity, } - if location: - tracking_info['location'] = location.pk - stock.add_tracking_entry( StockHistoryCode.RECEIVED_AGAINST_PURCHASE_ORDER, user, notes=notes, url=self.get_absolute_url(), - deltas=tracking_info + deltas=tracking_info, + location=location, + purchaseorder=self, + quantity=quantity ) # Update the number of parts received against the particular line item diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 613882ba1a..b9188979b7 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -199,17 +199,15 @@ class StockItem(MPTTModel): if add_note: tracking_info = { - 'quantity': self.quantity, 'status': self.status, } - if self.location: - tracking_info['location'] = self.location.pk - self.add_tracking_entry( StockHistoryCode.CREATED, user, - deltas=tracking_info + deltas=tracking_info, + location=self.location, + quantity=self.quantity, ) @property @@ -636,10 +634,6 @@ class StockItem(MPTTModel): tracking_info = {} - if location: - tracking_info['location'] = location.id - tracking_info['location_name'] = location.name - if self.customer: tracking_info['customer'] = self.customer.id tracking_info['customer_name'] = self.customer.name @@ -648,7 +642,8 @@ class StockItem(MPTTModel): StockHistoryCode.RETURNED_FROM_CUSTOMER, user, notes=notes, - deltas=tracking_info + deltas=tracking_info, + location=location ) self.customer = None @@ -856,22 +851,15 @@ class StockItem(MPTTModel): ) tracking_info = { - 'assembly': self.belongs_to.pk + 'stockitem': self.belongs_to.pk } - if location: - tracking_info['location'] = location.pk - tracking_info['location_name'] = location.name - url = location.get_absolute_url() - else: - url = '' - self.add_tracking_entry( StockHistoryCode.REMOVED_FROM_ASSEMBLY, user, notes=notes, - url=url, - deltas=tracking_info + deltas=tracking_info, + location=location, ) # Mark this stock item as *not* belonging to anyone @@ -935,7 +923,7 @@ class StockItem(MPTTModel): def has_tracking_info(self): return self.tracking_info_count > 0 - def add_tracking_entry(self, entry_type, user, deltas={}, notes='', url=''): + def add_tracking_entry(self, entry_type, user, deltas={}, notes='', url='', **kwargs): """ Add a history tracking entry for this StockItem @@ -947,6 +935,25 @@ class StockItem(MPTTModel): url - Optional URL associated with this tracking entry """ + # Has a location been specified? + location = kwargs.get('location', None) + + if location: + deltas['location'] = location.id + deltas['location_path'] = location.pathstring + + # Has a PurchaseOrder been specified? + po = kwargs.get('purchaseorder', None) + + if po: + deltas['purchaseorder'] = po.id + + # Quantity specified? + quantity = kwargs.get('quantity', None) + + if quantity: + deltas['quantity'] = float(quantity) + entry = StockItemTracking.objects.create( item=self, tracking_type=entry_type, @@ -1037,7 +1044,8 @@ class StockItem(MPTTModel): notes=notes, deltas={ 'serial': serial, - } + }, + location=location ) # Remove the equivalent number of items @@ -1126,7 +1134,8 @@ class StockItem(MPTTModel): notes=notes, deltas={ 'stockitem': self.pk, - } + }, + location=location, ) # Remove the specified quantity from THIS stock item @@ -1187,18 +1196,12 @@ class StockItem(MPTTModel): tracking_info = {} - if location: - tracking_info['location'] = location.pk - url = location.get_absolute_url() - else: - url = '' - self.add_tracking_entry( StockHistoryCode.STOCK_MOVE, user, notes=notes, deltas=tracking_info, - url=url, + location=location, ) self.save() @@ -1306,7 +1309,8 @@ class StockItem(MPTTModel): @transaction.atomic def take_stock(self, quantity, user, notes=''): - """ Remove items from stock + """ + Remove items from stock """ # Cannot remove items from a serialized part From a1203aa1e56c4e4406f044b6f491107f83fb6ff1 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 11 May 2021 21:22:54 +1000 Subject: [PATCH 09/16] Fixes for unit testing --- InvenTree/stock/models.py | 14 +++++++------- InvenTree/stock/tests.py | 11 ++++++----- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index b9188979b7..277d5166ee 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -207,7 +207,7 @@ class StockItem(MPTTModel): user, deltas=tracking_info, location=self.location, - quantity=self.quantity, + quantity=float(self.quantity), ) @property @@ -977,7 +977,7 @@ class StockItem(MPTTModel): Args: quantity: Number of items to serialize (integer) - serials: List of serial numbers (list) + serials: List of serial numbers user: User object associated with action notes: Optional notes for tracking location: If specified, serialized items will be placed in the given location @@ -1267,7 +1267,7 @@ class StockItem(MPTTModel): user, notes=notes, deltas={ - 'quantity': self.quantity, + 'quantity': float(self.quantity), } ) @@ -1300,8 +1300,8 @@ class StockItem(MPTTModel): user, notes=notes, deltas={ - 'added': quantity, - 'quantity': self.quantity + 'added': float(quantity), + 'quantity': float(self.quantity), } ) @@ -1332,8 +1332,8 @@ class StockItem(MPTTModel): user, notes=notes, deltas={ - 'removed': quantity, - 'quantity': self.quantity, + 'removed': float(quantity), + 'quantity': float(self.quantity), } ) diff --git a/InvenTree/stock/tests.py b/InvenTree/stock/tests.py index 08fa727547..6bc15b3505 100644 --- a/InvenTree/stock/tests.py +++ b/InvenTree/stock/tests.py @@ -5,6 +5,8 @@ from django.core.exceptions import ValidationError import datetime +from InvenTree.status_codes import StockHistoryCode + from .models import StockLocation, StockItem, StockItemTracking from .models import StockItemTestResult @@ -217,7 +219,7 @@ class StockTest(TestCase): track = StockItemTracking.objects.filter(item=it).latest('id') self.assertEqual(track.item, it) - self.assertIn('Moved to', track.title) + self.assertEqual(track.tracking_type, StockHistoryCode.STOCK_MOVE) self.assertEqual(track.notes, 'Moved to the bathroom') def test_self_move(self): @@ -284,8 +286,7 @@ class StockTest(TestCase): # Check that a tracking item was added track = StockItemTracking.objects.filter(item=it).latest('id') - self.assertIn('Counted', track.title) - self.assertIn('items', track.title) + self.assertEqual(track.tracking_type, StockHistoryCode.STOCK_COUNT) self.assertIn('Counted items', track.notes) n = it.tracking_info.count() @@ -304,7 +305,7 @@ class StockTest(TestCase): # Check that a tracking item was added track = StockItemTracking.objects.filter(item=it).latest('id') - self.assertIn('Added', track.title) + self.assertEqual(track.tracking_type, StockHistoryCode.STOCK_ADD) self.assertIn('Added some items', track.notes) self.assertFalse(it.add_stock(-10, None)) @@ -319,7 +320,7 @@ class StockTest(TestCase): # Check that a tracking item was added track = StockItemTracking.objects.filter(item=it).latest('id') - self.assertIn('Removed', track.title) + self.assertEqual(track.tracking_type, StockHistoryCode.STOCK_REMOVE) self.assertIn('Removed some items', track.notes) self.assertTrue(it.has_tracking_info) From 93e6eb6dc74b68708c753f71936e2913ff49d359 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 11 May 2021 22:26:03 +1000 Subject: [PATCH 10/16] Unit test fixes --- InvenTree/order/models.py | 2 +- .../migrations/0061_auto_20210511_0911.py | 1 - .../migrations/0062_auto_20210511_2151.py | 23 +++++++++++++++++++ InvenTree/stock/models.py | 19 ++++----------- 4 files changed, 28 insertions(+), 17 deletions(-) create mode 100644 InvenTree/stock/migrations/0062_auto_20210511_2151.py diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index e5a26386ac..ddb1f5ceea 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -370,13 +370,13 @@ class PurchaseOrder(Order): tracking_info = { 'status': status, + 'purchaseorder': self.pk, } stock.add_tracking_entry( StockHistoryCode.RECEIVED_AGAINST_PURCHASE_ORDER, user, notes=notes, - url=self.get_absolute_url(), deltas=tracking_info, location=location, purchaseorder=self, diff --git a/InvenTree/stock/migrations/0061_auto_20210511_0911.py b/InvenTree/stock/migrations/0061_auto_20210511_0911.py index 32cd96d71c..0ab37250c8 100644 --- a/InvenTree/stock/migrations/0061_auto_20210511_0911.py +++ b/InvenTree/stock/migrations/0061_auto_20210511_0911.py @@ -136,7 +136,6 @@ def update_history(apps, schema_editor): location = list(matches)[0] deltas['location'] = location.pk - deltas['location_path'] = location._path else: print(f"No location match: '{text}'") diff --git a/InvenTree/stock/migrations/0062_auto_20210511_2151.py b/InvenTree/stock/migrations/0062_auto_20210511_2151.py new file mode 100644 index 0000000000..18832819ff --- /dev/null +++ b/InvenTree/stock/migrations/0062_auto_20210511_2151.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2 on 2021-05-11 11:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0061_auto_20210511_0911'), + ] + + operations = [ + migrations.AlterField( + model_name='stockitemtracking', + name='notes', + field=models.CharField(blank=True, help_text='Entry notes', max_length=512, null=True, verbose_name='Notes'), + ), + migrations.AlterField( + model_name='stockitemtracking', + name='title', + field=models.CharField(blank=True, help_text='Tracking entry title', max_length=250, null=True, verbose_name='Title'), + ), + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 277d5166ee..ac870fad75 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -805,9 +805,8 @@ class StockItem(MPTTModel): StockHistoryCode.INSTALLED_INTO_ASSEMBLY, user, notes=notes, - url=self.get_absolute_url(), deltas={ - 'assembly': self.pk, + 'stockitem': self.pk, } ) @@ -816,7 +815,6 @@ class StockItem(MPTTModel): StockHistoryCode.INSTALLED_CHILD_ITEM, user, notes=notes, - url=stock_item.get_absolute_url(), deltas={ 'stockitem': stock_item.pk, } @@ -847,7 +845,6 @@ class StockItem(MPTTModel): 'stockitem': self.pk, }, notes=notes, - url=self.get_absolute_url(), ) tracking_info = { @@ -923,7 +920,7 @@ class StockItem(MPTTModel): def has_tracking_info(self): return self.tracking_info_count > 0 - def add_tracking_entry(self, entry_type, user, deltas={}, notes='', url='', **kwargs): + def add_tracking_entry(self, entry_type, user, deltas={}, notes='', **kwargs): """ Add a history tracking entry for this StockItem @@ -940,13 +937,6 @@ class StockItem(MPTTModel): if location: deltas['location'] = location.id - deltas['location_path'] = location.pathstring - - # Has a PurchaseOrder been specified? - po = kwargs.get('purchaseorder', None) - - if po: - deltas['purchaseorder'] = po.id # Quantity specified? quantity = kwargs.get('quantity', None) @@ -961,7 +951,6 @@ class StockItem(MPTTModel): date=datetime.now(), notes=notes, deltas=deltas, - link=url, system=True ) @@ -1639,14 +1628,14 @@ class StockItemTracking(models.Model): date = models.DateTimeField(auto_now_add=True, editable=False) title = models.CharField( - blank=True, + blank=True, null=True, max_length=250, verbose_name=_('Title'), help_text=_('Tracking entry title') ) notes = models.CharField( - blank=True, + blank=True, null=True, max_length=512, verbose_name=_('Notes'), help_text=_('Entry notes') From 0c19a94f5c060a82a4f3dde495ed2197d8c7cb51 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 11 May 2021 22:50:58 +1000 Subject: [PATCH 11/16] Add run-time addition of extra data to the API --- InvenTree/stock/api.py | 58 ++++++++++- InvenTree/templates/js/stock.js | 175 ++++++++++++++++++++++++-------- 2 files changed, 192 insertions(+), 41 deletions(-) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index c7914827e7..3443c9982f 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -22,7 +22,10 @@ from part.models import Part, PartCategory from part.serializers import PartBriefSerializer from company.models import SupplierPart -from company.serializers import SupplierPartSerializer +from company.serializers import CompanySerializer, SupplierPartSerializer + +from order.models import PurchaseOrder +from order.serializers import POSerializer import common.settings import common.models @@ -992,6 +995,59 @@ class StockTrackingList(generics.ListAPIView): return self.serializer_class(*args, **kwargs) + def list(self, request, *args, **kwargs): + + queryset = self.filter_queryset(self.get_queryset()) + + serializer = self.get_serializer(queryset, many=True) + + data = serializer.data + + # Attempt to add extra context information to the historical data + for item in data: + deltas = item['deltas'] + + # Add location detail + if 'location' in deltas: + try: + location = StockLocation.objects.get(pk=deltas['location']) + serializer = LocationSerializer(location) + deltas['location_detail'] = serializer.data + except: + pass + + # Add stockitem detail + if 'stockitem' in deltas: + try: + stockitem = StockItem.objects.get(pk=deltas['stockitem']) + serializer = StockItemSerializer(stockitem) + deltas['stockitem_detail'] = serializer.data + except: + pass + + # Add customer detail + if 'customer' in deltas: + try: + customer = Company.objects.get(pk=deltas['customer']) + serializer = CompanySerializer(location) + deltas['customer_detail'] = serializer.data + except: + pass + + # Add purchaseorder detail + if 'purchaseorder' in deltas: + try: + order = PurchaseOrder.objects.get(pk=deltas['purchaseorder']) + serializer = POSerializer(order) + deltas['purchaseorder_detail'] = serializer.data + except: + pass + + if request.is_ajax(): + return JsonResponse(data, safe=False) + else: + return Response(data) + def create(self, request, *args, **kwargs): """ Create a new StockItemTracking object diff --git a/InvenTree/templates/js/stock.js b/InvenTree/templates/js/stock.js index 4e5d78d8cd..f3f1c7a6bd 100644 --- a/InvenTree/templates/js/stock.js +++ b/InvenTree/templates/js/stock.js @@ -976,42 +976,28 @@ function loadStockLocationTable(table, options) { function loadStockTrackingTable(table, options) { - var cols = [ - { - field: 'pk', - visible: false, - }, - { - field: 'date', - title: '{% trans "Date" %}', - sortable: true, - formatter: function(value, row, index, field) { - var m = moment(value); - if (m.isValid()) { - var html = m.format('dddd MMMM Do YYYY'); // + '
' + m.format('h:mm a'); - return html; - } + var cols = []; - return 'N/A'; - } - }, - ]; + // Date + cols.push({ + field: 'date', + title: '{% trans "Date" %}', + sortable: true, + formatter: function(value, row, index, field) { + var m = moment(value); - // If enabled, provide a link to the referenced StockItem - if (options.partColumn) { - cols.push({ - field: 'item', - title: '{% trans "Stock Item" %}', - sortable: true, - formatter: function(value, row, index, field) { - return renderLink(value.part_name, value.url); + if (m.isValid()) { + var html = m.format('dddd MMMM Do YYYY'); // + '
' + m.format('h:mm a'); + return html; } - }); - } + + return '{% trans "Invalid date" %}'; + } + }); // Stock transaction description cols.push({ - field: 'title', + field: 'label', title: '{% trans "Description" %}', formatter: function(value, row, index, field) { var html = "" + value + ""; @@ -1020,20 +1006,129 @@ function loadStockTrackingTable(table, options) { html += "
" + row.notes + ""; } - if (row.link) { - html += "
" + row.link + ""; - } - return html; } }); + // Stock transaction details cols.push({ - field: 'quantity', - title: '{% trans "Quantity" %}', - formatter: function(value, row, index, field) { - return parseFloat(value); - }, + field: 'deltas', + title: '{% trans "Details" %}', + formatter: function(details, row, index, field) { + var html = ``; + + // Location information + if (details.location) { + + html += ``; + + html += ''; + } + + // Purchase Order Information + if (details.purchaseorder) { + + html += `'; + } + + // Customer information + if (details.customer) { + + html += `'; + } + + // Stockitem information + if (details.stockitem) { + html += ''; + } + + // Status information + if (details.status) { + + } + + // Quantity information + if (details.added) { + html += ''; + + html += ``; + + html += ''; + } + + if (details.removed) { + html += ''; + + html += ``; + + html += ''; + } + + if (details.quantity) { + html += ''; + + html += ``; + + html += ''; + } + + html += '
{% trans "Location" %}'; + + if (details.location_detail) { + // A valid location is provided + + html += renderLink( + details.location_detail.pathstring, + details.location_detail.url, + ); + } else { + // An invalid location (may have been deleted?) + html += `{% trans "Location no longer exists" %}`; + } + + html += '
{% trans "Purchase Order" %}`; + + html += ''; + + if (details.purchaseorder_detail) { + html += renderLink( + details.purchaseorder_detail.reference, + `/order/purchase-order/${details.purchaseorder}/` + ); + } else { + html += `{% trans "Purchase order no longer exists" %}`; + } + + html += '
{% trans "Customer" %}`; + + html += ''; + + if (details.customer_detail) { + html += renderLink( + details.customer_detail.name, + details.customer_detail.url + ); + } else { + html += `{% trans "Customer no longer exists" %}`; + } + + html += '
{% trans "Stock Item" %}'; + + html += ''; + + if (details.stockitem_detail) { + html += renderLink( + details.stockitem, + `/stock/item/${details.stockitem}/` + ); + } else { + html += `{% trans "Stock item no longer exists" %}`; + } + + html += '
{% trans "Added" %}${details.added}
{% trans "Removed" %}${details.removed}
{% trans "Quantity" %}${details.quantity}
'; + + return html; + } }); cols.push({ @@ -1056,7 +1151,7 @@ function loadStockTrackingTable(table, options) { sortable: false, formatter: function(value, row, index, field) { // Manually created entries can be edited or deleted - if (!row.system) { + if (false && !row.system) { var bEdit = ""; var bDel = ""; From 84bfffd5a716531956607006805031b617d62964 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 11 May 2021 22:51:29 +1000 Subject: [PATCH 12/16] Override 'add_note' when new StockItem is created - This allows us to prevent the issue of duplicate notes being created --- InvenTree/order/models.py | 2 +- InvenTree/stock/models.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index ddb1f5ceea..5305038b4f 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -366,7 +366,7 @@ class PurchaseOrder(Order): purchase_price=purchase_price, ) - stock.save() + stock.save(add_note=False) tracking_info = { 'status': status, diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index ac870fad75..4076b27c97 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -183,9 +183,12 @@ class StockItem(MPTTModel): self.validate_unique() self.clean() + # If 'add_note = False' specified, then no tracking note will be added for item creation + add_note = kwargs.pop('add_note', True) + if not self.pk: # StockItem has not yet been saved - add_note = True + add_note = add_note and True else: # StockItem has already been saved add_note = False From 03a231bffb2e5391bba1f3c34db145f3c8233764 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 11 May 2021 23:38:26 +1000 Subject: [PATCH 13/16] Updates - Add StockHistoryCode to custom context - Add simple form for editing stock item history - Add tracking entry when stock status is changed --- InvenTree/InvenTree/context.py | 2 ++ InvenTree/InvenTree/status_codes.py | 2 ++ InvenTree/stock/api.py | 28 ++++++++++----- InvenTree/stock/forms.py | 12 +++++++ InvenTree/stock/models.py | 35 ++++++++++++++++--- .../stock/templates/stock/item_base.html | 19 ++++++++++ InvenTree/stock/urls.py | 3 +- InvenTree/stock/views.py | 10 ++++++ InvenTree/templates/js/stock.js | 13 +++++++ InvenTree/templates/js/table_filters.js | 1 + InvenTree/templates/status_codes.html | 10 ++++-- 11 files changed, 119 insertions(+), 16 deletions(-) diff --git a/InvenTree/InvenTree/context.py b/InvenTree/InvenTree/context.py index 669b55b0c0..3e1f98ffc2 100644 --- a/InvenTree/InvenTree/context.py +++ b/InvenTree/InvenTree/context.py @@ -6,6 +6,7 @@ Provides extra global data to all templates. from InvenTree.status_codes import SalesOrderStatus, PurchaseOrderStatus from InvenTree.status_codes import BuildStatus, StockStatus +from InvenTree.status_codes import StockHistoryCode import InvenTree.status @@ -65,6 +66,7 @@ def status_codes(request): 'PurchaseOrderStatus': PurchaseOrderStatus, 'BuildStatus': BuildStatus, 'StockStatus': StockStatus, + 'StockHistoryCode': StockHistoryCode, } diff --git a/InvenTree/InvenTree/status_codes.py b/InvenTree/InvenTree/status_codes.py index 53b747a9ad..63fc8a491c 100644 --- a/InvenTree/InvenTree/status_codes.py +++ b/InvenTree/InvenTree/status_codes.py @@ -7,6 +7,8 @@ class StatusCode: This is used to map a set of integer values to text. """ + colors = {} + @classmethod def render(cls, key, large=False): """ diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 3443c9982f..b0b6ae1c24 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -21,7 +21,7 @@ from .models import StockItemTestResult from part.models import Part, PartCategory from part.serializers import PartBriefSerializer -from company.models import SupplierPart +from company.models import Company, SupplierPart from company.serializers import CompanySerializer, SupplierPartSerializer from order.models import PurchaseOrder @@ -100,6 +100,16 @@ class StockDetail(generics.RetrieveUpdateDestroyAPIView): return self.serializer_class(*args, **kwargs) + def update(self, request, *args, **kwargs): + """ + Record the user who updated the item + """ + + # TODO: Record the user! + # user = request.user + + return super().update(request, *args, **kwargs) + class StockFilter(FilterSet): """ FilterSet for advanced stock filtering. @@ -374,25 +384,25 @@ class StockList(generics.ListCreateAPIView): we can pre-fill the location automatically. """ + user = request.user + serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - item = serializer.save() + item = serializer.save(user=user, commit=False) # A location was *not* specified - try to infer it if 'location' not in request.data: - location = item.part.get_default_location() - - if location is not None: - item.location = location - item.save() + item.location = item.part.get_default_location() # An expiry date was *not* specified - try to infer it! if 'expiry_date' not in request.data: if item.part.default_expiry > 0: item.expiry_date = datetime.now().date() + timedelta(days=item.part.default_expiry) - item.save() + + # Finally, save the item + item.save(user=user) # Return a response headers = self.get_success_headers(serializer.data) @@ -1029,7 +1039,7 @@ class StockTrackingList(generics.ListAPIView): if 'customer' in deltas: try: customer = Company.objects.get(pk=deltas['customer']) - serializer = CompanySerializer(location) + serializer = CompanySerializer(customer) deltas['customer_detail'] = serializer.data except: pass diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index 3fb72ebe1e..6d7f8d40ed 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -393,6 +393,18 @@ class AdjustStockForm(forms.ModelForm): ] +class EditStockItemStatusForm(HelperForm): + """ + Simple form for editing StockItem status field + """ + + class Meta: + model = StockItem + fields = [ + 'status', + ] + + class EditStockItemForm(HelperForm): """ Form for editing a StockItem object. Note that not all fields can be edited here (even if they can be specified during creation. diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 4076b27c97..bae1bc5135 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -183,20 +183,46 @@ class StockItem(MPTTModel): self.validate_unique() self.clean() + user = kwargs.pop('user', None) + # If 'add_note = False' specified, then no tracking note will be added for item creation add_note = kwargs.pop('add_note', True) + notes = kwargs.pop('notes', '') + if not self.pk: # StockItem has not yet been saved add_note = add_note and True else: # StockItem has already been saved + + # Check if "interesting" fields have been changed + # (we wish to record these as historical records) + + try: + old = StockItem.objects.get(pk=self.pk) + + deltas = {} + + # Status changed? + if not old.status == self.status: + deltas['status'] = self.status + + # TODO - Other interesting changes we are interested in... + + if add_note and len(deltas) > 0: + self.add_tracking_entry( + StockHistoryCode.EDITED, + user, + deltas=deltas, + notes=notes, + ) + + except (ValueError, StockItem.DoesNotExist): + pass + add_note = False - user = kwargs.pop('user', None) - - add_note = add_note and kwargs.pop('note', True) - super(StockItem, self).save(*args, **kwargs) if add_note: @@ -209,6 +235,7 @@ class StockItem(MPTTModel): StockHistoryCode.CREATED, user, deltas=tracking_info, + notes=notes, location=self.location, quantity=float(self.quantity), ) diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index c7e0dc15dd..54d6b12e4f 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -94,7 +94,13 @@ {% if item.is_expired %} {% trans "Expired" %} {% else %} + {% if roles.stock.change %} + + {% endif %} {% stock_status_label item.status large=True %} + {% if roles.stock.change %} + + {% endif %} {% if item.is_stale %} {% trans "Stale" %} {% endif %} @@ -453,6 +459,7 @@ $("#print-label").click(function() { printStockItemLabels([{{ item.pk }}]); }); +{% if roles.stock.change %} $("#stock-duplicate").click(function() { createNewStockItem({ follow: true, @@ -472,6 +479,18 @@ $("#stock-edit").click(function () { ); }); +$('#stock-edit-status').click(function () { + launchModalForm( + "{% url 'stock-item-edit-status' item.id %}", + { + reload: true, + submit_text: '{% trans "Save" %}', + } + ); +}); + +{% endif %} + $("#show-qr-code").click(function() { launchModalForm("{% url 'stock-item-qr' item.id %}", { diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py index fe5472003f..dbdbdda317 100644 --- a/InvenTree/stock/urls.py +++ b/InvenTree/stock/urls.py @@ -4,7 +4,7 @@ URL lookup for Stock app from django.conf.urls import url, include -from . import views +from stock import views location_urls = [ @@ -24,6 +24,7 @@ location_urls = [ ] stock_item_detail_urls = [ + url(r'^edit_status/', views.StockItemEditStatus.as_view(), name='stock-item-edit-status'), url(r'^edit/', views.StockItemEdit.as_view(), name='stock-item-edit'), url(r'^convert/', views.StockItemConvert.as_view(), name='stock-item-convert'), url(r'^serialize/', views.StockItemSerialize.as_view(), name='stock-item-serialize'), diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 0984405055..38757d4cf2 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -1212,6 +1212,16 @@ class StockAdjust(AjaxView, FormMixin): return _("Deleted {n} stock items").format(n=count) +class StockItemEditStatus(AjaxUpdateView): + """ + View for editing stock item status field + """ + + model = StockItem + form_class = StockForms.EditStockItemStatusForm + ajax_form_title = _('Edit Stock Item Status') + + class StockItemEdit(AjaxUpdateView): """ View for editing details of a single StockItem diff --git a/InvenTree/templates/js/stock.js b/InvenTree/templates/js/stock.js index f3f1c7a6bd..a0601aeb13 100644 --- a/InvenTree/templates/js/stock.js +++ b/InvenTree/templates/js/stock.js @@ -1097,6 +1097,16 @@ function loadStockTrackingTable(table, options) { // Status information if (details.status) { + html += `{% trans "Status" %}`; + + html += ''; + html += stockStatusDisplay( + details.status, + { + classes: 'float-right', + } + ); + html += ''; } @@ -1147,6 +1157,8 @@ function loadStockTrackingTable(table, options) { } }); + /* + // 2021-05-11 - Ability to edit or delete StockItemTracking entries is now removed cols.push({ sortable: false, formatter: function(value, row, index, field) { @@ -1161,6 +1173,7 @@ function loadStockTrackingTable(table, options) { } } }); + */ table.inventreeTable({ method: 'get', diff --git a/InvenTree/templates/js/table_filters.js b/InvenTree/templates/js/table_filters.js index 775f0d9803..5f516e9419 100644 --- a/InvenTree/templates/js/table_filters.js +++ b/InvenTree/templates/js/table_filters.js @@ -3,6 +3,7 @@ {% load inventree_extras %} {% include "status_codes.html" with label='stock' options=StockStatus.list %} +{% include "status_codes.html" with label='stockHistory' options=StockHistoryCode.list %} {% include "status_codes.html" with label='build' options=BuildStatus.list %} {% include "status_codes.html" with label='purchaseOrder' options=PurchaseOrderStatus.list %} {% include "status_codes.html" with label='salesOrder' options=SalesOrderStatus.list %} diff --git a/InvenTree/templates/status_codes.html b/InvenTree/templates/status_codes.html index f032f97309..e7bc2e951c 100644 --- a/InvenTree/templates/status_codes.html +++ b/InvenTree/templates/status_codes.html @@ -14,7 +14,7 @@ var {{ label }}Codes = { * Uses the values specified in "status_codes.py" * This function is generated by the "status_codes.html" template */ -function {{ label }}StatusDisplay(key) { +function {{ label }}StatusDisplay(key, options={}) { key = String(key); @@ -31,5 +31,11 @@ function {{ label }}StatusDisplay(key) { label = ''; } - return `${value}`; + var classes = `label ${label}`; + + if (options.classes) { + classes += ' ' + options.classes; + } + + return `${value}`; } From 68b53acbf1253e6c7bdf2b6fc477b12ab0ba9fc6 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 12 May 2021 08:07:03 +1000 Subject: [PATCH 14/16] remove old fields from the StockItemTracking model --- InvenTree/stock/api.py | 2 +- InvenTree/stock/forms.py | 7 +++-- .../migrations/0063_auto_20210511_2343.py | 29 +++++++++++++++++++ InvenTree/stock/models.py | 22 -------------- 4 files changed, 34 insertions(+), 26 deletions(-) create mode 100644 InvenTree/stock/migrations/0063_auto_20210511_2343.py diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index b0b6ae1c24..809c354720 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -389,7 +389,7 @@ class StockList(generics.ListCreateAPIView): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - item = serializer.save(user=user, commit=False) + item = serializer.save(user=user) # A location was *not* specified - try to infer it if 'location' not in request.data: diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index 6d7f8d40ed..92089623f9 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -437,14 +437,15 @@ class EditStockItemForm(HelperForm): class TrackingEntryForm(HelperForm): - """ Form for creating / editing a StockItemTracking object. + """ + Form for creating / editing a StockItemTracking object. + + Note: 2021-05-11 - This form is not currently used - should delete? """ class Meta: model = StockItemTracking fields = [ - 'title', 'notes', - 'link', ] diff --git a/InvenTree/stock/migrations/0063_auto_20210511_2343.py b/InvenTree/stock/migrations/0063_auto_20210511_2343.py new file mode 100644 index 0000000000..dc8a391cde --- /dev/null +++ b/InvenTree/stock/migrations/0063_auto_20210511_2343.py @@ -0,0 +1,29 @@ +# Generated by Django 3.2 on 2021-05-11 13:43 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0062_auto_20210511_2151'), + ] + + operations = [ + migrations.RemoveField( + model_name='stockitemtracking', + name='link', + ), + migrations.RemoveField( + model_name='stockitemtracking', + name='quantity', + ), + migrations.RemoveField( + model_name='stockitemtracking', + name='system', + ), + migrations.RemoveField( + model_name='stockitemtracking', + name='title', + ), + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index bae1bc5135..3c702fdbe8 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -1626,13 +1626,10 @@ class StockItemTracking(models.Model): Attributes: item: ForeignKey reference to a particular StockItem date: Date that this tracking info was created - title: Title of this tracking info (legacy, no longer used!) tracking_type: The type of tracking information notes: Associated notes (input by user) - link: Optional URL to external page user: The user associated with this tracking info deltas: The changes associated with this history item - quantity: The StockItem quantity at this point in time """ def get_absolute_url(self): @@ -1657,13 +1654,6 @@ class StockItemTracking(models.Model): date = models.DateTimeField(auto_now_add=True, editable=False) - title = models.CharField( - blank=True, null=True, - max_length=250, - verbose_name=_('Title'), - help_text=_('Tracking entry title') - ) - notes = models.CharField( blank=True, null=True, max_length=512, @@ -1671,22 +1661,10 @@ class StockItemTracking(models.Model): help_text=_('Entry notes') ) - link = InvenTreeURLField(blank=True, verbose_name=_('Link'), help_text=_('Link to external page for further information')) - user = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True) - system = models.BooleanField(default=False) - deltas = models.JSONField(null=True, blank=True) - quantity = models.DecimalField( - max_digits=15, - decimal_places=5, - validators=[MinValueValidator(0)], - default=1, - verbose_name=_('Quantity') - ) - def rename_stock_item_test_result_attachment(instance, filename): From 30ff48d80315a17568fe8e9e95be3187cf1275e5 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 12 May 2021 10:09:02 +1000 Subject: [PATCH 15/16] Fixes --- InvenTree/stock/api.py | 3 ++- InvenTree/stock/models.py | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 809c354720..376d04f643 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -389,7 +389,8 @@ class StockList(generics.ListCreateAPIView): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - item = serializer.save(user=user) + # TODO - Save the user who created this item + item = serializer.save() # A location was *not* specified - try to infer it if 'location' not in request.data: diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 3c702fdbe8..28123ebc41 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -981,7 +981,6 @@ class StockItem(MPTTModel): date=datetime.now(), notes=notes, deltas=deltas, - system=True ) entry.save() @@ -1048,7 +1047,7 @@ class StockItem(MPTTModel): new_item.location = location # The item already has a transaction history, don't create a new note - new_item.save(user=user, note=False) + new_item.save(user=user, notes=notes) # Copy entire transaction history new_item.copyHistoryFrom(self) From 3b252b84099dfaaaeba2450080e0038893b29a1f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 12 May 2021 10:42:09 +1000 Subject: [PATCH 16/16] Record the user when updating stock items --- InvenTree/stock/serializers.py | 5 ----- InvenTree/stock/views.py | 22 ++++++++++++++++++++++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index d46dc7b3dc..9bcdc5182e 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -366,10 +366,7 @@ class StockTrackingSerializer(InvenTreeModelSerializer): 'date', 'deltas', 'label', - 'link', 'notes', - 'quantity', - 'title', 'tracking_type', 'user', 'user_detail', @@ -378,8 +375,6 @@ class StockTrackingSerializer(InvenTreeModelSerializer): read_only_fields = [ 'date', 'user', - 'system', - 'quantity', 'label', 'tracking_type', ] diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 38757d4cf2..a13f885e80 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -1221,6 +1221,17 @@ class StockItemEditStatus(AjaxUpdateView): form_class = StockForms.EditStockItemStatusForm ajax_form_title = _('Edit Stock Item Status') + def save(self, object, form, **kwargs): + """ + Override the save method, to track the user who updated the model + """ + + item = form.save(commit=False) + + item.save(user=self.request.user) + + return item + class StockItemEdit(AjaxUpdateView): """ @@ -1331,6 +1342,17 @@ class StockItemEdit(AjaxUpdateView): if not owner and not self.request.user.is_superuser: form.add_error('owner', _('Owner is required (ownership control is enabled)')) + def save(self, object, form, **kwargs): + """ + Override the save method, to track the user who updated the model + """ + + item = form.save(commit=False) + + item.save(user=self.request.user) + + return item + class StockItemConvert(AjaxUpdateView): """