2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-19 13:35:40 +00:00

Reference fields (#3267)

* Adds a configurable 'reference pattern' to the IndexingReferenceMixin class

* Expand tests for reference_pattern validator:

- Prevent inclusion of illegal characters
- Prevent multiple groups of hash (#) characters
- Add unit tests

* Validator now checks for valid strftime formatter

* Adds build order reference pattern

* Adds function for creating a valid regex from the supplied pattern

- More unit tests
- Use it to validate BuildOrder reference field

* Refactoring the whole thing again - try using python string.format

* remove datetime-matcher from requirements.txt

* Add some more formatting helper functions

- Construct a regular expression from a format string
- Extract named values from a string, based on a format string

* Fix validator for build order reference field

* Adding unit tests for the new format string functionality

* Adds validation for reference fields

* Require the 'ref' format key as part of a valid reference pattern

* Extend format extraction to allow specification of integer groups

* Remove unused import

* Fix requirements

* Add method for generating the 'next' reference field for a model

* Fix function for generating next BuildOrder reference value

- A function is required as class methods cannot be used
- Simply wraps the existing class method

* Remove BUILDORDER_REFERENCE_REGEX setting

* Add unit test for build order reference field validation

* Adds unit testing for extracting integer values from a reference field

* Fix bugs from previous commit

* Add unit test for generation of default build order reference

* Add data migration for BuildOrder model

- Update reference field with old prefix
- Construct new pattern based on old prefix

* Adds unit test for data migration

- Check that the BuildOrder reference field is updated as expected

* Remove 'BUILDORDER_REFERENCE_PREFIX' setting

* Adds new setting for SalesOrder reference pattern

* Update method by which next reference value is generated

* Improved error handling in api_tester code

* Improve automated generation of order reference fields

- Handle potential errors
- Return previous reference if something goes wrong

* SalesOrder reference has now been updated also

- New reference pattern setting
- Updated default and validator for reference field
- Updated serializer and API
- Added unit tests

* Migrate the "PurchaseOrder" reference field to the new system

* Data migration for SalesOrder and PurchaseOrder reference fields

* Remove PURCHASEORDER_REFERENCE_PREFIX

* Remove references to SALESORDER_REFERENCE_PREFIX

* Re-add maximum value validation

* Bug fixes

* Improve algorithm for generating new reference

- Handle case where most recent reference does not conform to the reference pattern

* Fixes for 'order' unit tests

* Unit test fixes for order app

* More unit test fixes

* More unit test fixing

* Revert behaviour for "extract_int" clipping function

* Unit test value fix

* Prevent build order notification if we are importing records
This commit is contained in:
Oliver
2022-07-11 00:01:46 +10:00
committed by GitHub
parent 6133c745d7
commit 648faf4ed2
45 changed files with 1166 additions and 294 deletions

View File

@ -4,7 +4,7 @@
- model: order.purchaseorder
pk: 1
fields:
reference: '0001'
reference: 'PO-0001'
description: "Ordering some screws"
supplier: 1
status: 10 # Pending
@ -13,7 +13,7 @@
- model: order.purchaseorder
pk: 2
fields:
reference: '0002'
reference: 'PO-0002'
description: "Ordering some more screws"
supplier: 3
status: 10 # Pending
@ -21,7 +21,7 @@
- model: order.purchaseorder
pk: 3
fields:
reference: '0003'
reference: 'PO-0003'
description: 'Another PO'
supplier: 3
status: 20 # Placed
@ -29,7 +29,7 @@
- model: order.purchaseorder
pk: 4
fields:
reference: '0004'
reference: 'PO-0004'
description: 'Another PO'
supplier: 3
status: 20 # Placed
@ -37,7 +37,7 @@
- model: order.purchaseorder
pk: 5
fields:
reference: '0005'
reference: 'PO-0005'
description: 'Another PO'
supplier: 3
status: 30 # Complete
@ -45,7 +45,7 @@
- model: order.purchaseorder
pk: 6
fields:
reference: '0006'
reference: 'PO-0006'
description: 'Another PO'
supplier: 3
status: 40 # Cancelled
@ -54,7 +54,7 @@
- model: order.purchaseorder
pk: 7
fields:
reference: '0007'
reference: 'PO-0007'
description: 'Another PO'
supplier: 2
status: 10 # Pending

View File

@ -1,7 +1,6 @@
# 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):
@ -14,11 +13,11 @@ class Migration(migrations.Migration):
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'),
field=models.CharField(default="PO", 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'),
field=models.CharField(default="SO", help_text='Order reference', max_length=64, unique=True, verbose_name='Reference'),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 3.2.14 on 2022-07-07 11:55
from django.db import migrations, models
import order.validators
class Migration(migrations.Migration):
dependencies = [
('order', '0071_auto_20220628_0133'),
]
operations = [
migrations.AlterField(
model_name='salesorder',
name='reference',
field=models.CharField(default=order.validators.generate_next_sales_order_reference, help_text='Order reference', max_length=64, unique=True, validators=[order.validators.validate_sales_order_reference], verbose_name='Reference'),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 3.2.14 on 2022-07-09 01:01
from django.db import migrations, models
import order.validators
class Migration(migrations.Migration):
dependencies = [
('order', '0072_alter_salesorder_reference'),
]
operations = [
migrations.AlterField(
model_name='purchaseorder',
name='reference',
field=models.CharField(default=order.validators.generate_next_purchase_order_reference, help_text='Order reference', max_length=64, unique=True, validators=[order.validators.validate_purchase_order_reference], verbose_name='Reference'),
),
]

View File

@ -0,0 +1,107 @@
# Generated by Django 3.2.14 on 2022-07-09 01:08
from django.db import migrations
def update_order_references(order_model, prefix):
"""Update all references of the given model, with the specified prefix"""
n = 0
for order in order_model.objects.all():
if not order.reference.startswith(prefix):
order.reference = prefix + order.reference
order.save()
n += 1
return n
def update_salesorder_reference(apps, schema_editor):
"""Migrate the reference pattern for the SalesOrder model"""
# Extract the existing "prefix" value
InvenTreeSetting = apps.get_model('common', 'inventreesetting')
try:
prefix = InvenTreeSetting.objects.get(key='SALESORDER_REFERENCE_PREFIX').value
except Exception:
prefix = 'SO-'
# Construct a reference pattern
pattern = prefix + '{ref:04d}'
# Create or update the BuildOrder.reference pattern
try:
setting = InvenTreeSetting.objects.get(key='SALESORDER_REFERENCE_PATTERN')
setting.value = pattern
setting.save()
except InvenTreeSetting.DoesNotExist:
setting = InvenTreeSetting.objects.create(
key='SALESORDER_REFERENCE_PATTERN',
value=pattern,
)
# Update any existing sales order references
SalesOrder = apps.get_model('order', 'salesorder')
n = update_order_references(SalesOrder, prefix)
if n > 0:
print(f"Updated reference field for {n} SalesOrder objects")
def update_purchaseorder_reference(apps, schema_editor):
"""Migrate the reference pattern for the PurchaseOrder model"""
# Extract the existing "prefix" value
InvenTreeSetting = apps.get_model('common', 'inventreesetting')
try:
prefix = InvenTreeSetting.objects.get(key='PURCHASEORDER_REFERENCE_PREFIX').value
except Exception:
prefix = 'PO-'
# Construct a reference pattern
pattern = prefix + '{ref:04d}'
# Create or update the BuildOrder.reference pattern
try:
setting = InvenTreeSetting.objects.get(key='PURCHASEORDER_REFERENCE_PATTERN')
setting.value = pattern
setting.save()
except InvenTreeSetting.DoesNotExist:
setting = InvenTreeSetting.objects.create(
key='PURCHASEORDER_REFERENCE_PATTERN',
value=pattern,
)
# Update any existing sales order references
PurchaseOrder = apps.get_model('order', 'purchaseorder')
n = update_order_references(PurchaseOrder, prefix)
if n > 0:
print(f"Updated reference field for {n} PurchaseOrder objects")
def nop(apps, schema_editor):
"""Empty function for reverse migration"""
pass
class Migration(migrations.Migration):
dependencies = [
('order', '0073_alter_purchaseorder_reference'),
]
operations = [
migrations.RunPython(
update_salesorder_reference,
reverse_code=nop,
),
migrations.RunPython(
update_purchaseorder_reference,
reverse_code=nop,
)
]

View File

@ -24,14 +24,14 @@ from mptt.models import TreeForeignKey
import InvenTree.helpers
import InvenTree.ready
import order.validators
from common.notifications import InvenTreeNotificationBodies
from common.settings import currency_code_default
from company.models import Company, SupplierPart
from InvenTree.exceptions import log_error
from InvenTree.fields import (InvenTreeModelMoneyField, InvenTreeNotesField,
RoundingDecimalField)
from InvenTree.helpers import (decimal2string, getSetting, increment,
notify_responsible)
from InvenTree.helpers import decimal2string, getSetting, notify_responsible
from InvenTree.models import InvenTreeAttachment, ReferenceIndexingMixin
from InvenTree.status_codes import (PurchaseOrderStatus, SalesOrderStatus,
StockHistoryCode, StockStatus)
@ -44,58 +44,6 @@ from users import models as UserModels
logger = logging.getLogger('inventree')
def get_next_po_number():
"""Returns the next available PurchaseOrder reference number."""
if PurchaseOrder.objects.count() == 0:
return '0001'
order = PurchaseOrder.objects.exclude(reference=None).last()
attempts = {order.reference}
reference = order.reference
while 1:
reference = increment(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 '0001'
order = SalesOrder.objects.exclude(reference=None).last()
attempts = {order.reference}
reference = order.reference
while 1:
reference = increment(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(MetadataMixin, ReferenceIndexingMixin):
"""Abstract model for an order.
@ -119,7 +67,7 @@ class Order(MetadataMixin, ReferenceIndexingMixin):
Ensures that the reference field is rebuilt whenever the instance is saved.
"""
self.rebuild_reference_field()
self.reference_int = self.rebuild_reference_field(self.reference)
if not self.creation_date:
self.creation_date = datetime.now().date()
@ -230,8 +178,21 @@ class PurchaseOrder(Order):
"""Return the API URL associated with the PurchaseOrder model"""
return reverse('api-po-list')
@classmethod
def api_defaults(cls, request):
"""Return default values for thsi model when issuing an API OPTIONS request"""
defaults = {
'reference': order.validators.generate_next_purchase_order_reference(),
}
return defaults
OVERDUE_FILTER = Q(status__in=PurchaseOrderStatus.OPEN) & ~Q(target_date=None) & Q(target_date__lte=datetime.now().date())
# Global setting for specifying reference pattern
REFERENCE_PATTERN_SETTING = 'PURCHASEORDER_REFERENCE_PATTERN'
@staticmethod
def filterByDate(queryset, min_date, max_date):
"""Filter by 'minimum and maximum date range'.
@ -269,9 +230,8 @@ class PurchaseOrder(Order):
def __str__(self):
"""Render a string representation of this PurchaseOrder"""
prefix = getSetting('PURCHASEORDER_REFERENCE_PREFIX')
return f"{prefix}{self.reference} - {self.supplier.name if self.supplier else _('deleted')}"
return f"{self.reference} - {self.supplier.name if self.supplier else _('deleted')}"
reference = models.CharField(
unique=True,
@ -279,7 +239,10 @@ class PurchaseOrder(Order):
blank=False,
verbose_name=_('Reference'),
help_text=_('Order reference'),
default=get_next_po_number,
default=order.validators.generate_next_purchase_order_reference,
validators=[
order.validators.validate_purchase_order_reference,
]
)
status = models.PositiveIntegerField(default=PurchaseOrderStatus.PENDING, choices=PurchaseOrderStatus.items(),
@ -595,8 +558,20 @@ class SalesOrder(Order):
"""Return the API URL associated with the SalesOrder model"""
return reverse('api-so-list')
@classmethod
def api_defaults(cls, request):
"""Return default values for this model when issuing an API OPTIONS request"""
defaults = {
'reference': order.validators.generate_next_sales_order_reference(),
}
return defaults
OVERDUE_FILTER = Q(status__in=SalesOrderStatus.OPEN) & ~Q(target_date=None) & Q(target_date__lte=datetime.now().date())
# Global setting for specifying reference pattern
REFERENCE_PATTERN_SETTING = 'SALESORDER_REFERENCE_PATTERN'
@staticmethod
def filterByDate(queryset, min_date, max_date):
"""Filter by "minimum and maximum date range".
@ -634,9 +609,8 @@ class SalesOrder(Order):
def __str__(self):
"""Render a string representation of this SalesOrder"""
prefix = getSetting('SALESORDER_REFERENCE_PREFIX')
return f"{prefix}{self.reference} - {self.customer.name if self.customer else _('deleted')}"
return f"{self.reference} - {self.customer.name if self.customer else _('deleted')}"
def get_absolute_url(self):
"""Return the web URL for the detail view of this order"""
@ -648,7 +622,10 @@ class SalesOrder(Order):
blank=False,
verbose_name=_('Reference'),
help_text=_('Order reference'),
default=get_next_so_number,
default=order.validators.generate_next_sales_order_reference,
validators=[
order.validators.validate_sales_order_reference,
]
)
customer = models.ForeignKey(

View File

@ -23,8 +23,7 @@ from InvenTree.helpers import extract_serial_numbers, normalize
from InvenTree.serializers import (InvenTreeAttachmentSerializer,
InvenTreeDecimalField,
InvenTreeModelSerializer,
InvenTreeMoneySerializer,
ReferenceIndexingSerializerMixin)
InvenTreeMoneySerializer)
from InvenTree.status_codes import (PurchaseOrderStatus, SalesOrderStatus,
StockStatus)
from part.serializers import PartBriefSerializer
@ -86,7 +85,7 @@ class AbstractExtraLineMeta:
]
class PurchaseOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
class PurchaseOrderSerializer(AbstractOrderSerializer, InvenTreeModelSerializer):
"""Serializer for a PurchaseOrder object."""
def __init__(self, *args, **kwargs):
@ -130,6 +129,14 @@ class PurchaseOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializ
reference = serializers.CharField(required=True)
def validate_reference(self, reference):
"""Custom validation for the reference field"""
# Ensure that the reference matches the required pattern
order.models.PurchaseOrder.validate_reference_field(reference)
return reference
responsible_detail = OwnerSerializer(source='responsible', read_only=True, many=False)
class Meta:
@ -639,7 +646,7 @@ class PurchaseOrderAttachmentSerializer(InvenTreeAttachmentSerializer):
]
class SalesOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
class SalesOrderSerializer(AbstractOrderSerializer, InvenTreeModelSerializer):
"""Serializers for the SalesOrder object."""
def __init__(self, *args, **kwargs):
@ -683,6 +690,14 @@ class SalesOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializerM
reference = serializers.CharField(required=True)
def validate_reference(self, reference):
"""Custom validation for the reference field"""
# Ensure that the reference matches the required pattern
order.models.SalesOrder.validate_reference_field(reference)
return reference
class Meta:
"""Metaclass options."""

View File

@ -82,7 +82,7 @@ src="{% static 'img/blank_image.png' %}"
<tr>
<td><span class='fas fa-hashtag'></span></td>
<td>{% trans "Order Reference" %}</td>
<td>{% settings_value 'PURCHASEORDER_REFERENCE_PREFIX' %}{{ order.reference }}{% include "clip.html"%}</td>
<td>{{ order.reference }}{% include "clip.html"%}</td>
</tr>
<tr>
<td><span class='fas fa-info-circle'></span></td>
@ -222,7 +222,7 @@ $("#edit-order").click(function() {
constructForm('{% url "api-po-detail" order.pk %}', {
fields: {
reference: {
prefix: global_settings.PURCHASEORDER_REFERENCE_PREFIX,
icon: 'fa-hashtag',
},
{% if order.lines.count == 0 and order.status == PurchaseOrderStatus.PENDING %}
supplier: {

View File

@ -78,7 +78,7 @@ src="{% static 'img/blank_image.png' %}"
<tr>
<td><span class='fas fa-hashtag'></span></td>
<td>{% trans "Order Reference" %}</td>
<td>{% settings_value 'SALESORDER_REFERENCE_PREFIX' %}{{ order.reference }}{% include "clip.html"%}</td>
<td>{{ order.reference }}{% include "clip.html"%}</td>
</tr>
<tr>
<td><span class='fas fa-info-circle'></span></td>
@ -209,7 +209,7 @@ $("#edit-order").click(function() {
constructForm('{% url "api-so-detail" order.pk %}', {
fields: {
reference: {
prefix: global_settings.SALESORDER_REFERENCE_PREFIX,
icon: 'fa-hashtag',
},
{% if order.lines.count == 0 and order.status == SalesOrderStatus.PENDING %}
customer: {

View File

@ -94,23 +94,28 @@ class PurchaseOrderTest(OrderTest):
self.assertEqual(data['description'], 'Ordering some screws')
def test_po_reference(self):
"""Test that a reference with a too big / small reference is not possible."""
"""Test that a reference with a too big / small reference is handled correctly."""
# get permissions
self.assignRole('purchase_order.add')
url = reverse('api-po-list')
huge_number = 9223372036854775808
huge_number = "PO-92233720368547758089999999999999999"
self.post(
response = self.post(
url,
{
'supplier': 1,
'reference': huge_number,
'description': 'PO not created via the API',
'description': 'PO created via the API',
},
expected_code=201,
)
order = models.PurchaseOrder.objects.get(pk=response.data['pk'])
self.assertEqual(order.reference, 'PO-92233720368547758089999999999999999')
self.assertEqual(order.reference_int, 0x7fffffff)
def test_po_attachments(self):
"""Test the list endpoint for the PurchaseOrderAttachment model"""
url = reverse('api-po-attachment-list')
@ -149,7 +154,7 @@ class PurchaseOrderTest(OrderTest):
url,
{
'supplier': 1,
'reference': '123456789-xyz',
'reference': 'PO-123456789',
'description': 'PO created via the API',
},
expected_code=201
@ -177,19 +182,19 @@ class PurchaseOrderTest(OrderTest):
# Get detail info!
response = self.get(url)
self.assertEqual(response.data['pk'], pk)
self.assertEqual(response.data['reference'], '123456789-xyz')
self.assertEqual(response.data['reference'], 'PO-123456789')
# Try to alter (edit) the PurchaseOrder
response = self.patch(
url,
{
'reference': '12345-abc',
'reference': 'PO-12345',
},
expected_code=200
)
# Reference should have changed
self.assertEqual(response.data['reference'], '12345-abc')
self.assertEqual(response.data['reference'], 'PO-12345')
# Now, let's try to delete it!
# Initially, we do *not* have the required permission!
@ -213,7 +218,7 @@ class PurchaseOrderTest(OrderTest):
self.post(
reverse('api-po-list'),
{
'reference': '12345678',
'reference': 'PO-12345678',
'supplier': 1,
'description': 'A test purchase order',
},
@ -807,7 +812,7 @@ class SalesOrderTest(OrderTest):
url,
{
'customer': 4,
'reference': '12345',
'reference': 'SO-12345',
'description': 'Sales order',
},
expected_code=201
@ -824,7 +829,7 @@ class SalesOrderTest(OrderTest):
url,
{
'customer': 4,
'reference': '12345',
'reference': 'SO-12345',
'description': 'Another sales order',
},
expected_code=400
@ -834,19 +839,28 @@ class SalesOrderTest(OrderTest):
# Extract detail info for the SalesOrder
response = self.get(url)
self.assertEqual(response.data['reference'], '12345')
self.assertEqual(response.data['reference'], 'SO-12345')
# Try to alter (edit) the SalesOrder
# Initially try with an invalid reference field value
response = self.patch(
url,
{
'reference': '12345-a',
'reference': 'SO-12345-a',
},
expected_code=400
)
response = self.patch(
url,
{
'reference': 'SO-12346',
},
expected_code=200
)
# Reference should have changed
self.assertEqual(response.data['reference'], '12345-a')
self.assertEqual(response.data['reference'], 'SO-12346')
# Now, let's try to delete this SalesOrder
# Initially, we do not have the required permission
@ -866,14 +880,29 @@ class SalesOrderTest(OrderTest):
"""Test that we can create a new SalesOrder via the API."""
self.assignRole('sales_order.add')
self.post(
reverse('api-so-list'),
url = reverse('api-so-list')
# Will fail due to invalid reference field
response = self.post(
url,
{
'reference': '1234566778',
'customer': 4,
'description': 'A test sales order',
},
expected_code=201
expected_code=400,
)
self.assertIn('Reference must match required pattern', str(response.data['reference']))
self.post(
url,
{
'reference': 'SO-12345',
'customer': 4,
'description': 'A better test sales order',
},
expected_code=201,
)
def test_so_cancel(self):

View File

@ -40,19 +40,27 @@ class SalesOrderTest(TestCase):
# Create a SalesOrder to ship against
self.order = SalesOrder.objects.create(
customer=self.customer,
reference='1234',
reference='SO-1234',
customer_reference='ABC 55555'
)
# Create a Shipment against this SalesOrder
self.shipment = SalesOrderShipment.objects.create(
order=self.order,
reference='001',
reference='SO-001',
)
# Create a line item
self.line = SalesOrderLineItem.objects.create(quantity=50, order=self.order, part=self.part)
def test_so_reference(self):
"""Unit tests for sales order generation"""
# Test that a good reference is created when we have no existing orders
SalesOrder.objects.all().delete()
self.assertEqual(SalesOrder.generate_reference(), 'SO-0001')
def test_rebuild_reference(self):
"""Test that the 'reference_int' field gets rebuilt when the model is saved"""

View File

@ -35,15 +35,17 @@ class OrderTest(TestCase):
def test_basics(self):
"""Basic tests e.g. repr functions etc."""
order = PurchaseOrder.objects.get(pk=1)
self.assertEqual(order.get_absolute_url(), '/order/purchase-order/1/')
for pk in range(1, 8):
self.assertEqual(str(order), 'PO0001 - ACME')
order = PurchaseOrder.objects.get(pk=pk)
self.assertEqual(order.get_absolute_url(), f'/order/purchase-order/{pk}/')
self.assertEqual(order.reference, f'PO-{pk:04d}')
line = PurchaseOrderLineItem.objects.get(pk=1)
self.assertEqual(str(line), "100 x ACME0001 from ACME (for PO0001 - ACME)")
self.assertEqual(str(line), "100 x ACME0001 from ACME (for PO-0001 - ACME)")
def test_rebuild_reference(self):
"""Test that the reference_int field is correctly updated when the model is saved"""

View File

@ -0,0 +1,49 @@
"""Validation methods for the order app"""
def generate_next_sales_order_reference():
"""Generate the next available SalesOrder reference"""
from order.models import SalesOrder
return SalesOrder.generate_reference()
def generate_next_purchase_order_reference():
"""Generate the next available PurchasesOrder reference"""
from order.models import PurchaseOrder
return PurchaseOrder.generate_reference()
def validate_sales_order_reference_pattern(pattern):
"""Validate the SalesOrder reference 'pattern' setting"""
from order.models import SalesOrder
SalesOrder.validate_reference_pattern(pattern)
def validate_purchase_order_reference_pattern(pattern):
"""Validate the PurchaseOrder reference 'pattern' setting"""
from order.models import PurchaseOrder
PurchaseOrder.validate_reference_pattern(pattern)
def validate_sales_order_reference(value):
"""Validate that the SalesOrder reference field matches the required pattern"""
from order.models import SalesOrder
SalesOrder.validate_reference_field(value)
def validate_purchase_order_reference(value):
"""Validate that the PurchaseOrder reference field matches the required pattern"""
from order.models import PurchaseOrder
PurchaseOrder.validate_reference_field(value)