We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[FR]Unit test for Schema allowance
**Is your feature request the result of a bug?**
#3414 #3411

**Problem**
Custom schema settings are not e2e or unit-tested in the API.

**Suggested solution**
Add tests

**Describe alternatives you've considered**
N/A

**Examples of other systems**
N/A

**Do you want to develop this?**
Yes


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `InvenTree/InvenTree/fields.py`
Content:
```
1 """Custom fields used in InvenTree."""
2 
3 import sys
4 from decimal import Decimal
5 
6 from django import forms
7 from django.core import validators
8 from django.db import models as models
9 from django.utils.translation import gettext_lazy as _
10 
11 from djmoney.forms.fields import MoneyField
12 from djmoney.models.fields import MoneyField as ModelMoneyField
13 from djmoney.models.validators import MinMoneyValidator
14 from rest_framework.fields import URLField as RestURLField
15 
16 import InvenTree.helpers
17 
18 from .validators import allowable_url_schemes
19 
20 
21 class InvenTreeRestURLField(RestURLField):
22     """Custom field for DRF with custom scheme vaildators."""
23     def __init__(self, **kwargs):
24         """Update schemes."""
25 
26         # Enforce 'max length' parameter in form validation
27         if 'max_length' not in kwargs:
28             kwargs['max_length'] = 200
29 
30         super().__init__(**kwargs)
31         self.validators[-1].schemes = allowable_url_schemes()
32 
33 
34 class InvenTreeURLField(models.URLField):
35     """Custom URL field which has custom scheme validators."""
36 
37     default_validators = [validators.URLValidator(schemes=allowable_url_schemes())]
38 
39     def __init__(self, **kwargs):
40         """Initialization method for InvenTreeURLField"""
41 
42         # Max length for InvenTreeURLField defaults to 200
43         if 'max_length' not in kwargs:
44             kwargs['max_length'] = 200
45 
46         super().__init__(**kwargs)
47 
48 
49 def money_kwargs():
50     """Returns the database settings for MoneyFields."""
51     from common.settings import currency_code_default, currency_code_mappings
52 
53     kwargs = {}
54     kwargs['currency_choices'] = currency_code_mappings()
55     kwargs['default_currency'] = currency_code_default()
56     return kwargs
57 
58 
59 class InvenTreeModelMoneyField(ModelMoneyField):
60     """Custom MoneyField for clean migrations while using dynamic currency settings."""
61 
62     def __init__(self, **kwargs):
63         """Overwrite default values and validators."""
64         # detect if creating migration
65         if 'migrate' in sys.argv or 'makemigrations' in sys.argv:
66             # remove currency information for a clean migration
67             kwargs['default_currency'] = ''
68             kwargs['currency_choices'] = []
69         else:
70             # set defaults
71             kwargs.update(money_kwargs())
72 
73         # Set a minimum value validator
74         validators = kwargs.get('validators', [])
75 
76         allow_negative = kwargs.pop('allow_negative', False)
77 
78         # If no validators are provided, add some "standard" ones
79         if len(validators) == 0:
80 
81             if not allow_negative:
82                 validators.append(
83                     MinMoneyValidator(0),
84                 )
85 
86         kwargs['validators'] = validators
87 
88         super().__init__(**kwargs)
89 
90     def formfield(self, **kwargs):
91         """Override form class to use own function."""
92         kwargs['form_class'] = InvenTreeMoneyField
93         return super().formfield(**kwargs)
94 
95 
96 class InvenTreeMoneyField(MoneyField):
97     """Custom MoneyField for clean migrations while using dynamic currency settings."""
98     def __init__(self, *args, **kwargs):
99         """Override initial values with the real info from database."""
100         kwargs.update(money_kwargs())
101         super().__init__(*args, **kwargs)
102 
103 
104 class DatePickerFormField(forms.DateField):
105     """Custom date-picker field."""
106 
107     def __init__(self, **kwargs):
108         """Set up custom values."""
109         help_text = kwargs.get('help_text', _('Enter date'))
110         label = kwargs.get('label', None)
111         required = kwargs.get('required', False)
112         initial = kwargs.get('initial', None)
113 
114         widget = forms.DateInput(
115             attrs={
116                 'type': 'date',
117             }
118         )
119 
120         forms.DateField.__init__(
121             self,
122             required=required,
123             initial=initial,
124             help_text=help_text,
125             widget=widget,
126             label=label
127         )
128 
129 
130 def round_decimal(value, places):
131     """Round value to the specified number of places."""
132     if value is not None:
133         # see https://docs.python.org/2/library/decimal.html#decimal.Decimal.quantize for options
134         return value.quantize(Decimal(10) ** -places)
135     return value
136 
137 
138 class RoundingDecimalFormField(forms.DecimalField):
139     """Custom FormField that automatically rounds inputs."""
140 
141     def to_python(self, value):
142         """Convert value to python type."""
143         value = super().to_python(value)
144         value = round_decimal(value, self.decimal_places)
145         return value
146 
147     def prepare_value(self, value):
148         """Override the 'prepare_value' method, to remove trailing zeros when displaying.
149 
150         Why? It looks nice!
151         """
152         if type(value) == Decimal:
153             return InvenTree.helpers.normalize(value)
154         else:
155             return value
156 
157 
158 class RoundingDecimalField(models.DecimalField):
159     """Custom Field that automatically rounds inputs."""
160 
161     def to_python(self, value):
162         """Convert value to python type."""
163         value = super().to_python(value)
164         return round_decimal(value, self.decimal_places)
165 
166     def formfield(self, **kwargs):
167         """Return a Field instance for this field."""
168 
169         kwargs['form_class'] = RoundingDecimalFormField
170 
171         return super().formfield(**kwargs)
172 
173 
174 class InvenTreeNotesField(models.TextField):
175     """Custom implementation of a 'notes' field"""
176 
177     # Maximum character limit for the various 'notes' fields
178     NOTES_MAX_LENGTH = 50000
179 
180     def __init__(self, **kwargs):
181         """Configure default initial values for this field"""
182         kwargs['max_length'] = self.NOTES_MAX_LENGTH
183         kwargs['verbose_name'] = _('Notes')
184         kwargs['blank'] = True
185         kwargs['null'] = True
186 
187         super().__init__(**kwargs)
188
```

