diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index b8f54ba72b..26e6ed3546 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -7,6 +7,7 @@ from __future__ import unicode_literals from django.utils.translation import ugettext_lazy as _ from django.conf.urls import url, include +from django.db.models import Q, F from django_filters import rest_framework as rest_filters from rest_framework import generics @@ -251,6 +252,39 @@ class POReceive(generics.CreateAPIView): return order +class POLineItemFilter(rest_filters.FilterSet): + """ + Custom filters for the POLineItemList endpoint + """ + + class Meta: + model = PurchaseOrderLineItem + 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" (or "not" completed) + + A line is completed when received >= quantity + """ + + value = str2bool(value) + + q = Q(received__gte=F('quantity')) + + if value: + queryset = queryset.filter(q) + else: + queryset = queryset.exclude(q) + + return queryset + + class POLineItemList(generics.ListCreateAPIView): """ API endpoint for accessing a list of POLineItem objects @@ -260,6 +294,7 @@ class POLineItemList(generics.ListCreateAPIView): queryset = PurchaseOrderLineItem.objects.all() serializer_class = POLineItemSerializer + filterset_class = POLineItemFilter def get_queryset(self, *args, **kwargs): diff --git a/InvenTree/order/forms.py b/InvenTree/order/forms.py index e0c500e5e3..87e042f4f3 100644 --- a/InvenTree/order/forms.py +++ b/InvenTree/order/forms.py @@ -8,8 +8,6 @@ from __future__ import unicode_literals from django import forms from django.utils.translation import ugettext_lazy as _ -from mptt.fields import TreeNodeChoiceField - from InvenTree.forms import HelperForm from InvenTree.fields import InvenTreeMoneyField, RoundingDecimalFormField @@ -19,7 +17,6 @@ from common.forms import MatchItemForm import part.models -from stock.models import StockLocation from .models import PurchaseOrder from .models import SalesOrder, SalesOrderLineItem from .models import SalesOrderAllocation @@ -80,22 +77,6 @@ class ShipSalesOrderForm(HelperForm): ] -class ReceivePurchaseOrderForm(HelperForm): - - location = TreeNodeChoiceField( - queryset=StockLocation.objects.all(), - required=False, - label=_("Destination"), - help_text=_("Set all received parts listed above to this location (if left blank, use \"Destination\" column value in above table)"), - ) - - class Meta: - model = PurchaseOrder - fields = [ - "location", - ] - - class AllocateSerialsToSalesOrderForm(forms.Form): """ Form for assigning stock to a sales order, diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index 842d422678..3886bfd3a5 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -225,6 +225,13 @@ class POLineItemReceiveSerializer(serializers.Serializer): required=True, ) + def validate_quantity(self, quantity): + + if quantity <= 0: + raise ValidationError(_("Quantity must be greater than zero")) + + return quantity + status = serializers.ChoiceField( choices=list(StockStatus.items()), default=StockStatus.OK, @@ -246,7 +253,7 @@ class POLineItemReceiveSerializer(serializers.Serializer): # Ignore empty barcode values if not barcode or barcode.strip() == '': - return + return None if stock.models.StockItem.objects.filter(uid=barcode).exists(): raise ValidationError(_('Barcode is already in use')) @@ -284,35 +291,11 @@ class POReceiveSerializer(serializers.Serializer): items = data.get('items', []) - if len(items) == 0: - raise ValidationError({ - 'items': _('Line items must be provided') - }) - - # Ensure barcodes are unique - unique_barcodes = set() - - for item in items: - barcode = item.get('barcode', '') - - if barcode: - if barcode in unique_barcodes: - raise ValidationError(_('Supplied barcode values must be unique')) - else: - unique_barcodes.add(barcode) - - return data - - def save(self): - - data = self.validated_data - - request = self.context['request'] - order = self.context['order'] - - items = data['items'] location = data.get('location', None) + if len(items) == 0: + raise ValidationError(_('Line items must be provided')) + # Check if the location is not specified for any particular item for item in items: @@ -331,14 +314,44 @@ class POReceiveSerializer(serializers.Serializer): 'location': _("Destination location must be specified"), }) + # Ensure barcodes are unique + unique_barcodes = set() + + for item in items: + barcode = item.get('barcode', '') + + if barcode: + if barcode in unique_barcodes: + raise ValidationError(_('Supplied barcode values must be unique')) + else: + unique_barcodes.add(barcode) + + return data + + def save(self): + """ + Perform the actual database transaction to receive purchase order items + """ + + data = self.validated_data + + request = self.context['request'] + order = self.context['order'] + + items = data['items'] + location = data.get('location', None) + # Now we can actually receive the items into stock with transaction.atomic(): for item in items: + # Select location + loc = item.get('location', None) or item['line_item'].get_destination() or location + try: order.receive_line_item( item['line_item'], - item['location'], + loc, item['quantity'], request.user, status=item['status'], diff --git a/InvenTree/order/templates/order/order_base.html b/InvenTree/order/templates/order/order_base.html index 0d46207c33..69e972da6c 100644 --- a/InvenTree/order/templates/order/order_base.html +++ b/InvenTree/order/templates/order/order_base.html @@ -49,7 +49,7 @@ src="{% static 'img/blank_image.png' %}" {% elif order.status == PurchaseOrderStatus.PLACED %} {% trans "Upload File" %} + {% elif order.status == PurchaseOrderStatus.PLACED %} + {% endif %} + {% endif %} +
+ +
@@ -207,6 +216,22 @@ $('#new-po-line').click(function() { }); }); +{% elif order.status == PurchaseOrderStatus.PLACED %} + + $('#receive-selected-items').click(function() { + var items = $("#po-line-table").bootstrapTable('getSelections'); + + receivePurchaseOrderItems( + {{ order.id }}, + items, + { + success: function() { + $("#po-line-table").bootstrapTable('refresh'); + } + } + ); + }); + {% endif %} loadPurchaseOrderLineItemTable('#po-line-table', { diff --git a/InvenTree/order/templates/order/receive_parts.html b/InvenTree/order/templates/order/receive_parts.html deleted file mode 100644 index 7b12101f7f..0000000000 --- a/InvenTree/order/templates/order/receive_parts.html +++ /dev/null @@ -1,81 +0,0 @@ -{% extends "modal_form.html" %} -{% load i18n %} -{% load inventree_extras %} -{% load status_codes %} - -{% block form %} - -{% blocktrans with desc=order.description %}Receive outstanding parts for {{order}} - {{desc}}{% endblocktrans %} - - - {% csrf_token %} - {% load crispy_forms_tags %} - - -

{% trans "Fill out number of parts received, the status and destination" %}

- -
- - - - - - - - - - - {% for line in lines %} - - {% if line.part %} - - - {% else %} - - {% endif %} - - - - - - - - {% endfor %} -
{% trans "Part" %}{% trans "Order Code" %}{% trans "On Order" %}{% trans "Received" %}{% trans "Receive" %}{% trans "Status" %}{% trans "Destination" %}
- {% include "hover_image.html" with image=line.part.part.image hover=False %} - {{ line.part.part.full_name }} - {{ line.part.SKU }}{% trans "Error: Referenced part has been removed" %}{% decimal line.quantity %}{% decimal line.received %} -
-
- -
-
-
-
- -
-
-
- -
-
- -
- - {% crispy form %} - -
{{ form_errors }}
- - -{% endblock %} \ No newline at end of file diff --git a/InvenTree/order/test_api.py b/InvenTree/order/test_api.py index 765c58cc3d..1f7905d1e3 100644 --- a/InvenTree/order/test_api.py +++ b/InvenTree/order/test_api.py @@ -251,7 +251,7 @@ class PurchaseOrderReceiveTest(OrderTest): expected_code=400 ).data - self.assertIn('Line items must be provided', str(data['items'])) + self.assertIn('Line items must be provided', str(data)) # No new stock items have been created self.assertEqual(self.n, StockItem.objects.count()) diff --git a/InvenTree/order/test_views.py b/InvenTree/order/test_views.py index 4b49b6c94e..220c1688db 100644 --- a/InvenTree/order/test_views.py +++ b/InvenTree/order/test_views.py @@ -10,7 +10,7 @@ from django.contrib.auth.models import Group from InvenTree.status_codes import PurchaseOrderStatus -from .models import PurchaseOrder, PurchaseOrderLineItem +from .models import PurchaseOrder import json @@ -103,86 +103,3 @@ class POTests(OrderViewTestCase): # Test that the order was actually placed order = PurchaseOrder.objects.get(pk=1) self.assertEqual(order.status, PurchaseOrderStatus.PLACED) - - -class TestPOReceive(OrderViewTestCase): - """ Tests for receiving a purchase order """ - - def setUp(self): - super().setUp() - - self.po = PurchaseOrder.objects.get(pk=1) - self.po.status = PurchaseOrderStatus.PLACED - self.po.save() - self.url = reverse('po-receive', args=(1,)) - - def post(self, data, validate=None): - - response = self.client.post(self.url, data, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - - if validate is not None: - - data = json.loads(response.content) - - if validate: - self.assertTrue(data['form_valid']) - else: - self.assertFalse(data['form_valid']) - - return response - - def test_get_dialog(self): - - data = { - } - - self.client.get(self.url, data, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - - def test_receive_lines(self): - - post_data = { - } - - self.post(post_data, validate=False) - - # Try with an invalid location - post_data['location'] = 12345 - - self.post(post_data, validate=False) - - # Try with a valid location - post_data['location'] = 1 - - # Should fail due to invalid quantity - self.post(post_data, validate=False) - - # Try to receive against an invalid line - post_data['line-800'] = 100 - - # Remove an invalid quantity of items - post_data['line-1'] = '7x5q' - - self.post(post_data, validate=False) - - # Receive negative number - post_data['line-1'] = -100 - - self.post(post_data, validate=False) - - # Receive 75 items - post_data['line-1'] = 75 - - self.post(post_data, validate=True) - - line = PurchaseOrderLineItem.objects.get(pk=1) - - self.assertEqual(line.received, 75) - - # Receive 30 more items - post_data['line-1'] = 30 - - self.post(post_data, validate=True) - - line = PurchaseOrderLineItem.objects.get(pk=1) - - self.assertEqual(line.received, 105) diff --git a/InvenTree/order/urls.py b/InvenTree/order/urls.py index 2ce90f1f81..5ea9a56867 100644 --- a/InvenTree/order/urls.py +++ b/InvenTree/order/urls.py @@ -13,7 +13,6 @@ purchase_order_detail_urls = [ url(r'^cancel/', views.PurchaseOrderCancel.as_view(), name='po-cancel'), url(r'^issue/', views.PurchaseOrderIssue.as_view(), name='po-issue'), - url(r'^receive/', views.PurchaseOrderReceive.as_view(), name='po-receive'), url(r'^complete/', views.PurchaseOrderComplete.as_view(), name='po-complete'), url(r'^upload/', views.PurchaseOrderUpload.as_view(), name='po-upload'), diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index e8b0dc03e9..8a5e709926 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -26,7 +26,7 @@ from .models import SalesOrderAllocation from .admin import POLineItemResource from build.models import Build from company.models import Company, SupplierPart # ManufacturerPart -from stock.models import StockItem, StockLocation +from stock.models import StockItem from part.models import Part from common.models import InvenTreeSetting @@ -42,7 +42,7 @@ from InvenTree.helpers import DownloadFile, str2bool from InvenTree.helpers import extract_serial_numbers from InvenTree.views import InvenTreeRoleMixin -from InvenTree.status_codes import PurchaseOrderStatus, StockStatus +from InvenTree.status_codes import PurchaseOrderStatus logger = logging.getLogger("inventree") @@ -468,202 +468,6 @@ class PurchaseOrderExport(AjaxView): return DownloadFile(filedata, filename) -class PurchaseOrderReceive(AjaxUpdateView): - """ View for receiving parts which are outstanding against a PurchaseOrder. - - Any parts which are outstanding are listed. - If all parts are marked as received, the order is closed out. - - """ - - form_class = order_forms.ReceivePurchaseOrderForm - ajax_form_title = _("Receive Parts") - ajax_template_name = "order/receive_parts.html" - - # Specify role as we do not specify a Model against this view - role_required = 'purchase_order.change' - - # Where the parts will be going (selected in POST request) - destination = None - - def get_context_data(self): - - ctx = { - 'order': self.order, - 'lines': self.lines, - 'stock_locations': StockLocation.objects.all(), - } - - return ctx - - def get_lines(self): - """ - Extract particular line items from the request, - or default to *all* pending line items if none are provided - """ - - lines = None - - if 'line' in self.request.GET: - line_id = self.request.GET.get('line') - - try: - lines = PurchaseOrderLineItem.objects.filter(pk=line_id) - except (PurchaseOrderLineItem.DoesNotExist, ValueError): - pass - - # TODO - Option to pass multiple lines? - - # No lines specified - default selection - if lines is None: - lines = self.order.pending_line_items() - - return lines - - def get(self, request, *args, **kwargs): - """ Respond to a GET request. Determines which parts are outstanding, - and presents a list of these parts to the user. - """ - - self.request = request - self.order = get_object_or_404(PurchaseOrder, pk=self.kwargs['pk']) - - self.lines = self.get_lines() - - for line in self.lines: - # Pre-fill the remaining quantity - line.receive_quantity = line.remaining() - - return self.renderJsonResponse(request, form=self.get_form()) - - def post(self, request, *args, **kwargs): - """ Respond to a POST request. Data checking and error handling. - If the request is valid, new StockItem objects will be made - for each received item. - """ - - self.request = request - self.order = get_object_or_404(PurchaseOrder, pk=self.kwargs['pk']) - errors = False - - self.lines = [] - self.destination = None - - msg = _("Items received") - - # Extract the destination for received parts - if 'location' in request.POST: - pk = request.POST['location'] - try: - self.destination = StockLocation.objects.get(id=pk) - except (StockLocation.DoesNotExist, ValueError): - pass - - # Extract information on all submitted line items - for item in request.POST: - if item.startswith('line-'): - pk = item.replace('line-', '') - - try: - line = PurchaseOrderLineItem.objects.get(id=pk) - except (PurchaseOrderLineItem.DoesNotExist, ValueError): - continue - - # Check that the StockStatus was set - status_key = 'status-{pk}'.format(pk=pk) - status = request.POST.get(status_key, StockStatus.OK) - - try: - status = int(status) - except ValueError: - status = StockStatus.OK - - if status in StockStatus.RECEIVING_CODES: - line.status_code = status - else: - line.status_code = StockStatus.OK - - # Check the destination field - line.destination = None - if self.destination: - # If global destination is set, overwrite line value - line.destination = self.destination - else: - destination_key = f'destination-{pk}' - destination = request.POST.get(destination_key, None) - - if destination: - try: - line.destination = StockLocation.objects.get(pk=destination) - except (StockLocation.DoesNotExist, ValueError): - pass - - # Check that line matches the order - if not line.order == self.order: - # TODO - Display a non-field error? - continue - - # Ignore a part that doesn't map to a SupplierPart - try: - if line.part is None: - continue - except SupplierPart.DoesNotExist: - continue - - receive = self.request.POST[item] - - try: - receive = Decimal(receive) - except InvalidOperation: - # In the case on an invalid input, reset to default - receive = line.remaining() - msg = _("Error converting quantity to number") - errors = True - - if receive < 0: - receive = 0 - errors = True - msg = _("Receive quantity less than zero") - - line.receive_quantity = receive - self.lines.append(line) - - if len(self.lines) == 0: - msg = _("No lines specified") - errors = True - - # No errors? Receive the submitted parts! - if errors is False: - self.receive_parts() - - data = { - 'form_valid': errors is False, - 'success': msg, - } - - return self.renderJsonResponse(request, data=data, form=self.get_form()) - - @transaction.atomic - def receive_parts(self): - """ Called once the form has been validated. - Create new stockitems against received parts. - """ - - for line in self.lines: - - if not line.part: - continue - - self.order.receive_line_item( - line, - line.destination, - line.receive_quantity, - self.request.user, - status=line.status_code, - purchase_price=line.purchase_price, - ) - - class OrderParts(AjaxView): """ View for adding various SupplierPart items to a Purchase Order. diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 060faf8b0d..4f1ba8cc8b 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -193,6 +193,7 @@ class PartBriefSerializer(InvenTreeModelSerializer): fields = [ 'pk', 'IPN', + 'default_location', 'name', 'revision', 'full_name', diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 463c3c9ae2..aa68b26dd4 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -973,7 +973,6 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) { `; - constructForm(`/api/build/${build_id}/allocate/`, { method: 'POST', fields: {}, diff --git a/InvenTree/templates/js/translated/filters.js b/InvenTree/templates/js/translated/filters.js index d7e8f45ca5..3e41003696 100644 --- a/InvenTree/templates/js/translated/filters.js +++ b/InvenTree/templates/js/translated/filters.js @@ -273,6 +273,11 @@ function setupFilterList(tableKey, table, target) { var element = $(target); + if (!element) { + console.log(`WARNING: setupFilterList could not find target '${target}'`); + return; + } + // One blank slate, please element.empty(); diff --git a/InvenTree/templates/js/translated/forms.js b/InvenTree/templates/js/translated/forms.js index bffb42403b..b43ce0cb2d 100644 --- a/InvenTree/templates/js/translated/forms.js +++ b/InvenTree/templates/js/translated/forms.js @@ -1515,6 +1515,7 @@ function initializeChoiceField(field, fields, options) { select.select2({ dropdownAutoWidth: false, dropdownParent: $(options.modal), + width: '100%', }); } diff --git a/InvenTree/templates/js/translated/model_renderers.js b/InvenTree/templates/js/translated/model_renderers.js index 3136ebee29..0c3dabc27e 100644 --- a/InvenTree/templates/js/translated/model_renderers.js +++ b/InvenTree/templates/js/translated/model_renderers.js @@ -112,7 +112,13 @@ function renderStockLocation(name, data, parameters, options) { var html = `${level}${data.pathstring}`; - if (data.description) { + var render_description = true; + + if ('render_description' in parameters) { + render_description = parameters['render_description']; + } + + if (render_description && data.description) { html += ` - ${data.description}`; } diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index 532ab81655..43d4b56936 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -12,6 +12,7 @@ loadTableFilters, makeIconBadge, purchaseOrderStatusDisplay, + receivePurchaseOrderItems, renderLink, salesOrderStatusDisplay, setupFilterList, @@ -234,6 +235,291 @@ function newPurchaseOrderFromOrderWizard(e) { }); } + +/** + * Receive stock items against a PurchaseOrder + * Uses the POReceive API endpoint + * + * arguments: + * - order_id, ID / PK for the PurchaseOrder instance + * - line_items: A list of PurchaseOrderLineItems objects to be allocated + * + * options: + * - + */ +function receivePurchaseOrderItems(order_id, line_items, options={}) { + + if (line_items.length == 0) { + showAlertDialog( + '{% trans "Select Line Items" %}', + '{% trans "At least one line item must be selected" %}', + ); + return; + } + + function renderLineItem(line_item, opts={}) { + + var pk = line_item.pk; + + // Part thumbnail + description + var thumb = thumbnailImage(line_item.part_detail.thumbnail); + + var quantity = (line_item.quantity || 0) - (line_item.received || 0); + + if (quantity < 0) { + quantity = 0; + } + + // Quantity to Receive + var quantity_input = constructField( + `items_quantity_${pk}`, + { + type: 'decimal', + min_value: 0, + value: quantity, + title: '{% trans "Quantity to receive" %}', + required: true, + }, + { + hideLabels: true, + } + ); + + // Construct list of StockItem status codes + var choices = []; + + for (var key in stockCodes) { + choices.push({ + value: key, + display_name: stockCodes[key].value, + }); + } + + var destination_input = constructField( + `items_location_${pk}`, + { + type: 'related field', + label: '{% trans "Location" %}', + required: false, + }, + { + hideLabels: true, + } + ); + + var status_input = constructField( + `items_status_${pk}`, + { + type: 'choice', + label: '{% trans "Stock Status" %}', + required: true, + choices: choices, + value: 10, // OK + }, + { + hideLabels: true, + } + ); + + // Button to remove the row + var delete_button = `
`; + + delete_button += makeIconButton( + 'fa-times icon-red', + 'button-row-remove', + pk, + '{% trans "Remove row" %}', + ); + + delete_button += '
'; + + var html = ` + + + ${thumb} ${line_item.part_detail.full_name} + + + ${line_item.supplier_part_detail.SKU} + + + ${line_item.quantity} + + + ${line_item.received} + + + ${quantity_input} + + + ${status_input} + + + ${destination_input} + + + ${delete_button} + + `; + + return html; + } + + var table_entries = ''; + + line_items.forEach(function(item) { + table_entries += renderLineItem(item); + }); + + var html = ``; + + // Add table + html += ` + + + + + + + + + + + + + + + ${table_entries} + +
{% trans "Part" %}{% trans "Order Code" %}{% trans "Ordered" %}{% trans "Received" %}{% trans "Receive" %}{% trans "Status" %}{% trans "Destination" %}
+ `; + + constructForm(`/api/order/po/${order_id}/receive/`, { + method: 'POST', + fields: { + location: {}, + }, + preFormContent: html, + confirm: true, + confirmMessage: '{% trans "Confirm receipt of items" %}', + title: '{% trans "Receive Purchase Order Items" %}', + afterRender: function(fields, opts) { + // Initialize the "destination" field for each item + line_items.forEach(function(item) { + + var pk = item.pk; + + var name = `items_location_${pk}`; + + var field_details = { + name: name, + api_url: '{% url "api-location-list" %}', + filters: { + + }, + type: 'related field', + model: 'stocklocation', + required: false, + auto_fill: false, + value: item.destination || item.part_detail.default_location, + render_description: false, + }; + + initializeRelatedField( + field_details, + null, + opts, + ); + + addClearCallback( + name, + field_details, + opts + ); + + initializeChoiceField( + { + name: `items_status_${pk}`, + }, + null, + opts + ); + }); + + // Add callbacks to remove rows + $(opts.modal).find('.button-row-remove').click(function() { + var pk = $(this).attr('pk'); + + $(opts.modal).find(`#receive_row_${pk}`).remove(); + }); + }, + onSubmit: function(fields, opts) { + // Extract data elements from the form + var data = { + items: [], + location: getFormFieldValue('location', {}, opts), + }; + + var item_pk_values = []; + + line_items.forEach(function(item) { + + var pk = item.pk; + + var quantity = getFormFieldValue(`items_quantity_${pk}`, {}, opts); + + var status = getFormFieldValue(`items_status_${pk}`, {}, opts); + + var location = getFormFieldValue(`items_location_${pk}`, {}, opts); + + if (quantity != null) { + data.items.push({ + line_item: pk, + quantity: quantity, + status: status, + location: location, + }); + + item_pk_values.push(pk); + } + + }); + + // Provide list of nested values + opts.nested = { + 'items': item_pk_values, + }; + + inventreePut( + opts.url, + data, + { + method: 'POST', + success: function(response) { + // Hide the modal + $(opts.modal).modal('hide'); + + if (options.success) { + options.success(response); + } + }, + error: function(xhr) { + switch (xhr.status) { + case 400: + handleFormErrors(xhr.responseJSON, fields, opts); + break; + default: + $(opts.modal).modal('hide'); + showApiError(xhr); + break; + } + } + } + ); + } + }); +} + + function editPurchaseOrderLineItem(e) { /* Edit a purchase order line item in a modal form. @@ -280,12 +566,10 @@ function loadPurchaseOrderTable(table, options) { filters[key] = options.params[key]; } - options.url = options.url || '{% url "api-po-list" %}'; - setupFilterList('purchaseorder', $(table)); $(table).inventreeTable({ - url: options.url, + url: '{% url "api-po-list" %}', queryParams: filters, name: 'purchaseorder', groupBy: false, @@ -379,6 +663,21 @@ function loadPurchaseOrderTable(table, options) { */ function loadPurchaseOrderLineItemTable(table, options={}) { + options.params = options.params || {}; + + options.params['order'] = options.order; + options.params['part_detail'] = true; + + var filters = loadTableFilters('purchaseorderlineitem'); + + for (var key in options.params) { + filters[key] = options.params[key]; + } + + var target = options.filter_target || '#filter-list-purchase-order-lines'; + + setupFilterList('purchaseorderlineitem', $(table), target); + function setupCallbacks() { if (options.allow_edit) { $(table).find('.button-line-edit').click(function() { @@ -424,22 +723,24 @@ function loadPurchaseOrderLineItemTable(table, options={}) { $(table).find('.button-line-receive').click(function() { var pk = $(this).attr('pk'); - launchModalForm(`/order/purchase-order/${options.order}/receive/`, { - success: function() { - $(table).bootstrapTable('refresh'); - }, - data: { - line: pk, - }, - secondary: [ - { - field: 'location', - label: '{% trans "New Location" %}', - title: '{% trans "Create new stock location" %}', - url: '{% url "stock-location-create" %}', - }, - ] - }); + var line_item = $(table).bootstrapTable('getRowByUniqueId', pk); + + if (!line_item) { + console.log('WARNING: getRowByUniqueId returned null'); + return; + } + + receivePurchaseOrderItems( + options.order, + [ + line_item, + ], + { + success: function() { + $(table).bootstrapTable('refresh'); + } + } + ); }); } } @@ -451,17 +752,15 @@ function loadPurchaseOrderLineItemTable(table, options={}) { formatNoMatches: function() { return '{% trans "No line items found" %}'; }, - queryParams: { - order: options.order, - part_detail: true - }, + queryParams: filters, + original: options.params, url: '{% url "api-po-line-list" %}', showFooter: true, + uniqueId: 'pk', columns: [ { - field: 'pk', - title: 'ID', - visible: false, + checkbox: true, + visible: true, switchable: false, }, { @@ -618,7 +917,7 @@ function loadPurchaseOrderLineItemTable(table, options={}) { } if (options.allow_receive && row.received < row.quantity) { - html += makeIconButton('fa-clipboard-check', 'button-line-receive', pk, '{% trans "Receive line item" %}'); + html += makeIconButton('fa-sign-in-alt', 'button-line-receive', pk, '{% trans "Receive line item" %}'); } html += ``; diff --git a/InvenTree/templates/js/translated/table_filters.js b/InvenTree/templates/js/translated/table_filters.js index b94bc324c7..4d12f69780 100644 --- a/InvenTree/templates/js/translated/table_filters.js +++ b/InvenTree/templates/js/translated/table_filters.js @@ -274,7 +274,16 @@ function getAvailableTableFilters(tableKey) { }; } - // Filters for the "Order" table + // Filters for PurchaseOrderLineItem table + if (tableKey == 'purchaseorderlineitem') { + return { + completed: { + type: 'bool', + title: '{% trans "Completed" %}', + }, + }; + } + // Filters for the PurchaseOrder table if (tableKey == 'purchaseorder') { return {