2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-29 20:16:44 +00:00

Replace "addTrasactionNote" function with "add_tracking_entry"

- Does not add translated strings to the database
This commit is contained in:
Oliver Walters 2021-05-11 17:17:48 +10:00
parent 82c6d10c33
commit af53b341f0
5 changed files with 299 additions and 96 deletions

View File

@ -22,7 +22,7 @@ from markdownx.models import MarkdownxField
from mptt.models import MPTTModel, TreeForeignKey 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.helpers import increment, getSetting, normalize, MakeBarcode
from InvenTree.validators import validate_build_order_reference from InvenTree.validators import validate_build_order_reference
from InvenTree.models import InvenTreeAttachment from InvenTree.models import InvenTreeAttachment
@ -811,6 +811,7 @@ class Build(MPTTModel):
# Select the location for the build output # Select the location for the build output
location = kwargs.get('location', self.destination) location = kwargs.get('location', self.destination)
status = kwargs.get('status', StockStatus.OK) status = kwargs.get('status', StockStatus.OK)
notes = kwargs.get('notes', '')
# List the allocated BuildItem objects for the given output # List the allocated BuildItem objects for the given output
allocated_items = output.items_to_install.all() allocated_items = output.items_to_install.all()
@ -834,10 +835,13 @@ class Build(MPTTModel):
output.save() output.save()
output.addTransactionNote( output.add_tracking_entry(
_('Completed build output'), StockHistoryCode.BUILD_OUTPUT_COMPLETED,
user, user,
system=True notes=notes,
deltas={
'status': status,
}
) )
# Increase the completed quantity for this build # Increase the completed quantity for this build

View File

