Rebuild the "PurchaseOrder" detail

- Use AJAX and bootstrap-table
- Display progress bar
This commit is contained in:
Oliver Walters 2020-04-24 12:52:08 +10:00
parent ba1d2063af
commit 9d25ed335c
10 changed files with 201 additions and 110 deletions

View File

@ -116,7 +116,7 @@
}
.icon-green {
color: #5c5;
color: #43bb43;
}
.icon-blue {

View File

@ -64,15 +64,11 @@ class CompanySerializer(InvenTreeModelSerializer):
class SupplierPartSerializer(InvenTreeModelSerializer):
""" Serializer for SupplierPart object """
url = serializers.CharField(source='get_absolute_url', read_only=True)
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
supplier_detail = CompanyBriefSerializer(source='supplier', many=False, read_only=True)
manufacturer_detail = CompanyBriefSerializer(source='manufacturer', many=False, read_only=True)
pricing = serializers.CharField(source='unit_pricing', read_only=True)
def __init__(self, *args, **kwargs):
part_detail = kwargs.pop('part_detail', False)
@ -94,7 +90,6 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
model = SupplierPart
fields = [
'pk',
'url',
'part',
'part_detail',
'supplier',
@ -105,7 +100,6 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
'description',
'MPN',
'link',
'pricing',
]

View File

@ -162,6 +162,17 @@ class POLineItemList(generics.ListCreateAPIView):
queryset = PurchaseOrderLineItem.objects.all()
serializer_class = POLineItemSerializer
def get_serializer(self, *args, **kwargs):
try:
kwargs['part_detail'] = str2bool(self.request.query_params.get('part_detail', False))
except AttributeError:
pass
kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs)
permission_classes = [
permissions.IsAuthenticated,
]

View File

@ -417,6 +417,10 @@ class PurchaseOrderLineItem(OrderLineItem):
help_text=_('Purchase Order')
)
def get_base_part(self):
""" Return the base-part for the line item """
return self.part.part
# TODO - Function callback for when the SupplierPart is deleted?
part = models.ForeignKey(

View File

@ -10,7 +10,7 @@ from rest_framework import serializers
from django.db.models import Count
from InvenTree.serializers import InvenTreeModelSerializer
from company.serializers import CompanyBriefSerializer
from company.serializers import CompanyBriefSerializer, SupplierPartSerializer
from part.serializers import PartBriefSerializer
from .models import PurchaseOrder, PurchaseOrderLineItem
@ -74,6 +74,22 @@ class POSerializer(InvenTreeModelSerializer):
class POLineItemSerializer(InvenTreeModelSerializer):
def __init__(self, *args, **kwargs):
part_detail = kwargs.pop('part_detail', False)
super().__init__(*args, **kwargs)
if part_detail is not True:
self.fields.pop('part_detail')
self.fields.pop('supplier_part_detail')
quantity = serializers.FloatField()
received = serializers.FloatField()
part_detail = PartBriefSerializer(source='get_base_part', many=False, read_only=True)
supplier_part_detail = SupplierPartSerializer(source='part', many=False, read_only=True)
class Meta:
model = PurchaseOrderLineItem
@ -84,6 +100,8 @@ class POLineItemSerializer(InvenTreeModelSerializer):
'notes',
'order',
'part',
'part_detail',
'supplier_part_detail',
'received',
]

View File

