mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
commit
0de2d8a3ce
@ -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 %}
|
{% endif %}
|
||||||
|
|
||||||
enableSidebar('buildorder');
|
enableSidebar('buildorder');
|
||||||
|
@ -325,14 +325,14 @@
|
|||||||
var parts = [];
|
var parts = [];
|
||||||
|
|
||||||
selections.forEach(function(item) {
|
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/", {
|
orderParts(
|
||||||
data: {
|
parts,
|
||||||
parts: parts,
|
);
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -396,14 +396,16 @@
|
|||||||
var parts = [];
|
var parts = [];
|
||||||
|
|
||||||
selections.forEach(function(item) {
|
selections.forEach(function(item) {
|
||||||
parts.push(item.part);
|
var part = item.part_detail;
|
||||||
|
parts.push(part);
|
||||||
});
|
});
|
||||||
|
|
||||||
launchModalForm("/order/purchase-order/order-parts/", {
|
orderParts(
|
||||||
data: {
|
parts,
|
||||||
parts: parts,
|
{
|
||||||
},
|
supplier: {{ company.pk }},
|
||||||
});
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -31,13 +31,11 @@
|
|||||||
{% include "admin_button.html" with url=url %}
|
{% include "admin_button.html" with url=url %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if roles.purchase_order.change %}
|
{% if roles.purchase_order.change %}
|
||||||
{% comment "for later" %}
|
{% if roles.purchase_order.add and part.part.purchaseable %}
|
||||||
{% if roles.purchase_order.add %}
|
|
||||||
<button type='button' class='btn btn-outline-secondary' id='order-part' title='{% trans "Order part" %}'>
|
<button type='button' class='btn btn-outline-secondary' id='order-part' title='{% trans "Order part" %}'>
|
||||||
<span class='fas fa-shopping-cart'></span>
|
<span class='fas fa-shopping-cart'></span>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endcomment %}
|
|
||||||
<button type='button' class='btn btn-outline-secondary' id='edit-part' title='{% trans "Edit manufacturer part" %}'>
|
<button type='button' class='btn btn-outline-secondary' id='edit-part' title='{% trans "Edit manufacturer part" %}'>
|
||||||
<span class='fas fa-edit icon-green'/>
|
<span class='fas fa-edit icon-green'/>
|
||||||
</button>
|
</button>
|
||||||
@ -130,6 +128,7 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
<li><a class='dropdown-item' href='#' id='supplier-part-delete' title='{% trans "Delete supplier parts" %}'><span class='fas fa-trash-alt icon-red'></span> {% trans "Delete" %}</a></li>
|
<li><a class='dropdown-item' href='#' id='supplier-part-delete' title='{% trans "Delete supplier parts" %}'><span class='fas fa-trash-alt icon-red'></span> {% trans "Delete" %}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
{% include "filter_list.html" with id='supplier-part' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -300,14 +299,20 @@ linkButtonsToSelection($("#supplier-table"), ['#supplier-part-options']);
|
|||||||
linkButtonsToSelection($("#parameter-table"), ['#parameter-options']);
|
linkButtonsToSelection($("#parameter-table"), ['#parameter-options']);
|
||||||
|
|
||||||
$('#order-part, #order-part2').click(function() {
|
$('#order-part, #order-part2').click(function() {
|
||||||
launchModalForm(
|
|
||||||
"{% url 'order-parts' %}",
|
inventreeGet(
|
||||||
|
'{% url "api-part-detail" part.part.pk %}', {},
|
||||||
{
|
{
|
||||||
data: {
|
success: function(response) {
|
||||||
part: {{ part.part.id }},
|
|
||||||
},
|
orderParts([response], {
|
||||||
reload: true,
|
manufacturer_part: {{ part.pk }},
|
||||||
},
|
{% if part.manufacturer %}
|
||||||
|
manufacturer: {{ part.manufacturer.pk }},
|
||||||
|
{% endif %}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -165,7 +165,8 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
</div>
|
</div>
|
||||||
<div class='panel-content'>
|
<div class='panel-content'>
|
||||||
<div id='button-bar'>
|
<div id='button-bar'>
|
||||||
<div class='btn-group'>
|
<div class='btn-group' role='group'>
|
||||||
|
{% include "filter_list.html" with id='purchaseorder' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table class='table table-striped table-condensed po-table' id='purchase-order-table' data-toolbar='#button-bar'>
|
<table class='table table-striped table-condensed po-table' id='purchase-order-table' data-toolbar='#button-bar'>
|
||||||
@ -326,14 +327,19 @@ $("#item-create").click(function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$('#order-part, #order-part2').click(function() {
|
$('#order-part, #order-part2').click(function() {
|
||||||
launchModalForm(
|
|
||||||
"{% url 'order-parts' %}",
|
inventreeGet(
|
||||||
|
'{% url "api-part-detail" part.part.pk %}', {},
|
||||||
{
|
{
|
||||||
data: {
|
success: function(response) {
|
||||||
part: {{ part.part.id }},
|
orderParts([response], {
|
||||||
},
|
supplier_part: {{ part.pk }},
|
||||||
reload: true,
|
{% if part.supplier %}
|
||||||
},
|
supplier: {{ part.supplier.pk }},
|
||||||
|
{% endif %}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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 %}
|
|
||||||
|
|
||||||
<h4>
|
|
||||||
{% trans "Step 1 of 2 - Select Part Suppliers" %}
|
|
||||||
</h4>
|
|
||||||
|
|
||||||
{% if parts|length > 0 %}
|
|
||||||
<div class='alert alert-info alert-block' role='alert'>
|
|
||||||
{% trans "Select suppliers" %}
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class='alert alert-warning alert-block' role='alert'>
|
|
||||||
{% trans "No purchaseable parts selected" %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<form method="post" action='' class='js-modal-form' enctype="multipart/form-data">
|
|
||||||
{% csrf_token %}
|
|
||||||
{% load crispy_forms_tags %}
|
|
||||||
|
|
||||||
<input type='hidden' name='form_step' value='select_parts'/>
|
|
||||||
|
|
||||||
<table class='table table-condensed table-striped' id='order-wizard-part-table'>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "Part" %}</th>
|
|
||||||
<th colspan='2'>{% trans "Select Supplier" %}</th>
|
|
||||||
<th>{% trans "Quantity" %}</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
{% for part in parts %}
|
|
||||||
<tr id='part_row_{{ part.id }}'>
|
|
||||||
<td>
|
|
||||||
{% include "hover_image.html" with image=part.image hover=False %}
|
|
||||||
{{ part.full_name }} <small><em>{{ part.description }}</em></small>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<button class='btn btn-outline-secondary btn-create' onClick='newSupplierPartFromOrderWizard()' id='new_supplier_part_{{ part.id }}' part='{{ part.pk }}' title='{% trans "Create new supplier part" %}' type='button'>
|
|
||||||
<span part='{{ part.pk }}' class='fas fa-plus-circle'></span>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class='control-group'>
|
|
||||||
<div class='controls'>
|
|
||||||
<select class='select' id='id_supplier_part_{{ part.id }}' name="part-supplier-{{ part.id }}">
|
|
||||||
<option value=''>---------</option>
|
|
||||||
{% for supplier in part.supplier_parts.all %}
|
|
||||||
<option value="{{ supplier.id }}"{% if part.order_supplier == supplier.id %} selected="selected"{% endif %}>
|
|
||||||
{% if show_price %}
|
|
||||||
{% call_method supplier 'get_price' part.order_quantity as price %}
|
|
||||||
{% if price != None %}{% include "price.html" with price=price %}{% else %}{% trans 'No price' %}{% endif %} -
|
|
||||||
{% endif %}
|
|
||||||
{{ supplier }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
{% if not part.order_supplier %}
|
|
||||||
<span class='help-inline'>{% blocktrans with name=part.name %}Select a supplier for <em>{{name}}</em>{% endblocktrans %}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class='control-group'>
|
|
||||||
<div class='controls'>
|
|
||||||
<input class='numberinput' type='number' min='0' value='{% decimal part.order_quantity %}' name='part-quantity-{{ part.id }}'/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<button class='btn btn-outline-secondary btn-remove' onclick='removeOrderRowFromOrderWizard()' id='del_item_{{ part.id }}' title='{% trans "Remove part" %}' type='button'>
|
|
||||||
<span row='part_row_{{ part.id }}' class='fas fa-trash-alt icon-red'></span>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
|
@ -1,77 +0,0 @@
|
|||||||
{% extends "modal_form.html" %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block form %}
|
|
||||||
|
|
||||||
<h4>
|
|
||||||
{% trans "Step 2 of 2 - Select Purchase Orders" %}
|
|
||||||
</h4>
|
|
||||||
|
|
||||||
<div class='alert alert-info alert-block' role='alert'>
|
|
||||||
{% trans "Select existing purchase orders, or create new orders." %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form method='post' action='' class='js-modal-form' enctype='multipart/form-data'>
|
|
||||||
{% csrf_token %}
|
|
||||||
{% load crispy_forms_tags %}
|
|
||||||
|
|
||||||
<input type='hidden' name='form_step' value='select_purchase_orders'/>
|
|
||||||
|
|
||||||
{% for supplier in suppliers %}
|
|
||||||
{% for item in supplier.order_items %}
|
|
||||||
<input type='hidden' name='part-supplier-{{ item.id }}' value='{{ item.order_supplier }}'/>
|
|
||||||
<input type='hidden' name='part-quantity-{{ item.id }}' value='{{ item.order_quantity }}'/>
|
|
||||||
{% endfor %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<table class='table table-condensed table-striped' id='order-wizard-po-table'>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "Supplier" %}</th>
|
|
||||||
<th>{% trans "Items" %}</th>
|
|
||||||
<th colspan='2'>{% trans "Select Purchase Order" %}</th>
|
|
||||||
</tr>
|
|
||||||
{% for supplier in suppliers %}
|
|
||||||
<tr id='suppier_row_{{ supplier.id }}'>
|
|
||||||
<td>
|
|
||||||
{% include 'hover_image.html' with image=supplier.image hover=False %}
|
|
||||||
{{ supplier.name }}
|
|
||||||
</td>
|
|
||||||
<td>{{ supplier.order_items|length }}</td>
|
|
||||||
<td>
|
|
||||||
<button
|
|
||||||
class='btn btn-outline-secondary btn-create'
|
|
||||||
id='new_po_{{ supplier.id }}'
|
|
||||||
title='{% blocktrans with name=supplier.name %}Create new purchase order for {{name}}{% endblocktrans %}'
|
|
||||||
type='button'
|
|
||||||
supplierid='{{ supplier.id }}'
|
|
||||||
onclick='newPurchaseOrderFromOrderWizard()'>
|
|
||||||
<span supplierid='{{ supplier.id }}' class='fas fa-plus-circle'></span>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class='control-group'>
|
|
||||||
<div class='controls'>
|
|
||||||
<select
|
|
||||||
class='select'
|
|
||||||
id='id-purchase-order-{{ supplier.id }}'
|
|
||||||
name='purchase-order-{{ supplier.id }}'>
|
|
||||||
<option value=''>---------</option>
|
|
||||||
{% for order in supplier.pending_purchase_orders %}
|
|
||||||
<option value='{{ order.id }}'{% if supplier.selected_purchase_order == order.id %} selected='selected'{% endif %}>
|
|
||||||
{{ order }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
{% if not supplier.selected_purchase_order %}
|
|
||||||
<span class='help-inline'>{% blocktrans with name=supplier.name %}Select a purchase order for {{name}}{% endblocktrans %}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
</table>
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
|
@ -23,7 +23,6 @@ purchase_order_detail_urls = [
|
|||||||
|
|
||||||
purchase_order_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'),
|
re_path(r'^pricing/', views.LineItemPricing.as_view(), name='line-pricing'),
|
||||||
|
|
||||||
# Display detail view for a single purchase order
|
# Display detail view for a single purchase order
|
||||||
|
@ -5,7 +5,6 @@ Django views for interacting with Order app
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import transaction
|
|
||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
from django.http.response import JsonResponse
|
from django.http.response import JsonResponse
|
||||||
from django.shortcuts import get_object_or_404
|
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 PurchaseOrder, PurchaseOrderLineItem
|
||||||
from .models import SalesOrder, SalesOrderLineItem
|
from .models import SalesOrder, SalesOrderLineItem
|
||||||
from .admin import PurchaseOrderLineItemResource, SalesOrderLineItemResource
|
from .admin import PurchaseOrderLineItemResource, SalesOrderLineItemResource
|
||||||
from build.models import Build
|
from company.models import SupplierPart # ManufacturerPart
|
||||||
from company.models import Company, SupplierPart # ManufacturerPart
|
|
||||||
from stock.models import StockItem
|
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
|
|
||||||
from common.forms import UploadFileForm, MatchFieldForm
|
from common.forms import UploadFileForm, MatchFieldForm
|
||||||
@ -37,8 +34,6 @@ from InvenTree.views import AjaxView, AjaxUpdateView
|
|||||||
from InvenTree.helpers import DownloadFile, str2bool
|
from InvenTree.helpers import DownloadFile, str2bool
|
||||||
from InvenTree.views import InvenTreeRoleMixin
|
from InvenTree.views import InvenTreeRoleMixin
|
||||||
|
|
||||||
from InvenTree.status_codes import PurchaseOrderStatus
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger("inventree")
|
logger = logging.getLogger("inventree")
|
||||||
|
|
||||||
@ -448,346 +443,6 @@ class PurchaseOrderExport(AjaxView):
|
|||||||
return DownloadFile(filedata, filename)
|
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-<pk> : The ID of the selected supplier
|
|
||||||
- quantity-<pk> : 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):
|
class LineItemPricing(PartPricing):
|
||||||
""" View for inspecting part pricing information """
|
""" View for inspecting part pricing information """
|
||||||
|
|
||||||
|
@ -754,12 +754,18 @@
|
|||||||
|
|
||||||
|
|
||||||
$("#part-order2").click(function() {
|
$("#part-order2").click(function() {
|
||||||
launchModalForm("{% url 'order-parts' %}", {
|
inventreeGet(
|
||||||
data: {
|
'{% url "api-part-detail" part.pk %}',
|
||||||
part: {{ part.id }},
|
{},
|
||||||
},
|
{
|
||||||
reload: true,
|
success: function(part) {
|
||||||
});
|
orderParts(
|
||||||
|
[part],
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
onPanelLoad("test-templates", function() {
|
onPanelLoad("test-templates", function() {
|
||||||
|
@ -549,15 +549,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return;
|
|
||||||
|
|
||||||
launchModalForm("{% url 'order-parts' %}", {
|
|
||||||
data: {
|
|
||||||
part: {{ part.id }},
|
|
||||||
},
|
|
||||||
reload: true,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
{% if roles.part.add %}
|
{% if roles.part.add %}
|
||||||
|
@ -163,17 +163,15 @@ def after_save(sender, instance, created, **kwargs):
|
|||||||
|
|
||||||
if created:
|
if created:
|
||||||
trigger_event(
|
trigger_event(
|
||||||
'instance.created',
|
f'{table}.created',
|
||||||
id=instance.id,
|
id=instance.id,
|
||||||
model=sender.__name__,
|
model=sender.__name__,
|
||||||
table=table,
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
trigger_event(
|
trigger_event(
|
||||||
'instance.saved',
|
f'{table}.saved',
|
||||||
id=instance.id,
|
id=instance.id,
|
||||||
model=sender.__name__,
|
model=sender.__name__,
|
||||||
table=table,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -189,9 +187,8 @@ def after_delete(sender, instance, **kwargs):
|
|||||||
return
|
return
|
||||||
|
|
||||||
trigger_event(
|
trigger_event(
|
||||||
'instance.deleted',
|
f'{table}.deleted',
|
||||||
model=sender.__name__,
|
model=sender.__name__,
|
||||||
table=table,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1532,13 +1532,18 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
|||||||
|
|
||||||
var pk = $(this).attr('pk');
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
launchModalForm('{% url "order-parts" %}', {
|
inventreeGet(
|
||||||
data: {
|
`/api/part/${pk}/`,
|
||||||
parts: [
|
{},
|
||||||
pk,
|
{
|
||||||
]
|
success: function(part) {
|
||||||
|
orderParts(
|
||||||
|
[part],
|
||||||
|
{}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Callback for 'build' button
|
// Callback for 'build' button
|
||||||
|
@ -485,10 +485,17 @@ function orderParts(parts_list, options={}) {
|
|||||||
|
|
||||||
var parts = [];
|
var parts = [];
|
||||||
|
|
||||||
|
var parts_seen = {};
|
||||||
|
|
||||||
parts_list.forEach(function(part) {
|
parts_list.forEach(function(part) {
|
||||||
if (part.purchaseable) {
|
if (part.purchaseable) {
|
||||||
|
|
||||||
|
// Prevent duplicates
|
||||||
|
if (!(part.pk in parts_seen)) {
|
||||||
|
parts_seen[part.pk] = true;
|
||||||
parts.push(part);
|
parts.push(part);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (parts.length == 0) {
|
if (parts.length == 0) {
|
||||||
@ -596,6 +603,17 @@ function orderParts(parts_list, options={}) {
|
|||||||
return html;
|
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 = '';
|
var table_entries = '';
|
||||||
|
|
||||||
parts.forEach(function(part) {
|
parts.forEach(function(part) {
|
||||||
@ -622,14 +640,50 @@ function orderParts(parts_list, options={}) {
|
|||||||
</table>
|
</table>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// 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({}, {
|
constructFormBody({}, {
|
||||||
preFormContent: html,
|
preFormContent: html,
|
||||||
title: '{% trans "Order Parts" %}',
|
title: '{% trans "Order Parts" %}',
|
||||||
preventSubmit: true,
|
preventSubmit: true,
|
||||||
closeText: '{% trans "Close" %}',
|
closeText: '{% trans "Close" %}',
|
||||||
afterRender: function(fields, opts) {
|
afterRender: function(fields, opts) {
|
||||||
// TODO
|
|
||||||
parts.forEach(function(part) {
|
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
|
// Configure the "supplier part" field
|
||||||
initializeRelatedField({
|
initializeRelatedField({
|
||||||
name: `part_${part.pk}`,
|
name: `part_${part.pk}`,
|
||||||
@ -638,11 +692,8 @@ function orderParts(parts_list, options={}) {
|
|||||||
required: true,
|
required: true,
|
||||||
type: 'related field',
|
type: 'related field',
|
||||||
auto_fill: true,
|
auto_fill: true,
|
||||||
filters: {
|
value: options.supplier_part,
|
||||||
part: part.pk,
|
filters: supplier_part_filters,
|
||||||
supplier_detail: true,
|
|
||||||
part_detail: false,
|
|
||||||
},
|
|
||||||
noResults: function(query) {
|
noResults: function(query) {
|
||||||
return '{% trans "No matching supplier parts" %}';
|
return '{% trans "No matching supplier parts" %}';
|
||||||
}
|
}
|
||||||
@ -656,10 +707,8 @@ function orderParts(parts_list, options={}) {
|
|||||||
required: true,
|
required: true,
|
||||||
type: 'related field',
|
type: 'related field',
|
||||||
auto_fill: false,
|
auto_fill: false,
|
||||||
filters: {
|
value: options.order,
|
||||||
status: {{ PurchaseOrderStatus.PENDING }},
|
filters: order_filters,
|
||||||
supplier_detail: true,
|
|
||||||
},
|
|
||||||
noResults: function(query) {
|
noResults: function(query) {
|
||||||
return '{% trans "No matching purchase orders" %}';
|
return '{% trans "No matching purchase orders" %}';
|
||||||
}
|
}
|
||||||
@ -689,8 +738,7 @@ function orderParts(parts_list, options={}) {
|
|||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
success: function(response) {
|
success: function(response) {
|
||||||
// Remove the row
|
removeRow(pk, opts);
|
||||||
$(opts.modal).find(`#order_row_${pk}`).remove();
|
|
||||||
},
|
},
|
||||||
error: function(xhr) {
|
error: function(xhr) {
|
||||||
switch (xhr.status) {
|
switch (xhr.status) {
|
||||||
@ -711,7 +759,7 @@ function orderParts(parts_list, options={}) {
|
|||||||
$(opts.modal).find('.button-row-remove').click(function() {
|
$(opts.modal).find('.button-row-remove').click(function() {
|
||||||
var pk = $(this).attr('pk');
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
$(opts.modal).find(`#order_row_${pk}`).remove();
|
removeRow(pk, opts);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add callback for "new supplier part" button
|
// Add callback for "new supplier part" button
|
||||||
@ -3242,13 +3290,18 @@ function loadSalesOrderLineItemTable(table, options={}) {
|
|||||||
$(table).find('.button-buy').click(function() {
|
$(table).find('.button-buy').click(function() {
|
||||||
var pk = $(this).attr('pk');
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
launchModalForm('{% url "order-parts" %}', {
|
inventreeGet(
|
||||||
data: {
|
`/api/part/${pk}/`,
|
||||||
parts: [
|
{},
|
||||||
pk
|
{
|
||||||
],
|
success: function(part) {
|
||||||
},
|
orderParts(
|
||||||
});
|
[part],
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Callback for displaying price
|
// Callback for displaying price
|
||||||
|
@ -2043,17 +2043,17 @@ function loadStockTable(table, options) {
|
|||||||
$('#multi-item-order').click(function() {
|
$('#multi-item-order').click(function() {
|
||||||
var selections = $(table).bootstrapTable('getSelections');
|
var selections = $(table).bootstrapTable('getSelections');
|
||||||
|
|
||||||
var stock = [];
|
var parts = [];
|
||||||
|
|
||||||
selections.forEach(function(item) {
|
selections.forEach(function(item) {
|
||||||
stock.push(item.pk);
|
var part = item.part_detail;
|
||||||
|
|
||||||
|
if (part) {
|
||||||
|
parts.push(part);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
launchModalForm('/order/purchase-order/order-parts/', {
|
orderParts(parts, {});
|
||||||
data: {
|
|
||||||
stock: stock,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#multi-item-set-status').click(function() {
|
$('#multi-item-set-status').click(function() {
|
||||||
|
Loading…
Reference in New Issue
Block a user