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 { .icon-green {
color: #5c5; color: #43bb43;
} }
.icon-blue { .icon-blue {

View File

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

View File

@ -162,6 +162,17 @@ class POLineItemList(generics.ListCreateAPIView):
queryset = PurchaseOrderLineItem.objects.all() queryset = PurchaseOrderLineItem.objects.all()
serializer_class = POLineItemSerializer 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 = [ permission_classes = [
permissions.IsAuthenticated, permissions.IsAuthenticated,
] ]

View File

@ -417,6 +417,10 @@ class PurchaseOrderLineItem(OrderLineItem):
help_text=_('Purchase Order') 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? # TODO - Function callback for when the SupplierPart is deleted?
part = models.ForeignKey( part = models.ForeignKey(

View File

@ -10,7 +10,7 @@ from rest_framework import serializers
from django.db.models import Count from django.db.models import Count
from InvenTree.serializers import InvenTreeModelSerializer from InvenTree.serializers import InvenTreeModelSerializer
from company.serializers import CompanyBriefSerializer from company.serializers import CompanyBriefSerializer, SupplierPartSerializer
from part.serializers import PartBriefSerializer from part.serializers import PartBriefSerializer
from .models import PurchaseOrder, PurchaseOrderLineItem from .models import PurchaseOrder, PurchaseOrderLineItem
@ -74,6 +74,22 @@ class POSerializer(InvenTreeModelSerializer):
class POLineItemSerializer(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: class Meta:
model = PurchaseOrderLineItem model = PurchaseOrderLineItem
@ -84,6 +100,8 @@ class POLineItemSerializer(InvenTreeModelSerializer):
'notes', 'notes',
'order', 'order',
'part', 'part',
'part_detail',
'supplier_part_detail',
'received', 'received',
] ]

View File

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

View File

@ -12,73 +12,14 @@
<hr> <hr>
<div id='order-toolbar-buttons' class='btn-group' style='float: right;'> <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> <button type='button' class='btn btn-default' id='new-po-line'>{% trans "Add Line Item" %}</button>
{% endif %} {% endif %}
</div> </div>
<h4>{% trans "Purchase Order Items" %}</h4> <h4>{% trans "Purchase Order Items" %}</h4>
<table class='table table-striped table-condensed' id='po-lines-table' data-toolbar='#order-toolbar-buttons'> <table class='table table-striped table-condensed' id='po-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> </table>
{% endblock %} {% endblock %}
@ -87,27 +28,6 @@
{{ block.super }} {{ 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() { $("#receive-order").click(function() {
launchModalForm("{% url 'po-receive' order.id %}", { launchModalForm("{% url 'po-receive' order.id %}", {
@ -115,8 +35,8 @@ $("#receive-order").click(function() {
secondary: [ secondary: [
{ {
field: 'location', field: 'location',
label: 'New Location', label: '{% trans "New Location" %}',
title: 'Create new stock location', title: '{% trans "Create new stock location" %}',
url: "{% url 'stock-location-create' %}", url: "{% url 'stock-location-create' %}",
}, },
] ]
@ -133,7 +53,7 @@ $("#export-order").click(function() {
location.href = "{% url 'po-export' order.id %}"; location.href = "{% url 'po-export' order.id %}";
}); });
{% if order.status == OrderStatus.PENDING %} {% if order.status == PurchaseOrderStatus.PENDING %}
$('#new-po-line').click(function() { $('#new-po-line').click(function() {
launchModalForm("{% url 'po-line-item-create' %}", launchModalForm("{% url 'po-line-item-create' %}",
{ {
@ -144,8 +64,8 @@ $('#new-po-line').click(function() {
secondary: [ secondary: [
{ {
field: 'part', field: 'part',
label: 'New Supplier Part', label: '{% trans "New Supplier Part" %}',
title: 'Create new supplier part', title: '{% trans "Create new supplier part" %}',
url: "{% url 'supplier-part-create' %}", url: "{% url 'supplier-part-create' %}",
data: { data: {
supplier: {{ order.supplier.id }}, supplier: {{ order.supplier.id }},
@ -157,7 +77,153 @@ $('#new-po-line').click(function() {
}); });
{% endif %} {% 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> <td>{{ order.shipment_date }}<span class='badge'>{{ order.shipped_by }}</span></td>
</tr> </tr>
{% endif %} {% endif %}
{% if order.status == OrderStatus.COMPLETE %} {% if order.status == PurchaseOrderStatus.COMPLETE %}
<tr> <tr>
<td><span class='fas fa-calendar-alt'></span></td> <td><span class='fas fa-calendar-alt'></span></td>
<td>{% trans "Received" %}</td> <td>{% trans "Received" %}</td>

View File

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

View File

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