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:
@ -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
|
||||
|
@ -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'),
|
||||
),
|
||||
]
|
||||
|
@ -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'),
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
107
InvenTree/order/migrations/0074_auto_20220709_0108.py
Normal file
107
InvenTree/order/migrations/0074_auto_20220709_0108.py
Normal 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,
|
||||
)
|
||||
]
|
@ -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(
|
||||
|
@ -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."""
|
||||
|
||||
|
@ -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: {
|
||||
|
@ -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: {
|
||||
|
@ -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):
|
||||
|
@ -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"""
|
||||
|
||||
|
@ -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"""
|
||||
|
49
InvenTree/order/validators.py
Normal file
49
InvenTree/order/validators.py
Normal 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)
|
Reference in New Issue
Block a user