mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #2102 from SchrodingersGat/purchase-order-receive
[Refactor] Purchase order receive
This commit is contained in:
commit
8dcd2ab7ca
@ -7,6 +7,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.conf.urls import url, include
|
from django.conf.urls import url, include
|
||||||
|
from django.db.models import Q, F
|
||||||
|
|
||||||
from django_filters import rest_framework as rest_filters
|
from django_filters import rest_framework as rest_filters
|
||||||
from rest_framework import generics
|
from rest_framework import generics
|
||||||
@ -251,6 +252,39 @@ class POReceive(generics.CreateAPIView):
|
|||||||
return order
|
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):
|
class POLineItemList(generics.ListCreateAPIView):
|
||||||
""" API endpoint for accessing a list of POLineItem objects
|
""" API endpoint for accessing a list of POLineItem objects
|
||||||
|
|
||||||
@ -260,6 +294,7 @@ class POLineItemList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
queryset = PurchaseOrderLineItem.objects.all()
|
queryset = PurchaseOrderLineItem.objects.all()
|
||||||
serializer_class = POLineItemSerializer
|
serializer_class = POLineItemSerializer
|
||||||
|
filterset_class = POLineItemFilter
|
||||||
|
|
||||||
def get_queryset(self, *args, **kwargs):
|
def get_queryset(self, *args, **kwargs):
|
||||||
|
|
||||||
|
@ -8,8 +8,6 @@ from __future__ import unicode_literals
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from mptt.fields import TreeNodeChoiceField
|
|
||||||
|
|
||||||
from InvenTree.forms import HelperForm
|
from InvenTree.forms import HelperForm
|
||||||
from InvenTree.fields import InvenTreeMoneyField, RoundingDecimalFormField
|
from InvenTree.fields import InvenTreeMoneyField, RoundingDecimalFormField
|
||||||
|
|
||||||
@ -19,7 +17,6 @@ from common.forms import MatchItemForm
|
|||||||
|
|
||||||
import part.models
|
import part.models
|
||||||
|
|
||||||
from stock.models import StockLocation
|
|
||||||
from .models import PurchaseOrder
|
from .models import PurchaseOrder
|
||||||
from .models import SalesOrder, SalesOrderLineItem
|
from .models import SalesOrder, SalesOrderLineItem
|
||||||
from .models import SalesOrderAllocation
|
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):
|
class AllocateSerialsToSalesOrderForm(forms.Form):
|
||||||
"""
|
"""
|
||||||
Form for assigning stock to a sales order,
|
Form for assigning stock to a sales order,
|
||||||
|
@ -225,6 +225,13 @@ class POLineItemReceiveSerializer(serializers.Serializer):
|
|||||||
required=True,
|
required=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def validate_quantity(self, quantity):
|
||||||
|
|
||||||
|
if quantity <= 0:
|
||||||
|
raise ValidationError(_("Quantity must be greater than zero"))
|
||||||
|
|
||||||
|
return quantity
|
||||||
|
|
||||||
status = serializers.ChoiceField(
|
status = serializers.ChoiceField(
|
||||||
choices=list(StockStatus.items()),
|
choices=list(StockStatus.items()),
|
||||||
default=StockStatus.OK,
|
default=StockStatus.OK,
|
||||||
@ -246,7 +253,7 @@ class POLineItemReceiveSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
# Ignore empty barcode values
|
# Ignore empty barcode values
|
||||||
if not barcode or barcode.strip() == '':
|
if not barcode or barcode.strip() == '':
|
||||||
return
|
return None
|
||||||
|
|
||||||
if stock.models.StockItem.objects.filter(uid=barcode).exists():
|
if stock.models.StockItem.objects.filter(uid=barcode).exists():
|
||||||
raise ValidationError(_('Barcode is already in use'))
|
raise ValidationError(_('Barcode is already in use'))
|
||||||
@ -284,35 +291,11 @@ class POReceiveSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
items = data.get('items', [])
|
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)
|
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
|
# Check if the location is not specified for any particular item
|
||||||
for item in items:
|
for item in items:
|
||||||
|
|
||||||
@ -331,14 +314,44 @@ class POReceiveSerializer(serializers.Serializer):
|
|||||||
'location': _("Destination location must be specified"),
|
'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
|
# Now we can actually receive the items into stock
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
for item in items:
|
for item in items:
|
||||||
|
|
||||||
|
# Select location
|
||||||
|
loc = item.get('location', None) or item['line_item'].get_destination() or location
|
||||||
|
|
||||||
try:
|
try:
|
||||||
order.receive_line_item(
|
order.receive_line_item(
|
||||||
item['line_item'],
|
item['line_item'],
|
||||||
item['location'],
|
loc,
|
||||||
item['quantity'],
|
item['quantity'],
|
||||||
request.user,
|
request.user,
|
||||||
status=item['status'],
|
status=item['status'],
|
||||||
|
@ -49,7 +49,7 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
</button>
|
</button>
|
||||||
{% elif order.status == PurchaseOrderStatus.PLACED %}
|
{% elif order.status == PurchaseOrderStatus.PLACED %}
|
||||||
<button type='button' class='btn btn-default' id='receive-order' title='{% trans "Receive items" %}'>
|
<button type='button' class='btn btn-default' id='receive-order' title='{% trans "Receive items" %}'>
|
||||||
<span class='fas fa-clipboard-check'></span>
|
<span class='fas fa-sign-in-alt'></span>
|
||||||
</button>
|
</button>
|
||||||
<button type='button' class='btn btn-default' id='complete-order' title='{% trans "Mark order as complete" %}'>
|
<button type='button' class='btn btn-default' id='complete-order' title='{% trans "Mark order as complete" %}'>
|
||||||
<span class='fas fa-check-circle'></span>
|
<span class='fas fa-check-circle'></span>
|
||||||
@ -188,17 +188,27 @@ $("#edit-order").click(function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$("#receive-order").click(function() {
|
$("#receive-order").click(function() {
|
||||||
launchModalForm("{% url 'po-receive' order.id %}", {
|
|
||||||
reload: true,
|
// Auto select items which have not been fully allocated
|
||||||
secondary: [
|
var items = $("#po-line-table").bootstrapTable('getData');
|
||||||
{
|
|
||||||
field: 'location',
|
var items_to_receive = [];
|
||||||
label: '{% trans "New Location" %}',
|
|
||||||
title: '{% trans "Create new stock location" %}',
|
items.forEach(function(item) {
|
||||||
url: "{% url 'stock-location-create' %}",
|
if (item.received < item.quantity) {
|
||||||
},
|
items_to_receive.push(item);
|
||||||
]
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
receivePurchaseOrderItems(
|
||||||
|
{{ order.id }},
|
||||||
|
items_to_receive,
|
||||||
|
{
|
||||||
|
success: function() {
|
||||||
|
$("#po-line-table").bootstrapTable('refresh');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#complete-order").click(function() {
|
$("#complete-order").click(function() {
|
||||||
|
@ -18,14 +18,23 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class='panel-content'>
|
<div class='panel-content'>
|
||||||
<div id='order-toolbar-buttons' class='btn-group' style='float: right;'>
|
<div id='order-toolbar-buttons' class='btn-group' style='float: right;'>
|
||||||
{% if order.status == PurchaseOrderStatus.PENDING and roles.purchase_order.change %}
|
{% if roles.purchase_order.change %}
|
||||||
|
{% if order.status == PurchaseOrderStatus.PENDING %}
|
||||||
<button type='button' class='btn btn-success' id='new-po-line'>
|
<button type='button' class='btn btn-success' id='new-po-line'>
|
||||||
<span class='fas fa-plus-circle'></span> {% trans "Add Line Item" %}
|
<span class='fas fa-plus-circle'></span> {% trans "Add Line Item" %}
|
||||||
</button>
|
</button>
|
||||||
<a class='btn btn-primary' href='{% url "po-upload" order.id %}' role='button'>
|
<a class='btn btn-primary' href='{% url "po-upload" order.id %}' role='button'>
|
||||||
<span class='fas fa-file-upload side-icon'></span> {% trans "Upload File" %}
|
<span class='fas fa-file-upload side-icon'></span> {% trans "Upload File" %}
|
||||||
</a>
|
</a>
|
||||||
|
{% elif order.status == PurchaseOrderStatus.PLACED %}
|
||||||
|
<button type='button' class='btn btn-success' id='receive-selected-items' title='{% trans "Receive selected items" %}'>
|
||||||
|
<span class='fas fa-sign-in-alt'></span> {% trans "Receive Items" %}
|
||||||
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
<div class='filter-list' id='filter-list-purchase-order-lines'>
|
||||||
|
<!-- An empty div in which the filter list will be constructed-->
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class='table table-striped table-condensed' id='po-line-table' data-toolbar='#order-toolbar-buttons'>
|
<table class='table table-striped table-condensed' id='po-line-table' data-toolbar='#order-toolbar-buttons'>
|
||||||
@ -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 %}
|
{% endif %}
|
||||||
|
|
||||||
loadPurchaseOrderLineItemTable('#po-line-table', {
|
loadPurchaseOrderLineItemTable('#po-line-table', {
|
||||||
|
@ -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 <strong>{{order}}</strong> - <em>{{desc}}</em>{% endblocktrans %}
|
|
||||||
|
|
||||||
<form method='post' action='' class='js-modal-form' enctype='multipart/form-data'>
|
|
||||||
{% csrf_token %}
|
|
||||||
{% load crispy_forms_tags %}
|
|
||||||
|
|
||||||
<label class='control-label'>{% trans "Parts" %}</label>
|
|
||||||
<p class='help-block'>{% trans "Fill out number of parts received, the status and destination" %}</p>
|
|
||||||
|
|
||||||
<table class='table table-striped'>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "Part" %}</th>
|
|
||||||
<th>{% trans "Order Code" %}</th>
|
|
||||||
<th>{% trans "On Order" %}</th>
|
|
||||||
<th>{% trans "Received" %}</th>
|
|
||||||
<th>{% trans "Receive" %}</th>
|
|
||||||
<th>{% trans "Status" %}</th>
|
|
||||||
<th>{% trans "Destination" %}</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
{% for line in lines %}
|
|
||||||
<tr id='line_row_{{ line.id }}'>
|
|
||||||
{% if line.part %}
|
|
||||||
<td>
|
|
||||||
{% include "hover_image.html" with image=line.part.part.image hover=False %}
|
|
||||||
{{ line.part.part.full_name }}
|
|
||||||
</td>
|
|
||||||
<td>{{ line.part.SKU }}</td>
|
|
||||||
{% else %}
|
|
||||||
<td colspan='2'>{% trans "Error: Referenced part has been removed" %}</td>
|
|
||||||
{% endif %}
|
|
||||||
<td>{% decimal line.quantity %}</td>
|
|
||||||
<td>{% decimal line.received %}</td>
|
|
||||||
<td>
|
|
||||||
<div class='control-group'>
|
|
||||||
<div class='controls'>
|
|
||||||
<input class='numberinput' type='number' min='0' value='{% decimal line.receive_quantity %}' name='line-{{ line.id }}'/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class='control-group'>
|
|
||||||
<select class='select' name='status-{{ line.id }}'>
|
|
||||||
{% for code in StockStatus.RECEIVING_CODES %}
|
|
||||||
<option value="{{ code }}" {% if code|add:"0" == line.status_code|add:"0" %}selected="selected"{% endif %}>{% stock_status_text code %}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class='control-group'>
|
|
||||||
<select class='select' name='destination-{{ line.id }}'>
|
|
||||||
<option value="">----------</option>
|
|
||||||
{% for location in stock_locations %}
|
|
||||||
<option value="{{ location.pk }}" {% if location == line.get_destination %}selected="selected"{% endif %}>{{ location }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<button class='btn btn-default btn-remove' onClick="removeOrderRowFromOrderWizard()" id='del_item_{{ line.id }}' title='{% trans "Remove line" %}' type='button'>
|
|
||||||
<span row='line_row_{{ line.id }}' class='fas fa-times-circle icon-red'></span>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
|
|
||||||
{% crispy form %}
|
|
||||||
|
|
||||||
<div id='form-errors'>{{ form_errors }}</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
@ -251,7 +251,7 @@ class PurchaseOrderReceiveTest(OrderTest):
|
|||||||
expected_code=400
|
expected_code=400
|
||||||
).data
|
).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
|
# No new stock items have been created
|
||||||
self.assertEqual(self.n, StockItem.objects.count())
|
self.assertEqual(self.n, StockItem.objects.count())
|
||||||
|
@ -10,7 +10,7 @@ from django.contrib.auth.models import Group
|
|||||||
|
|
||||||
from InvenTree.status_codes import PurchaseOrderStatus
|
from InvenTree.status_codes import PurchaseOrderStatus
|
||||||
|
|
||||||
from .models import PurchaseOrder, PurchaseOrderLineItem
|
from .models import PurchaseOrder
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
@ -103,86 +103,3 @@ class POTests(OrderViewTestCase):
|
|||||||
# Test that the order was actually placed
|
# Test that the order was actually placed
|
||||||
order = PurchaseOrder.objects.get(pk=1)
|
order = PurchaseOrder.objects.get(pk=1)
|
||||||
self.assertEqual(order.status, PurchaseOrderStatus.PLACED)
|
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)
|
|
||||||
|
@ -13,7 +13,6 @@ purchase_order_detail_urls = [
|
|||||||
|
|
||||||
url(r'^cancel/', views.PurchaseOrderCancel.as_view(), name='po-cancel'),
|
url(r'^cancel/', views.PurchaseOrderCancel.as_view(), name='po-cancel'),
|
||||||
url(r'^issue/', views.PurchaseOrderIssue.as_view(), name='po-issue'),
|
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'^complete/', views.PurchaseOrderComplete.as_view(), name='po-complete'),
|
||||||
|
|
||||||
url(r'^upload/', views.PurchaseOrderUpload.as_view(), name='po-upload'),
|
url(r'^upload/', views.PurchaseOrderUpload.as_view(), name='po-upload'),
|
||||||
|
@ -26,7 +26,7 @@ from .models import SalesOrderAllocation
|
|||||||
from .admin import POLineItemResource
|
from .admin import POLineItemResource
|
||||||
from build.models import Build
|
from build.models import Build
|
||||||
from company.models import Company, SupplierPart # ManufacturerPart
|
from company.models import Company, SupplierPart # ManufacturerPart
|
||||||
from stock.models import StockItem, StockLocation
|
from stock.models import StockItem
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
|
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
@ -42,7 +42,7 @@ from InvenTree.helpers import DownloadFile, str2bool
|
|||||||
from InvenTree.helpers import extract_serial_numbers
|
from InvenTree.helpers import extract_serial_numbers
|
||||||
from InvenTree.views import InvenTreeRoleMixin
|
from InvenTree.views import InvenTreeRoleMixin
|
||||||
|
|
||||||
from InvenTree.status_codes import PurchaseOrderStatus, StockStatus
|
from InvenTree.status_codes import PurchaseOrderStatus
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger("inventree")
|
logger = logging.getLogger("inventree")
|
||||||
@ -468,202 +468,6 @@ class PurchaseOrderExport(AjaxView):
|
|||||||
return DownloadFile(filedata, filename)
|
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):
|
class OrderParts(AjaxView):
|
||||||
""" View for adding various SupplierPart items to a Purchase Order.
|
""" View for adding various SupplierPart items to a Purchase Order.
|
||||||
|
|
||||||
|
@ -193,6 +193,7 @@ class PartBriefSerializer(InvenTreeModelSerializer):
|
|||||||
fields = [
|
fields = [
|
||||||
'pk',
|
'pk',
|
||||||
'IPN',
|
'IPN',
|
||||||
|
'default_location',
|
||||||
'name',
|
'name',
|
||||||
'revision',
|
'revision',
|
||||||
'full_name',
|
'full_name',
|
||||||
|
@ -973,7 +973,6 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
|
|||||||
</table>
|
</table>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
||||||
constructForm(`/api/build/${build_id}/allocate/`, {
|
constructForm(`/api/build/${build_id}/allocate/`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
fields: {},
|
fields: {},
|
||||||
|
@ -273,6 +273,11 @@ function setupFilterList(tableKey, table, target) {
|
|||||||
|
|
||||||
var element = $(target);
|
var element = $(target);
|
||||||
|
|
||||||
|
if (!element) {
|
||||||
|
console.log(`WARNING: setupFilterList could not find target '${target}'`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// One blank slate, please
|
// One blank slate, please
|
||||||
element.empty();
|
element.empty();
|
||||||
|
|
||||||
|
@ -1515,6 +1515,7 @@ function initializeChoiceField(field, fields, options) {
|
|||||||
select.select2({
|
select.select2({
|
||||||
dropdownAutoWidth: false,
|
dropdownAutoWidth: false,
|
||||||
dropdownParent: $(options.modal),
|
dropdownParent: $(options.modal),
|
||||||
|
width: '100%',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,7 +112,13 @@ function renderStockLocation(name, data, parameters, options) {
|
|||||||
|
|
||||||
var html = `<span>${level}${data.pathstring}</span>`;
|
var html = `<span>${level}${data.pathstring}</span>`;
|
||||||
|
|
||||||
if (data.description) {
|
var render_description = true;
|
||||||
|
|
||||||
|
if ('render_description' in parameters) {
|
||||||
|
render_description = parameters['render_description'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (render_description && data.description) {
|
||||||
html += ` - <i>${data.description}</i>`;
|
html += ` - <i>${data.description}</i>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
loadTableFilters,
|
loadTableFilters,
|
||||||
makeIconBadge,
|
makeIconBadge,
|
||||||
purchaseOrderStatusDisplay,
|
purchaseOrderStatusDisplay,
|
||||||
|
receivePurchaseOrderItems,
|
||||||
renderLink,
|
renderLink,
|
||||||
salesOrderStatusDisplay,
|
salesOrderStatusDisplay,
|
||||||
setupFilterList,
|
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 = `<div class='btn-group float-right' role='group'>`;
|
||||||
|
|
||||||
|
delete_button += makeIconButton(
|
||||||
|
'fa-times icon-red',
|
||||||
|
'button-row-remove',
|
||||||
|
pk,
|
||||||
|
'{% trans "Remove row" %}',
|
||||||
|
);
|
||||||
|
|
||||||
|
delete_button += '</div>';
|
||||||
|
|
||||||
|
var html = `
|
||||||
|
<tr id='receive_row_${pk}' class='stock-receive-row'>
|
||||||
|
<td id='part_${pk}'>
|
||||||
|
${thumb} ${line_item.part_detail.full_name}
|
||||||
|
</td>
|
||||||
|
<td id='sku_${pk}'>
|
||||||
|
${line_item.supplier_part_detail.SKU}
|
||||||
|
</td>
|
||||||
|
<td id='on_order_${pk}'>
|
||||||
|
${line_item.quantity}
|
||||||
|
</td>
|
||||||
|
<td id='received_${pk}'>
|
||||||
|
${line_item.received}
|
||||||
|
</td>
|
||||||
|
<td id='quantity_${pk}'>
|
||||||
|
${quantity_input}
|
||||||
|
</td>
|
||||||
|
<td id='status_${pk}'>
|
||||||
|
${status_input}
|
||||||
|
</td>
|
||||||
|
<td id='desination_${pk}'>
|
||||||
|
${destination_input}
|
||||||
|
</td>
|
||||||
|
<td id='actions_${pk}'>
|
||||||
|
${delete_button}
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
var table_entries = '';
|
||||||
|
|
||||||
|
line_items.forEach(function(item) {
|
||||||
|
table_entries += renderLineItem(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
var html = ``;
|
||||||
|
|
||||||
|
// Add table
|
||||||
|
html += `
|
||||||
|
<table class='table table-striped table-condensed' id='order-receive-table'>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Part" %}</th>
|
||||||
|
<th>{% trans "Order Code" %}</th>
|
||||||
|
<th>{% trans "Ordered" %}</th>
|
||||||
|
<th>{% trans "Received" %}</th>
|
||||||
|
<th style='min-width: 50px;'>{% trans "Receive" %}</th>
|
||||||
|
<th style='min-width: 150px;'>{% trans "Status" %}</th>
|
||||||
|
<th style='min-width: 300px;'>{% trans "Destination" %}</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${table_entries}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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) {
|
function editPurchaseOrderLineItem(e) {
|
||||||
|
|
||||||
/* Edit a purchase order line item in a modal form.
|
/* Edit a purchase order line item in a modal form.
|
||||||
@ -280,12 +566,10 @@ function loadPurchaseOrderTable(table, options) {
|
|||||||
filters[key] = options.params[key];
|
filters[key] = options.params[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
options.url = options.url || '{% url "api-po-list" %}';
|
|
||||||
|
|
||||||
setupFilterList('purchaseorder', $(table));
|
setupFilterList('purchaseorder', $(table));
|
||||||
|
|
||||||
$(table).inventreeTable({
|
$(table).inventreeTable({
|
||||||
url: options.url,
|
url: '{% url "api-po-list" %}',
|
||||||
queryParams: filters,
|
queryParams: filters,
|
||||||
name: 'purchaseorder',
|
name: 'purchaseorder',
|
||||||
groupBy: false,
|
groupBy: false,
|
||||||
@ -379,6 +663,21 @@ function loadPurchaseOrderTable(table, options) {
|
|||||||
*/
|
*/
|
||||||
function loadPurchaseOrderLineItemTable(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() {
|
function setupCallbacks() {
|
||||||
if (options.allow_edit) {
|
if (options.allow_edit) {
|
||||||
$(table).find('.button-line-edit').click(function() {
|
$(table).find('.button-line-edit').click(function() {
|
||||||
@ -424,22 +723,24 @@ function loadPurchaseOrderLineItemTable(table, options={}) {
|
|||||||
$(table).find('.button-line-receive').click(function() {
|
$(table).find('.button-line-receive').click(function() {
|
||||||
var pk = $(this).attr('pk');
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
launchModalForm(`/order/purchase-order/${options.order}/receive/`, {
|
var line_item = $(table).bootstrapTable('getRowByUniqueId', pk);
|
||||||
success: function() {
|
|
||||||
$(table).bootstrapTable('refresh');
|
if (!line_item) {
|
||||||
},
|
console.log('WARNING: getRowByUniqueId returned null');
|
||||||
data: {
|
return;
|
||||||
line: pk,
|
}
|
||||||
},
|
|
||||||
secondary: [
|
receivePurchaseOrderItems(
|
||||||
{
|
options.order,
|
||||||
field: 'location',
|
[
|
||||||
label: '{% trans "New Location" %}',
|
line_item,
|
||||||
title: '{% trans "Create new stock location" %}',
|
],
|
||||||
url: '{% url "stock-location-create" %}',
|
{
|
||||||
},
|
success: function() {
|
||||||
]
|
$(table).bootstrapTable('refresh');
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -451,17 +752,15 @@ function loadPurchaseOrderLineItemTable(table, options={}) {
|
|||||||
formatNoMatches: function() {
|
formatNoMatches: function() {
|
||||||
return '{% trans "No line items found" %}';
|
return '{% trans "No line items found" %}';
|
||||||
},
|
},
|
||||||
queryParams: {
|
queryParams: filters,
|
||||||
order: options.order,
|
original: options.params,
|
||||||
part_detail: true
|
|
||||||
},
|
|
||||||
url: '{% url "api-po-line-list" %}',
|
url: '{% url "api-po-line-list" %}',
|
||||||
showFooter: true,
|
showFooter: true,
|
||||||
|
uniqueId: 'pk',
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
field: 'pk',
|
checkbox: true,
|
||||||
title: 'ID',
|
visible: true,
|
||||||
visible: false,
|
|
||||||
switchable: false,
|
switchable: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -618,7 +917,7 @@ function loadPurchaseOrderLineItemTable(table, options={}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (options.allow_receive && row.received < row.quantity) {
|
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 += `</div>`;
|
html += `</div>`;
|
||||||
|
@ -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') {
|
if (tableKey == 'purchaseorder') {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
Loading…
Reference in New Issue
Block a user