@ -28,16 +28,16 @@ src="{% static 'img/blank_image.png' %}"
<div class='btn-row'>
<div class='btn-group action-buttons'>
<button type='button' class='btn btn-default' id='edit-order' title='Edit order information'>
<span class='fas fa-edit'></span>
<span class='fas fa-edit icon-green'></span>
</button>
<button type='button' class='btn btn-default' id='export-order' title='Export order to file'>
<span class='fas fa-file-download'></span>
</button>
{% if order.status == OrderStatus.PENDING and order.lines.count > 0 %}
{% if order.status == PurchaseOrderStatus.PENDING and order.lines.count > 0 %}
<button type='button' class='btn btn-default' id='place-order' title='Place order'>
<span class='fas fa-paper-plane'></span>
<span class='fas fa-paper-plane icon-blue'></span>
</button>
{% elif order.status == OrderStatus.PLACED %}
{% elif order.status == PurchaseOrderStatus.PLACED %}
<button type='button' class='btn btn-default' id='receive-order' title='Receive items'>
<span class='fas fa-clipboard-check'></span>
</button>
@ -45,9 +45,9 @@ src="{% static 'img/blank_image.png' %}"
<span class='fas fa-check-circle'></span>
</button>
{% endif %}
{% if order.status == OrderStatus.PENDING or order.status == OrderStatus.PLACED %}
{% if order.status == PurchaseOrderStatus.PENDING or order.status == PurchaseOrderStatus.PLACED %}
<button type='button' class='btn btn-default' id='cancel-order' title='Cancel order'>
<span class='fas fa-times-circle'></span>
<span class='fas fa-times-circle icon-red'></span>
</button>
{% endif %}
</div>
@ -100,7 +100,7 @@ src="{% static 'img/blank_image.png' %}"
<td>{{ order.issue_date }}</td>
</tr>
{% endif %}
{% if order.status == OrderStatus.COMPLETE %}
{% if order.status == PurchaseOrderStatus.COMPLETE %}
<tr>
<td><span class='fas fa-calendar-alt'></span></td>
<td>{% trans "Received" %}</td>
@ -113,7 +113,7 @@ src="{% static 'img/blank_image.png' %}"
{% block js_ready %}
{{ block.super }}
{% if order.status == OrderStatus.PENDING and order.lines.count > 0 %}
{% if order.status == PurchaseOrderStatus.PENDING and order.lines.count > 0 %}
$("#place-order").click(function() {
launchModalForm("{% url 'po-issue' order.id %}",
{

View File

@ -12,73 +12,14 @@
<hr>
<div id='order-toolbar-buttons' class='btn-group' style='float: right;'>
{% if order.status == OrderStatus.PENDING %}
{% if order.status == PurchaseOrderStatus.PENDING %}
<button type='button' class='btn btn-default' id='new-po-line'>{% trans "Add Line Item" %}</button>
{% endif %}
</div>
<h4>{% trans "Purchase Order Items" %}</h4>
<table class='table table-striped table-condensed' id='po-lines-table' data-toolbar='#order-toolbar-buttons'>
<thead>
<tr>
<th data-sortable='true'>{% trans "Line" %}</th>
<th data-sortable='true'>{% trans "Part" %}</th>
<th>{% trans "Description" %}</th>
<th data-sortable='true'>{% trans "Order Code" %}</th>
<th data-sortable='true'>{% trans "Reference" %}</th>
<th data-sortable='true'>{% trans "Quantity" %}</th>
{% if not order.status == OrderStatus.PENDING %}
<th data-sortable='true'>{% trans "Received" %}</th>
{% endif %}
<th>{% trans "Note" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for line in order.lines.all %}
<tr{% if order.status == OrderStatus.PLACED %} class={% if line.received < line.quantity %}'rowinvalid'{% else %}'rowvalid'{% endif %}{% endif %}>
<td>
{{ forloop.counter }}
</td>
{% if line.part %}
<td>
{% include "hover_image.html" with image=line.part.part.image hover=True %}
<a href="{% url 'part-detail' line.part.part.id %}">{{ line.part.part.full_name }}</a>
</td>
<td>{{ line.part.part.description }}</td>
<td><a href="{% url 'supplier-part-detail' line.part.id %}">{{ line.part.SKU }}</a></td>
{% else %}
<td colspan='3'><strong>Warning: Part has been deleted.</strong></td>
{% endif %}
<td>{{ line.reference }}</td>
<td>{% decimal line.quantity %}</td>
{% if not order.status == OrderStatus.PENDING %}
<td>{% decimal line.received %}</td>
{% endif %}
<td>
{{ line.notes }}
</td>
<td>
<div class='btn-group'>
{% if order.status == OrderStatus.PENDING %}
<button class='btn btn-default btn-glyph' line='{{ line.id }}' id='edit-line-item-{{ line.id }} title='Edit line item' onclick='editPurchaseOrderLineItem()'>
<span url="{% url 'po-line-item-edit' line.id %}" line='{{ line.id }}' class='glyphicon glyphicon-edit'></span>
</button>
<button class='btn btn-default btn-glyph' line='{{ line.id }}' id='remove-line-item-{{ line.id }' title='Remove line item' type='button' onclick='removePurchaseOrderLineItem()'>
<span url="{% url 'po-line-item-delete' line.id %}" line='{{ line.id }}' class='glyphicon glyphicon-remove'></span>
</button>
{% endif %}
{% if order.status == OrderStatus.PLACED and line.received < line.quantity %}
<button class='btn btn-default btn-glyph line-receive' pk='{{ line.pk }}' title='Receive item(s)'>
<span class='glyphicon glyphicon-check'></span>
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
<table class='table table-striped table-condensed' id='po-table' data-toolbar='#order-toolbar-buttons'>
</table>
{% endblock %}
@ -87,27 +28,6 @@
{{ block.super }}
$("#po-lines-table").on('click', ".line-receive", function() {
var button = $(this);
console.log('clicked! ' + button.attr('pk'));
launchModalForm("{% url 'po-receive' order.id %}", {
reload: true,
data: {
line: button.attr('pk')
},
secondary: [
{
field: 'location',
label: 'New Location',
title: 'Create new stock location',
url: "{% url 'stock-location-create' %}",
},
]
});
});
$("#receive-order").click(function() {
launchModalForm("{% url 'po-receive' order.id %}", {
@ -115,8 +35,8 @@ $("#receive-order").click(function() {
secondary: [
{
field: 'location',
label: 'New Location',
title: 'Create new stock location',
label: '{% trans "New Location" %}',
title: '{% trans "Create new stock location" %}',
url: "{% url 'stock-location-create' %}",
},
]
@ -133,7 +53,7 @@ $("#export-order").click(function() {
location.href = "{% url 'po-export' order.id %}";
});
{% if order.status == OrderStatus.PENDING %}
{% if order.status == PurchaseOrderStatus.PENDING %}
$('#new-po-line').click(function() {
launchModalForm("{% url 'po-line-item-create' %}",
{
@ -144,8 +64,8 @@ $('#new-po-line').click(function() {
secondary: [
{
field: 'part',
label: 'New Supplier Part',
title: 'Create new supplier part',
label: '{% trans "New Supplier Part" %}',
title: '{% trans "Create new supplier part" %}',
url: "{% url 'supplier-part-create' %}",
data: {
supplier: {{ order.supplier.id }},
@ -157,7 +77,153 @@ $('#new-po-line').click(function() {
});
{% endif %}
$("#po-lines-table").inventreeTable({
function reloadTable() {
$("#po-table").bootstrapTable("refresh");
}
function setupCallbacks() {
// Setup callbacks for the line buttons
var table = $("#po-table");
{% if order.status == PurchaseOrderStatus.PENDING %}
table.find(".button-line-edit").click(function() {
var pk = $(this).attr('pk');
launchModalForm(`/order/purchase-order/line/${pk}/edit/`, {
success: reloadTable,
});
});
table.find(".button-line-delete").click(function() {
var pk = $(this).attr('pk');
launchModalForm(`/order/purchase-order/line/${pk}/delete/`, {
success: reloadTable,
});
});
{% endif %}
table.find(".button-line-receive").click(function() {
var pk = $(this).attr('pk');
launchModalForm("{% url 'po-receive' order.id %}", {
success: reloadTable,
data: {
line: pk,
},
secondary: [
{
field: 'location',
label: '{% trans "New Location" %}',
title: '{% trans "Create new stock location" %}',
url: "{% url 'stock-location-create' %}",
},
]
});
});
}
$("#po-table").inventreeTable({
onPostBody: setupCallbacks,
formatNoMatches: function() { return "{% trans 'No line items found' %}"; },
queryParams: {
order: {{ order.id }},
part_detail: true,
},
url: "{% url 'api-po-line-list' %}",
columns: [
{
field: 'pk',
title: 'ID',
visible: false,
},
{
field: 'part',
sortable: true,
title: '{% trans "Part" %}',
formatter: function(value, row, index, field) {
if (row.part) {
return imageHoverIcon(row.part_detail.thumbnail) + renderLink(row.part_detail.full_name, `/part/${value}/`);
} else {
return '-';
}
},
},
{
sortable: true,
field: 'part_detail.description',
title: '{% trans "Description" %}',
},
{
sortable: true,
field: 'supplier_part_detail.SKU',
title: '{% trans "Order Code" %}',
formatter: function(value, row, index, field) {
return renderLink(value, `/supplier-part/${row.part}/`);
},
},
{
sortable: true,
field: 'reference',
title: '{% trans "Reference" %}',
},
{
sortable: true,
field: 'quantity',
title: '{% trans "Quantity" %}'
},
{
sortable: true,
field: 'received',
title: '{% trans "Received" %}',
formatter: function(value, row, index, field) {
return makeProgressBar(row.received, row.quantity, {
id: `order-line-progress-${row.pk}`,
});
},
sorter: function(valA, valB, rowA, rowB) {
if (rowA.received == 0 && rowB.received == 0) {
return (rowA.quantity > rowB.quantity) ? 1 : -1;
}
var progressA = parseFloat(rowA.received) / rowA.quantity;
var progressB = parseFloat(rowB.received) / rowB.quantity;
return (progressA < progressB) ? 1 : -1;
}
},
{
field: 'notes',
title: '{% trans "Notes" %}',
},
{
field: 'buttons',
title: '',
formatter: function(value, row, index, field) {
var html = `<div class='btn-group'>`;
var pk = row.pk;
{% if order.status == PurchaseOrderStatus.PENDING %}
html += makeIconButton('fa-edit', 'button-line-edit', pk, '{% trans "Edit line item" %}');
html += makeIconButton('fa-trash-alt icon-red', 'button-line-delete', pk, '{% trans "Delete line item" %}');
{% endif %}
{% if order.status == PurchaseOrderStatus.PLACED %}
if (row.received < row.quantity) {
html += makeIconButton('fa-clipboard-check', 'button-line-receive', pk, '{% trans "Receive line item" %}');
}
{% endif %}
html += `</div>`;
return html;
},
}
]
});

View File

@ -99,7 +99,7 @@ src="{% static 'img/blank_image.png' %}"
<td>{{ order.shipment_date }}<span class='badge'>{{ order.shipped_by }}</span></td>
</tr>
{% endif %}
{% if order.status == OrderStatus.COMPLETE %}
{% if order.status == PurchaseOrderStatus.COMPLETE %}
<tr>
<td><span class='fas fa-calendar-alt'></span></td>
<td>{% trans "Received" %}</td>

View File

@ -160,8 +160,8 @@ $("#so-lines-table").inventreeTable({
return (rowA.quantity > rowB.quantity) ? 1 : -1;
}
var progressA = rowA.allocated / rowA.quantity;
var progressB = rowB.allocated / rowA.quantity;
var progressA = parseFloat(rowA.allocated) / rowA.quantity;
var progressB = parseFloat(rowB.allocated) / rowB.quantity;
return (progressA < progressB) ? 1 : -1;
}
@ -204,7 +204,7 @@ $("#so-lines-table").inventreeTable({
],
});
function setupCallbacks(table) {
function setupCallbacks() {
var table = $("#so-lines-table");

View File

@ -52,14 +52,12 @@ class PartThumbSerializer(serializers.Serializer):
class PartBriefSerializer(InvenTreeModelSerializer):
""" Serializer for Part (brief detail) """
url = serializers.CharField(source='get_absolute_url', read_only=True)
thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True)
class Meta:
model = Part
fields = [
'pk',
'url',
'full_name',
'description',
'thumbnail',