2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-17 12:35:46 +00:00

Merge branch 'inventree:master' into so_fix_clean

This commit is contained in:
Matthias Mair
2022-05-05 11:27:44 +02:00
committed by GitHub
48 changed files with 933 additions and 1767 deletions

View File

@ -286,7 +286,58 @@ class PurchaseOrderDetail(generics.RetrieveUpdateDestroyAPIView):
return queryset
class PurchaseOrderReceive(generics.CreateAPIView):
class PurchaseOrderContextMixin:
""" Mixin to add purchase order object as serializer context variable """
def get_serializer_context(self):
""" Add the PurchaseOrder object to the serializer context """
context = super().get_serializer_context()
# Pass the purchase order through to the serializer for validation
try:
context['order'] = models.PurchaseOrder.objects.get(pk=self.kwargs.get('pk', None))
except:
pass
context['request'] = self.request
return context
class PurchaseOrderCancel(PurchaseOrderContextMixin, generics.CreateAPIView):
"""
API endpoint to 'cancel' a purchase order.
The purchase order must be in a state which can be cancelled
"""
queryset = models.PurchaseOrder.objects.all()
serializer_class = serializers.PurchaseOrderCancelSerializer
class PurchaseOrderComplete(PurchaseOrderContextMixin, generics.CreateAPIView):
"""
API endpoint to 'complete' a purchase order
"""
queryset = models.PurchaseOrder.objects.all()
serializer_class = serializers.PurchaseOrderCompleteSerializer
class PurchaseOrderIssue(PurchaseOrderContextMixin, generics.CreateAPIView):
"""
API endpoint to 'complete' a purchase order
"""
queryset = models.PurchaseOrder.objects.all()
serializer_class = serializers.PurchaseOrderIssueSerializer
class PurchaseOrderReceive(PurchaseOrderContextMixin, generics.CreateAPIView):
"""
API endpoint to receive stock items against a purchase order.
@ -303,20 +354,6 @@ class PurchaseOrderReceive(generics.CreateAPIView):
serializer_class = serializers.PurchaseOrderReceiveSerializer
def get_serializer_context(self):
context = super().get_serializer_context()
# Pass the purchase order through to the serializer for validation
try:
context['order'] = models.PurchaseOrder.objects.get(pk=self.kwargs.get('pk', None))
except:
pass
context['request'] = self.request
return context
class PurchaseOrderLineItemFilter(rest_filters.FilterSet):
"""
@ -834,13 +871,8 @@ class SalesOrderLineItemDetail(generics.RetrieveUpdateDestroyAPIView):
serializer_class = serializers.SalesOrderLineItemSerializer
class SalesOrderComplete(generics.CreateAPIView):
"""
API endpoint for manually marking a SalesOrder as "complete".
"""
queryset = models.SalesOrder.objects.all()
serializer_class = serializers.SalesOrderCompleteSerializer
class SalesOrderContextMixin:
""" Mixin to add sales order object as serializer context variable """
def get_serializer_context(self):
@ -856,7 +888,22 @@ class SalesOrderComplete(generics.CreateAPIView):
return ctx
class SalesOrderAllocateSerials(generics.CreateAPIView):
class SalesOrderCancel(SalesOrderContextMixin, generics.CreateAPIView):
queryset = models.SalesOrder.objects.all()
serializer_class = serializers.SalesOrderCancelSerializer
class SalesOrderComplete(SalesOrderContextMixin, generics.CreateAPIView):
"""
API endpoint for manually marking a SalesOrder as "complete".
"""
queryset = models.SalesOrder.objects.all()
serializer_class = serializers.SalesOrderCompleteSerializer
class SalesOrderAllocateSerials(SalesOrderContextMixin, generics.CreateAPIView):
"""
API endpoint to allocation stock items against a SalesOrder,
by specifying serial numbers.
@ -865,22 +912,8 @@ class SalesOrderAllocateSerials(generics.CreateAPIView):
queryset = models.SalesOrder.objects.none()
serializer_class = serializers.SalesOrderSerialAllocationSerializer
def get_serializer_context(self):
ctx = super().get_serializer_context()
# Pass through the SalesOrder object to the serializer
try:
ctx['order'] = models.SalesOrder.objects.get(pk=self.kwargs.get('pk', None))
except:
pass
ctx['request'] = self.request
return ctx
class SalesOrderAllocate(generics.CreateAPIView):
class SalesOrderAllocate(SalesOrderContextMixin, generics.CreateAPIView):
"""
API endpoint to allocate stock items against a SalesOrder
@ -891,20 +924,6 @@ class SalesOrderAllocate(generics.CreateAPIView):
queryset = models.SalesOrder.objects.none()
serializer_class = serializers.SalesOrderShipmentAllocationSerializer
def get_serializer_context(self):
ctx = super().get_serializer_context()
# Pass through the SalesOrder object to the serializer
try:
ctx['order'] = models.SalesOrder.objects.get(pk=self.kwargs.get('pk', None))
except:
pass
ctx['request'] = self.request
return ctx
class SalesOrderAllocationDetail(generics.RetrieveUpdateDestroyAPIView):
"""
@ -1106,7 +1125,10 @@ order_api_urls = [
# Individual purchase order detail URLs
re_path(r'^(?P<pk>\d+)/', include([
re_path(r'^issue/', PurchaseOrderIssue.as_view(), name='api-po-issue'),
re_path(r'^receive/', PurchaseOrderReceive.as_view(), name='api-po-receive'),
re_path(r'^cancel/', PurchaseOrderCancel.as_view(), name='api-po-cancel'),
re_path(r'^complete/', PurchaseOrderComplete.as_view(), name='api-po-complete'),
re_path(r'.*$', PurchaseOrderDetail.as_view(), name='api-po-detail'),
])),
@ -1143,6 +1165,7 @@ order_api_urls = [
# Sales order detail view
re_path(r'^(?P<pk>\d+)/', include([
re_path(r'^cancel/', SalesOrderCancel.as_view(), name='api-so-cancel'),
re_path(r'^complete/', SalesOrderComplete.as_view(), name='api-so-complete'),
re_path(r'^allocate/', SalesOrderAllocate.as_view(), name='api-so-allocate'),
re_path(r'^allocate-serials/', SalesOrderAllocateSerials.as_view(), name='api-so-allocate-serials'),

View File

@ -8,60 +8,12 @@ from __future__ import unicode_literals
from django import forms
from django.utils.translation import gettext_lazy as _
from InvenTree.forms import HelperForm
from InvenTree.fields import InvenTreeMoneyField
from InvenTree.helpers import clean_decimal
from common.forms import MatchItemForm
from .models import PurchaseOrder
from .models import SalesOrder
class IssuePurchaseOrderForm(HelperForm):
confirm = forms.BooleanField(required=True, initial=False, label=_('Confirm'), help_text=_('Place order'))
class Meta:
model = PurchaseOrder
fields = [
'confirm',
]
class CompletePurchaseOrderForm(HelperForm):
confirm = forms.BooleanField(required=True, label=_('Confirm'), help_text=_("Mark order as complete"))
class Meta:
model = PurchaseOrder
fields = [
'confirm',
]
class CancelPurchaseOrderForm(HelperForm):
confirm = forms.BooleanField(required=True, label=_('Confirm'), help_text=_('Cancel order'))
class Meta:
model = PurchaseOrder
fields = [
'confirm',
]
class CancelSalesOrderForm(HelperForm):
confirm = forms.BooleanField(required=True, label=_('Confirm'), help_text=_('Cancel order'))
class Meta:
model = SalesOrder
fields = [
'confirm',
]
class OrderMatchItemForm(MatchItemForm):
""" Override MatchItemForm fields """

View File

@ -381,6 +381,7 @@ class PurchaseOrder(Order):
PurchaseOrderStatus.PENDING
]
@transaction.atomic
def cancel_order(self):
""" Marks the PurchaseOrder as CANCELLED. """

View File

@ -179,6 +179,72 @@ class PurchaseOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializ
]
class PurchaseOrderCancelSerializer(serializers.Serializer):
"""
Serializer for cancelling a PurchaseOrder
"""
class Meta:
fields = [],
def get_context_data(self):
"""
Return custom context information about the order
"""
self.order = self.context['order']
return {
'can_cancel': self.order.can_cancel(),
}
def save(self):
order = self.context['order']
if not order.can_cancel():
raise ValidationError(_("Order cannot be cancelled"))
order.cancel_order()
class PurchaseOrderCompleteSerializer(serializers.Serializer):
"""
Serializer for completing a purchase order
"""
class Meta:
fields = []
def get_context_data(self):
"""
Custom context information for this serializer
"""
order = self.context['order']
return {
'is_complete': order.is_complete,
}
def save(self):
order = self.context['order']
order.complete_order()
class PurchaseOrderIssueSerializer(serializers.Serializer):
""" Serializer for issuing (sending) a purchase order """
class Meta:
fields = []
def save(self):
order = self.context['order']
order.place_order()
class PurchaseOrderLineItemSerializer(InvenTreeModelSerializer):
@staticmethod
@ -974,6 +1040,25 @@ class SalesOrderCompleteSerializer(serializers.Serializer):
order.complete_order(user)
class SalesOrderCancelSerializer(serializers.Serializer):
""" Serializer for marking a SalesOrder as cancelled
"""
def get_context_data(self):
order = self.context['order']
return {
'can_cancel': order.can_cancel(),
}
def save(self):
order = self.context['order']
order.cancel_order()
class SalesOrderSerialAllocationSerializer(serializers.Serializer):
"""
DRF serializer for allocation of serial numbers against a sales order / shipment

