diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html
index 42bc51bb2f..2fb96e88e3 100644
--- a/InvenTree/build/templates/build/detail.html
+++ b/InvenTree/build/templates/build/detail.html
@@ -546,14 +546,6 @@ $('#allocate-selected-items').click(function() {
);
});
-$("#btn-order-parts").click(function() {
- launchModalForm("/order/purchase-order/order-parts/", {
- data: {
- build: {{ build.id }},
- },
- });
-});
-
{% endif %}
enableSidebar('buildorder');
diff --git a/InvenTree/company/templates/company/detail.html b/InvenTree/company/templates/company/detail.html
index 3d715e288c..4474278613 100644
--- a/InvenTree/company/templates/company/detail.html
+++ b/InvenTree/company/templates/company/detail.html
@@ -325,14 +325,14 @@
var parts = [];
selections.forEach(function(item) {
- parts.push(item.part);
+ var part = item.part_detail;
+ part.manufacturer_part = item.pk;
+ parts.push(part);
});
- launchModalForm("/order/purchase-order/order-parts/", {
- data: {
- parts: parts,
- },
- });
+ orderParts(
+ parts,
+ );
});
{% endif %}
@@ -396,14 +396,16 @@
var parts = [];
selections.forEach(function(item) {
- parts.push(item.part);
+ var part = item.part_detail;
+ parts.push(part);
});
- launchModalForm("/order/purchase-order/order-parts/", {
- data: {
- parts: parts,
- },
- });
+ orderParts(
+ parts,
+ {
+ supplier: {{ company.pk }},
+ }
+ );
});
{% endif %}
diff --git a/InvenTree/company/templates/company/manufacturer_part.html b/InvenTree/company/templates/company/manufacturer_part.html
index fb33128a77..a3a2bbc65e 100644
--- a/InvenTree/company/templates/company/manufacturer_part.html
+++ b/InvenTree/company/templates/company/manufacturer_part.html
@@ -31,13 +31,11 @@
{% include "admin_button.html" with url=url %}
{% endif %}
{% if roles.purchase_order.change %}
-{% comment "for later" %}
-{% if roles.purchase_order.add %}
+{% if roles.purchase_order.add and part.part.purchaseable %}
{% endif %}
-{% endcomment %}
@@ -130,6 +128,7 @@ src="{% static 'img/blank_image.png' %}"
-
+
+ {% include "filter_list.html" with id='purchaseorder' %}
@@ -326,14 +327,19 @@ $("#item-create").click(function() {
});
$('#order-part, #order-part2').click(function() {
- launchModalForm(
- "{% url 'order-parts' %}",
+
+ inventreeGet(
+ '{% url "api-part-detail" part.part.pk %}', {},
{
- data: {
- part: {{ part.part.id }},
- },
- reload: true,
- },
+ success: function(response) {
+ orderParts([response], {
+ supplier_part: {{ part.pk }},
+ {% if part.supplier %}
+ supplier: {{ part.supplier.pk }},
+ {% endif %}
+ });
+ }
+ }
);
});
diff --git a/InvenTree/order/templates/order/order_wizard/select_parts.html b/InvenTree/order/templates/order/order_wizard/select_parts.html
deleted file mode 100644
index 9d0ccdfb82..0000000000
--- a/InvenTree/order/templates/order/order_wizard/select_parts.html
+++ /dev/null
@@ -1,85 +0,0 @@
-{% extends "modal_form.html" %}
-
-{% load inventree_extras %}
-{% load i18n %}
-
-{% block form %}
-{% default_currency as currency %}
-{% settings_value 'PART_SHOW_PRICE_IN_FORMS' as show_price %}
-
-
- {% trans "Step 1 of 2 - Select Part Suppliers" %}
-
-
-{% if parts|length > 0 %}
-
- {% trans "Select suppliers" %}
-
-{% else %}
-
- {% trans "No purchaseable parts selected" %}
-
-{% endif %}
-
-
-{% endblock %}
\ No newline at end of file
diff --git a/InvenTree/order/templates/order/order_wizard/select_pos.html b/InvenTree/order/templates/order/order_wizard/select_pos.html
deleted file mode 100644
index 6ef2f6c910..0000000000
--- a/InvenTree/order/templates/order/order_wizard/select_pos.html
+++ /dev/null
@@ -1,77 +0,0 @@
-{% extends "modal_form.html" %}
-
-{% load i18n %}
-
-{% block form %}
-
-
- {% trans "Step 2 of 2 - Select Purchase Orders" %}
-
-
-
- {% trans "Select existing purchase orders, or create new orders." %}
-
-
-
-{% endblock %}
\ No newline at end of file
diff --git a/InvenTree/order/urls.py b/InvenTree/order/urls.py
index 54be93905f..f82a581828 100644
--- a/InvenTree/order/urls.py
+++ b/InvenTree/order/urls.py
@@ -23,7 +23,6 @@ purchase_order_detail_urls = [
purchase_order_urls = [
- re_path(r'^order-parts/', views.OrderParts.as_view(), name='order-parts'),
re_path(r'^pricing/', views.LineItemPricing.as_view(), name='line-pricing'),
# Display detail view for a single purchase order
diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py
index 35f8b973f4..68b45ebe86 100644
--- a/InvenTree/order/views.py
+++ b/InvenTree/order/views.py
@@ -5,7 +5,6 @@ Django views for interacting with Order app
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
-from django.db import transaction
from django.db.utils import IntegrityError
from django.http.response import JsonResponse
from django.shortcuts import get_object_or_404
@@ -21,9 +20,7 @@ from decimal import Decimal, InvalidOperation
from .models import PurchaseOrder, PurchaseOrderLineItem
from .models import SalesOrder, SalesOrderLineItem
from .admin import PurchaseOrderLineItemResource, SalesOrderLineItemResource
-from build.models import Build
-from company.models import Company, SupplierPart # ManufacturerPart
-from stock.models import StockItem
+from company.models import SupplierPart # ManufacturerPart
from part.models import Part
from common.forms import UploadFileForm, MatchFieldForm
@@ -37,8 +34,6 @@ from InvenTree.views import AjaxView, AjaxUpdateView
from InvenTree.helpers import DownloadFile, str2bool
from InvenTree.views import InvenTreeRoleMixin
-from InvenTree.status_codes import PurchaseOrderStatus
-
logger = logging.getLogger("inventree")
@@ -448,346 +443,6 @@ class PurchaseOrderExport(AjaxView):
return DownloadFile(filedata, filename)
-class OrderParts(AjaxView):
- """ View for adding various SupplierPart items to a Purchase Order.
-
- SupplierParts can be selected from a variety of 'sources':
-
- - ?supplier_parts[]= -> Direct list of SupplierPart objects
- - ?parts[]= -> List of base Part objects (user must then select supplier parts)
- - ?stock[]= -> List of StockItem objects (user must select supplier parts)
- - ?build= -> A Build object (user must select parts, then supplier parts)
-
- """
-
- ajax_form_title = _("Order Parts")
- ajax_template_name = 'order/order_wizard/select_parts.html'
-
- role_required = [
- 'part.view',
- 'purchase_order.change',
- ]
-
- # List of Parts we wish to order
- parts = []
- suppliers = []
-
- def get_context_data(self):
-
- ctx = {}
-
- ctx['parts'] = sorted(self.parts, key=lambda part: int(part.order_quantity), reverse=True)
- ctx['suppliers'] = self.suppliers
-
- return ctx
-
- def get_data(self):
- """ enrich respone json data """
- data = super().get_data()
- # if in selection-phase, add a button to update the prices
- if getattr(self, 'form_step', 'select_parts') == 'select_parts':
- data['buttons'] = [{'name': 'update_price', 'title': _('Update prices')}] # set buttons
- data['hideErrorMessage'] = '1' # hide the error message
- return data
-
- def get_suppliers(self):
- """ Calculates a list of suppliers which the user will need to create PurchaseOrders for.
- This is calculated AFTER the user finishes selecting the parts to order.
- Crucially, get_parts() must be called before get_suppliers()
- """
-
- suppliers = {}
-
- for supplier in self.suppliers:
- supplier.order_items = []
-
- suppliers[supplier.name] = supplier
-
- for part in self.parts:
- supplier_part_id = part.order_supplier
-
- try:
- supplier = SupplierPart.objects.get(pk=supplier_part_id).supplier
- except SupplierPart.DoesNotExist:
- continue
-
- if supplier.name not in suppliers:
- supplier.order_items = []
-
- # Attempt to auto-select a purchase order
- orders = PurchaseOrder.objects.filter(supplier=supplier, status__in=PurchaseOrderStatus.OPEN)
-
- if orders.count() == 1:
- supplier.selected_purchase_order = orders.first().id
- else:
- supplier.selected_purchase_order = None
-
- suppliers[supplier.name] = supplier
-
- suppliers[supplier.name].order_items.append(part)
-
- self.suppliers = [suppliers[key] for key in suppliers.keys()]
-
- def get_parts(self):
- """ Determine which parts the user wishes to order.
- This is performed on the initial GET request.
- """
-
- self.parts = []
-
- part_ids = set()
-
- # User has passed a list of stock items
- if 'stock[]' in self.request.GET:
-
- stock_id_list = self.request.GET.getlist('stock[]')
-
- """ Get a list of all the parts associated with the stock items.
- - Base part must be purchaseable.
- - Return a set of corresponding Part IDs
- """
- stock_items = StockItem.objects.filter(
- part__purchaseable=True,
- id__in=stock_id_list)
-
- for item in stock_items:
- part_ids.add(item.part.id)
-
- # User has passed a single Part ID
- elif 'part' in self.request.GET:
- try:
- part_id = self.request.GET.get('part')
- part = Part.objects.get(id=part_id)
-
- part_ids.add(part.id)
-
- except Part.DoesNotExist:
- pass
-
- # User has passed a list of part ID values
- elif 'parts[]' in self.request.GET:
- part_id_list = self.request.GET.getlist('parts[]')
-
- parts = Part.objects.filter(
- purchaseable=True,
- id__in=part_id_list)
-
- for part in parts:
- part_ids.add(part.id)
-
- # User has provided a Build ID
- elif 'build' in self.request.GET:
- build_id = self.request.GET.get('build')
- try:
- build = Build.objects.get(id=build_id)
-
- parts = build.required_parts
-
- for part in parts:
-
- # If ordering from a Build page, ignore parts that we have enough of
- if part.quantity_to_order <= 0:
- continue
- part_ids.add(part.id)
- except Build.DoesNotExist:
- pass
-
- # Create the list of parts
- for id in part_ids:
- try:
- part = Part.objects.get(id=id)
- # Pre-fill the 'order quantity' value
- part.order_quantity = part.quantity_to_order
-
- default_supplier = part.get_default_supplier()
-
- if default_supplier:
- part.order_supplier = default_supplier.id
- else:
- part.order_supplier = None
- except Part.DoesNotExist:
- continue
-
- self.parts.append(part)
-
- def get(self, request, *args, **kwargs):
-
- self.request = request
-
- self.get_parts()
-
- return self.renderJsonResponse(request)
-
- def post(self, request, *args, **kwargs):
- """ Handle the POST action for part selection.
-
- - Validates each part / quantity / supplier / etc
-
- Part selection form contains the following fields for each part:
-
- - supplier- : The ID of the selected supplier
- - quantity- : The quantity to add to the order
- """
-
- self.request = request
-
- self.parts = []
- self.suppliers = []
-
- # Any errors for the part selection form?
- part_errors = False
- supplier_errors = False
-
- # Extract part information from the form
- for item in self.request.POST:
-
- if item.startswith('part-supplier-'):
-
- pk = item.replace('part-supplier-', '')
-
- # Check that the part actually exists
- try:
- part = Part.objects.get(id=pk)
- except (Part.DoesNotExist, ValueError):
- continue
-
- supplier_part_id = self.request.POST[item]
-
- quantity = self.request.POST.get('part-quantity-' + str(pk), 0)
-
- # Ensure a valid supplier has been passed
- try:
- supplier_part = SupplierPart.objects.get(id=supplier_part_id)
- except (SupplierPart.DoesNotExist, ValueError):
- supplier_part = None
-
- # Ensure a valid quantity is passed
- try:
- quantity = int(quantity)
-
- # Eliminate lines where the quantity is zero
- if quantity == 0:
- continue
- except ValueError:
- quantity = part.quantity_to_order
-
- part.order_supplier = supplier_part.id if supplier_part else None
- part.order_quantity = quantity
-
- # set supplier-price
- if supplier_part:
- supplier_price = supplier_part.get_price(quantity)
- if supplier_price:
- part.purchase_price = supplier_price / quantity
- if not hasattr(part, 'purchase_price'):
- part.purchase_price = None
-
- self.parts.append(part)
-
- if supplier_part is None:
- part_errors = True
-
- elif quantity < 0:
- part_errors = True
-
- elif item.startswith('purchase-order-'):
- # Which purchase order is selected for a given supplier?
- pk = item.replace('purchase-order-', '')
-
- # Check that the Supplier actually exists
- try:
- supplier = Company.objects.get(id=pk)
- except Company.DoesNotExist:
- # Skip this item
- continue
-
- purchase_order_id = self.request.POST[item]
-
- # Ensure that a valid purchase order has been passed
- try:
- purchase_order = PurchaseOrder.objects.get(pk=purchase_order_id)
- except (PurchaseOrder.DoesNotExist, ValueError):
- purchase_order = None
-
- supplier.selected_purchase_order = purchase_order.id if purchase_order else None
-
- self.suppliers.append(supplier)
-
- if supplier.selected_purchase_order is None:
- supplier_errors = True
-
- form_step = request.POST.get('form_step')
-
- # Map parts to suppliers
- self.get_suppliers()
-
- valid = False
-
- if form_step == 'select_parts':
- # No errors? and the price-update button was not used to submit? Proceed to PO selection form
- if part_errors is False and 'act-btn_update_price' not in request.POST:
- self.ajax_template_name = 'order/order_wizard/select_pos.html'
- self.form_step = 'select_purchase_orders' # set step (important for get_data)
-
- else:
- self.ajax_template_name = 'order/order_wizard/select_parts.html'
-
- elif form_step == 'select_purchase_orders':
-
- self.ajax_template_name = 'order/order_wizard/select_pos.html'
-
- valid = part_errors is False and supplier_errors is False
-
- # Form wizard is complete! Add items to purchase orders
- if valid:
- self.order_items()
-
- data = {
- 'form_valid': valid,
- 'success': _('Ordered {n} parts').format(n=len(self.parts))
- }
-
- return self.renderJsonResponse(self.request, data=data)
-
- @transaction.atomic
- def order_items(self):
- """ Add the selected items to the purchase orders. """
-
- for supplier in self.suppliers:
-
- # Check that the purchase order does actually exist
- try:
- order = PurchaseOrder.objects.get(pk=supplier.selected_purchase_order)
- except PurchaseOrder.DoesNotExist:
- logger.critical('Could not add items to purchase order {po} - Order does not exist'.format(po=supplier.selected_purchase_order))
- continue
-
- for item in supplier.order_items:
-
- # Ensure that the quantity is valid
- try:
- quantity = int(item.order_quantity)
- if quantity <= 0:
- continue
- except ValueError:
- logger.warning("Did not add part to purchase order - incorrect quantity")
- continue
-
- # Check that the supplier part does actually exist
- try:
- supplier_part = SupplierPart.objects.get(pk=item.order_supplier)
- except SupplierPart.DoesNotExist:
- logger.critical("Could not add part '{part}' to purchase order - selected supplier part '{sp}' does not exist.".format(
- part=item,
- sp=item.order_supplier))
- continue
-
- # get purchase price
- purchase_price = item.purchase_price
-
- order.add_line_item(supplier_part, quantity, purchase_price=purchase_price)
-
-
class LineItemPricing(PartPricing):
""" View for inspecting part pricing information """
diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html
index 5ec1821b3d..aa3ad4963a 100644
--- a/InvenTree/part/templates/part/detail.html
+++ b/InvenTree/part/templates/part/detail.html
@@ -754,12 +754,18 @@
$("#part-order2").click(function() {
- launchModalForm("{% url 'order-parts' %}", {
- data: {
- part: {{ part.id }},
- },
- reload: true,
- });
+ inventreeGet(
+ '{% url "api-part-detail" part.pk %}',
+ {},
+ {
+ success: function(part) {
+ orderParts(
+ [part],
+ {}
+ );
+ }
+ }
+ );
});
onPanelLoad("test-templates", function() {
diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html
index 649aaf6705..4e875c1f97 100644
--- a/InvenTree/part/templates/part/part_base.html
+++ b/InvenTree/part/templates/part/part_base.html
@@ -549,15 +549,6 @@
}
}
);
-
- return;
-
- launchModalForm("{% url 'order-parts' %}", {
- data: {
- part: {{ part.id }},
- },
- reload: true,
- });
});
{% if roles.part.add %}
diff --git a/InvenTree/plugin/events.py b/InvenTree/plugin/events.py
index 4948366bfa..b54581bf71 100644
--- a/InvenTree/plugin/events.py
+++ b/InvenTree/plugin/events.py
@@ -163,17 +163,15 @@ def after_save(sender, instance, created, **kwargs):
if created:
trigger_event(
- 'instance.created',
+ f'{table}.created',
id=instance.id,
model=sender.__name__,
- table=table,
)
else:
trigger_event(
- 'instance.saved',
+ f'{table}.saved',
id=instance.id,
model=sender.__name__,
- table=table,
)
@@ -189,9 +187,8 @@ def after_delete(sender, instance, **kwargs):
return
trigger_event(
- 'instance.deleted',
+ f'{table}.deleted',
model=sender.__name__,
- table=table,
)
diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js
index c9ebbe0e22..d68b319a25 100644
--- a/InvenTree/templates/js/translated/build.js
+++ b/InvenTree/templates/js/translated/build.js
@@ -1532,13 +1532,18 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
var pk = $(this).attr('pk');
- launchModalForm('{% url "order-parts" %}', {
- data: {
- parts: [
- pk,
- ]
+ inventreeGet(
+ `/api/part/${pk}/`,
+ {},
+ {
+ success: function(part) {
+ orderParts(
+ [part],
+ {}
+ );
+ }
}
- });
+ );
});
// Callback for 'build' button
diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js
index 7daaffff09..d5ca7caf42 100644
--- a/InvenTree/templates/js/translated/order.js
+++ b/InvenTree/templates/js/translated/order.js
@@ -485,9 +485,16 @@ function orderParts(parts_list, options={}) {
var parts = [];
+ var parts_seen = {};
+
parts_list.forEach(function(part) {
if (part.purchaseable) {
- parts.push(part);
+
+ // Prevent duplicates
+ if (!(part.pk in parts_seen)) {
+ parts_seen[part.pk] = true;
+ parts.push(part);
+ }
}
});
@@ -596,6 +603,17 @@ function orderParts(parts_list, options={}) {
return html;
}
+ // Remove a single row form this dialog
+ function removeRow(pk, opts) {
+ // Remove the row
+ $(opts.modal).find(`#order_row_${pk}`).remove();
+
+ // If the modal is now "empty", dismiss it
+ if (!($(opts.modal).find('.part-order-row').exists())) {
+ closeModal(opts.modal);
+ }
+ }
+
var table_entries = '';
parts.forEach(function(part) {
@@ -622,14 +640,50 @@ function orderParts(parts_list, options={}) {
`;
+ // Construct API filters for the SupplierPart field
+ var supplier_part_filters = {
+ supplier_detail: true,
+ part_detail: true,
+ };
+
+ if (options.supplier) {
+ supplier_part_filters.supplier = options.supplier;
+ }
+
+ if (options.manufacturer) {
+ supplier_part_filters.manufacturer = options.manufacturer;
+ }
+
+ if (options.manufacturer_part) {
+ supplier_part_filters.manufacturer_part = options.manufacturer_part;
+ }
+
+ // Construct API filtres for the PurchaseOrder field
+ var order_filters = {
+ status: {{ PurchaseOrderStatus.PENDING }},
+ supplier_detail: true,
+ };
+
+ if (options.supplier) {
+ order_filters.supplier = options.supplier;
+ }
+
constructFormBody({}, {
preFormContent: html,
title: '{% trans "Order Parts" %}',
preventSubmit: true,
closeText: '{% trans "Close" %}',
afterRender: function(fields, opts) {
- // TODO
parts.forEach(function(part) {
+
+ // Filter by base part
+ supplier_part_filters.part = part.pk;
+
+ if (part.manufacturer_part) {
+ // Filter by manufacturer part
+ supplier_part_filters.manufacturer_part = part.manufacturer_part;
+ }
+
// Configure the "supplier part" field
initializeRelatedField({
name: `part_${part.pk}`,
@@ -638,11 +692,8 @@ function orderParts(parts_list, options={}) {
required: true,
type: 'related field',
auto_fill: true,
- filters: {
- part: part.pk,
- supplier_detail: true,
- part_detail: false,
- },
+ value: options.supplier_part,
+ filters: supplier_part_filters,
noResults: function(query) {
return '{% trans "No matching supplier parts" %}';
}
@@ -656,10 +707,8 @@ function orderParts(parts_list, options={}) {
required: true,
type: 'related field',
auto_fill: false,
- filters: {
- status: {{ PurchaseOrderStatus.PENDING }},
- supplier_detail: true,
- },
+ value: options.order,
+ filters: order_filters,
noResults: function(query) {
return '{% trans "No matching purchase orders" %}';
}
@@ -689,8 +738,7 @@ function orderParts(parts_list, options={}) {
{
method: 'POST',
success: function(response) {
- // Remove the row
- $(opts.modal).find(`#order_row_${pk}`).remove();
+ removeRow(pk, opts);
},
error: function(xhr) {
switch (xhr.status) {
@@ -711,7 +759,7 @@ function orderParts(parts_list, options={}) {
$(opts.modal).find('.button-row-remove').click(function() {
var pk = $(this).attr('pk');
- $(opts.modal).find(`#order_row_${pk}`).remove();
+ removeRow(pk, opts);
});
// Add callback for "new supplier part" button
@@ -3242,13 +3290,18 @@ function loadSalesOrderLineItemTable(table, options={}) {
$(table).find('.button-buy').click(function() {
var pk = $(this).attr('pk');
- launchModalForm('{% url "order-parts" %}', {
- data: {
- parts: [
- pk
- ],
- },
- });
+ inventreeGet(
+ `/api/part/${pk}/`,
+ {},
+ {
+ success: function(part) {
+ orderParts(
+ [part],
+ {}
+ );
+ }
+ }
+ );
});
// Callback for displaying price
diff --git a/InvenTree/templates/js/translated/stock.js b/InvenTree/templates/js/translated/stock.js
index de42528142..94d21fe5b0 100644
--- a/InvenTree/templates/js/translated/stock.js
+++ b/InvenTree/templates/js/translated/stock.js
@@ -2043,17 +2043,17 @@ function loadStockTable(table, options) {
$('#multi-item-order').click(function() {
var selections = $(table).bootstrapTable('getSelections');
- var stock = [];
+ var parts = [];
selections.forEach(function(item) {
- stock.push(item.pk);
+ var part = item.part_detail;
+
+ if (part) {
+ parts.push(part);
+ }
});
- launchModalForm('/order/purchase-order/order-parts/', {
- data: {
- stock: stock,
- },
- });
+ orderParts(parts, {});
});
$('#multi-item-set-status').click(function() {