mirror of
https://github.com/inventree/InvenTree.git
synced 2025-05-01 13:06:45 +00:00
Updates
- Add StockHistoryCode to custom context - Add simple form for editing stock item history - Add tracking entry when stock status is changed
This commit is contained in:
parent
84bfffd5a7
commit
03a231bffb
@ -6,6 +6,7 @@ Provides extra global data to all templates.
|
|||||||
|
|
||||||
from InvenTree.status_codes import SalesOrderStatus, PurchaseOrderStatus
|
from InvenTree.status_codes import SalesOrderStatus, PurchaseOrderStatus
|
||||||
from InvenTree.status_codes import BuildStatus, StockStatus
|
from InvenTree.status_codes import BuildStatus, StockStatus
|
||||||
|
from InvenTree.status_codes import StockHistoryCode
|
||||||
|
|
||||||
import InvenTree.status
|
import InvenTree.status
|
||||||
|
|
||||||
@ -65,6 +66,7 @@ def status_codes(request):
|
|||||||
'PurchaseOrderStatus': PurchaseOrderStatus,
|
'PurchaseOrderStatus': PurchaseOrderStatus,
|
||||||
'BuildStatus': BuildStatus,
|
'BuildStatus': BuildStatus,
|
||||||
'StockStatus': StockStatus,
|
'StockStatus': StockStatus,
|
||||||
|
'StockHistoryCode': StockHistoryCode,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,6 +7,8 @@ class StatusCode:
|
|||||||
This is used to map a set of integer values to text.
|
This is used to map a set of integer values to text.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
colors = {}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def render(cls, key, large=False):
|
def render(cls, key, large=False):
|
||||||
"""
|
"""
|
||||||
|
@ -21,7 +21,7 @@ from .models import StockItemTestResult
|
|||||||
from part.models import Part, PartCategory
|
from part.models import Part, PartCategory
|
||||||
from part.serializers import PartBriefSerializer
|
from part.serializers import PartBriefSerializer
|
||||||
|
|
||||||
from company.models import SupplierPart
|
from company.models import Company, SupplierPart
|
||||||
from company.serializers import CompanySerializer, SupplierPartSerializer
|
from company.serializers import CompanySerializer, SupplierPartSerializer
|
||||||
|
|
||||||
from order.models import PurchaseOrder
|
from order.models import PurchaseOrder
|
||||||
@ -100,6 +100,16 @@ class StockDetail(generics.RetrieveUpdateDestroyAPIView):
|
|||||||
|
|
||||||
return self.serializer_class(*args, **kwargs)
|
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):
|
class StockFilter(FilterSet):
|
||||||
""" FilterSet for advanced stock filtering.
|
""" FilterSet for advanced stock filtering.
|
||||||
@ -374,25 +384,25 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
we can pre-fill the location automatically.
|
we can pre-fill the location automatically.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
user = request.user
|
||||||
|
|
||||||
serializer = self.get_serializer(data=request.data)
|
serializer = self.get_serializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
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
|
# A location was *not* specified - try to infer it
|
||||||
if 'location' not in request.data:
|
if 'location' not in request.data:
|
||||||
location = item.part.get_default_location()
|
item.location = item.part.get_default_location()
|
||||||
|
|
||||||
if location is not None:
|
|
||||||
item.location = location
|
|
||||||
item.save()
|
|
||||||
|
|
||||||
# An expiry date was *not* specified - try to infer it!
|
# An expiry date was *not* specified - try to infer it!
|
||||||
if 'expiry_date' not in request.data:
|
if 'expiry_date' not in request.data:
|
||||||
|
|
||||||
if item.part.default_expiry > 0:
|
if item.part.default_expiry > 0:
|
||||||
item.expiry_date = datetime.now().date() + timedelta(days=item.part.default_expiry)
|
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
|
# Return a response
|
||||||
headers = self.get_success_headers(serializer.data)
|
headers = self.get_success_headers(serializer.data)
|
||||||
@ -1029,7 +1039,7 @@ class StockTrackingList(generics.ListAPIView):
|
|||||||
if 'customer' in deltas:
|
if 'customer' in deltas:
|
||||||
try:
|
try:
|
||||||
customer = Company.objects.get(pk=deltas['customer'])
|
customer = Company.objects.get(pk=deltas['customer'])
|
||||||
serializer = CompanySerializer(location)
|
serializer = CompanySerializer(customer)
|
||||||
deltas['customer_detail'] = serializer.data
|
deltas['customer_detail'] = serializer.data
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
@ -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):
|
class EditStockItemForm(HelperForm):
|
||||||
""" Form for editing a StockItem object.
|
""" Form for editing a StockItem object.
|
||||||
Note that not all fields can be edited here (even if they can be specified during creation.
|
Note that not all fields can be edited here (even if they can be specified during creation.
|
||||||
|
@ -183,20 +183,46 @@ class StockItem(MPTTModel):
|
|||||||
self.validate_unique()
|
self.validate_unique()
|
||||||
self.clean()
|
self.clean()
|
||||||
|
|
||||||
|
user = kwargs.pop('user', None)
|
||||||
|
|
||||||
# If 'add_note = False' specified, then no tracking note will be added for item creation
|
# If 'add_note = False' specified, then no tracking note will be added for item creation
|
||||||
add_note = kwargs.pop('add_note', True)
|
add_note = kwargs.pop('add_note', True)
|
||||||
|
|
||||||
|
notes = kwargs.pop('notes', '')
|
||||||
|
|
||||||
if not self.pk:
|
if not self.pk:
|
||||||
# StockItem has not yet been saved
|
# StockItem has not yet been saved
|
||||||
add_note = add_note and True
|
add_note = add_note and True
|
||||||
else:
|
else:
|
||||||
# StockItem has already been saved
|
# 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
|
add_note = False
|
||||||
|
|
||||||
user = kwargs.pop('user', None)
|
|
||||||
|
|
||||||
add_note = add_note and kwargs.pop('note', True)
|
|
||||||
|
|
||||||
super(StockItem, self).save(*args, **kwargs)
|
super(StockItem, self).save(*args, **kwargs)
|
||||||
|
|
||||||
if add_note:
|
if add_note:
|
||||||
@ -209,6 +235,7 @@ class StockItem(MPTTModel):
|
|||||||
StockHistoryCode.CREATED,
|
StockHistoryCode.CREATED,
|
||||||
user,
|
user,
|
||||||
deltas=tracking_info,
|
deltas=tracking_info,
|
||||||
|
notes=notes,
|
||||||
location=self.location,
|
location=self.location,
|
||||||
quantity=float(self.quantity),
|
quantity=float(self.quantity),
|
||||||
)
|
)
|
||||||
|
@ -94,7 +94,13 @@
|
|||||||
{% if item.is_expired %}
|
{% if item.is_expired %}
|
||||||
<span class='label label-large label-large-red'>{% trans "Expired" %}</span>
|
<span class='label label-large label-large-red'>{% trans "Expired" %}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
{% if roles.stock.change %}
|
||||||
|
<a href='#' id='stock-edit-status'>
|
||||||
|
{% endif %}
|
||||||
{% stock_status_label item.status large=True %}
|
{% stock_status_label item.status large=True %}
|
||||||
|
{% if roles.stock.change %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
{% if item.is_stale %}
|
{% if item.is_stale %}
|
||||||
<span class='label label-large label-large-yellow'>{% trans "Stale" %}</span>
|
<span class='label label-large label-large-yellow'>{% trans "Stale" %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -453,6 +459,7 @@ $("#print-label").click(function() {
|
|||||||
printStockItemLabels([{{ item.pk }}]);
|
printStockItemLabels([{{ item.pk }}]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
{% if roles.stock.change %}
|
||||||
$("#stock-duplicate").click(function() {
|
$("#stock-duplicate").click(function() {
|
||||||
createNewStockItem({
|
createNewStockItem({
|
||||||
follow: true,
|
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() {
|
$("#show-qr-code").click(function() {
|
||||||
launchModalForm("{% url 'stock-item-qr' item.id %}",
|
launchModalForm("{% url 'stock-item-qr' item.id %}",
|
||||||
{
|
{
|
||||||
|
@ -4,7 +4,7 @@ URL lookup for Stock app
|
|||||||
|
|
||||||
from django.conf.urls import url, include
|
from django.conf.urls import url, include
|
||||||
|
|
||||||
from . import views
|
from stock import views
|
||||||
|
|
||||||
location_urls = [
|
location_urls = [
|
||||||
|
|
||||||
@ -24,6 +24,7 @@ location_urls = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
stock_item_detail_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'^edit/', views.StockItemEdit.as_view(), name='stock-item-edit'),
|
||||||
url(r'^convert/', views.StockItemConvert.as_view(), name='stock-item-convert'),
|
url(r'^convert/', views.StockItemConvert.as_view(), name='stock-item-convert'),
|
||||||
url(r'^serialize/', views.StockItemSerialize.as_view(), name='stock-item-serialize'),
|
url(r'^serialize/', views.StockItemSerialize.as_view(), name='stock-item-serialize'),
|
||||||
|
@ -1212,6 +1212,16 @@ class StockAdjust(AjaxView, FormMixin):
|
|||||||
return _("Deleted {n} stock items").format(n=count)
|
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):
|
class StockItemEdit(AjaxUpdateView):
|
||||||
"""
|
"""
|
||||||
View for editing details of a single StockItem
|
View for editing details of a single StockItem
|
||||||
|
@ -1097,6 +1097,16 @@ function loadStockTrackingTable(table, options) {
|
|||||||
|
|
||||||
// Status information
|
// Status information
|
||||||
if (details.status) {
|
if (details.status) {
|
||||||
|
html += `<tr><th>{% trans "Status" %}</td>`;
|
||||||
|
|
||||||
|
html += '<td>';
|
||||||
|
html += stockStatusDisplay(
|
||||||
|
details.status,
|
||||||
|
{
|
||||||
|
classes: 'float-right',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
html += '</td></tr>';
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1147,6 +1157,8 @@ function loadStockTrackingTable(table, options) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
// 2021-05-11 - Ability to edit or delete StockItemTracking entries is now removed
|
||||||
cols.push({
|
cols.push({
|
||||||
sortable: false,
|
sortable: false,
|
||||||
formatter: function(value, row, index, field) {
|
formatter: function(value, row, index, field) {
|
||||||
@ -1161,6 +1173,7 @@ function loadStockTrackingTable(table, options) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
table.inventreeTable({
|
table.inventreeTable({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
|
|
||||||
{% include "status_codes.html" with label='stock' options=StockStatus.list %}
|
{% 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='build' options=BuildStatus.list %}
|
||||||
{% include "status_codes.html" with label='purchaseOrder' options=PurchaseOrderStatus.list %}
|
{% include "status_codes.html" with label='purchaseOrder' options=PurchaseOrderStatus.list %}
|
||||||
{% include "status_codes.html" with label='salesOrder' options=SalesOrderStatus.list %}
|
{% include "status_codes.html" with label='salesOrder' options=SalesOrderStatus.list %}
|
||||||
|
@ -14,7 +14,7 @@ var {{ label }}Codes = {
|
|||||||
* Uses the values specified in "status_codes.py"
|
* Uses the values specified in "status_codes.py"
|
||||||
* This function is generated by the "status_codes.html" template
|
* This function is generated by the "status_codes.html" template
|
||||||
*/
|
*/
|
||||||
function {{ label }}StatusDisplay(key) {
|
function {{ label }}StatusDisplay(key, options={}) {
|
||||||
|
|
||||||
key = String(key);
|
key = String(key);
|
||||||
|
|
||||||
@ -31,5 +31,11 @@ function {{ label }}StatusDisplay(key) {
|
|||||||
label = '';
|
label = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return `<span class='label ${label}'>${value}</span>`;
|
var classes = `label ${label}`;
|
||||||
|
|
||||||
|
if (options.classes) {
|
||||||
|
classes += ' ' + options.classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<span class='${classes}'>${value}</span>`;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user