2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-15 19:45:46 +00:00

Receiving items against a purchase order now makes use of the API forms

- Delete old unused code
- Improve serializer validation
This commit is contained in:
Oliver
2021-10-05 16:05:18 +11:00
parent a579bc8721
commit dd4428464d
9 changed files with 168 additions and 356 deletions

View File

@ -80,22 +80,6 @@ class ShipSalesOrderForm(HelperForm):
]
class ReceivePurchaseOrderForm(HelperForm):
location = TreeNodeChoiceField(
queryset=StockLocation.objects.all(),
required=False,
label=_("Destination"),
help_text=_("Set all received parts listed above to this location (if left blank, use \"Destination\" column value in above table)"),
)
class Meta:
model = PurchaseOrder
fields = [
"location",
]
class AllocateSerialsToSalesOrderForm(forms.Form):
"""
Form for assigning stock to a sales order,

View File

@ -225,6 +225,13 @@ class POLineItemReceiveSerializer(serializers.Serializer):
required=True,
)
def validate_quantity(self, quantity):
if quantity <= 0:
raise ValidationError(_("Quantity must be greater than zero"))
return quantity
status = serializers.ChoiceField(
choices=list(StockStatus.items()),
default=StockStatus.OK,
@ -246,7 +253,7 @@ class POLineItemReceiveSerializer(serializers.Serializer):
# Ignore empty barcode values
if not barcode or barcode.strip() == '':
return
return None
if stock.models.StockItem.objects.filter(uid=barcode).exists():
raise ValidationError(_('Barcode is already in use'))
@ -284,10 +291,28 @@ class POReceiveSerializer(serializers.Serializer):
items = data.get('items', [])
location = data.get('location', None)
if len(items) == 0:
raise ValidationError({
'items': _('Line items must be provided')
})
raise ValidationError(_('Line items must be provided'))
# Check if the location is not specified for any particular item
for item in items:
line = item['line_item']
if not item.get('location', None):
# If a global location is specified, use that
item['location'] = location
if not item['location']:
# The line item specifies a location?
item['location'] = line.get_destination()
if not item['location']:
raise ValidationError({
'location': _("Destination location must be specified"),
})
# Ensure barcodes are unique
unique_barcodes = set()
@ -313,24 +338,6 @@ class POReceiveSerializer(serializers.Serializer):
items = data['items']
location = data.get('location', None)
# Check if the location is not specified for any particular item
for item in items:
line = item['line_item']
if not item.get('location', None):
# If a global location is specified, use that
item['location'] = location
if not item['location']:
# The line item specifies a location?
item['location'] = line.get_destination()
if not item['location']:
raise ValidationError({
'location': _("Destination location must be specified"),
})
# Now we can actually receive the items into stock
with transaction.atomic():
for item in items:

View File

@ -204,22 +204,11 @@ $("#receive-order").click(function() {
{{ order.id }},
items_to_receive,
{
success: function() {
$("#po-line-table").bootstrapTable('refresh');
}
}
);
return;
launchModalForm("{% url 'po-receive' order.id %}", {
reload: true,
secondary: [
{
field: 'location',
label: '{% trans "New Location" %}',
title: '{% trans "Create new stock location" %}',
url: "{% url 'stock-location-create' %}",
},
]
});
});
$("#complete-order").click(function() {

View File

@ -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 %}

View File

@ -13,7 +13,6 @@ purchase_order_detail_urls = [
url(r'^cancel/', views.PurchaseOrderCancel.as_view(), name='po-cancel'),
url(r'^issue/', views.PurchaseOrderIssue.as_view(), name='po-issue'),
url(r'^receive/', views.PurchaseOrderReceive.as_view(), name='po-receive'),
url(r'^complete/', views.PurchaseOrderComplete.as_view(), name='po-complete'),
url(r'^upload/', views.PurchaseOrderUpload.as_view(), name='po-upload'),

View File

@ -468,202 +468,6 @@ class PurchaseOrderExport(AjaxView):
return DownloadFile(filedata, filename)
class PurchaseOrderReceive(AjaxUpdateView):
""" View for receiving parts which are outstanding against a PurchaseOrder.
Any parts which are outstanding are listed.
If all parts are marked as received, the order is closed out.
"""
form_class = order_forms.ReceivePurchaseOrderForm
ajax_form_title = _("Receive Parts")
ajax_template_name = "order/receive_parts.html"
# Specify role as we do not specify a Model against this view
role_required = 'purchase_order.change'
# Where the parts will be going (selected in POST request)
destination = None
def get_context_data(self):
ctx = {
'order': self.order,
'lines': self.lines,
'stock_locations': StockLocation.objects.all(),
}
return ctx
def get_lines(self):
"""
Extract particular line items from the request,
or default to *all* pending line items if none are provided
"""
lines = None
if 'line' in self.request.GET:
line_id = self.request.GET.get('line')
try:
lines = PurchaseOrderLineItem.objects.filter(pk=line_id)
except (PurchaseOrderLineItem.DoesNotExist, ValueError):
pass
# TODO - Option to pass multiple lines?
# No lines specified - default selection
if lines is None:
lines = self.order.pending_line_items()
return lines
def get(self, request, *args, **kwargs):
""" Respond to a GET request. Determines which parts are outstanding,
and presents a list of these parts to the user.
"""
self.request = request
self.order = get_object_or_404(PurchaseOrder, pk=self.kwargs['pk'])
self.lines = self.get_lines()
for line in self.lines:
# Pre-fill the remaining quantity
line.receive_quantity = line.remaining()
return self.renderJsonResponse(request, form=self.get_form())
def post(self, request, *args, **kwargs):
""" Respond to a POST request. Data checking and error handling.
If the request is valid, new StockItem objects will be made
for each received item.
"""
self.request = request
self.order = get_object_or_404(PurchaseOrder, pk=self.kwargs['pk'])
errors = False
self.lines = []
self.destination = None
msg = _("Items received")
# Extract the destination for received parts
if 'location' in request.POST:
pk = request.POST['location']
try:
self.destination = StockLocation.objects.get(id=pk)
except (StockLocation.DoesNotExist, ValueError):
pass
# Extract information on all submitted line items
for item in request.POST:
if item.startswith('line-'):
pk = item.replace('line-', '')
try:
line = PurchaseOrderLineItem.objects.get(id=pk)
except (PurchaseOrderLineItem.DoesNotExist, ValueError):
continue
# Check that the StockStatus was set
status_key = 'status-{pk}'.format(pk=pk)
status = request.POST.get(status_key, StockStatus.OK)
try:
status = int(status)
except ValueError:
status = StockStatus.OK
if status in StockStatus.RECEIVING_CODES:
line.status_code = status
else:
line.status_code = StockStatus.OK
# Check the destination field
line.destination = None
if self.destination:
# If global destination is set, overwrite line value
line.destination = self.destination
else:
destination_key = f'destination-{pk}'
destination = request.POST.get(destination_key, None)
if destination:
try:
line.destination = StockLocation.objects.get(pk=destination)
except (StockLocation.DoesNotExist, ValueError):
pass
# Check that line matches the order
if not line.order == self.order:
# TODO - Display a non-field error?
continue
# Ignore a part that doesn't map to a SupplierPart
try:
if line.part is None:
continue
except SupplierPart.DoesNotExist:
continue
receive = self.request.POST[item]
try:
receive = Decimal(receive)
except InvalidOperation:
# In the case on an invalid input, reset to default
receive = line.remaining()
msg = _("Error converting quantity to number")
errors = True
if receive < 0:
receive = 0
errors = True
msg = _("Receive quantity less than zero")
line.receive_quantity = receive
self.lines.append(line)
if len(self.lines) == 0:
msg = _("No lines specified")
errors = True
# No errors? Receive the submitted parts!
if errors is False:
self.receive_parts()
data = {
'form_valid': errors is False,
'success': msg,
}
return self.renderJsonResponse(request, data=data, form=self.get_form())
@transaction.atomic
def receive_parts(self):
""" Called once the form has been validated.
Create new stockitems against received parts.
"""
for line in self.lines:
if not line.part:
continue
self.order.receive_line_item(
line,
line.destination,
line.receive_quantity,
self.request.user,
status=line.status_code,
purchase_price=line.purchase_price,
)
class OrderParts(AjaxView):
""" View for adding various SupplierPart items to a Purchase Order.