diff --git a/InvenTree/company/migrations/0040_alter_company_currency.py b/InvenTree/company/migrations/0040_alter_company_currency.py new file mode 100644 index 0000000000..f26f470191 --- /dev/null +++ b/InvenTree/company/migrations/0040_alter_company_currency.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.4 on 2021-07-02 13:21 + +import InvenTree.validators +import common.settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('company', '0039_auto_20210701_0509'), + ] + + operations = [ + migrations.AlterField( + model_name='company', + name='currency', + field=models.CharField(blank=True, default=common.settings.currency_code_default, help_text='Default currency used for this company', max_length=3, validators=[InvenTree.validators.validate_currency_code], verbose_name='Currency'), + ), + ] diff --git a/InvenTree/company/templates/company/company_base.html b/InvenTree/company/templates/company/company_base.html index dcf7d9d3a4..8c6b077137 100644 --- a/InvenTree/company/templates/company/company_base.html +++ b/InvenTree/company/templates/company/company_base.html @@ -115,16 +115,11 @@ }); $("#company-order-2").click(function() { - launchModalForm("{% url 'po-create' %}", - { - data: { - supplier: {{ company.id }}, - }, - follow: true, + createPurchaseOrder({ + supplier: {{ company.pk }}, }); }); - $('#company-delete').click(function() { constructForm('{% url "api-company-detail" company.pk %}', { method: 'DELETE', diff --git a/InvenTree/company/templates/company/purchase_orders.html b/InvenTree/company/templates/company/purchase_orders.html index 0403c8cc59..f23d360a8f 100644 --- a/InvenTree/company/templates/company/purchase_orders.html +++ b/InvenTree/company/templates/company/purchase_orders.html @@ -39,14 +39,9 @@ } }); - function newOrder() { - launchModalForm("{% url 'po-create' %}", - { - data: { - supplier: {{ company.id }}, - }, - follow: true, + createPurchaseOrder({ + supplier: {{ company.pk }}, }); } diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index 748cd360d8..446b5a4313 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -5,11 +5,12 @@ JSON API for the Order app # -*- coding: utf-8 -*- from __future__ import unicode_literals +from django.conf.urls import url, include + from django_filters.rest_framework import DjangoFilterBackend from rest_framework import generics -from rest_framework import filters - -from django.conf.urls import url, include +from rest_framework import filters, status +from rest_framework.response import Response from InvenTree.helpers import str2bool from InvenTree.api import AttachmentMixin @@ -38,6 +39,20 @@ class POList(generics.ListCreateAPIView): queryset = PurchaseOrder.objects.all() serializer_class = POSerializer + def create(self, request, *args, **kwargs): + """ + Save user information on create + """ + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + + item = serializer.save() + item.created_by = request.user + item.save() + + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + def get_serializer(self, *args, **kwargs): try: @@ -279,6 +294,20 @@ class SOList(generics.ListCreateAPIView): queryset = SalesOrder.objects.all() serializer_class = SalesOrderSerializer + def create(self, request, *args, **kwargs): + """ + Save user information on create + """ + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + + item = serializer.save() + item.created_by = request.user + item.save() + + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + def get_serializer(self, *args, **kwargs): try: diff --git a/InvenTree/order/forms.py b/InvenTree/order/forms.py index 74b6d5f47b..86b1ac5076 100644 --- a/InvenTree/order/forms.py +++ b/InvenTree/order/forms.py @@ -97,41 +97,6 @@ class ReceivePurchaseOrderForm(HelperForm): ] -class EditPurchaseOrderForm(HelperForm): - """ Form for editing a PurchaseOrder object """ - - def __init__(self, *args, **kwargs): - - self.field_prefix = { - 'reference': 'PO', - 'link': 'fa-link', - 'target_date': 'fa-calendar-alt', - } - - self.field_placeholder = { - 'reference': _('Purchase Order reference'), - } - - super().__init__(*args, **kwargs) - - target_date = DatePickerFormField( - label=_('Target Date'), - help_text=_('Target date for order delivery. Order will be overdue after this date.'), - ) - - class Meta: - model = PurchaseOrder - fields = [ - 'reference', - 'supplier', - 'supplier_reference', - 'description', - 'target_date', - 'link', - 'responsible', - ] - - class EditSalesOrderForm(HelperForm): """ Form for editing a SalesOrder object """ diff --git a/InvenTree/order/migrations/0048_auto_20210702_2321.py b/InvenTree/order/migrations/0048_auto_20210702_2321.py new file mode 100644 index 0000000000..d6785e669e --- /dev/null +++ b/InvenTree/order/migrations/0048_auto_20210702_2321.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.4 on 2021-07-02 13:21 + +from django.db import migrations, models +import order.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0047_auto_20210701_0509'), + ] + + operations = [ + migrations.AlterField( + model_name='purchaseorder', + name='reference', + field=models.CharField(default=order.models.get_next_po_number, help_text='Order reference', max_length=64, unique=True, verbose_name='Reference'), + ), + migrations.AlterField( + model_name='salesorder', + name='reference', + field=models.CharField(default=order.models.get_next_so_number, help_text='Order reference', max_length=64, unique=True, verbose_name='Reference'), + ), + ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index c35e93757a..d621bcfd88 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -31,6 +31,60 @@ from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockS from InvenTree.models import InvenTreeAttachment +def get_next_po_number(): + """ + Returns the next available PurchaseOrder reference number + """ + + if PurchaseOrder.objects.count() == 0: + return + + order = PurchaseOrder.objects.exclude(reference=None).last() + + attempts = set([order.reference]) + + while 1: + reference = increment(order.reference) + + if reference in attempts: + # Escape infinite recursion + return reference + + if PurchaseOrder.objects.filter(reference=reference).exists(): + attempts.add(reference) + else: + break + + return reference + + +def get_next_so_number(): + """ + Returns the next available SalesOrder reference number + """ + + if SalesOrder.objects.count() == 0: + return + + order = SalesOrder.objects.exclude(reference=None).last() + + attempts = set([order.reference]) + + while 1: + reference = increment(order.reference) + + if reference in attempts: + # Escape infinite recursion + return reference + + if SalesOrder.objects.filter(reference=reference).exists(): + attempts.add(reference) + else: + break + + return reference + + class Order(models.Model): """ Abstract model for an order. @@ -72,6 +126,8 @@ class Order(models.Model): while 1: new_ref = increment(ref) + print("Reference:", new_ref) + if new_ref in tries: # We are in a looping situation - simply return the original one return ref @@ -95,8 +151,6 @@ class Order(models.Model): class Meta: abstract = True - reference = models.CharField(unique=True, max_length=64, blank=False, verbose_name=_('Reference'), help_text=_('Order reference')) - description = models.CharField(max_length=250, verbose_name=_('Description'), help_text=_('Order description')) link = models.URLField(blank=True, verbose_name=_('Link'), help_text=_('Link to external page')) @@ -181,6 +235,15 @@ class PurchaseOrder(Order): return f"{prefix}{self.reference} - {self.supplier.name}" + reference = models.CharField( + unique=True, + max_length=64, + blank=False, + verbose_name=_('Reference'), + help_text=_('Order reference'), + default=get_next_po_number, + ) + status = models.PositiveIntegerField(default=PurchaseOrderStatus.PENDING, choices=PurchaseOrderStatus.items(), help_text=_('Purchase order status')) @@ -459,6 +522,15 @@ class SalesOrder(Order): def get_absolute_url(self): return reverse('so-detail', kwargs={'pk': self.id}) + reference = models.CharField( + unique=True, + max_length=64, + blank=False, + verbose_name=_('Reference'), + help_text=_('Order reference'), + default=get_next_so_number, + ) + customer = models.ForeignKey( Company, on_delete=models.SET_NULL, diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index 31a96bf635..83eb01ea99 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -69,6 +69,8 @@ class POSerializer(InvenTreeModelSerializer): overdue = serializers.BooleanField(required=False, read_only=True) + reference = serializers.CharField(required=True) + class Meta: model = PurchaseOrder @@ -212,6 +214,8 @@ class SalesOrderSerializer(InvenTreeModelSerializer): overdue = serializers.BooleanField(required=False, read_only=True) + reference = serializers.CharField(required=True) + class Meta: model = SalesOrder diff --git a/InvenTree/order/templates/order/purchase_orders.html b/InvenTree/order/templates/order/purchase_orders.html index 15d58788bc..de1c0dd8a9 100644 --- a/InvenTree/order/templates/order/purchase_orders.html +++ b/InvenTree/order/templates/order/purchase_orders.html @@ -176,18 +176,7 @@ $("#order-print").click(function() { }) $("#po-create").click(function() { - launchModalForm("{% url 'po-create' %}", - { - follow: true, - secondary: [ - { - field: 'supplier', - label: '{% trans "New Supplier" %}', - title: '{% trans "Create new Supplier" %}', - } - ] - } - ); + createPurchaseOrder(); }); loadPurchaseOrderTable("#purchase-order-table", { diff --git a/InvenTree/order/test_views.py b/InvenTree/order/test_views.py index b0bd8ce95d..22f193b0ff 100644 --- a/InvenTree/order/test_views.py +++ b/InvenTree/order/test_views.py @@ -152,22 +152,6 @@ class POTests(OrderViewTestCase): keys = response.context.keys() self.assertIn('PurchaseOrderStatus', keys) - def test_po_create(self): - """ Launch forms to create new PurchaseOrder""" - url = reverse('po-create') - - # Without a supplier ID - response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - - # With a valid supplier ID - response = self.client.get(url, {'supplier': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - - # With an invalid supplier ID - response = self.client.get(url, {'supplier': 'goat'}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - def test_po_export(self): """ Export PurchaseOrder """ diff --git a/InvenTree/order/tests.py b/InvenTree/order/tests.py index 5452aec383..b68e0c3ff1 100644 --- a/InvenTree/order/tests.py +++ b/InvenTree/order/tests.py @@ -60,12 +60,6 @@ class OrderTest(TestCase): order.save() self.assertFalse(order.is_overdue) - def test_increment(self): - - next_ref = PurchaseOrder.getNextOrderNumber() - - self.assertEqual(next_ref, '0008') - def test_on_order(self): """ There should be 3 separate items on order for the M2x4 LPHS part """ diff --git a/InvenTree/order/urls.py b/InvenTree/order/urls.py index ce2421b684..8cbd99911a 100644 --- a/InvenTree/order/urls.py +++ b/InvenTree/order/urls.py @@ -28,8 +28,6 @@ purchase_order_detail_urls = [ purchase_order_urls = [ - url(r'^new/', views.PurchaseOrderCreate.as_view(), name='po-create'), - url(r'^order-parts/', views.OrderParts.as_view(), name='order-parts'), url(r'^pricing/', views.LineItemPricing.as_view(), name='line-pricing'), diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 647176862d..83a7fe12f8 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -143,43 +143,6 @@ class SalesOrderNotes(InvenTreeRoleMixin, UpdateView): return ctx -class PurchaseOrderCreate(AjaxCreateView): - """ - View for creating a new PurchaseOrder object using a modal form - """ - - model = PurchaseOrder - ajax_form_title = _("Create Purchase Order") - form_class = order_forms.EditPurchaseOrderForm - - def get_initial(self): - initials = super().get_initial().copy() - - initials['reference'] = PurchaseOrder.getNextOrderNumber() - initials['status'] = PurchaseOrderStatus.PENDING - - supplier_id = self.request.GET.get('supplier', None) - - if supplier_id: - try: - supplier = Company.objects.get(id=supplier_id) - initials['supplier'] = supplier - except (Company.DoesNotExist, ValueError): - pass - - return initials - - def save(self, form, **kwargs): - """ - Record the user who created this PurchaseOrder - """ - - order = form.save(commit=False) - order.created_by = self.request.user - - return super().save(form) - - class SalesOrderCreate(AjaxCreateView): """ View for creating a new SalesOrder object """ diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index b946d66aa5..088811f0f2 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -2,17 +2,21 @@ JSON API for the Stock app """ -from django_filters.rest_framework import FilterSet, DjangoFilterBackend -from django_filters import NumberFilter - -from rest_framework import status - from django.conf.urls import url, include from django.urls import reverse from django.http import JsonResponse from django.db.models import Q from django.utils.translation import ugettext_lazy as _ +from rest_framework import status +from rest_framework.serializers import ValidationError +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import generics, filters, permissions + +from django_filters.rest_framework import FilterSet, DjangoFilterBackend +from django_filters import NumberFilter + from .models import StockLocation, StockItem from .models import StockItemTracking from .models import StockItemAttachment @@ -44,11 +48,6 @@ from decimal import Decimal, InvalidOperation from datetime import datetime, timedelta -from rest_framework.serializers import ValidationError -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import generics, filters, permissions - class StockCategoryTree(TreeSerializer): title = _('Stock') diff --git a/InvenTree/templates/js/order.js b/InvenTree/templates/js/order.js index e4aa4850e6..16d21a33ac 100644 --- a/InvenTree/templates/js/order.js +++ b/InvenTree/templates/js/order.js @@ -1,6 +1,38 @@ {% load i18n %} {% load inventree_extras %} + +// Create a new purchase order +function createPurchaseOrder(options={}) { + + constructForm('{% url "api-po-list" %}', { + method: 'POST', + fields: { + reference: { + prefix: "{% settings_value 'PURCHASEORDER_REFERENCE_PREFIX' %}", + }, + supplier: { + value: options.supplier, + }, + description: {}, + target_date: { + icon: 'fa-calendar-alt', + }, + link: { + icon: 'fa-link', + }, + responsible: { + icon: 'fa-user', + } + }, + onSuccess: function(data) { + location.href = `/order/purchase-order/${data.pk}/`; + }, + title: '{% trans "Create Purchase Order" %}', + }); +} + + function removeOrderRowFromOrderWizard(e) { /* Remove a part selection from an order form. */