View File

@ -1,7 +0,0 @@
{% extends "modal_delete_form.html" %}
{% load i18n %}
{% block pre_form_content %}
{% trans "Are you sure you want to delete this attachment?" %}
<br>
{% endblock %}

View File

@ -192,10 +192,14 @@ src="{% static 'img/blank_image.png' %}"
{% if order.status == PurchaseOrderStatus.PENDING %}
$("#place-order").click(function() {
launchModalForm("{% url 'po-issue' order.id %}",
{
reload: true,
});
issuePurchaseOrder(
{{ order.pk }},
{
reload: true,
}
);
});
{% endif %}
@ -258,15 +262,27 @@ $("#receive-order").click(function() {
});
$("#complete-order").click(function() {
launchModalForm("{% url 'po-complete' order.id %}", {
reload: true,
});
completePurchaseOrder(
{{ order.pk }},
{
onSuccess: function() {
window.location.reload();
}
}
);
});
$("#cancel-order").click(function() {
launchModalForm("{% url 'po-cancel' order.id %}", {
reload: true,
});
cancelPurchaseOrder(
{{ order.pk }},
{
onSuccess: function() {
window.location.reload();
}
},
);
});
$("#export-order").click(function() {

View File

@ -1,11 +0,0 @@
{% extends "modal_form.html" %}
{% load i18n %}
{% block pre_form_content %}
<div class='alert alert-danger alert-block'>
{% trans "Cancelling this order means that the order and line items will no longer be editable." %}
</div>
{% endblock %}

View File

@ -1,15 +0,0 @@
{% extends "modal_form.html" %}
{% load i18n %}
{% block pre_form_content %}
{% trans 'Mark this order as complete?' %}
{% if not order.is_complete %}
<div class='alert alert-warning alert-block' style='margin-top:12px'>
{% trans 'This order has line items which have not been marked as received.' %}</br>
{% trans 'Completing this order means that the order and line items will no longer be editable.' %}
</div>
{% endif %}
{% endblock %}

View File

@ -1,11 +0,0 @@
{% extends "modal_form.html" %}
{% load i18n %}
{% block pre_form_content %}
<div class='alert alert-warning alert-block'>
{% trans 'After placing this purchase order, line items will no longer be editable.' %}
</div>
{% endblock %}

View File

@ -224,9 +224,13 @@ $("#edit-order").click(function() {
});
$("#cancel-order").click(function() {
launchModalForm("{% url 'so-cancel' order.id %}", {
reload: true,
});
cancelSalesOrder(
{{ order.pk }},
{
reload: true,
}
);
});
$("#complete-order").click(function() {

View File

@ -1,12 +0,0 @@
{% extends "modal_form.html" %}
{% load i18n %}
{% block pre_form_content %}
<div class='alert alert-block alert-warning'>
<h4>{% trans "Warning" %}</h4>
{% trans "Cancelling this order means that the order will no longer be editable." %}
</div>
{% endblock %}

View File

@ -9,7 +9,7 @@ from rest_framework import status
from django.urls import reverse
from InvenTree.api_tester import InvenTreeAPITestCase
from InvenTree.status_codes import PurchaseOrderStatus
from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus
from part.models import Part
from stock.models import StockItem
@ -239,6 +239,73 @@ class PurchaseOrderTest(OrderTest):
expected_code=201
)
def test_po_cancel(self):
"""
Test the PurchaseOrderCancel API endpoint
"""
po = models.PurchaseOrder.objects.get(pk=1)
self.assertEqual(po.status, PurchaseOrderStatus.PENDING)
url = reverse('api-po-cancel', kwargs={'pk': po.pk})
# Try to cancel the PO, but without reqiured permissions
self.post(url, {}, expected_code=403)
self.assignRole('purchase_order.add')
self.post(
url,
{},
expected_code=201,
)
po.refresh_from_db()
self.assertEqual(po.status, PurchaseOrderStatus.CANCELLED)
# Try to cancel again (should fail)
self.post(url, {}, expected_code=400)
def test_po_complete(self):
""" Test the PurchaseOrderComplete API endpoint """
po = models.PurchaseOrder.objects.get(pk=3)
url = reverse('api-po-complete', kwargs={'pk': po.pk})
self.assertEqual(po.status, PurchaseOrderStatus.PLACED)
# Try to complete the PO, without required permissions
self.post(url, {}, expected_code=403)
self.assignRole('purchase_order.add')
self.post(url, {}, expected_code=201)
po.refresh_from_db()
self.assertEqual(po.status, PurchaseOrderStatus.COMPLETE)
def test_po_issue(self):
""" Test the PurchaseOrderIssue API endpoint """
po = models.PurchaseOrder.objects.get(pk=2)
url = reverse('api-po-issue', kwargs={'pk': po.pk})
# Try to issue the PO, without required permissions
self.post(url, {}, expected_code=403)
self.assignRole('purchase_order.add')
self.post(url, {}, expected_code=201)
po.refresh_from_db()
self.assertEqual(po.status, PurchaseOrderStatus.PLACED)
class PurchaseOrderReceiveTest(OrderTest):
"""
@ -788,6 +855,26 @@ class SalesOrderTest(OrderTest):
expected_code=201
)
def test_so_cancel(self):
""" Test API endpoint for cancelling a SalesOrder """
so = models.SalesOrder.objects.get(pk=1)
self.assertEqual(so.status, SalesOrderStatus.PENDING)
url = reverse('api-so-cancel', kwargs={'pk': so.pk})
# Try to cancel, without permission
self.post(url, {}, expected_code=403)
self.assignRole('sales_order.add')
self.post(url, {}, expected_code=201)
so.refresh_from_db()
self.assertEqual(so.status, SalesOrderStatus.CANCELLED)
class SalesOrderAllocateTest(OrderTest):
"""

View File

@ -8,12 +8,6 @@ from django.urls import reverse
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from InvenTree.status_codes import PurchaseOrderStatus
from .models import PurchaseOrder
import json
class OrderViewTestCase(TestCase):
@ -76,30 +70,3 @@ class PurchaseOrderTests(OrderViewTestCase):
# Response should be streaming-content (file download)
self.assertIn('streaming_content', dir(response))
def test_po_issue(self):
""" Test PurchaseOrderIssue view """
url = reverse('po-issue', args=(1,))
order = PurchaseOrder.objects.get(pk=1)
self.assertEqual(order.status, PurchaseOrderStatus.PENDING)
# Test without confirmation
response = self.client.post(url, {'confirm': 0}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
data = json.loads(response.content)
self.assertFalse(data['form_valid'])
# Test WITH confirmation
response = self.client.post(url, {'confirm': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
data = json.loads(response.content)
self.assertTrue(data['form_valid'])
# Test that the order was actually placed
order = PurchaseOrder.objects.get(pk=1)
self.assertEqual(order.status, PurchaseOrderStatus.PLACED)

View File

@ -11,10 +11,6 @@ from . import views
purchase_order_detail_urls = [
re_path(r'^cancel/', views.PurchaseOrderCancel.as_view(), name='po-cancel'),
re_path(r'^issue/', views.PurchaseOrderIssue.as_view(), name='po-issue'),
re_path(r'^complete/', views.PurchaseOrderComplete.as_view(), name='po-complete'),
re_path(r'^upload/', views.PurchaseOrderUpload.as_view(), name='po-upload'),
re_path(r'^export/', views.PurchaseOrderExport.as_view(), name='po-export'),
@ -33,7 +29,6 @@ purchase_order_urls = [
]
sales_order_detail_urls = [
re_path(r'^cancel/', views.SalesOrderCancel.as_view(), name='so-cancel'),
re_path(r'^export/', views.SalesOrderExport.as_view(), name='so-export'),
re_path(r'^.*$', views.SalesOrderDetail.as_view(), name='so-detail'),

View File

@ -30,9 +30,8 @@ from common.files import FileManager
from . import forms as order_forms
from part.views import PartPricing
from InvenTree.views import AjaxView, AjaxUpdateView
from InvenTree.helpers import DownloadFile, str2bool
from InvenTree.views import InvenTreeRoleMixin
from InvenTree.helpers import DownloadFile
from InvenTree.views import InvenTreeRoleMixin, AjaxView
logger = logging.getLogger("inventree")
@ -87,123 +86,6 @@ class SalesOrderDetail(InvenTreeRoleMixin, DetailView):
template_name = 'order/sales_order_detail.html'
class PurchaseOrderCancel(AjaxUpdateView):
""" View for cancelling a purchase order """
model = PurchaseOrder
ajax_form_title = _('Cancel Order')
ajax_template_name = 'order/order_cancel.html'
form_class = order_forms.CancelPurchaseOrderForm
def validate(self, order, form, **kwargs):
confirm = str2bool(form.cleaned_data.get('confirm', False))
if not confirm:
form.add_error('confirm', _('Confirm order cancellation'))
if not order.can_cancel():
form.add_error(None, _('Order cannot be cancelled'))
def save(self, order, form, **kwargs):
"""
Cancel the PurchaseOrder
"""
order.cancel_order()
class SalesOrderCancel(AjaxUpdateView):
""" View for cancelling a sales order """
model = SalesOrder
ajax_form_title = _("Cancel sales order")
ajax_template_name = "order/sales_order_cancel.html"
form_class = order_forms.CancelSalesOrderForm
def validate(self, order, form, **kwargs):
confirm = str2bool(form.cleaned_data.get('confirm', False))
if not confirm:
form.add_error('confirm', _('Confirm order cancellation'))
if not order.can_cancel():
form.add_error(None, _('Order cannot be cancelled'))
def save(self, order, form, **kwargs):
"""
Once the form has been validated, cancel the SalesOrder
"""
order.cancel_order()
class PurchaseOrderIssue(AjaxUpdateView):
""" View for changing a purchase order from 'PENDING' to 'ISSUED' """
model = PurchaseOrder
ajax_form_title = _('Issue Order')
ajax_template_name = "order/order_issue.html"
form_class = order_forms.IssuePurchaseOrderForm
def validate(self, order, form, **kwargs):
confirm = str2bool(self.request.POST.get('confirm', False))
if not confirm:
form.add_error('confirm', _('Confirm order placement'))
def save(self, order, form, **kwargs):
"""
Once the form has been validated, place the order.
"""
order.place_order()
def get_data(self):
return {
'success': _('Purchase order issued')
}
class PurchaseOrderComplete(AjaxUpdateView):
""" View for marking a PurchaseOrder as complete.
"""
form_class = order_forms.CompletePurchaseOrderForm
model = PurchaseOrder
ajax_template_name = "order/order_complete.html"
ajax_form_title = _("Complete Order")
context_object_name = 'order'
def get_context_data(self):
ctx = {
'order': self.get_object(),
}
return ctx
def validate(self, order, form, **kwargs):
confirm = str2bool(form.cleaned_data.get('confirm', False))
if not confirm:
form.add_error('confirm', _('Confirm order completion'))
def save(self, order, form, **kwargs):
"""
Complete the PurchaseOrder
"""
order.complete_order()
def get_data(self):
return {
'success': _('Purchase order completed')
}
class PurchaseOrderUpload(FileManagementFormView):
''' PurchaseOrder: Upload file, match to fields and parts (using multi-Step form) '''