Path: `InvenTree/InvenTree/validators.py`
Content:
```
1 """Custom field validators for InvenTree."""
2 
3 import re
4 from decimal import Decimal, InvalidOperation
5 
6 from django.conf import settings
7 from django.core.exceptions import FieldDoesNotExist, ValidationError
8 from django.utils.translation import gettext_lazy as _
9 
10 from moneyed import CURRENCIES
11 
12 import common.models
13 
14 
15 def validate_currency_code(code):
16     """Check that a given code is a valid currency code."""
17     if code not in CURRENCIES:
18         raise ValidationError(_('Not a valid currency code'))
19 
20 
21 def allowable_url_schemes():
22     """Return the list of allowable URL schemes.
23 
24     In addition to the default schemes allowed by Django,
25     the install configuration file (config.yaml) can specify
26     extra schemas
27     """
28     # Default schemes
29     schemes = ['http', 'https', 'ftp', 'ftps']
30 
31     extra = settings.EXTRA_URL_SCHEMES
32 
33     for e in extra:
34         if e.lower() not in schemes:
35             schemes.append(e.lower())
36 
37     return schemes
38 
39 
40 def validate_part_name(value):
41     """Prevent some illegal characters in part names."""
42     for c in ['|', '#', '$', '{', '}']:
43         if c in str(value):
44             raise ValidationError(
45                 _('Invalid character in part name')
46             )
47 
48 
49 def validate_part_ipn(value):
50     """Validate the Part IPN against regex rule."""
51     pattern = common.models.InvenTreeSetting.get_setting('PART_IPN_REGEX')
52 
53     if pattern:
54         match = re.search(pattern, value)
55 
56         if match is None:
57             raise ValidationError(_('IPN must match regex pattern {pat}').format(pat=pattern))
58 
59 
60 def validate_purchase_order_reference(value):
61     """Validate the 'reference' field of a PurchaseOrder."""
62     pattern = common.models.InvenTreeSetting.get_setting('PURCHASEORDER_REFERENCE_REGEX')
63 
64     if pattern:
65         match = re.search(pattern, value)
66 
67         if match is None:
68             raise ValidationError(_('Reference must match pattern {pattern}').format(pattern=pattern))
69 
70 
71 def validate_sales_order_reference(value):
72     """Validate the 'reference' field of a SalesOrder."""
73     pattern = common.models.InvenTreeSetting.get_setting('SALESORDER_REFERENCE_REGEX')
74 
75     if pattern:
76         match = re.search(pattern, value)
77 
78         if match is None:
79             raise ValidationError(_('Reference must match pattern {pattern}').format(pattern=pattern))
80 
81 
82 def validate_tree_name(value):
83     """Placeholder for legacy function used in migrations."""
84 
85 
86 def validate_overage(value):
87     """Validate that a BOM overage string is properly formatted.
88 
89     An overage string can look like:
90 
91     - An integer number ('1' / 3 / 4)
92     - A decimal number ('0.123')
93     - A percentage ('5%' / '10 %')
94     """
95     value = str(value).lower().strip()
96 
97     # First look for a simple numerical value
98     try:
99         i = Decimal(value)
100 
101         if i < 0:
102             raise ValidationError(_("Overage value must not be negative"))
103 
104         # Looks like a number
105         return True
106     except (ValueError, InvalidOperation):
107         pass
108 
109     # Now look for a percentage value
110     if value.endswith('%'):
111         v = value[:-1].strip()
112 
113         # Does it look like a number?
114         try:
115             f = float(v)
116 
117             if f < 0:
118                 raise ValidationError(_("Overage value must not be negative"))
119             elif f > 100:
120                 raise ValidationError(_("Overage must not exceed 100%"))
121 
122             return True
123         except ValueError:
124             pass
125 
126     raise ValidationError(
127         _("Invalid value for overage")
128     )
129 
130 
131 def validate_part_name_format(self):
132     """Validate part name format.
133 
134     Make sure that each template container has a field of Part Model
135     """
136     jinja_template_regex = re.compile('{{.*?}}')
137     field_name_regex = re.compile('(?<=part\\.)[A-z]+')
138     for jinja_template in jinja_template_regex.findall(str(self)):
139         # make sure at least one and only one field is present inside the parser
140         field_names = field_name_regex.findall(jinja_template)
141         if len(field_names) < 1:
142             raise ValidationError({
143                 'value': 'At least one field must be present inside a jinja template container i.e {{}}'
144             })
145 
146         # Make sure that the field_name exists in Part model
147         from part.models import Part
148 
149         for field_name in field_names:
150             try:
151                 Part._meta.get_field(field_name)
152             except FieldDoesNotExist:
153                 raise ValidationError({
154                     'value': f'{field_name} does not exist in Part Model'
155                 })
156 
157     return True
158
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/InvenTree/InvenTree/fields.py b/InvenTree/InvenTree/fields.py
--- a/InvenTree/InvenTree/fields.py
+++ b/InvenTree/InvenTree/fields.py
@@ -4,7 +4,6 @@
 from decimal import Decimal
 
 from django import forms
-from django.core import validators
 from django.db import models as models
 from django.utils.translation import gettext_lazy as _
 
@@ -15,7 +14,7 @@
 
 import InvenTree.helpers
 
-from .validators import allowable_url_schemes
+from .validators import AllowedURLValidator, allowable_url_schemes
 
 
 class InvenTreeRestURLField(RestURLField):
@@ -34,7 +33,7 @@
 class InvenTreeURLField(models.URLField):
     """Custom URL field which has custom scheme validators."""
 
-    default_validators = [validators.URLValidator(schemes=allowable_url_schemes())]
+    default_validators = [AllowedURLValidator()]
 
     def __init__(self, **kwargs):
         """Initialization method for InvenTreeURLField"""
diff --git a/InvenTree/InvenTree/validators.py b/InvenTree/InvenTree/validators.py
--- a/InvenTree/InvenTree/validators.py
+++ b/InvenTree/InvenTree/validators.py
@@ -4,6 +4,7 @@
 from decimal import Decimal, InvalidOperation
 
 from django.conf import settings
+from django.core import validators
 from django.core.exceptions import FieldDoesNotExist, ValidationError
 from django.utils.translation import gettext_lazy as _
 
@@ -37,6 +38,14 @@
     return schemes
 
 
+class AllowedURLValidator(validators.URLValidator):
+    """Custom URL validator to allow for custom schemes."""
+    def __call__(self, value):
+        """Validate the URL."""
+        self.schemes = allowable_url_schemes()
+        super().__call__(value)
+
+
 def validate_part_name(value):
     """Prevent some illegal characters in part names."""
     for c in ['|', '#', '$', '{', '}']:

{"golden_diff": "diff --git a/InvenTree/InvenTree/fields.py b/InvenTree/InvenTree/fields.py\n--- a/InvenTree/InvenTree/fields.py\n+++ b/InvenTree/InvenTree/fields.py\n@@ -4,7 +4,6 @@\n from decimal import Decimal\n \n from django import forms\n-from django.core import validators\n from django.db import models as models\n from django.utils.translation import gettext_lazy as _\n \n@@ -15,7 +14,7 @@\n \n import InvenTree.helpers\n \n-from .validators import allowable_url_schemes\n+from .validators import AllowedURLValidator, allowable_url_schemes\n \n \n class InvenTreeRestURLField(RestURLField):\n@@ -34,7 +33,7 @@\n class InvenTreeURLField(models.URLField):\n     \"\"\"Custom URL field which has custom scheme validators.\"\"\"\n \n-    default_validators = [validators.URLValidator(schemes=allowable_url_schemes())]\n+    default_validators = [AllowedURLValidator()]\n \n     def __init__(self, **kwargs):\n         \"\"\"Initialization method for InvenTreeURLField\"\"\"\ndiff --git a/InvenTree/InvenTree/validators.py b/InvenTree/InvenTree/validators.py\n--- a/InvenTree/InvenTree/validators.py\n+++ b/InvenTree/InvenTree/validators.py\n@@ -4,6 +4,7 @@\n from decimal import Decimal, InvalidOperation\n \n from django.conf import settings\n+from django.core import validators\n from django.core.exceptions import FieldDoesNotExist, ValidationError\n from django.utils.translation import gettext_lazy as _\n \n@@ -37,6 +38,14 @@\n     return schemes\n \n \n+class AllowedURLValidator(validators.URLValidator):\n+    \"\"\"Custom URL validator to allow for custom schemes.\"\"\"\n+    def __call__(self, value):\n+        \"\"\"Validate the URL.\"\"\"\n+        self.schemes = allowable_url_schemes()\n+        super().__call__(value)\n+\n+\n def validate_part_name(value):\n     \"\"\"Prevent some illegal characters in part names.\"\"\"\n     for c in ['|', '#', '$', '{', '}']:\n", "issue": "[FR]Unit test for Schema allowance\n**Is your feature request the result of a bug?**\r\n#3414 #3411\r\n\r\n**Problem**\r\nCustom schema settings are not e2e or unit-tested in the API.\r\n\r\n**Suggested solution**\r\nAdd tests\r\n\r\n**Describe alternatives you've considered**\r\nN/A\r\n\r\n**Examples of other systems**\r\nN/A\r\n\r\n**Do you want to develop this?**\r\nYes\r\n\n", "before_files": [{"content": "\"\"\"Custom fields used in InvenTree.\"\"\"\n\nimport sys\nfrom decimal import Decimal\n\nfrom django import forms\nfrom django.core import validators\nfrom django.db import models as models\nfrom django.utils.translation import gettext_lazy as _\n\nfrom djmoney.forms.fields import MoneyField\nfrom djmoney.models.fields import MoneyField as ModelMoneyField\nfrom djmoney.models.validators import MinMoneyValidator\nfrom rest_framework.fields import URLField as RestURLField\n\nimport InvenTree.helpers\n\nfrom .validators import allowable_url_schemes\n\n\nclass InvenTreeRestURLField(RestURLField):\n    \"\"\"Custom field for DRF with custom scheme vaildators.\"\"\"\n    def __init__(self, **kwargs):\n        \"\"\"Update schemes.\"\"\"\n\n        # Enforce 'max length' parameter in form validation\n        if 'max_length' not in kwargs:\n            kwargs['max_length'] = 200\n\n        super().__init__(**kwargs)\n        self.validators[-1].schemes = allowable_url_schemes()\n\n\nclass InvenTreeURLField(models.URLField):\n    \"\"\"Custom URL field which has custom scheme validators.\"\"\"\n\n    default_validators = [validators.URLValidator(schemes=allowable_url_schemes())]\n\n    def __init__(self, **kwargs):\n        \"\"\"Initialization method for InvenTreeURLField\"\"\"\n\n        # Max length for InvenTreeURLField defaults to 200\n        if 'max_length' not in kwargs:\n            kwargs['max_length'] = 200\n\n        super().__init__(**kwargs)\n\n\ndef money_kwargs():\n    \"\"\"Returns the database settings for MoneyFields.\"\"\"\n    from common.settings import currency_code_default, currency_code_mappings\n\n    kwargs = {}\n    kwargs['currency_choices'] = currency_code_mappings()\n    kwargs['default_currency'] = currency_code_default()\n    return kwargs\n\n\nclass InvenTreeModelMoneyField(ModelMoneyField):\n    \"\"\"Custom MoneyField for clean migrations while using dynamic currency settings.\"\"\"\n\n    def __init__(self, **kwargs):\n        \"\"\"Overwrite default values and validators.\"\"\"\n        # detect if creating migration\n        if 'migrate' in sys.argv or 'makemigrations' in sys.argv:\n            # remove currency information for a clean migration\n            kwargs['default_currency'] = ''\n            kwargs['currency_choices'] = []\n        else:\n            # set defaults\n            kwargs.update(money_kwargs())\n\n        # Set a minimum value validator\n        validators = kwargs.get('validators', [])\n\n        allow_negative = kwargs.pop('allow_negative', False)\n\n        # If no validators are provided, add some \"standard\" ones\n        if len(validators) == 0:\n\n            if not allow_negative:\n                validators.append(\n                    MinMoneyValidator(0),\n                )\n\n        kwargs['validators'] = validators\n\n        super().__init__(**kwargs)\n\n    def formfield(self, **kwargs):\n        \"\"\"Override form class to use own function.\"\"\"\n        kwargs['form_class'] = InvenTreeMoneyField\n        return super().formfield(**kwargs)\n\n\nclass InvenTreeMoneyField(MoneyField):\n    \"\"\"Custom MoneyField for clean migrations while using dynamic currency settings.\"\"\"\n    def __init__(self, *args, **kwargs):\n        \"\"\"Override initial values with the real info from database.\"\"\"\n        kwargs.update(money_kwargs())\n        super().__init__(*args, **kwargs)\n\n\nclass DatePickerFormField(forms.DateField):\n    \"\"\"Custom date-picker field.\"\"\"\n\n    def __init__(self, **kwargs):\n        \"\"\"Set up custom values.\"\"\"\n        help_text = kwargs.get('help_text', _('Enter date'))\n        label = kwargs.get('label', None)\n        required = kwargs.get('required', False)\n        initial = kwargs.get('initial', None)\n\n        widget = forms.DateInput(\n            attrs={\n                'type': 'date',\n            }\n        )\n\n        forms.DateField.__init__(\n            self,\n            required=required,\n            initial=initial,\n            help_text=help_text,\n            widget=widget,\n            label=label\n        )\n\n\ndef round_decimal(value, places):\n    \"\"\"Round value to the specified number of places.\"\"\"\n    if value is not None:\n        # see https://docs.python.org/2/library/decimal.html#decimal.Decimal.quantize for options\n        return value.quantize(Decimal(10) ** -places)\n    return value\n\n\nclass RoundingDecimalFormField(forms.DecimalField):\n    \"\"\"Custom FormField that automatically rounds inputs.\"\"\"\n\n    def to_python(self, value):\n        \"\"\"Convert value to python type.\"\"\"\n        value = super().to_python(value)\n        value = round_decimal(value, self.decimal_places)\n        return value\n\n    def prepare_value(self, value):\n        \"\"\"Override the 'prepare_value' method, to remove trailing zeros when displaying.\n\n        Why? It looks nice!\n        \"\"\"\n        if type(value) == Decimal:\n            return InvenTree.helpers.normalize(value)\n        else:\n            return value\n\n\nclass RoundingDecimalField(models.DecimalField):\n    \"\"\"Custom Field that automatically rounds inputs.\"\"\"\n\n    def to_python(self, value):\n        \"\"\"Convert value to python type.\"\"\"\n        value = super().to_python(value)\n        return round_decimal(value, self.decimal_places)\n\n    def formfield(self, **kwargs):\n        \"\"\"Return a Field instance for this field.\"\"\"\n\n        kwargs['form_class'] = RoundingDecimalFormField\n\n        return super().formfield(**kwargs)\n\n\nclass InvenTreeNotesField(models.TextField):\n    \"\"\"Custom implementation of a 'notes' field\"\"\"\n\n    # Maximum character limit for the various 'notes' fields\n    NOTES_MAX_LENGTH = 50000\n\n    def __init__(self, **kwargs):\n        \"\"\"Configure default initial values for this field\"\"\"\n        kwargs['max_length'] = self.NOTES_MAX_LENGTH\n        kwargs['verbose_name'] = _('Notes')\n        kwargs['blank'] = True\n        kwargs['null'] = True\n\n        super().__init__(**kwargs)\n", "path": "InvenTree/InvenTree/fields.py"}, {"content": "\"\"\"Custom field validators for InvenTree.\"\"\"\n\nimport re\nfrom decimal import Decimal, InvalidOperation\n\nfrom django.conf import settings\nfrom django.core.exceptions import FieldDoesNotExist, ValidationError\nfrom django.utils.translation import gettext_lazy as _\n\nfrom moneyed import CURRENCIES\n\nimport common.models\n\n\ndef validate_currency_code(code):\n    \"\"\"Check that a given code is a valid currency code.\"\"\"\n    if code not in CURRENCIES:\n        raise ValidationError(_('Not a valid currency code'))\n\n\ndef allowable_url_schemes():\n    \"\"\"Return the list of allowable URL schemes.\n\n    In addition to the default schemes allowed by Django,\n    the install configuration file (config.yaml) can specify\n    extra schemas\n    \"\"\"\n    # Default schemes\n    schemes = ['http', 'https', 'ftp', 'ftps']\n\n    extra = settings.EXTRA_URL_SCHEMES\n\n    for e in extra:\n        if e.lower() not in schemes:\n            schemes.append(e.lower())\n\n    return schemes\n\n\ndef validate_part_name(value):\n    \"\"\"Prevent some illegal characters in part names.\"\"\"\n    for c in ['|', '#', '$', '{', '}']:\n        if c in str(value):\n            raise ValidationError(\n                _('Invalid character in part name')\n            )\n\n\ndef validate_part_ipn(value):\n    \"\"\"Validate the Part IPN against regex rule.\"\"\"\n    pattern = common.models.InvenTreeSetting.get_setting('PART_IPN_REGEX')\n\n    if pattern:\n        match = re.search(pattern, value)\n\n        if match is None:\n            raise ValidationError(_('IPN must match regex pattern {pat}').format(pat=pattern))\n\n\ndef validate_purchase_order_reference(value):\n    \"\"\"Validate the 'reference' field of a PurchaseOrder.\"\"\"\n    pattern = common.models.InvenTreeSetting.get_setting('PURCHASEORDER_REFERENCE_REGEX')\n\n    if pattern:\n        match = re.search(pattern, value)\n\n        if match is None:\n            raise ValidationError(_('Reference must match pattern {pattern}').format(pattern=pattern))\n\n\ndef validate_sales_order_reference(value):\n    \"\"\"Validate the 'reference' field of a SalesOrder.\"\"\"\n    pattern = common.models.InvenTreeSetting.get_setting('SALESORDER_REFERENCE_REGEX')\n\n    if pattern:\n        match = re.search(pattern, value)\n\n        if match is None:\n            raise ValidationError(_('Reference must match pattern {pattern}').format(pattern=pattern))\n\n\ndef validate_tree_name(value):\n    \"\"\"Placeholder for legacy function used in migrations.\"\"\"\n\n\ndef validate_overage(value):\n    \"\"\"Validate that a BOM overage string is properly formatted.\n\n    An overage string can look like:\n\n    - An integer number ('1' / 3 / 4)\n    - A decimal number ('0.123')\n    - A percentage ('5%' / '10 %')\n    \"\"\"\n    value = str(value).lower().strip()\n\n    # First look for a simple numerical value\n    try:\n        i = Decimal(value)\n\n        if i < 0:\n            raise ValidationError(_(\"Overage value must not be negative\"))\n\n        # Looks like a number\n        return True\n    except (ValueError, InvalidOperation):\n        pass\n\n    # Now look for a percentage value\n    if value.endswith('%'):\n        v = value[:-1].strip()\n\n        # Does it look like a number?\n        try:\n            f = float(v)\n\n            if f < 0:\n                raise ValidationError(_(\"Overage value must not be negative\"))\n            elif f > 100:\n                raise ValidationError(_(\"Overage must not exceed 100%\"))\n\n            return True\n        except ValueError:\n            pass\n\n    raise ValidationError(\n        _(\"Invalid value for overage\")\n    )\n\n\ndef validate_part_name_format(self):\n    \"\"\"Validate part name format.\n\n    Make sure that each template container has a field of Part Model\n    \"\"\"\n    jinja_template_regex = re.compile('{{.*?}}')\n    field_name_regex = re.compile('(?<=part\\\\.)[A-z]+')\n    for jinja_template in jinja_template_regex.findall(str(self)):\n        # make sure at least one and only one field is present inside the parser\n        field_names = field_name_regex.findall(jinja_template)\n        if len(field_names) < 1:\n            raise ValidationError({\n                'value': 'At least one field must be present inside a jinja template container i.e {{}}'\n            })\n\n        # Make sure that the field_name exists in Part model\n        from part.models import Part\n\n        for field_name in field_names:\n            try:\n                Part._meta.get_field(field_name)\n            except FieldDoesNotExist:\n                raise ValidationError({\n                    'value': f'{field_name} does not exist in Part Model'\n                })\n\n    return True\n", "path": "InvenTree/InvenTree/validators.py"}], "after_files": [{"content": "\"\"\"Custom fields used in InvenTree.\"\"\"\n\nimport sys\nfrom decimal import Decimal\n\nfrom django import forms\nfrom django.db import models as models\nfrom django.utils.translation import gettext_lazy as _\n\nfrom djmoney.forms.fields import MoneyField\nfrom djmoney.models.fields import MoneyField as ModelMoneyField\nfrom djmoney.models.validators import MinMoneyValidator\nfrom rest_framework.fields import URLField as RestURLField\n\nimport InvenTree.helpers\n\nfrom .validators import AllowedURLValidator, allowable_url_schemes\n\n\nclass InvenTreeRestURLField(RestURLField):\n    \"\"\"Custom field for DRF with custom scheme vaildators.\"\"\"\n    def __init__(self, **kwargs):\n        \"\"\"Update schemes.\"\"\"\n\n        # Enforce 'max length' parameter in form validation\n        if 'max_length' not in kwargs:\n            kwargs['max_length'] = 200\n\n        super().__init__(**kwargs)\n        self.validators[-1].schemes = allowable_url_schemes()\n\n\nclass InvenTreeURLField(models.URLField):\n    \"\"\"Custom URL field which has custom scheme validators.\"\"\"\n\n    default_validators = [AllowedURLValidator()]\n\n    def __init__(self, **kwargs):\n        \"\"\"Initialization method for InvenTreeURLField\"\"\"\n\n        # Max length for InvenTreeURLField defaults to 200\n        if 'max_length' not in kwargs:\n            kwargs['max_length'] = 200\n\n        super().__init__(**kwargs)\n\n\ndef money_kwargs():\n    \"\"\"Returns the database settings for MoneyFields.\"\"\"\n    from common.settings import currency_code_default, currency_code_mappings\n\n    kwargs = {}\n    kwargs['currency_choices'] = currency_code_mappings()\n    kwargs['default_currency'] = currency_code_default()\n    return kwargs\n\n\nclass InvenTreeModelMoneyField(ModelMoneyField):\n    \"\"\"Custom MoneyField for clean migrations while using dynamic currency settings.\"\"\"\n\n    def __init__(self, **kwargs):\n        \"\"\"Overwrite default values and validators.\"\"\"\n        # detect if creating migration\n        if 'migrate' in sys.argv or 'makemigrations' in sys.argv:\n            # remove currency information for a clean migration\n            kwargs['default_currency'] = ''\n            kwargs['currency_choices'] = []\n        else:\n            # set defaults\n            kwargs.update(money_kwargs())\n\n        # Set a minimum value validator\n        validators = kwargs.get('validators', [])\n\n        allow_negative = kwargs.pop('allow_negative', False)\n\n        # If no validators are provided, add some \"standard\" ones\n        if len(validators) == 0:\n\n            if not allow_negative:\n                validators.append(\n                    MinMoneyValidator(0),\n                )\n\n        kwargs['validators'] = validators\n\n        super().__init__(**kwargs)\n\n    def formfield(self, **kwargs):\n        \"\"\"Override form class to use own function.\"\"\"\n        kwargs['form_class'] = InvenTreeMoneyField\n        return super().formfield(**kwargs)\n\n\nclass InvenTreeMoneyField(MoneyField):\n    \"\"\"Custom MoneyField for clean migrations while using dynamic currency settings.\"\"\"\n    def __init__(self, *args, **kwargs):\n        \"\"\"Override initial values with the real info from database.\"\"\"\n        kwargs.update(money_kwargs())\n        super().__init__(*args, **kwargs)\n\n\nclass DatePickerFormField(forms.DateField):\n    \"\"\"Custom date-picker field.\"\"\"\n\n    def __init__(self, **kwargs):\n        \"\"\"Set up custom values.\"\"\"\n        help_text = kwargs.get('help_text', _('Enter date'))\n        label = kwargs.get('label', None)\n        required = kwargs.get('required', False)\n        initial = kwargs.get('initial', None)\n\n        widget = forms.DateInput(\n            attrs={\n                'type': 'date',\n            }\n        )\n\n        forms.DateField.__init__(\n            self,\n            required=required,\n            initial=initial,\n            help_text=help_text,\n            widget=widget,\n            label=label\n        )\n\n\ndef round_decimal(value, places):\n    \"\"\"Round value to the specified number of places.\"\"\"\n    if value is not None:\n        # see https://docs.python.org/2/library/decimal.html#decimal.Decimal.quantize for options\n        return value.quantize(Decimal(10) ** -places)\n    return value\n\n\nclass RoundingDecimalFormField(forms.DecimalField):\n    \"\"\"Custom FormField that automatically rounds inputs.\"\"\"\n\n    def to_python(self, value):\n        \"\"\"Convert value to python type.\"\"\"\n        value = super().to_python(value)\n        value = round_decimal(value, self.decimal_places)\n        return value\n\n    def prepare_value(self, value):\n        \"\"\"Override the 'prepare_value' method, to remove trailing zeros when displaying.\n\n        Why? It looks nice!\n        \"\"\"\n        if type(value) == Decimal:\n            return InvenTree.helpers.normalize(value)\n        else:\n            return value\n\n\nclass RoundingDecimalField(models.DecimalField):\n    \"\"\"Custom Field that automatically rounds inputs.\"\"\"\n\n    def to_python(self, value):\n        \"\"\"Convert value to python type.\"\"\"\n        value = super().to_python(value)\n        return round_decimal(value, self.decimal_places)\n\n    def formfield(self, **kwargs):\n        \"\"\"Return a Field instance for this field.\"\"\"\n\n        kwargs['form_class'] = RoundingDecimalFormField\n\n        return super().formfield(**kwargs)\n\n\nclass InvenTreeNotesField(models.TextField):\n    \"\"\"Custom implementation of a 'notes' field\"\"\"\n\n    # Maximum character limit for the various 'notes' fields\n    NOTES_MAX_LENGTH = 50000\n\n    def __init__(self, **kwargs):\n        \"\"\"Configure default initial values for this field\"\"\"\n        kwargs['max_length'] = self.NOTES_MAX_LENGTH\n        kwargs['verbose_name'] = _('Notes')\n        kwargs['blank'] = True\n        kwargs['null'] = True\n\n        super().__init__(**kwargs)\n", "path": "InvenTree/InvenTree/fields.py"}, {"content": "\"\"\"Custom field validators for InvenTree.\"\"\"\n\nimport re\nfrom decimal import Decimal, InvalidOperation\n\nfrom django.conf import settings\nfrom django.core import validators\nfrom django.core.exceptions import FieldDoesNotExist, ValidationError\nfrom django.utils.translation import gettext_lazy as _\n\nfrom moneyed import CURRENCIES\n\nimport common.models\n\n\ndef validate_currency_code(code):\n    \"\"\"Check that a given code is a valid currency code.\"\"\"\n    if code not in CURRENCIES:\n        raise ValidationError(_('Not a valid currency code'))\n\n\ndef allowable_url_schemes():\n    \"\"\"Return the list of allowable URL schemes.\n\n    In addition to the default schemes allowed by Django,\n    the install configuration file (config.yaml) can specify\n    extra schemas\n    \"\"\"\n    # Default schemes\n    schemes = ['http', 'https', 'ftp', 'ftps']\n\n    extra = settings.EXTRA_URL_SCHEMES\n\n    for e in extra:\n        if e.lower() not in schemes:\n            schemes.append(e.lower())\n\n    return schemes\n\n\nclass AllowedURLValidator(validators.URLValidator):\n    \"\"\"Custom URL validator to allow for custom schemes.\"\"\"\n    def __call__(self, value):\n        \"\"\"Validate the URL.\"\"\"\n        self.schemes = allowable_url_schemes()\n        super().__call__(value)\n\n\ndef validate_part_name(value):\n    \"\"\"Prevent some illegal characters in part names.\"\"\"\n    for c in ['|', '#', '$', '{', '}']:\n        if c in str(value):\n            raise ValidationError(\n                _('Invalid character in part name')\n            )\n\n\ndef validate_part_ipn(value):\n    \"\"\"Validate the Part IPN against regex rule.\"\"\"\n    pattern = common.models.InvenTreeSetting.get_setting('PART_IPN_REGEX')\n\n    if pattern:\n        match = re.search(pattern, value)\n\n        if match is None:\n            raise ValidationError(_('IPN must match regex pattern {pat}').format(pat=pattern))\n\n\ndef validate_purchase_order_reference(value):\n    \"\"\"Validate the 'reference' field of a PurchaseOrder.\"\"\"\n    pattern = common.models.InvenTreeSetting.get_setting('PURCHASEORDER_REFERENCE_REGEX')\n\n    if pattern:\n        match = re.search(pattern, value)\n\n        if match is None:\n            raise ValidationError(_('Reference must match pattern {pattern}').format(pattern=pattern))\n\n\ndef validate_sales_order_reference(value):\n    \"\"\"Validate the 'reference' field of a SalesOrder.\"\"\"\n    pattern = common.models.InvenTreeSetting.get_setting('SALESORDER_REFERENCE_REGEX')\n\n    if pattern:\n        match = re.search(pattern, value)\n\n        if match is None:\n            raise ValidationError(_('Reference must match pattern {pattern}').format(pattern=pattern))\n\n\ndef validate_tree_name(value):\n    \"\"\"Placeholder for legacy function used in migrations.\"\"\"\n\n\ndef validate_overage(value):\n    \"\"\"Validate that a BOM overage string is properly formatted.\n\n    An overage string can look like:\n\n    - An integer number ('1' / 3 / 4)\n    - A decimal number ('0.123')\n    - A percentage ('5%' / '10 %')\n    \"\"\"\n    value = str(value).lower().strip()\n\n    # First look for a simple numerical value\n    try:\n        i = Decimal(value)\n\n        if i < 0:\n            raise ValidationError(_(\"Overage value must not be negative\"))\n\n        # Looks like a number\n        return True\n    except (ValueError, InvalidOperation):\n        pass\n\n    # Now look for a percentage value\n    if value.endswith('%'):\n        v = value[:-1].strip()\n\n        # Does it look like a number?\n        try:\n            f = float(v)\n\n            if f < 0:\n                raise ValidationError(_(\"Overage value must not be negative\"))\n            elif f > 100:\n                raise ValidationError(_(\"Overage must not exceed 100%\"))\n\n            return True\n        except ValueError:\n            pass\n\n    raise ValidationError(\n        _(\"Invalid value for overage\")\n    )\n\n\ndef validate_part_name_format(self):\n    \"\"\"Validate part name format.\n\n    Make sure that each template container has a field of Part Model\n    \"\"\"\n    jinja_template_regex = re.compile('{{.*?}}')\n    field_name_regex = re.compile('(?<=part\\\\.)[A-z]+')\n    for jinja_template in jinja_template_regex.findall(str(self)):\n        # make sure at least one and only one field is present inside the parser\n        field_names = field_name_regex.findall(jinja_template)\n        if len(field_names) < 1:\n            raise ValidationError({\n                'value': 'At least one field must be present inside a jinja template container i.e {{}}'\n            })\n\n        # Make sure that the field_name exists in Part model\n        from part.models import Part\n\n        for field_name in field_names:\n            try:\n                Part._meta.get_field(field_name)\n            except FieldDoesNotExist:\n                raise ValidationError({\n                    'value': f'{field_name} does not exist in Part Model'\n                })\n\n    return True\n", "path": "InvenTree/InvenTree/validators.py"}]}