diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py
index 13b770539c..da96c7ee79 100644
--- a/InvenTree/InvenTree/helpers.py
+++ b/InvenTree/InvenTree/helpers.py
@@ -19,9 +19,18 @@ from django.contrib.auth.models import Permission
import InvenTree.version
+from common.models import InvenTreeSetting
from .settings import MEDIA_URL, STATIC_URL
+def getSetting(key, backup_value=None):
+ """
+ Shortcut for reading a setting value from the database
+ """
+
+ return InvenTreeSetting.get_setting(key, backup_value=backup_value)
+
+
def generateTestKey(test_name):
"""
Generate a test 'key' for a given test name.
diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py
index d718871851..0b4292cbd9 100644
--- a/InvenTree/InvenTree/urls.py
+++ b/InvenTree/InvenTree/urls.py
@@ -69,10 +69,14 @@ apipatterns = [
settings_urls = [
url(r'^user/?', SettingsView.as_view(template_name='InvenTree/settings/user.html'), name='settings-user'),
+ url(r'^theme/?', ColorThemeSelectView.as_view(), name='settings-theme'),
+
url(r'^currency/?', SettingsView.as_view(template_name='InvenTree/settings/currency.html'), name='settings-currency'),
url(r'^part/?', SettingsView.as_view(template_name='InvenTree/settings/part.html'), name='settings-part'),
- url(r'^theme/?', ColorThemeSelectView.as_view(), name='settings-theme'),
- url(r'^other/?', SettingsView.as_view(template_name='InvenTree/settings/other.html'), name='settings-other'),
+ url(r'^stock/?', SettingsView.as_view(template_name='InvenTree/settings/stock.html'), name='settings-stock'),
+ url(r'^build/?', SettingsView.as_view(template_name='InvenTree/settings/build.html'), name='settings-build'),
+ url(r'^purchase-order/?', SettingsView.as_view(template_name='InvenTree/settings/po.html'), name='settings-po'),
+ url(r'^sales-order/?', SettingsView.as_view(template_name='InvenTree/settings/so.html'), name='settings-so'),
# Catch any other urls
url(r'^.*$', SettingsView.as_view(template_name='InvenTree/settings/user.html'), name='settings'),
diff --git a/InvenTree/InvenTree/validators.py b/InvenTree/InvenTree/validators.py
index 548bce12ab..e85dc40810 100644
--- a/InvenTree/InvenTree/validators.py
+++ b/InvenTree/InvenTree/validators.py
@@ -43,7 +43,7 @@ def validate_part_name(value):
def validate_part_ipn(value):
""" Validate the Part IPN against regex rule """
- pattern = common.models.InvenTreeSetting.get_setting('part_ipn_regex')
+ pattern = common.models.InvenTreeSetting.get_setting('PART_IPN_REGEX')
if pattern:
match = re.search(pattern, value)
@@ -52,6 +52,48 @@ def validate_part_ipn(value):
raise ValidationError(_('IPN must match regex pattern') + " '{pat}'".format(pat=pattern))
+def validate_build_order_reference(value):
+ """
+ Validate the 'reference' field of a BuildOrder
+ """
+
+ pattern = common.models.InvenTreeSetting.get_setting('BUILDORDER_REFERENCE_REGEX')
+
+ if pattern:
+ match = re.search(pattern, value)
+
+ if match is None:
+ raise ValidationError(_('Reference must match pattern') + f" '{pattern}'")
+
+
+def validate_purchase_order_reference(value):
+ """
+ Validate the 'reference' field of a PurchaseOrder
+ """
+
+ pattern = common.models.InvenTreeSetting.get_setting('PURCHASEORDER_REFERENCE_REGEX')
+
+ if pattern:
+ match = re.search(pattern, value)
+
+ if match is None:
+ raise ValidationError(_('Reference must match pattern') + f" '{pattern}'")
+
+
+def validate_sales_order_reference(value):
+ """
+ Validate the 'reference' field of a SalesOrder
+ """
+
+ pattern = common.models.InvenTreeSetting.get_setting('SALESORDER_REFERENCE_REGEX')
+
+ if pattern:
+ match = re.search(pattern, value)
+
+ if match is None:
+ raise ValidationError(_('Reference must match pattern') + f" '{pattern}'")
+
+
def validate_tree_name(value):
""" Prevent illegal characters in tree item names """
diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py
index 827769c32a..1e545cc68e 100644
--- a/InvenTree/InvenTree/version.py
+++ b/InvenTree/InvenTree/version.py
@@ -12,7 +12,7 @@ INVENTREE_SW_VERSION = "0.1.4 pre"
def inventreeInstanceName():
""" Returns the InstanceName settings for the current database """
- return common.models.InvenTreeSetting.get_setting("InstanceName", "")
+ return common.models.InvenTreeSetting.get_setting("INVENTREE_INSTANCE", "")
def inventreeVersion():
diff --git a/InvenTree/build/admin.py b/InvenTree/build/admin.py
index b47d38e36d..86592c7a81 100644
--- a/InvenTree/build/admin.py
+++ b/InvenTree/build/admin.py
@@ -10,17 +10,16 @@ from .models import Build, BuildItem
class BuildAdmin(ImportExportModelAdmin):
list_display = (
+ 'reference',
+ 'title',
'part',
'status',
'batch',
'quantity',
- 'creation_date',
- 'completion_date',
- 'title',
- 'notes',
)
search_fields = [
+ 'reference',
'title',
'part__name',
'part__description',
diff --git a/InvenTree/build/fixtures/build.yaml b/InvenTree/build/fixtures/build.yaml
index 532d502098..0ac5d00a20 100644
--- a/InvenTree/build/fixtures/build.yaml
+++ b/InvenTree/build/fixtures/build.yaml
@@ -5,6 +5,7 @@
fields:
part: 25
batch: 'B1'
+ reference: "0001"
title: 'Building 7 parts'
quantity: 7
notes: 'Some simple notes'
@@ -20,6 +21,7 @@
pk: 2
fields:
part: 50
+ reference: "0002"
title: 'Making things'
batch: 'B2'
status: 40 # COMPLETE
diff --git a/InvenTree/build/forms.py b/InvenTree/build/forms.py
index f60c257cef..74227adb9c 100644
--- a/InvenTree/build/forms.py
+++ b/InvenTree/build/forms.py
@@ -17,14 +17,26 @@ class EditBuildForm(HelperForm):
""" Form for editing a Build object.
"""
+ field_prefix = {
+ 'reference': 'BO',
+ 'link': 'fa-link',
+ 'batch': 'fa-layer-group',
+ 'location': 'fa-map-marker-alt',
+ }
+
+ field_placeholder = {
+ 'reference': _('Build Order reference')
+ }
+
class Meta:
model = Build
fields = [
+ 'reference',
'title',
'part',
+ 'quantity',
'parent',
'sales_order',
- 'quantity',
'take_from',
'batch',
'link',
diff --git a/InvenTree/build/migrations/0018_build_reference.py b/InvenTree/build/migrations/0018_build_reference.py
new file mode 100644
index 0000000000..bcf4f9b9d4
--- /dev/null
+++ b/InvenTree/build/migrations/0018_build_reference.py
@@ -0,0 +1,64 @@
+# Generated by Django 3.0.7 on 2020-10-19 11:25
+
+from django.db import migrations, models
+
+
+def add_default_reference(apps, schema_editor):
+ """
+ Add a "default" build-order reference for any existing build orders.
+ Best we can do is use the PK of the build order itself.
+ """
+
+ Build = apps.get_model('build', 'Build')
+
+ count = 0
+
+ for build in Build.objects.all():
+
+ build.reference = str(build.pk)
+ build.save()
+ count += 1
+
+ print(f"\nUpdated build reference for {count} existing BuildOrder objects")
+
+
+def reverse_default_reference(apps, schema_editor):
+ """
+ Do nothing! But we need to have a function here so the whole process is reversible.
+ """
+ pass
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('build', '0017_auto_20200426_0612'),
+ ]
+
+ operations = [
+ # Initial operation - create a 'reference' field for the Build object:
+ migrations.AddField(
+ model_name='build',
+ name='reference',
+ field=models.CharField(help_text='Build Order Reference', blank=True, max_length=64, unique=False, verbose_name='Reference'),
+ ),
+
+ # Auto-populate the new reference field for any existing build order objects
+ migrations.RunPython(
+ add_default_reference,
+ reverse_code=reverse_default_reference
+ ),
+
+ # Now that each build has a non-empty, unique reference, update the field requirements!
+ migrations.AlterField(
+ model_name='build',
+ name='reference',
+ field=models.CharField(
+ help_text='Build Order Reference',
+ max_length=64,
+ blank=False,
+ unique=True,
+ verbose_name='Reference'
+ )
+ )
+ ]
diff --git a/InvenTree/build/migrations/0019_auto_20201019_1302.py b/InvenTree/build/migrations/0019_auto_20201019_1302.py
new file mode 100644
index 0000000000..c767d3a3d9
--- /dev/null
+++ b/InvenTree/build/migrations/0019_auto_20201019_1302.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.0.7 on 2020-10-19 13:02
+
+import InvenTree.validators
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('build', '0018_build_reference'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='build',
+ options={'verbose_name': 'Build Order', 'verbose_name_plural': 'Build Orders'},
+ ),
+ migrations.AlterField(
+ model_name='build',
+ name='reference',
+ field=models.CharField(help_text='Build Order Reference', max_length=64, unique=True, validators=[InvenTree.validators.validate_build_order_reference], verbose_name='Reference'),
+ ),
+ ]
diff --git a/InvenTree/build/migrations/0020_auto_20201019_1325.py b/InvenTree/build/migrations/0020_auto_20201019_1325.py
new file mode 100644
index 0000000000..1406530c8d
--- /dev/null
+++ b/InvenTree/build/migrations/0020_auto_20201019_1325.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.0.7 on 2020-10-19 13:25
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('build', '0019_auto_20201019_1302'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='build',
+ name='title',
+ field=models.CharField(help_text='Brief description of the build', max_length=100, verbose_name='Description'),
+ ),
+ ]
diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py
index 35870adde4..877affd144 100644
--- a/InvenTree/build/models.py
+++ b/InvenTree/build/models.py
@@ -22,7 +22,9 @@ from markdownx.models import MarkdownxField
from mptt.models import MPTTModel, TreeForeignKey
from InvenTree.status_codes import BuildStatus
-from InvenTree.helpers import decimal2string
+from InvenTree.helpers import increment, getSetting
+from InvenTree.validators import validate_build_order_reference
+
import InvenTree.fields
from stock import models as StockModels
@@ -34,6 +36,7 @@ class Build(MPTTModel):
Attributes:
part: The part to be built (from component BOM items)
+ reference: Build order reference (required, must be unique)
title: Brief title describing the build (required)
quantity: Number of units to be built
parent: Reference to a Build object for which this Build is required
@@ -47,8 +50,15 @@ class Build(MPTTModel):
notes: Text notes
"""
+ class Meta:
+ verbose_name = _("Build Order")
+ verbose_name_plural = _("Build Orders")
+
def __str__(self):
- return "{q} x {part}".format(q=decimal2string(self.quantity), part=str(self.part.full_name))
+
+ prefix = getSetting("BUILDORDER_REFERENCE_PREFIX")
+
+ return f"{prefix}{self.reference}"
def get_absolute_url(self):
return reverse('build-detail', kwargs={'pk': self.id})
@@ -69,8 +79,19 @@ class Build(MPTTModel):
except PartModels.Part.DoesNotExist:
pass
+ reference = models.CharField(
+ unique=True,
+ max_length=64,
+ blank=False,
+ help_text=_('Build Order Reference'),
+ verbose_name=_('Reference'),
+ validators=[
+ validate_build_order_reference
+ ]
+ )
+
title = models.CharField(
- verbose_name=_('Build Title'),
+ verbose_name=_('Description'),
blank=False,
max_length=100,
help_text=_('Brief description of the build')
@@ -165,6 +186,38 @@ class Build(MPTTModel):
def output_count(self):
return self.build_outputs.count()
+ @classmethod
+ def getNextBuildNumber(cls):
+ """
+ Try to predict the next Build Order reference:
+ """
+
+ if cls.objects.count() == 0:
+ return None
+
+ build = cls.objects.last()
+ ref = build.reference
+
+ if not ref:
+ return None
+
+ tries = set()
+
+ while 1:
+ new_ref = increment(ref)
+
+ if new_ref in tries:
+ # We are potentially stuck in a loop - simply return the original reference
+ return ref
+
+ if cls.objects.filter(reference=new_ref).exists():
+ tries.add(new_ref)
+ new_ref = increment(new_ref)
+ else:
+ break
+
+ return new_ref
+
@transaction.atomic
def cancelBuild(self, user):
""" Mark the Build as CANCELLED
diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py
index 6480004699..53e75942f0 100644
--- a/InvenTree/build/serializers.py
+++ b/InvenTree/build/serializers.py
@@ -41,6 +41,7 @@ class BuildSerializer(InvenTreeModelSerializer):
'completion_date',
'part',
'part_detail',
+ 'reference',
'sales_order',
'quantity',
'status',
diff --git a/InvenTree/build/templates/build/build_base.html b/InvenTree/build/templates/build/build_base.html
index f076013def..a366459908 100644
--- a/InvenTree/build/templates/build/build_base.html
+++ b/InvenTree/build/templates/build/build_base.html
@@ -5,18 +5,18 @@
{% load status_codes %}
{% block page_title %}
-InvenTree | {% trans "Build" %} - {{ build }}
+InvenTree | {% trans "Build Order" %} - {{ build }}
{% endblock %}
{% block pre_content %}
{% if build.sales_order %}
{% endif %}
{% if build.parent %}
{% endif %}
{% endblock %}
@@ -31,14 +31,15 @@ src="{% static 'img/blank_image.png' %}"
{% endblock %}
{% block page_data %}
-{% trans "Build" %} {% build_status_label build.status large=True %}
-
-
- {{ build.quantity }} x {{ build.part.full_name }}
+
+ {% trans "Build Order" %} {{ build }}
{% if user.is_staff and roles.build.change %}
-{% endif %}
-
+ {% endif %}
+
+{% build_status_label build.status large=True %}
+
+{{ build.title }}