2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-17 12:35:46 +00:00

More stuffs:

- Allow filtering of salesorderlineitem by "completed" status
- Allow deletion of (empty) shipment
- Show which items are going to be shipped
This commit is contained in:
Oliver
2021-12-02 23:52:53 +11:00
parent e74e7138a9
commit e1668c8662
9 changed files with 283 additions and 56 deletions

View File

@ -551,6 +551,39 @@ class SODetail(generics.RetrieveUpdateDestroyAPIView):
return queryset
class SOLineItemFilter(rest_filters.FilterSet):
"""
Custom filters for SOLineItemList endpoint
"""
class Meta:
model = models.SalesOrderLineItem
fields = [
'order',
'part',
]
completed = rest_filters.BooleanFilter(label='completed', method='filter_completed')
def filter_completed(self, queryset, name, value):
"""
Filter by lines which are "completed"
A line is completed when shipped >= quantity
"""
value = str2bool(value)
q = Q(shipped__gte=F('quantity'))
if value:
queryset = queryset.filter(q)
else:
queryset = queryset.exclude(q)
return queryset
class SOLineItemList(generics.ListCreateAPIView):
"""
API endpoint for accessing a list of SalesOrderLineItem objects.
@ -558,6 +591,7 @@ class SOLineItemList(generics.ListCreateAPIView):
queryset = models.SalesOrderLineItem.objects.all()
serializer_class = serializers.SOLineItemSerializer
filterset_class = SOLineItemFilter
def get_serializer(self, *args, **kwargs):
@ -620,6 +654,28 @@ class SOLineItemDetail(generics.RetrieveUpdateDestroyAPIView):
serializer_class = serializers.SOLineItemSerializer
class SalesOrderComplete(generics.CreateAPIView):
"""
API endpoint for manually marking a SalesOrder as "complete".
"""
queryset = models.SalesOrder.objects.all()
serializer_class = serializers.SalesOrderShipmentCompleteSerializer
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['request'] = self.request
try:
ctx['order'] = models.SalesOrder.objects.get(pk=self.kwargs.get('pk', None))
except:
pass
return ctx
class SalesOrderAllocate(generics.CreateAPIView):
"""
API endpoint to allocate stock items against a SalesOrder
@ -758,7 +814,7 @@ class SOShipmentList(generics.ListCreateAPIView):
]
class SOShipmentDetail(generics.RetrieveUpdateAPIView):
class SOShipmentDetail(generics.RetrieveUpdateDestroyAPIView):
"""
API detail endpooint for SalesOrderShipment model
"""
@ -863,6 +919,7 @@ order_api_urls = [
# Sales order detail view
url(r'^(?P<pk>\d+)/', include([
url(r'^complete/', SalesOrderComplete.as_view(), name='api-so-complete'),
url(r'^allocate/', SalesOrderAllocate.as_view(), name='api-so-allocate'),
url(r'^.*$', SODetail.as_view(), name='api-so-detail'),
])),

View File

@ -363,11 +363,30 @@ class PurchaseOrder(Order):
return self.lines.filter(quantity__gt=F('received'))
def completed_line_items(self):
"""
Return a list of completed line items against this order
"""
return self.lines.filter(quantity__lte=F('received'))
@property
def line_count(self):
return self.lines.count()
@property
def completed_line_count(self):
return self.completed_line_items().count()
@property
def pending_line_count(self):
return self.pending_line_items().count()
@property
def is_complete(self):
""" Return True if all line items have been received """
return self.pending_line_items().count() == 0
return self.lines.count() > 0 and self.pending_line_items().count() == 0
@transaction.atomic
def receive_line_item(self, line, location, quantity, user, status=StockStatus.OK, purchase_price=None, **kwargs):
@ -601,8 +620,7 @@ class SalesOrder(Order):
and mark it as "shipped" if so.
"""
return all([line.is_completed() for line in self.lines.all()])
return self.lines.count() > 0 and all([line.is_completed() for line in self.lines.all()])
def can_cancel(self):
"""
@ -635,6 +653,30 @@ class SalesOrder(Order):
return True
@property
def line_count(self):
return self.lines.count()
def completed_line_items(self):
"""
Return a queryset of the completed line items for this order
"""
return self.lines.filter(shipped__gte=F('quantity'))
def pending_line_items(self):
"""
Return a queryset of the pending line items for this order
"""
return self.lines.filter(shipped__lt=F('quantity'))
@property
def completed_line_count(self):
return self.completed_line_items().count()
@property
def pending_line_count(self):
return self.pending_line_items().count()
class PurchaseOrderAttachment(InvenTreeAttachment):
"""

View File

@ -489,6 +489,8 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
item_detail = stock.serializers.StockItemSerializer(source='item', many=False, read_only=True)
location_detail = stock.serializers.LocationSerializer(source='item.location', many=False, read_only=True)
shipment_date = serializers.DateField(source='shipment.shipment_date', read_only=True)
def __init__(self, *args, **kwargs):
order_detail = kwargs.pop('order_detail', False)
@ -527,6 +529,7 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
'part',
'part_detail',
'shipment',
'shipment_date',
]
@ -734,6 +737,20 @@ class SOShipmentAllocationItemSerializer(serializers.Serializer):
return data
class SalesOrderCompleteSerializer(serializers.Serializer):
"""
DRF serializer for manually marking a sales order as complete
"""
def save(self):
request = self.context['request']
order = self.context['order']
data = self.validated_data
class SOShipmentAllocationSerializer(serializers.Serializer):
"""
DRF serializer for allocation of stock items against a sales order / shipment

View File

@ -119,6 +119,18 @@ src="{% static 'img/blank_image.png' %}"
<td>{{ order.supplier_reference }}{% include "clip.html"%}</td>
</tr>
{% endif %}
<tr>
<td><span class='fas fa-tasks'></span></td>
<td>{% trans "Completed Line Items" %}</td>
<td>
{{ order.completed_line_count }} / {{ order.line_count }}
{% if order.is_complete %}
<span class='badge bg-success badge-right rounded-pill'>{% trans "Complete" %}</span>
{% else %}
<span class='badge bg-danger badge-right rounded-pill'>{% trans "Incomplete" %}</span>
{% endif %}
</td>
</tr>
{% if order.link %}
<tr>
<td><span class='fas fa-link'></span></td>

View File

@ -37,7 +37,7 @@
</div>
<div class='panel-content'>
<div id='order-toolbar-buttons' class='btn-group' style='float: right;'>
{% include "filter_list.html" with id="order-lines" %}
{% include "filter_list.html" with id="purchase-order-lines" %}
</div>
<table class='table table-striped table-condensed' id='po-line-table' data-toolbar='#order-toolbar-buttons'>
@ -190,6 +190,10 @@ $('#new-po-line').click(function() {
$('#receive-selected-items').click(function() {
var items = $("#po-line-table").bootstrapTable('getSelections');
if (items.length == 0) {
items = $("#po-line-table").bootstrapTable('getData');
}
receivePurchaseOrderItems(
{{ order.id }},
items,

View File

@ -63,8 +63,8 @@ src="{% static 'img/blank_image.png' %}"
</div>
{% if order.status == SalesOrderStatus.PENDING %}
<button type='button' class='btn btn-success' id='ship-order' title='{% trans "Ship Order" %}'>
<span class='fas fa-truck'></span> {% trans "Ship Order" %}
<button type='button' class='btn btn-success' id='complete-order' title='{% trans "Complete Sales Order" %}'>
<span class='fas fa-check-circle'></span> {% trans "Complete Order" %}
</button>
{% endif %}
{% endif %}
@ -123,6 +123,18 @@ src="{% static 'img/blank_image.png' %}"
<td>{{ order.customer_reference }}{% include "clip.html"%}</td>
</tr>
{% endif %}
<tr>
<td><span class='fas fa-tasks'></span></td>
<td>{% trans "Completed Line Items" %}</td>
<td>
{{ order.completed_line_count }} / {{ order.line_count }}
{% if order.is_completed %}
<span class='badge bg-success badge-right rounded-pill'>{% trans "Complete" %}</span>
{% else %}
<span class='badge bg-danger badge-right rounded-pill'>{% trans "Incomplete" %}</span>
{% endif %}
</td>
</tr>
{% if order.link %}
<tr>
<td><span class='fas fa-link'></span></td>
@ -149,13 +161,6 @@ src="{% static 'img/blank_image.png' %}"
<td>{{ order.shipment_date }}<span class='badge badge-right rounded-pill bg-dark'>{{ order.shipped_by }}</span></td>
</tr>
{% endif %}
{% if order.status == PurchaseOrderStatus.COMPLETE %}
<tr>
<td><span class='fas fa-calendar-alt'></span></td>
<td>{% trans "Received" %}</td>
<td>{{ order.complete_date }}<span class='badge badge-right rounded-pill bg-dark'>{{ order.received_by }}</span></td>
</tr>
{% endif %}
{% if order.responsible %}
<tr>
<td><span class='fas fa-users'></span></td>
@ -203,10 +208,8 @@ $("#cancel-order").click(function() {
});
});
$("#ship-order").click(function() {
launchModalForm("{% url 'so-ship' order.id %}", {
reload: true,
});
$("#complete-order").click(function() {
completeSalesOrder({{ order.pk }});
});
{% if report_enabled %}