@ -28,7 +28,7 @@ from company.models import Company, SupplierPart
from InvenTree.fields import RoundingDecimalField from InvenTree.fields import RoundingDecimalField
from InvenTree.helpers import decimal2string, increment, getSetting 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 from InvenTree.models import InvenTreeAttachment
@ -336,10 +336,12 @@ class PurchaseOrder(Order):
return self.pending_line_items().count() == 0 return self.pending_line_items().count() == 0
@transaction.atomic @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 """ Receive a line item (or partial line item) against this PO
""" """
notes = kwargs.get('notes', '')
if not self.status == PurchaseOrderStatus.PLACED: if not self.status == PurchaseOrderStatus.PLACED:
raise ValidationError({"status": _("Lines can only be received against an order marked as '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") text = _("Received items")
note = _('Received {n} items against order {name}').format(n=quantity, name=str(self)) note = _('Received {n} items against order {name}').format(n=quantity, name=str(self))
# Add a new transaction note to the newly created stock item tracking_info = {
stock.addTransactionNote(text, user, note) '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 # Update the number of parts received against the particular line item
line.received += quantity line.received += quantity

View File

@ -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'),
),
]

View File

@ -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)
]

View File

@ -34,7 +34,7 @@ import common.models
import report.models import report.models
import label.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.models import InvenTreeTree, InvenTreeAttachment
from InvenTree.fields import InvenTreeURLField from InvenTree.fields import InvenTreeURLField
@ -198,14 +198,18 @@ class StockItem(MPTTModel):
if add_note: 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 if self.location:
self.addTransactionNote( tracking_info['location'] = self.location.pk
_('Created stock item'),
self.add_tracking_entry(
StockHistoryCode.CREATED,
user, user,
note, deltas=tracking_info
system=True
) )
@property @property
@ -610,31 +614,45 @@ class StockItem(MPTTModel):
# TODO - Remove any stock item allocations from this stock item # TODO - Remove any stock item allocations from this stock item
item.addTransactionNote( item.add_tracking_entry(
_("Assigned to Customer"), StockHistoryCode.SENT_TO_CUSTOMER,
user, 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 the reference to the stock item
return 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. Return stock item from customer, back into the specified location.
""" """
self.addTransactionNote( notes = kwargs.get('notes', '')
_("Returned from customer {name}").format(name=self.customer.name),
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, user,
notes=_("Returned to location {loc}").format(loc=location.name), notes=notes,
system=True deltas=tracking_info
) )
self.customer = None self.customer = None
self.location = location self.location = location
self.sales_order = None
self.save() self.save()
@ -788,18 +806,25 @@ class StockItem(MPTTModel):
stock_item.save() stock_item.save()
# Add a transaction note to the other item # Add a transaction note to the other item
stock_item.addTransactionNote( stock_item.add_tracking_entry(
_('Installed into stock item {pk}').format(str(self.pk)), StockHistoryCode.INSTALLED_INTO_ASSEMBLY,
user, user,
notes=notes, notes=notes,
url=self.get_absolute_url() url=self.get_absolute_url(),
deltas={
'assembly': self.pk,
}
) )
# Add a transaction note to this item # Add a transaction note to this item (the assembly)
self.addTransactionNote( self.add_tracking_entry(
_('Installed stock item {pk}').format(str(stock_item.pk)), StockHistoryCode.INSTALLED_CHILD_ITEM,
user, notes=notes, user,
url=stock_item.get_absolute_url() notes=notes,
url=stock_item.get_absolute_url(),
deltas={
'stockitem': stock_item.pk,
}
) )
@transaction.atomic @transaction.atomic
@ -820,32 +845,41 @@ class StockItem(MPTTModel):
# TODO - Are there any other checks that need to be performed at this stage? # TODO - Are there any other checks that need to be performed at this stage?
# Add a transaction note to the parent item # Add a transaction note to the parent item
self.belongs_to.addTransactionNote( self.belongs_to.add_tracking_entry(
_("Uninstalled stock item {pk}").format(pk=str(self.pk)), StockHistoryCode.REMOVED_CHILD_ITEM,
user, user,
deltas={
'stockitem': self.pk,
},
notes=notes, notes=notes,
url=self.get_absolute_url(), 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 # Mark this stock item as *not* belonging to anyone
self.belongs_to = None self.belongs_to = None
self.location = location self.location = location
self.save() 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 @property
def children(self): def children(self):
""" Return a list of the child items which have been split from this stock item """ """ 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): def has_tracking_info(self):
return self.tracking_info_count > 0 return self.tracking_info_count > 0
def addTransactionNote(self, title, user, notes='', url='', system=True): def add_tracking_entry(self, entry_type, user, deltas={}, notes='', url=''):
""" Generation a stock transaction note for this item. """
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, item=self,
title=title, tracking_type=entry_type,
user=user, user=user,
quantity=self.quantity, date=datetime.now(),
date=datetime.now().date(),
notes=notes, notes=notes,
deltas=deltas,
link=url, link=url,
system=system system=True
) )
track.save() entry.save()
@transaction.atomic @transaction.atomic
def serializeStock(self, quantity, serials, user, notes='', location=None): def serializeStock(self, quantity, serials, user, notes='', location=None):
@ -991,10 +1031,17 @@ class StockItem(MPTTModel):
new_item.copyTestResultsFrom(self) new_item.copyTestResultsFrom(self)
# Create a new stock tracking item # 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 # 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 @transaction.atomic
def copyHistoryFrom(self, other): def copyHistoryFrom(self, other):
@ -1018,7 +1065,7 @@ class StockItem(MPTTModel):
result.save() result.save()
@transaction.atomic @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. """ Split this stock item into two items, in the same location.
Stock tracking notes for this StockItem will be duplicated, Stock tracking notes for this StockItem will be duplicated,
and added to the new StockItem. 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. 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 # Do not split a serialized part
if self.serialized: if self.serialized:
return self return self
@ -1071,17 +1120,20 @@ class StockItem(MPTTModel):
new_stock.copyTestResultsFrom(self) new_stock.copyTestResultsFrom(self)
# Add a new tracking item for the new stock item # Add a new tracking item for the new stock item
new_stock.addTransactionNote( new_stock.add_tracking_entry(
_("Split from existing stock"), StockHistoryCode.SPLIT_FROM_PARENT,
user, user,
_('Split {n} items').format(n=helpers.normalize(quantity)) notes=notes,
deltas={
'stockitem': self.pk,
}
) )
# Remove the specified quantity from THIS stock item # Remove the specified quantity from THIS stock item
self.take_stock( self.take_stock(
quantity, quantity,
user, user,
f"{_('Split')} {quantity} {_('items into new stock item')}" notes=notes
) )
# Return a copy of the "new" stock item # Return a copy of the "new" stock item
@ -1138,11 +1190,21 @@ class StockItem(MPTTModel):
self.location = location self.location = location
self.addTransactionNote( tracking_info = {}
msg,
if location:
tracking_info['location'] = location.pk
url = location.get_absolute_url()
else:
url = ''
self.add_tracking_entry(
StockHistoryCode.STOCK_MOVE,
user, user,
notes=notes, notes=notes,
system=True) deltas=tracking_info,
url=url,
)
self.save() self.save()
@ -1202,13 +1264,13 @@ class StockItem(MPTTModel):
if self.updateQuantity(count): if self.updateQuantity(count):
text = _('Counted {n} items').format(n=helpers.normalize(count)) self.add_tracking_entry(
StockHistoryCode.STOCK_COUNT,
self.addTransactionNote(
text,
user, user,
notes=notes, notes=notes,
system=True deltas={
'quantity': self.quantity,
}
) )
return True return True
@ -1234,13 +1296,15 @@ class StockItem(MPTTModel):
return False return False
if self.updateQuantity(self.quantity + quantity): if self.updateQuantity(self.quantity + quantity):
text = _('Added {n} items').format(n=helpers.normalize(quantity))
self.addTransactionNote( self.add_tracking_entry(
text, StockHistoryCode.STOCK_ADD,
user, user,
notes=notes, notes=notes,
system=True deltas={
'added': quantity,
'quantity': self.quantity
}
) )
return True return True
@ -1264,12 +1328,15 @@ class StockItem(MPTTModel):
if self.updateQuantity(self.quantity - quantity): if self.updateQuantity(self.quantity - quantity):
text = _('Removed {n1} items').format(n1=helpers.normalize(quantity)) self.add_tracking_entry(
StockHistoryCode.STOCK_REMOVE,
self.addTransactionNote(text, user,
user, notes=notes,
notes=notes, deltas={
system=True) 'removed': quantity,
'quantity': self.quantity,
}
)
return True return True
@ -1527,30 +1594,57 @@ class StockItemAttachment(InvenTreeAttachment):
class StockItemTracking(models.Model): 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: Attributes:
item: Link to StockItem item: ForeignKey reference to a particular StockItem
date: Date that this tracking info was created 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) notes: Associated notes (input by user)
link: Optional URL to external page link: Optional URL to external page
user: The user associated with this tracking info 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 quantity: The StockItem quantity at this point in time
""" """
def get_absolute_url(self): def get_absolute_url(self):
return '/stock/track/{pk}'.format(pk=self.id) 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, tracking_type = models.IntegerField(
related_name='tracking_info') default=StockHistoryCode.LEGACY,
)
item = models.ForeignKey(
StockItem,
on_delete=models.CASCADE,
related_name='tracking_info'
)
date = models.DateTimeField(auto_now_add=True, editable=False) 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')) 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) 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 quantity = models.DecimalField(
# image = models.ImageField(upload_to=func, max_length=255, null=True, blank=True) max_digits=15,
decimal_places=5,
# TODO validators=[MinValueValidator(0)],
# file = models.FileField() default=1,
verbose_name=_('Quantity')
)
def rename_stock_item_test_result_attachment(instance, filename): def rename_stock_item_test_result_attachment(instance, filename):