mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Rebuild the "PurchaseOrder" detail
- Use AJAX and bootstrap-table - Display progress bar
This commit is contained in:
parent
ba1d2063af
commit
9d25ed335c
@ -116,7 +116,7 @@
|
||||
}
|
||||
|
||||
.icon-green {
|
||||
color: #5c5;
|
||||
color: #43bb43;
|
||||
}
|
||||
|
||||
.icon-blue {
|
||||
|
@ -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',
|
||||
]
|
||||
|
||||
|
||||
|
@ -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,
|
||||
]
|
||||
|
@ -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(
|
||||
|
@ -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',
|
||||
]
|
||||
|
||||
|
@ -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 %}",
|
||||
{
|
||||
|
@ -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;
|
||||
},
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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");
|
||||
|
||||
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user