Adds API endpoint to "ship" a sales order shipment

This commit is contained in:
Oliver 2021-11-30 00:02:03 +11:00
parent c6b11b5e38
commit f3f3030b37
5 changed files with 119 additions and 9 deletions

View File

@ -767,6 +767,32 @@ class SOShipmentDetail(generics.RetrieveUpdateAPIView):
serializer_class = serializers.SalesOrderShipmentSerializer serializer_class = serializers.SalesOrderShipmentSerializer
class SOShipmentComplete(generics.CreateAPIView):
"""
API endpoint for completing (shipping) a SalesOrderShipment
"""
queryset = models.SalesOrderShipment.objects.all()
serializer_class = serializers.SalesOrderShipmentCompleteSerializer
def get_serializer_context(self):
"""
Pass the request object to the serializer
"""
ctx = super().get_serializer_context()
ctx['request'] = self.request
try:
ctx['shipment'] = models.SalesOrderShipment.objects.get(
pk=self.kwargs.get('pk', None)
)
except:
pass
return ctx
class POAttachmentList(generics.ListCreateAPIView, AttachmentMixin): class POAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
""" """
API endpoint for listing (and creating) a PurchaseOrderAttachment (file upload) API endpoint for listing (and creating) a PurchaseOrderAttachment (file upload)
@ -829,6 +855,7 @@ order_api_urls = [
url(r'^shipment/', include([ url(r'^shipment/', include([
url(r'^(?P<pk>\d+)/', include([ url(r'^(?P<pk>\d+)/', include([
url(r'^ship/$', SOShipmentComplete.as_view(), name='api-so-shipment-ship'),
url(r'^.*$', SOShipmentDetail.as_view(), name='api-so-shipment-detail'), url(r'^.*$', SOShipmentDetail.as_view(), name='api-so-shipment-detail'),
])), ])),
url(r'^.*$', SOShipmentList.as_view(), name='api-so-shipment-list'), url(r'^.*$', SOShipmentList.as_view(), name='api-so-shipment-list'),

View File

@ -996,6 +996,15 @@ class SalesOrderShipment(models.Model):
help_text=_('Shipment tracking information'), help_text=_('Shipment tracking information'),
) )
def check_can_complete(self):
if self.shipment_date:
# Shipment has already been sent!
raise ValidationError(_("Shipment has already been sent"))
if self.allocations.count() == 0:
raise ValidationError(_("Shipment has no allocated stock items"))
@transaction.atomic @transaction.atomic
def complete_shipment(self, user): def complete_shipment(self, user):
""" """
@ -1006,12 +1015,13 @@ class SalesOrderShipment(models.Model):
3. Set the "shipment_date" to now 3. Set the "shipment_date" to now
""" """
if self.shipment_date: # Check if the shipment can be completed (throw error if not)
# Ignore, shipment has already been sent! self.check_can_complete()
return
allocations = self.allocations.all()
# Iterate through each stock item assigned to this shipment # Iterate through each stock item assigned to this shipment
for allocation in self.allocations.all(): for allocation in allocations:
# Mark the allocation as "complete" # Mark the allocation as "complete"
allocation.complete_allocation(user) allocation.complete_allocation(user)

View File

@ -25,7 +25,6 @@ from InvenTree.helpers import normalize
from InvenTree.serializers import InvenTreeModelSerializer from InvenTree.serializers import InvenTreeModelSerializer
from InvenTree.serializers import InvenTreeDecimalField from InvenTree.serializers import InvenTreeDecimalField
from InvenTree.serializers import InvenTreeMoneySerializer from InvenTree.serializers import InvenTreeMoneySerializer
from InvenTree.serializers import InvenTreeAttachmentSerializerField
from InvenTree.status_codes import StockStatus from InvenTree.status_codes import StockStatus
import order.models import order.models
@ -617,6 +616,46 @@ class SalesOrderShipmentSerializer(InvenTreeModelSerializer):
] ]
class SalesOrderShipmentCompleteSerializer(serializers.ModelSerializer):
"""
Serializer for completing (shipping) a SalesOrderShipment
"""
class Meta:
model = order.models.SalesOrderShipment
fields = [
'tracking_number',
]
def validate(self, data):
data = super().validate(data)
shipment = self.context.get('shipment', None)
if not shipment:
raise ValidationError(_("No shipment details provided"))
shipment.check_can_complete()
return data
def save(self):
shipment = self.context.get('shipment', None)
if not shipment:
return
data = self.validated_data
request = self.context['request']
user = request.user
shipment.complete_shipment(user)
class SOShipmentAllocationItemSerializer(serializers.Serializer): class SOShipmentAllocationItemSerializer(serializers.Serializer):
""" """
A serializer for allocating a single stock-item against a SalesOrder shipment A serializer for allocating a single stock-item against a SalesOrder shipment

View File

@ -20,6 +20,7 @@
/* exported /* exported
allocateStockToSalesOrder, allocateStockToSalesOrder,
completeShipment,
createSalesOrder, createSalesOrder,
editPurchaseOrderLineItem, editPurchaseOrderLineItem,
exportOrder, exportOrder,
@ -52,6 +53,26 @@ function salesOrderShipmentFields(options={}) {
} }
/*
* Complete a shipment
*/
function completeShipment(shipment_id) {
constructForm(`/api/order/so/shipment/${shipment_id}/ship/`, {
method: 'POST',
title: '{% trans "Complete Shipment" %}',
fields: {
tracking_number: {},
},
confirm: true,
confirmMessage: '{% trans "Confirm Shipment" %}',
onSuccess: function(data) {
// TODO
}
});
}
// Open a dialog to create a new sales order shipment // Open a dialog to create a new sales order shipment
function createSalesOrderShipment(options={}) { function createSalesOrderShipment(options={}) {
constructForm('{% url "api-so-shipment-list" %}', { constructForm('{% url "api-so-shipment-list" %}', {
@ -1183,6 +1204,8 @@ function loadSalesOrderShipmentTable(table, options={}) {
html += makeIconButton('fa-edit icon-blue', 'button-shipment-edit', pk, '{% trans "Edit shipment" %}'); html += makeIconButton('fa-edit icon-blue', 'button-shipment-edit', pk, '{% trans "Edit shipment" %}');
html += makeIconButton('fa-truck icon-green', 'button-shipment-ship', pk, '{% trans "Complete shipment" %}');
html += `</div>`; html += `</div>`;
return html; return html;
@ -1205,6 +1228,12 @@ function loadSalesOrderShipmentTable(table, options={}) {
} }
}); });
}); });
$(table).find('.button-shipment-ship').click(function() {
var pk = $(this).attr('pk');
completeShipment(pk);
});
} }
$(table).inventreeTable({ $(table).inventreeTable({
@ -1505,7 +1534,7 @@ function allocateStockToSalesOrder(order_id, line_items, options={}) {
// Exclude expired stock? // Exclude expired stock?
if (global_settings.STOCK_ENABLE_EXPIRY && !global_settings.STOCK_ALLOW_EXPIRED_SALE) { if (global_settings.STOCK_ENABLE_EXPIRY && !global_settings.STOCK_ALLOW_EXPIRED_SALE) {
fields.item.filters.expired = false; filters.expired = false;
} }
return filters; return filters;
@ -1781,14 +1810,16 @@ function showAllocationSubTable(index, row, element, options) {
title: '{% trans "Location" %}', title: '{% trans "Location" %}',
formatter: function(value, row, index, field) { formatter: function(value, row, index, field) {
// Location specified if (shipped) {
if (row.location) { return `<em>{% trans "Shipped to customer" %}</em>`;
} else if (row.location) {
// Location specified
return renderLink( return renderLink(
row.location_detail.pathstring || '{% trans "Location" %}', row.location_detail.pathstring || '{% trans "Location" %}',
`/stock/location/${row.location}/` `/stock/location/${row.location}/`
); );
} else { } else {
return `<i>{% trans "Stock location not specified" %}`; return `<em>{% trans "Stock location not specified" %}</em>`;
} }
}, },
}, },

View File

@ -94,6 +94,9 @@ function serializeStockItem(pk, options={}) {
}); });
} }
options.confirm = true;
options.confirmMessage = '{% trans "Confirm Stock Serialization" %}';
constructForm(url, options); constructForm(url, options);
} }