mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-04 15:15:42 +00:00 
			
		
		
		
	Add PO wildcard default setting (#8532)
* add PO wildcard default setting
* Revert "add PO wildcard default setting"
This reverts commit 6cc577fa73.
* use custom format spec for "wildcard with default" reference pattern
* add wildcard with default to docs
* add test for wildcard with default
			
			
This commit is contained in:
		@@ -32,8 +32,9 @@ When building a reference, the following variables are available for use:
 | 
			
		||||
 | 
			
		||||
| Variable | Description |
 | 
			
		||||
| --- | --- |
 | 
			
		||||
| `{% raw %}{ref}{% endraw %}` | Incrementing portion of the reference (**required*)). Determines which part of the reference field auto-increments |
 | 
			
		||||
| `{% raw %}{ref}{% endraw %}` | Incrementing portion of the reference (**required*). Determines which part of the reference field auto-increments |
 | 
			
		||||
| `{% raw %}{date}{% endraw %}` | The current date / time. This is a [Python datetime object](https://docs.python.org/3/library/datetime.html#datetime.datetime.now) |
 | 
			
		||||
| `{% raw %}{?:default}{% endraw %}` | A wildcard *with default*. Any character(s) will be accepted in this position, but the reference pattern suggests the character(s) specified. |
 | 
			
		||||
 | 
			
		||||
The reference field pattern uses <a href="https://www.w3schools.com/python/ref_string_format.asp">Python string formatting</a> for value substitution.
 | 
			
		||||
 | 
			
		||||
@@ -44,8 +45,9 @@ The reference field pattern uses <a href="https://www.w3schools.com/python/ref_s
 | 
			
		||||
 | 
			
		||||
Some examples below demonstrate how the variable substitution can be implemented:
 | 
			
		||||
 | 
			
		||||
| Pattern | Description | Example Output |
 | 
			
		||||
| Pattern | Description | Example Output(s) |
 | 
			
		||||
| --- | --- | --- |
 | 
			
		||||
| `{% raw %}PO-{ref}{% endraw %}` | Render the *reference* variable without any custom formatting | PO-123 |
 | 
			
		||||
| `{% raw %}PO-{ref:05d}{% endraw %}` | Render the *reference* variable as a 5-digit decimal number | PO-00123 |
 | 
			
		||||
| `{% raw %}PO-{ref:05d}-{?:A}{% endraw %}` | *Require* a wildcard suffix with default suggested suffix `"A"`. | PO-00123-A <br> PO-00123-B |
 | 
			
		||||
| `{% raw %}PO-{ref:05d}-{date:%Y-%m-%d}{% endraw %}` | Render the *date* variable in isoformat | PO-00123-2023-01-17 |
 | 
			
		||||
 
 | 
			
		||||
@@ -113,6 +113,10 @@ def construct_format_regex(fmt_string: str) -> str:
 | 
			
		||||
            # TODO: Introspect required width
 | 
			
		||||
            w = '+'
 | 
			
		||||
 | 
			
		||||
            # replace invalid regex group name '?' with a valid name
 | 
			
		||||
            if name == '?':
 | 
			
		||||
                name = 'wild'
 | 
			
		||||
 | 
			
		||||
            pattern += f'(?P<{name}>{c}{w})'
 | 
			
		||||
 | 
			
		||||
    pattern += '$'
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@
 | 
			
		||||
 | 
			
		||||
import logging
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
from string import Formatter
 | 
			
		||||
 | 
			
		||||
from django.contrib.auth import get_user_model
 | 
			
		||||
from django.contrib.contenttypes.models import ContentType
 | 
			
		||||
@@ -315,8 +316,9 @@ class ReferenceIndexingMixin(models.Model):
 | 
			
		||||
 | 
			
		||||
        - Returns a python dict object which contains the context data for formatting the reference string.
 | 
			
		||||
        - The default implementation provides some default context information
 | 
			
		||||
        - The '?' key is required to accept our wildcard-with-default syntax {?:default}
 | 
			
		||||
        """
 | 
			
		||||
        return {'ref': cls.get_next_reference(), 'date': datetime.now()}
 | 
			
		||||
        return {'ref': cls.get_next_reference(), 'date': datetime.now(), '?': '?'}
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def get_most_recent_item(cls):
 | 
			
		||||
@@ -363,8 +365,18 @@ class ReferenceIndexingMixin(models.Model):
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def generate_reference(cls):
 | 
			
		||||
        """Generate the next 'reference' field based on specified pattern."""
 | 
			
		||||
        fmt = cls.get_reference_pattern()
 | 
			
		||||
 | 
			
		||||
        # Based on https://stackoverflow.com/a/57570269/14488558
 | 
			
		||||
        class ReferenceFormatter(Formatter):
 | 
			
		||||
            def format_field(self, value, format_spec):
 | 
			
		||||
                if isinstance(value, str) and value == '?':
 | 
			
		||||
                    value = format_spec
 | 
			
		||||
                    format_spec = ''
 | 
			
		||||
                return super().format_field(value, format_spec)
 | 
			
		||||
 | 
			
		||||
        ref_ptn = cls.get_reference_pattern()
 | 
			
		||||
        ctx = cls.get_reference_context()
 | 
			
		||||
        fmt = ReferenceFormatter()
 | 
			
		||||
 | 
			
		||||
        reference = None
 | 
			
		||||
 | 
			
		||||
@@ -372,7 +384,7 @@ class ReferenceIndexingMixin(models.Model):
 | 
			
		||||
 | 
			
		||||
        while reference is None:
 | 
			
		||||
            try:
 | 
			
		||||
                ref = fmt.format(**ctx)
 | 
			
		||||
                ref = fmt.format(ref_ptn, **ctx)
 | 
			
		||||
 | 
			
		||||
                if ref in attempts:
 | 
			
		||||
                    # We are stuck in a loop!
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@
 | 
			
		||||
 | 
			
		||||
import base64
 | 
			
		||||
import io
 | 
			
		||||
import json
 | 
			
		||||
from datetime import datetime, timedelta
 | 
			
		||||
 | 
			
		||||
from django.core.exceptions import ValidationError
 | 
			
		||||
@@ -15,6 +16,7 @@ from rest_framework import status
 | 
			
		||||
 | 
			
		||||
from common.currency import currency_codes
 | 
			
		||||
from common.models import InvenTreeSetting
 | 
			
		||||
from common.settings import set_global_setting
 | 
			
		||||
from company.models import Company, SupplierPart, SupplierPriceBreak
 | 
			
		||||
from InvenTree.unit_test import InvenTreeAPITestCase
 | 
			
		||||
from order import models
 | 
			
		||||
@@ -256,6 +258,49 @@ class PurchaseOrderTest(OrderTest):
 | 
			
		||||
        self.assertEqual(order.reference, 'PO-92233720368547758089999999999999999')
 | 
			
		||||
        self.assertEqual(order.reference_int, 0x7FFFFFFF)
 | 
			
		||||
 | 
			
		||||
    def test_po_reference_wildcard_default(self):
 | 
			
		||||
        """Test that a reference with a wildcard default."""
 | 
			
		||||
        # get permissions
 | 
			
		||||
        self.assignRole('purchase_order.add')
 | 
			
		||||
 | 
			
		||||
        # set PO reference setting
 | 
			
		||||
        set_global_setting('PURCHASEORDER_REFERENCE_PATTERN', '{?:PO}-{ref:04d}')
 | 
			
		||||
 | 
			
		||||
        url = reverse('api-po-list')
 | 
			
		||||
 | 
			
		||||
        # first, check that the default character is suggested by OPTIONS
 | 
			
		||||
        options = json.loads(self.options(url).content)
 | 
			
		||||
        suggested_reference = options['actions']['POST']['reference']['default']
 | 
			
		||||
        self.assertTrue(suggested_reference.startswith('PO-'))
 | 
			
		||||
 | 
			
		||||
        # next, check that certain variations of a provided reference are accepted
 | 
			
		||||
        test_accepted_references = ['PO-9991', 'P-9992', 'T-9993', 'ABC-9994']
 | 
			
		||||
        for ref in test_accepted_references:
 | 
			
		||||
            response = self.post(
 | 
			
		||||
                url,
 | 
			
		||||
                {
 | 
			
		||||
                    'supplier': 1,
 | 
			
		||||
                    'reference': ref,
 | 
			
		||||
                    'description': 'PO created via the API',
 | 
			
		||||
                },
 | 
			
		||||
                expected_code=201,
 | 
			
		||||
            )
 | 
			
		||||
            order = models.PurchaseOrder.objects.get(pk=response.data['pk'])
 | 
			
		||||
            self.assertEqual(order.reference, ref)
 | 
			
		||||
 | 
			
		||||
        # finally, check that certain provided referencees are rejected (because the wildcard character is required!)
 | 
			
		||||
        test_rejected_references = ['9995', '-9996']
 | 
			
		||||
        for ref in test_rejected_references:
 | 
			
		||||
            response = self.post(
 | 
			
		||||
                url,
 | 
			
		||||
                {
 | 
			
		||||
                    'supplier': 1,
 | 
			
		||||
                    'reference': ref,
 | 
			
		||||
                    'description': 'PO created via the API',
 | 
			
		||||
                },
 | 
			
		||||
                expected_code=400,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    def test_po_attachments(self):
 | 
			
		||||
        """Test the list endpoint for the PurchaseOrderAttachment model."""
 | 
			
		||||
        url = reverse('api-attachment-list')
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user