mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +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 = 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
|
||||
|
||||
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
|
||||
- Adds 'available_stock' 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):
|
||||
"""API endpoint for serializing a stock item."""
|
||||
|
||||
queryset = StockItem.objects.none()
|
||||
queryset = StockItem.objects.all()
|
||||
serializer_class = StockSerializers.SerializeStockItemSerializer
|
||||
|
||||
|
||||
@ -118,17 +118,24 @@ class StockItemInstall(StockItemContextMixin, generics.CreateAPIView):
|
||||
- stock_item must be serialized (and not belong to another item)
|
||||
"""
|
||||
|
||||
queryset = StockItem.objects.none()
|
||||
queryset = StockItem.objects.all()
|
||||
serializer_class = StockSerializers.InstallStockItemSerializer
|
||||
|
||||
|
||||
class StockItemUninstall(StockItemContextMixin, generics.CreateAPIView):
|
||||
"""API endpoint for removing (uninstalling) items from this item."""
|
||||
|
||||
queryset = StockItem.objects.none()
|
||||
queryset = StockItem.objects.all()
|
||||
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):
|
||||
"""A generic class for handling stocktake actions.
|
||||
|
||||
@ -1370,6 +1377,7 @@ stock_api_urls = [
|
||||
re_path(r'^(?P<pk>\d+)/', include([
|
||||
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'^return/', StockItemReturn.as_view(), name='api-stock-item-return'),
|
||||
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'^.*$', StockDetail.as_view(), name='api-stock-detail'),
|
||||
|
@ -5,21 +5,6 @@ from InvenTree.forms import HelperForm
|
||||
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):
|
||||
"""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 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."""
|
||||
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):
|
||||
"""Serializer for a simple tree view."""
|
||||
|
||||
|
@ -632,11 +632,21 @@ $('#stock-remove').click(function() {
|
||||
{% else %}
|
||||
|
||||
$("#stock-return-from-customer").click(function() {
|
||||
launchModalForm("{% url 'stock-item-return' item.id %}",
|
||||
{
|
||||
reload: true,
|
||||
}
|
||||
);
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
{% endif %}
|
||||
|
@ -663,6 +663,45 @@ class StockItemTest(StockAPITestCase):
|
||||
self.assertIsNone(sub_item.belongs_to)
|
||||
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):
|
||||
"""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'^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'^return/', views.StockItemReturnToStock.as_view(), name='stock-item-return'),
|
||||
|
||||
re_path(r'^add_tracking/', views.StockItemTrackingCreate.as_view(), name='stock-tracking-create'),
|
||||
|
||||
|
@ -125,35 +125,6 @@ class StockLocationQRCode(QRCodeView):
|
||||
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):
|
||||
"""View for deleting all test data."""
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user