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 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

View File

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

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 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):