mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Adds API endpoint to "ship" a sales order shipment
This commit is contained in:
parent
c6b11b5e38
commit
f3f3030b37
@ -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'),
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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>`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -94,6 +94,9 @@ function serializeStockItem(pk, options={}) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
options.confirm = true;
|
||||||
|
options.confirmMessage = '{% trans "Confirm Stock Serialization" %}';
|
||||||
|
|
||||||
constructForm(url, options);
|
constructForm(url, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user