diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index b9a4d73740..8c5d59181d 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -120,6 +120,59 @@ def normalize(d): return d.quantize(Decimal(1)) if d == d.to_integral() else d.normalize() +def increment(n): + """ + Attempt to increment an integer (or a string that looks like an integer!) + + e.g. + + 001 -> 002 + 2 -> 3 + AB01 -> AB02 + QQQ -> QQQ + + """ + + value = str(n).strip() + + # Ignore empty strings + if not value: + return value + + pattern = r"(.*?)(\d+)?$" + + result = re.search(pattern, value) + + # No match! + if result is None: + return value + + groups = result.groups() + + # If we cannot match the regex, then simply return the provided value + if not len(groups) == 2: + return value + + prefix, number = groups + + # No number extracted? Simply return the prefix (without incrementing!) + if not number: + return prefix + + # Record the width of the number + width = len(number) + + try: + number = int(number) + 1 + number = str(number) + except ValueError: + pass + + number = number.zfill(width) + + return prefix + number + + def decimal2string(d): """ Format a Decimal number as a string, diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index 203748de3e..877adab919 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -108,6 +108,29 @@ class TestQuoteWrap(TestCase): self.assertEqual(helpers.WrapWithQuotes('hello"'), '"hello"') +class TestIncrement(TestCase): + + def tests(self): + """ Test 'intelligent' incrementing function """ + + tests = [ + ("", ""), + (1, "2"), + ("001", "002"), + ("1001", "1002"), + ("ABC123", "ABC124"), + ("XYZ0", "XYZ1"), + ("123Q", "123Q"), + ("QQQ", "QQQ"), + ] + + for test in tests: + a, b = test + + result = helpers.increment(a) + self.assertEqual(result, b) + + class TestMakeBarcode(TestCase): """ Tests for barcode string creation """ diff --git a/InvenTree/company/templates/company/company_base.html b/InvenTree/company/templates/company/company_base.html index da73a7b7b3..9de0e9e764 100644 --- a/InvenTree/company/templates/company/company_base.html +++ b/InvenTree/company/templates/company/company_base.html @@ -85,10 +85,10 @@ InvenTree | {% trans "Company" %} - {{ company.name }} $('#company-edit').click(function() { launchModalForm( - "{% url 'company-edit' company.id %}", - { - reload: true - }); + "{% url 'company-edit' company.id %}", + { + reload: true + }); }); $("#company-order-2").click(function() { @@ -104,10 +104,10 @@ InvenTree | {% trans "Company" %} - {{ company.name }} $('#company-delete').click(function() { launchModalForm( - "{% url 'company-delete' company.id %}", - { - redirect: "{% url 'company-index' %}" - }); + "{% url 'company-delete' company.id %}", + { + redirect: "{% url 'company-index' %}" + }); }); enableDragAndDrop( @@ -123,11 +123,11 @@ InvenTree | {% trans "Company" %} - {{ company.name }} $("#company-thumb").click(function() { launchModalForm( - "{% url 'company-image' company.id %}", - { - reload: true - } - ); + "{% url 'company-image' company.id %}", + { + reload: true + } + ); }); {% endblock %} \ No newline at end of file diff --git a/InvenTree/company/templates/company/sales_orders.html b/InvenTree/company/templates/company/sales_orders.html index facfbce189..0b64bed2f5 100644 --- a/InvenTree/company/templates/company/sales_orders.html +++ b/InvenTree/company/templates/company/sales_orders.html @@ -33,9 +33,16 @@ } }); - $("#new-sales-order").click(function() { - // TODO - Create a new sales order + launchModalForm( + "{% url 'so-create' %}", + { + data: { + customer: {{ company.id }}, + }, + follow: true, + }, + ); }); {% endblock %} \ No newline at end of file diff --git a/InvenTree/order/fixtures/order.yaml b/InvenTree/order/fixtures/order.yaml index 0ba4bdbeb5..ed4eae84eb 100644 --- a/InvenTree/order/fixtures/order.yaml +++ b/InvenTree/order/fixtures/order.yaml @@ -4,7 +4,7 @@ - model: order.purchaseorder pk: 1 fields: - reference: 0001 + reference: '0001' description: "Ordering some screws" supplier: 1 @@ -12,7 +12,7 @@ - model: order.purchaseorder pk: 2 fields: - reference: 0002 + reference: '0002' description: "Ordering some more screws" supplier: 3 diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 56da6c783d..2ebc6a6793 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -24,7 +24,7 @@ from stock import models as stock_models from company.models import Company, SupplierPart from InvenTree.fields import RoundingDecimalField -from InvenTree.helpers import decimal2string +from InvenTree.helpers import decimal2string, increment from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockStatus from InvenTree.models import InvenTreeAttachment @@ -49,6 +49,43 @@ class Order(models.Model): ORDER_PREFIX = "" + @classmethod + def getNextOrderNumber(cls): + """ + Try to predict the next order-number + """ + + if cls.objects.count() == 0: + return None + + # We will assume that the latest pk has the highest PO number + order = cls.objects.last() + ref = order.reference + + if not ref: + return None + + tries = set() + + tries.add(ref) + + while 1: + new_ref = increment(ref) + + if new_ref in tries: + # We are in a looping situation - simply return the original one + return ref + + # Check that the new ref does not exist in the database + if cls.objects.filter(reference=new_ref).exists(): + tries.add(new_ref) + new_ref = increment(new_ref) + + else: + break + + return new_ref + def __str__(self): el = [] diff --git a/InvenTree/order/tests.py b/InvenTree/order/tests.py index ca24b9586d..7b118dc60e 100644 --- a/InvenTree/order/tests.py +++ b/InvenTree/order/tests.py @@ -31,11 +31,17 @@ class OrderTest(TestCase): self.assertEqual(order.get_absolute_url(), '/order/purchase-order/1/') - self.assertEqual(str(order), 'PO 1 - ACME') + self.assertEqual(str(order), 'PO 0001 - ACME') line = PurchaseOrderLineItem.objects.get(pk=1) - self.assertEqual(str(line), "100 x ACME0001 from ACME (for PO 1 - ACME)") + self.assertEqual(str(line), "100 x ACME0001 from ACME (for PO 0001 - ACME)") + + def test_increment(self): + + next_ref = PurchaseOrder.getNextOrderNumber() + + self.assertEqual(next_ref, '0003') def test_on_order(self): """ There should be 3 separate items on order for the M2x4 LPHS part """ diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 41a5b231a7..0585c34cd7 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -29,7 +29,7 @@ from . import forms as order_forms from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView from InvenTree.helpers import DownloadFile, str2bool -from InvenTree.status_codes import PurchaseOrderStatus, StockStatus +from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockStatus logger = logging.getLogger(__name__) @@ -290,6 +290,7 @@ class PurchaseOrderCreate(AjaxCreateView): 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) @@ -320,7 +321,8 @@ class SalesOrderCreate(AjaxCreateView): def get_initial(self): initials = super().get_initial().copy() - initials['status'] = PurchaseOrderStatus.PENDING + initials['reference'] = SalesOrder.getNextOrderNumber() + initials['status'] = SalesOrderStatus.PENDING customer_id = self.request.GET.get('customer', None)