mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-29 12:06:44 +00:00
Return from customer (#3120)
* Adds ability to return item into stock via the API * Remove old server-side form / view for returning stock from a customer * Add unit tests for new API endpoint
This commit is contained in:
parent
5fef6563d8
commit
4b3f77763d
@ -2,11 +2,14 @@
|
|||||||
|
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 54
|
INVENTREE_API_VERSION = 55
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||||
|
|
||||||
|
v55 -> 2022-06-02 : https://github.com/inventree/InvenTree/pull/3120
|
||||||
|
- Converts the 'StockItemReturn' functionality to make use of the API
|
||||||
|
|
||||||
v54 -> 2022-06-02 : https://github.com/inventree/InvenTree/pull/3117
|
v54 -> 2022-06-02 : https://github.com/inventree/InvenTree/pull/3117
|
||||||
- Adds 'available_stock' annotation on the SalesOrderLineItem API
|
- Adds 'available_stock' annotation on the SalesOrderLineItem API
|
||||||
- Adds (well, fixes) 'overdue' annotation on the SalesOrderLineItem API
|
- Adds (well, fixes) 'overdue' annotation on the SalesOrderLineItem API
|
||||||
|
@ -106,7 +106,7 @@ class StockItemContextMixin:
|
|||||||
class StockItemSerialize(StockItemContextMixin, generics.CreateAPIView):
|
class StockItemSerialize(StockItemContextMixin, generics.CreateAPIView):
|
||||||
"""API endpoint for serializing a stock item."""
|
"""API endpoint for serializing a stock item."""
|
||||||
|
|
||||||
queryset = StockItem.objects.none()
|
queryset = StockItem.objects.all()
|
||||||
serializer_class = StockSerializers.SerializeStockItemSerializer
|
serializer_class = StockSerializers.SerializeStockItemSerializer
|
||||||
|
|
||||||
|
|
||||||
@ -118,17 +118,24 @@ class StockItemInstall(StockItemContextMixin, generics.CreateAPIView):
|
|||||||
- stock_item must be serialized (and not belong to another item)
|
- stock_item must be serialized (and not belong to another item)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
queryset = StockItem.objects.none()
|
queryset = StockItem.objects.all()
|
||||||
serializer_class = StockSerializers.InstallStockItemSerializer
|
serializer_class = StockSerializers.InstallStockItemSerializer
|
||||||
|
|
||||||
|
|
||||||
class StockItemUninstall(StockItemContextMixin, generics.CreateAPIView):
|
class StockItemUninstall(StockItemContextMixin, generics.CreateAPIView):
|
||||||
"""API endpoint for removing (uninstalling) items from this item."""
|
"""API endpoint for removing (uninstalling) items from this item."""
|
||||||
|
|
||||||
queryset = StockItem.objects.none()
|
queryset = StockItem.objects.all()
|
||||||
serializer_class = StockSerializers.UninstallStockItemSerializer
|
serializer_class = StockSerializers.UninstallStockItemSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class StockItemReturn(StockItemContextMixin, generics.CreateAPIView):
|
||||||
|
"""API endpoint for returning a stock item from a customer"""
|
||||||
|
|
||||||
|
queryset = StockItem.objects.all()
|
||||||
|
serializer_class = StockSerializers.ReturnStockItemSerializer
|
||||||
|
|
||||||
|
|
||||||
class StockAdjustView(generics.CreateAPIView):
|
class StockAdjustView(generics.CreateAPIView):
|
||||||
"""A generic class for handling stocktake actions.
|
"""A generic class for handling stocktake actions.
|
||||||
|
|
||||||
@ -1370,6 +1377,7 @@ stock_api_urls = [
|
|||||||
re_path(r'^(?P<pk>\d+)/', include([
|
re_path(r'^(?P<pk>\d+)/', include([
|
||||||
re_path(r'^install/', StockItemInstall.as_view(), name='api-stock-item-install'),
|
re_path(r'^install/', StockItemInstall.as_view(), name='api-stock-item-install'),
|
||||||
re_path(r'^metadata/', StockMetadata.as_view(), name='api-stock-item-metadata'),
|
re_path(r'^metadata/', StockMetadata.as_view(), name='api-stock-item-metadata'),
|
||||||
|
re_path(r'^return/', StockItemReturn.as_view(), name='api-stock-item-return'),
|
||||||
re_path(r'^serialize/', StockItemSerialize.as_view(), name='api-stock-item-serialize'),
|
re_path(r'^serialize/', StockItemSerialize.as_view(), name='api-stock-item-serialize'),
|
||||||
re_path(r'^uninstall/', StockItemUninstall.as_view(), name='api-stock-item-uninstall'),
|
re_path(r'^uninstall/', StockItemUninstall.as_view(), name='api-stock-item-uninstall'),
|
||||||
re_path(r'^.*$', StockDetail.as_view(), name='api-stock-detail'),
|
re_path(r'^.*$', StockDetail.as_view(), name='api-stock-detail'),
|
||||||
|
@ -5,21 +5,6 @@ from InvenTree.forms import HelperForm
|
|||||||
from .models import StockItem, StockItemTracking
|
from .models import StockItem, StockItemTracking
|
||||||
|
|
||||||
|
|
||||||
class ReturnStockItemForm(HelperForm):
|
|
||||||
"""Form for manually returning a StockItem into stock.
|
|
||||||
|
|
||||||
TODO: This could be a simple API driven form!
|
|
||||||
"""
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
"""Metaclass options."""
|
|
||||||
|
|
||||||
model = StockItem
|
|
||||||
fields = [
|
|
||||||
'location',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class ConvertStockItemForm(HelperForm):
|
class ConvertStockItemForm(HelperForm):
|
||||||
"""Form for converting a StockItem to a variant of its current part.
|
"""Form for converting a StockItem to a variant of its current part.
|
||||||
|
|
||||||
|
@ -894,7 +894,8 @@ class StockItem(MetadataMixin, MPTTModel):
|
|||||||
# Return the reference to the stock item
|
# Return the reference to the stock item
|
||||||
return item
|
return item
|
||||||
|
|
||||||
def returnFromCustomer(self, location, user=None, **kwargs):
|
@transaction.atomic
|
||||||
|
def return_from_customer(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."""
|
||||||
notes = kwargs.get('notes', '')
|
notes = kwargs.get('notes', '')
|
||||||
|
|
||||||
|
@ -464,6 +464,48 @@ class UninstallStockItemSerializer(serializers.Serializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ReturnStockItemSerializer(serializers.Serializer):
|
||||||
|
"""DRF serializer for returning a stock item from a customer"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Metaclass options"""
|
||||||
|
|
||||||
|
fields = [
|
||||||
|
'location',
|
||||||
|
'note',
|
||||||
|
]
|
||||||
|
|
||||||
|
location = serializers.PrimaryKeyRelatedField(
|
||||||
|
queryset=StockLocation.objects.all(),
|
||||||
|
many=False, required=True, allow_null=False,
|
||||||
|
label=_('Location'),
|
||||||
|
help_text=_('Destination location for returned item'),
|
||||||
|
)
|
||||||
|
|
||||||
|
notes = serializers.CharField(
|
||||||
|
label=_('Notes'),
|
||||||
|
help_text=_('Add transaction note (optional)'),
|
||||||
|
required=False, allow_blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
"""Save the serialzier to return the item into stock"""
|
||||||
|
|
||||||
|
item = self.context['item']
|
||||||
|
request = self.context['request']
|
||||||
|
|
||||||
|
data = self.validated_data
|
||||||
|
|
||||||
|
location = data['location']
|
||||||
|
notes = data.get('notes', '')
|
||||||
|
|
||||||
|
item.return_from_customer(
|
||||||
|
location,
|
||||||
|
user=request.user,
|
||||||
|
notes=notes
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class LocationTreeSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
class LocationTreeSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||||
"""Serializer for a simple tree view."""
|
"""Serializer for a simple tree view."""
|
||||||
|
|
||||||
|
@ -632,11 +632,21 @@ $('#stock-remove').click(function() {
|
|||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
$("#stock-return-from-customer").click(function() {
|
$("#stock-return-from-customer").click(function() {
|
||||||
launchModalForm("{% url 'stock-item-return' item.id %}",
|
|
||||||
{
|
constructForm('{% url "api-stock-item-return" item.pk %}', {
|
||||||
|
fields: {
|
||||||
|
location: {
|
||||||
|
{% if item.part.default_location %}
|
||||||
|
value: {{ item.part.default_location.pk }},
|
||||||
|
{% endif %}
|
||||||
|
},
|
||||||
|
notes: {},
|
||||||
|
},
|
||||||
|
method: 'POST',
|
||||||
|
title: '{% trans "Return to Stock" %}',
|
||||||
reload: true,
|
reload: true,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -663,6 +663,45 @@ class StockItemTest(StockAPITestCase):
|
|||||||
self.assertIsNone(sub_item.belongs_to)
|
self.assertIsNone(sub_item.belongs_to)
|
||||||
self.assertEqual(sub_item.location.pk, 1)
|
self.assertEqual(sub_item.location.pk, 1)
|
||||||
|
|
||||||
|
def test_return_from_customer(self):
|
||||||
|
"""Test that we can return a StockItem from a customer, via the API"""
|
||||||
|
|
||||||
|
# Assign item to customer
|
||||||
|
item = StockItem.objects.get(pk=521)
|
||||||
|
customer = company.models.Company.objects.get(pk=4)
|
||||||
|
|
||||||
|
item.customer = customer
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
n_entries = item.tracking_info_count
|
||||||
|
|
||||||
|
url = reverse('api-stock-item-return', kwargs={'pk': item.pk})
|
||||||
|
|
||||||
|
# Empty POST will fail
|
||||||
|
response = self.post(
|
||||||
|
url, {},
|
||||||
|
expected_code=400
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn('This field is required', str(response.data['location']))
|
||||||
|
|
||||||
|
response = self.post(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
'location': '1',
|
||||||
|
'notes': 'Returned from this customer for testing',
|
||||||
|
},
|
||||||
|
expected_code=201,
|
||||||
|
)
|
||||||
|
|
||||||
|
item.refresh_from_db()
|
||||||
|
|
||||||
|
# A new stock tracking entry should have been created
|
||||||
|
self.assertEqual(n_entries + 1, item.tracking_info_count)
|
||||||
|
|
||||||
|
# The item is now in stock
|
||||||
|
self.assertIsNone(item.customer)
|
||||||
|
|
||||||
|
|
||||||
class StocktakeTest(StockAPITestCase):
|
class StocktakeTest(StockAPITestCase):
|
||||||
"""Series of tests for the Stocktake API."""
|
"""Series of tests for the Stocktake API."""
|
||||||
|
@ -21,7 +21,6 @@ stock_item_detail_urls = [
|
|||||||
re_path(r'^delete/', views.StockItemDelete.as_view(), name='stock-item-delete'),
|
re_path(r'^delete/', views.StockItemDelete.as_view(), name='stock-item-delete'),
|
||||||
re_path(r'^qr_code/', views.StockItemQRCode.as_view(), name='stock-item-qr'),
|
re_path(r'^qr_code/', views.StockItemQRCode.as_view(), name='stock-item-qr'),
|
||||||
re_path(r'^delete_test_data/', views.StockItemDeleteTestData.as_view(), name='stock-item-delete-test-data'),
|
re_path(r'^delete_test_data/', views.StockItemDeleteTestData.as_view(), name='stock-item-delete-test-data'),
|
||||||
re_path(r'^return/', views.StockItemReturnToStock.as_view(), name='stock-item-return'),
|
|
||||||
|
|
||||||
re_path(r'^add_tracking/', views.StockItemTrackingCreate.as_view(), name='stock-tracking-create'),
|
re_path(r'^add_tracking/', views.StockItemTrackingCreate.as_view(), name='stock-tracking-create'),
|
||||||
|
|
||||||
|
@ -125,35 +125,6 @@ class StockLocationQRCode(QRCodeView):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class StockItemReturnToStock(AjaxUpdateView):
|
|
||||||
"""View for returning a stock item (which is assigned to a customer) to stock."""
|
|
||||||
|
|
||||||
model = StockItem
|
|
||||||
ajax_form_title = _("Return to Stock")
|
|
||||||
context_object_name = "item"
|
|
||||||
form_class = StockForms.ReturnStockItemForm
|
|
||||||
|
|
||||||
def validate(self, item, form, **kwargs):
|
|
||||||
"""Make sure required data is there."""
|
|
||||||
location = form.cleaned_data.get('location', None)
|
|
||||||
|
|
||||||
if not location:
|
|
||||||
form.add_error('location', _('Specify a valid location'))
|
|
||||||
|
|
||||||
def save(self, item, form, **kwargs):
|
|
||||||
"""Return stock."""
|
|
||||||
location = form.cleaned_data.get('location', None)
|
|
||||||
|
|
||||||
if location:
|
|
||||||
item.returnFromCustomer(location, self.request.user)
|
|
||||||
|
|
||||||
def get_data(self):
|
|
||||||
"""Set success message."""
|
|
||||||
return {
|
|
||||||
'success': _('Stock item returned from customer')
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class StockItemDeleteTestData(AjaxUpdateView):
|
class StockItemDeleteTestData(AjaxUpdateView):
|
||||||
"""View for deleting all test data."""
|
"""View for deleting all test data."""
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user