From 9d25ed335c9247a5bfe19500cc6f960eb4942a22 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 24 Apr 2020 12:52:08 +1000 Subject: [PATCH] Rebuild the "PurchaseOrder" detail - Use AJAX and bootstrap-table - Display progress bar --- InvenTree/InvenTree/static/css/inventree.css | 2 +- InvenTree/company/serializers.py | 6 - InvenTree/order/api.py | 11 + InvenTree/order/models.py | 4 + InvenTree/order/serializers.py | 20 +- .../order/templates/order/order_base.html | 16 +- .../order/purchase_order_detail.html | 242 +++++++++++------- .../templates/order/sales_order_base.html | 2 +- .../templates/order/sales_order_detail.html | 6 +- InvenTree/part/serializers.py | 2 - 10 files changed, 201 insertions(+), 110 deletions(-) diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index 19b92e2817..bdfeb42f4c 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -116,7 +116,7 @@ } .icon-green { - color: #5c5; + color: #43bb43; } .icon-blue { diff --git a/InvenTree/company/serializers.py b/InvenTree/company/serializers.py index 701c1faabf..780492cdd5 100644 --- a/InvenTree/company/serializers.py +++ b/InvenTree/company/serializers.py @@ -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', ] diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index 08a69f7dec..d8568ddf3b 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -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, ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index d448e0d444..2eadd59138 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -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( diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index df3b68c924..992e05a80c 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -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', ] diff --git a/InvenTree/order/templates/order/order_base.html b/InvenTree/order/templates/order/order_base.html index 64bf832c02..3f76d98a77 100644 --- a/InvenTree/order/templates/order/order_base.html +++ b/InvenTree/order/templates/order/order_base.html @@ -28,16 +28,16 @@ src="{% static 'img/blank_image.png' %}"
- {% if order.status == OrderStatus.PENDING and order.lines.count > 0 %} + {% if order.status == PurchaseOrderStatus.PENDING and order.lines.count > 0 %} - {% elif order.status == OrderStatus.PLACED %} + {% elif order.status == PurchaseOrderStatus.PLACED %} @@ -45,9 +45,9 @@ src="{% static 'img/blank_image.png' %}" {% endif %} - {% if order.status == OrderStatus.PENDING or order.status == OrderStatus.PLACED %} + {% if order.status == PurchaseOrderStatus.PENDING or order.status == PurchaseOrderStatus.PLACED %} {% endif %}
@@ -100,7 +100,7 @@ src="{% static 'img/blank_image.png' %}" {{ order.issue_date }} {% endif %} - {% if order.status == OrderStatus.COMPLETE %} + {% if order.status == PurchaseOrderStatus.COMPLETE %} {% trans "Received" %} @@ -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 %}", { diff --git a/InvenTree/order/templates/order/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html index 444da8f88f..73c794126c 100644 --- a/InvenTree/order/templates/order/purchase_order_detail.html +++ b/InvenTree/order/templates/order/purchase_order_detail.html @@ -12,73 +12,14 @@
- {% if order.status == OrderStatus.PENDING %} + {% if order.status == PurchaseOrderStatus.PENDING %} {% endif %}

{% trans "Purchase Order Items" %}

- - - - - - - - - - {% if not order.status == OrderStatus.PENDING %} - - {% endif %} - - - - - - {% for line in order.lines.all %} - - - {% if line.part %} - - - - {% else %} - - {% endif %} - - - {% if not order.status == OrderStatus.PENDING %} - - {% endif %} - - - - {% endfor %} - +
{% trans "Line" %}{% trans "Part" %}{% trans "Description" %}{% trans "Order Code" %}{% trans "Reference" %}{% trans "Quantity" %}{% trans "Received" %}{% trans "Note" %}
- {{ forloop.counter }} - - {% include "hover_image.html" with image=line.part.part.image hover=True %} - {{ line.part.part.full_name }} - {{ line.part.part.description }}{{ line.part.SKU }}Warning: Part has been deleted.{{ line.reference }}{% decimal line.quantity %}{% decimal line.received %} - {{ line.notes }} - -
- {% if order.status == OrderStatus.PENDING %} - - - {% endif %} - {% if order.status == OrderStatus.PLACED and line.received < line.quantity %} - - {% endif %} -
-
{% 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 = `
`; + + 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 += `
`; + + return html; + }, + } + ] }); diff --git a/InvenTree/order/templates/order/sales_order_base.html b/InvenTree/order/templates/order/sales_order_base.html index 9a8a061a79..385b669215 100644 --- a/InvenTree/order/templates/order/sales_order_base.html +++ b/InvenTree/order/templates/order/sales_order_base.html @@ -99,7 +99,7 @@ src="{% static 'img/blank_image.png' %}" {{ order.shipment_date }}{{ order.shipped_by }} {% endif %} - {% if order.status == OrderStatus.COMPLETE %} + {% if order.status == PurchaseOrderStatus.COMPLETE %} {% trans "Received" %} diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html index fb6feeab78..9d6aad6543 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -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"); diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 352d6c9546..26017a4e4e 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -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',