Merge pull request #1055 from SchrodingersGat/build-reference

Build reference
This commit is contained in:
Oliver 2020-10-20 09:03:25 +11:00 committed by GitHub
commit d242e04e64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 2055 additions and 1184 deletions

View File

@ -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.

View File

@ -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'),

View File

@ -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 """

View File

@ -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():

View File

@ -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',

View File

@ -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

View File

@ -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',

View File

@ -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'
)
)
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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

View File

@ -41,6 +41,7 @@ class BuildSerializer(InvenTreeModelSerializer):
'completion_date',
'part',
'part_detail',
'reference',
'sales_order',
'quantity',
'status',

View File

@ -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 %}
<div class='alert alert-block alert-info'>
{% trans "This build is allocated to Sales Order" %} <b><a href="{% url 'so-detail' build.sales_order.id %}">{{ build.sales_order }}</a></b>
{% trans "This Build Order is allocated to Sales Order" %} <b><a href="{% url 'so-detail' build.sales_order.id %}">{{ build.sales_order }}</a></b>
</div>
{% endif %}
{% if build.parent %}
<div class='alert alert-block alert-info'>
{% trans "This build is a child of Build" %} <b><a href="{% url 'build-detail' build.parent.id %}">{{ build.parent }}</a></b>
{% trans "This Build Order is a child of Build Order" %} <b><a href="{% url 'build-detail' build.parent.id %}">{{ build.parent }}</a></b>
</div>
{% endif %}
{% endblock %}
@ -31,14 +31,15 @@ src="{% static 'img/blank_image.png' %}"
{% endblock %}
{% block page_data %}
<h3>{% trans "Build" %} {% build_status_label build.status large=True %}</h3>
<hr>
<h4>
{{ build.quantity }} x {{ build.part.full_name }}
<h3>
{% trans "Build Order" %} {{ build }}
{% if user.is_staff and roles.build.change %}
<a href="{% url 'admin:build_build_change' build.pk %}"><span title="{% trans 'Admin view' %}" class='fas fa-user-shield'></span></a>
{% endif %}
</h4>
{% endif %}
</h3>
<h3>{% build_status_label build.status large=True %}</h3>
<hr>
<p>{{ build.title }}</p>
<div class='btn-row'>
<div class='btn-group action-buttons'>
{% if roles.build.change %}
@ -68,9 +69,9 @@ src="{% static 'img/blank_image.png' %}"
<h4>{% trans "Build Details" %}</h4>
<table class='table table-striped table-condensed'>
<tr>
<td></td>
<td>{% trans "Build Title" %}</td>
<td>{{ build.title }}</td>
<td><span class='fas fa-hashtag'></span></td>
<td>{% trans "Build Order Reference" %}</td>
<td>{{ build }}</td>
</tr>
<tr>
<td><span class='fas fa-shapes'></span></td>

View File

@ -54,7 +54,7 @@ class BuildTestSimple(TestCase):
self.assertEqual(b.batch, 'B2')
self.assertEqual(b.quantity, 21)
self.assertEqual(str(b), '21 x Orphan')
self.assertEqual(str(b), 'BO0002')
def test_url(self):
b1 = Build.objects.get(pk=1)

View File

@ -400,7 +400,7 @@ class BuildCreate(AjaxCreateView):
model = Build
context_object_name = 'build'
form_class = forms.EditBuildForm
ajax_form_title = _('Start new Build')
ajax_form_title = _('New Build Order')
ajax_template_name = 'modal_form.html'
role_required = 'build.add'
@ -415,6 +415,8 @@ class BuildCreate(AjaxCreateView):
# User has provided a Part ID
initials['part'] = self.request.GET.get('part', None)
initials['reference'] = Build.getNextBuildNumber()
initials['parent'] = self.request.GET.get('parent', None)
# User has provided a SalesOrder ID

View File

@ -14,7 +14,7 @@ class CurrencyAdmin(ImportExportModelAdmin):
class SettingsAdmin(ImportExportModelAdmin):
list_display = ('key', 'value', 'description')
list_display = ('key', 'value')
admin.site.register(Currency, CurrencyAdmin)

View File

@ -1,10 +1,6 @@
from django.apps import AppConfig
from django.db.utils import OperationalError, ProgrammingError
import os
import uuid
import yaml
class CommonConfig(AppConfig):
name = 'common'
@ -12,45 +8,8 @@ class CommonConfig(AppConfig):
def ready(self):
""" Will be called when the Common app is first loaded """
self.populate_default_settings()
self.add_instance_name()
def populate_default_settings(self):
""" Populate the default values for InvenTree key:value pairs.
If a setting does not exist, it will be created.
"""
# Import this here, rather than at the global-level,
# otherwise it is called all the time, and we don't want that,
# as the InvenTreeSetting model may have not been instantiated yet.
from .models import InvenTreeSetting
here = os.path.dirname(os.path.abspath(__file__))
settings_file = os.path.join(here, 'kvp.yaml')
with open(settings_file) as kvp:
values = yaml.safe_load(kvp)
for value in values:
key = value['key']
default = value['default']
description = value['description']
try:
# If a particular setting does not exist in the database, create it now
if not InvenTreeSetting.objects.filter(key=key).exists():
setting = InvenTreeSetting(
key=key,
value=default,
description=description
)
setting.save()
print("Creating new key: '{k}' = '{v}'".format(k=key, v=default))
except (OperationalError, ProgrammingError):
# Migrations have not yet been applied - table does not exist
break
self.add_default_settings()
def add_instance_name(self):
"""
@ -61,20 +20,73 @@ class CommonConfig(AppConfig):
# See note above
from .models import InvenTreeSetting
"""
Note: The "old" instance name was stored under the key 'InstanceName',
but has now been renamed to 'INVENTREE_INSTANCE'.
"""
try:
if not InvenTreeSetting.objects.filter(key='InstanceName').exists():
val = uuid.uuid4().hex
# Quick exit if a value already exists for 'inventree_instance'
if InvenTreeSetting.objects.filter(key='INVENTREE_INSTANCE').exists():
return
print("No 'InstanceName' found - generating random name '{n}'".format(n=val))
# Default instance name
instance_name = 'InvenTree Server'
name = InvenTreeSetting(
key="InstanceName",
value=val,
description="Instance name for this InvenTree database installation."
)
# Use the old name if it exists
if InvenTreeSetting.objects.filter(key='InstanceName').exists():
instance = InvenTreeSetting.objects.get(key='InstanceName')
instance_name = instance.value
# Delete the legacy key
instance.delete()
# Create new value
InvenTreeSetting.objects.create(
key='INVENTREE_INSTANCE',
value=instance_name
)
name.save()
except (OperationalError, ProgrammingError):
# Migrations have not yet been applied - table does not exist
pass
def add_default_settings(self):
"""
Create all required settings, if they do not exist.
"""
from .models import InvenTreeSetting
for key in InvenTreeSetting.DEFAULT_VALUES.keys():
try:
settings = InvenTreeSetting.objects.filter(key__iexact=key)
if settings.count() == 0:
value = InvenTreeSetting.DEFAULT_VALUES[key]
print(f"Creating default setting for {key} -> '{value}'")
InvenTreeSetting.objects.create(
key=key,
value=value
)
return
elif settings.count() > 1:
# Prevent multiple shadow copies of the same setting!
for setting in settings[1:]:
setting.delete()
# Ensure that the key has the correct case
setting = settings[0]
if not setting.key == key:
setting.key = key
setting.save()
except (OperationalError, ProgrammingError):
# Table might not yet exist
pass

View File

@ -7,7 +7,7 @@ from __future__ import unicode_literals
from InvenTree.forms import HelperForm
from .models import Currency
from .models import Currency, InvenTreeSetting
class CurrencyEditForm(HelperForm):
@ -22,3 +22,17 @@ class CurrencyEditForm(HelperForm):
'value',
'base'
]
class SettingEditForm(HelperForm):
"""
Form for creating / editing a settings object
"""
class Meta:
model = InvenTreeSetting
fields = [
'key',
'value'
]

View File

@ -1,16 +0,0 @@
# This file contains the default values for the key:value settings available in InvenTree
# This file should not be edited locally.
# Note: The description strings provided here will be translatable,
# so ensure that any translations are provided as appropriate.
# TODO: Update the formatting here to include logical separators e.g. double-underscore
# TODO: This is so when there are enough options, we will be able to display them as a tree
- key: 'part_ipn_regex'
default: ''
description: 'Format string for internal part number'
- key: part_deep_copy
default: True
description: 'Parts are deep-copied by default'

View File

@ -0,0 +1,17 @@
# Generated by Django 3.0.7 on 2020-10-19 13:02
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('common', '0007_colortheme'),
]
operations = [
migrations.RemoveField(
model_name='inventreesetting',
name='description',
),
]

View File

@ -27,6 +27,30 @@ class InvenTreeSetting(models.Model):
even if that key does not exist.
"""
# Dict of default values for various internal settings
DEFAULT_VALUES = {
# Global inventree settings
'INVENTREE_INSTANCE': 'InvenTree Server',
# Part settings
'PART_IPN_REGEX': '',
'PART_COPY_BOM': True,
'PART_COPY_PARAMETERS': True,
'PART_COPY_TESTS': True,
# Stock settings
# Build Order settings
'BUILDORDER_REFERENCE_PREFIX': 'BO',
'BUILDORDER_REFERENCE_REGEX': '',
# Purchase Order Settings
'PURCHASEORDER_REFERENCE_PREFIX': 'PO',
# Sales Order Settings
'SALESORDER_REFERENCE_PREFIX': 'SO',
}
class Meta:
verbose_name = "InvenTree Setting"
verbose_name_plural = "InvenTree Settings"
@ -38,9 +62,17 @@ class InvenTreeSetting(models.Model):
If it does not exist, return the backup value (default = None)
"""
# If no backup value is specified, atttempt to retrieve a "default" value
if backup_value is None:
backup_value = InvenTreeSetting.DEFAULT_VALUES.get(key, None)
try:
setting = InvenTreeSetting.objects.get(key__iexact=key)
return setting.value
settings = InvenTreeSetting.objects.filter(key__iexact=key)
if len(settings) > 0:
return settings[0].value
else:
return backup_value
except InvenTreeSetting.DoesNotExist:
return backup_value
@ -69,15 +101,13 @@ class InvenTreeSetting(models.Model):
else:
return
setting.value = value
setting.value = str(value)
setting.save()
key = models.CharField(max_length=50, blank=False, unique=True, help_text=_('Settings key (must be unique - case insensitive'))
value = models.CharField(max_length=200, blank=True, unique=False, help_text=_('Settings value'))
description = models.CharField(max_length=200, blank=True, unique=False, help_text=_('Settings description'))
def validate_unique(self, exclude=None):
""" Ensure that the key:value pair is unique.
In addition to the base validators, this ensures that the 'key'

View File

@ -2,8 +2,9 @@
from __future__ import unicode_literals
from django.test import TestCase
from django.contrib.auth import get_user_model
from .models import Currency
from .models import Currency, InvenTreeSetting
class CurrencyTest(TestCase):
@ -17,3 +18,32 @@ class CurrencyTest(TestCase):
# Simple test for now (improve this later!)
self.assertEqual(Currency.objects.count(), 2)
class SettingsTest(TestCase):
"""
Tests for the 'settings' model
"""
def setUp(self):
User = get_user_model()
self.user = User.objects.create_user('username', 'user@email.com', 'password')
self.user.is_staff = True
self.user.save()
self.client.login(username='username', password='password')
def test_defaults(self):
"""
Populate the settings with default values
"""
for key in InvenTreeSetting.DEFAULT_VALUES.keys():
value = InvenTreeSetting.DEFAULT_VALUES[key]
InvenTreeSetting.set_setting(key, value, self.user)
self.assertEqual(str(value), InvenTreeSetting.get_setting(key))

View File

@ -35,3 +35,14 @@ class CurrencyDelete(AjaxDeleteView):
model = models.Currency
ajax_form_title = _('Delete Currency')
ajax_template_name = "common/delete_currency.html"
class SettingEdit(AjaxUpdateView):
"""
View for editing an InvenTree key:value settings object,
(or creating it if the key does not already exist)
"""
model = models.InvenTreeSetting
ajax_form_title = _('Change Setting')
form_class = forms.SettingEditForm

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -96,7 +96,7 @@ class EditPurchaseOrderForm(HelperForm):
}
self.field_placeholder = {
'reference': _('Enter purchase order number'),
'reference': _('Purchase Order reference'),
}
super().__init__(*args, **kwargs)

View File

@ -20,46 +20,44 @@ src="{% static 'img/blank_image.png' %}"
{% endblock %}
{% block page_data %}
<h3>{% trans "Purchase Order" %} {% purchase_order_status_label order.status large=True %}</h3>
<hr>
<h4>
{{ order }}
<h3>
{% trans "Purchase Order" %} {{ order.reference }}
{% if user.is_staff and roles.purchase_order.change %}
<a href="{% url 'admin:order_purchaseorder_change' order.pk %}"><span title='{% trans "Admin view" %}' class='fas fa-user-shield'></span></a>
{% endif %}
</h4>
</h3>
<h3>{% purchase_order_status_label order.status large=True %}</h3>
<hr>
<p>{{ order.description }}</p>
<p>
<div class='btn-row'>
<div class='btn-group action-buttons'>
{% if roles.purchase_order.change %}
<button type='button' class='btn btn-default' id='edit-order' title='{% trans "Edit order information" %}'>
<span class='fas fa-edit icon-green'></span>
</button>
{% if order.status == PurchaseOrderStatus.PENDING and order.lines.count > 0 %}
<button type='button' class='btn btn-default' id='place-order' title='{% trans "Place order" %}'>
<span class='fas fa-paper-plane icon-blue'></span>
</button>
{% elif order.status == PurchaseOrderStatus.PLACED %}
<button type='button' class='btn btn-default' id='receive-order' title='{% trans "Receive items" %}'>
<span class='fas fa-clipboard-check'></span>
</button>
<button type='button' class='btn btn-default' id='complete-order' title='{% trans "Mark order as complete" %}'>
<span class='fas fa-check-circle'></span>
</button>
{% endif %}
{% if order.status == PurchaseOrderStatus.PENDING or order.status == PurchaseOrderStatus.PLACED %}
<button type='button' class='btn btn-default' id='cancel-order' title='{% trans "Cancel order" %}'>
<span class='fas fa-times-circle icon-red'></span>
</button>
{% endif %}
{% endif %}
<button type='button' class='btn btn-default' id='export-order' title='{% trans "Export order to file" %}'>
<span class='fas fa-file-download'></span>
</button>
</div>
<div class='btn-row'>
<div class='btn-group action-buttons'>
{% if roles.purchase_order.change %}
<button type='button' class='btn btn-default' id='edit-order' title='{% trans "Edit order information" %}'>
<span class='fas fa-edit icon-green'></span>
</button>
{% if order.status == PurchaseOrderStatus.PENDING and order.lines.count > 0 %}
<button type='button' class='btn btn-default' id='place-order' title='{% trans "Place order" %}'>
<span class='fas fa-paper-plane icon-blue'></span>
</button>
{% elif order.status == PurchaseOrderStatus.PLACED %}
<button type='button' class='btn btn-default' id='receive-order' title='{% trans "Receive items" %}'>
<span class='fas fa-clipboard-check'></span>
</button>
<button type='button' class='btn btn-default' id='complete-order' title='{% trans "Mark order as complete" %}'>
<span class='fas fa-check-circle'></span>
</button>
{% endif %}
{% if order.status == PurchaseOrderStatus.PENDING or order.status == PurchaseOrderStatus.PLACED %}
<button type='button' class='btn btn-default' id='cancel-order' title='{% trans "Cancel order" %}'>
<span class='fas fa-times-circle icon-red'></span>
</button>
{% endif %}
{% endif %}
<button type='button' class='btn btn-default' id='export-order' title='{% trans "Export order to file" %}'>
<span class='fas fa-file-download'></span>
</button>
</div>
</p>
</div>
{% endblock %}
{% block page_details %}

View File

@ -27,17 +27,18 @@ src="{% static 'img/blank_image.png' %}"
/>
{% endblock %}
{% block page_data %}
<h3>{% trans "Sales Order" %} {% sales_order_status_label order.status large=True %}</h3>
<hr>
<h4>
{{ order }}
<h3>
{% trans "Sales Order" %} {{ order.reference }}
{% if user.is_staff and roles.sales_order.change %}
<a href="{% url 'admin:order_salesorder_change' order.pk %}"><span title='{% trans "Admin view" %}' class='fas fa-user-shield'></span></a>
{% endif %}
</h4>
</h3>
<h3>
{% sales_order_status_label order.status large=True %}
</h3>
<hr>
<p>{{ order.description }}</p>
<div class='btn-row'>
<div class='btn-group action-buttons'>

View File

@ -88,7 +88,7 @@ def inventree_docs_url(*args, **kwargs):
@register.simple_tag()
def inventree_setting(key, *args, **kwargs):
return InvenTreeSetting.get_setting(key)
return InvenTreeSetting.get_setting(key, backup_value=kwargs.get('backup', None))
@register.simple_tag()

View File

@ -493,9 +493,9 @@ class PartDuplicate(AjaxCreateView):
else:
initials = super(AjaxCreateView, self).get_initial()
initials['bom_copy'] = str2bool(InvenTreeSetting.get_setting('part_deep_copy', True))
# Create new entry in InvenTree/common/kvp.yaml?
initials['parameters_copy'] = str2bool(InvenTreeSetting.get_setting('part_deep_copy', True))
initials['bom_copy'] = str2bool(InvenTreeSetting.get_setting('PART_COPY_BOM', True))
initials['parameters_copy'] = str2bool(InvenTreeSetting.get_setting('PART_COPY_PARAMETERS', True))
return initials

View File

@ -0,0 +1,32 @@
{% extends "InvenTree/settings/settings.html" %}
{% load i18n %}
{% load inventree_extras %}
{% block tabs %}
{% include "InvenTree/settings/tabs.html" with tab='build' %}
{% endblock %}
{% block subtitle %}
{% trans "Build Order Settings" %}
{% endblock %}
{% block settings %}
<table class='table table-striped table-condensed'>
<thead></thead>
<tbody>
<tr>
<th>{% trans "Reference Prefix" %}</th>
<th>{% inventree_setting 'BUILDORDER_REFERENCE_PREFIX' backup='BO' %}</th>
<td>{% trans "Prefix for Build Order reference" %}</td>
</tr>
<tr>
<th>{% trans "Reference Regex" %}</th>
<th>{% inventree_setting 'BUILDORDER_REFERENCE_REGEX' %}</th>
<td>{% trans "Regex validator for Build Order reference" %}</td>
<td></td>
</tr>
</tbody>
</table>
{% endblock %}

View File

@ -1,4 +1,9 @@
{% extends "InvenTree/settings/settings.html" %}
{% load i18n %}
{% block subtitle %}
{% trans "General Settings" %}
{% endblock %}
{% block tabs %}
{% include "InvenTree/settings/tabs.html" with tab='currency' %}
@ -6,10 +11,10 @@
{% block settings %}
<h4>Currencies</h4>
<h4>{% trans "Currencies" %}</h4>
<div id='currency-buttons'>
<button class='btn btn-success' id='new-currency'>New Currency</button>
<button class='btn btn-success' id='new-currency'>{% trans "New Currency" %}</button>
</div>
<table class='table table-striped table-condensed' id='currency-table' data-toolbar='#currency-buttons'>

View File

@ -1,37 +0,0 @@
{% extends "InvenTree/settings/settings.html" %}
{% block tabs %}
{% include "InvenTree/settings/tabs.html" with tab='other' %}
{% endblock %}
{% block settings %}
<h4>InvenTree Settings</h4>
<table class='table table-striped table-condensed' id='other-table'>
<thead>
<tr>
<th>Setting</th>
<th>Value</th>
<th>Description</th>
</tr>
</thead>
<tbody>
{% for setting in settings %}
<tr>
<td>{{ setting.key }}</td>
<td>{{ setting.value }}</td>
<td>{{ setting.description }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
{% block js_ready %}
{{ block.super }}
$("#other-table").bootstrapTable();
{% endblock %}

View File

@ -5,7 +5,12 @@
{% include "InvenTree/settings/tabs.html" with tab='part' %}
{% endblock %}
{% block subtitle %}
{% trans "Part Settings" %}
{% endblock %}
{% block settings %}
<h4>{% trans "Part Parameter Templates" %}</h4>
<div id='param-buttons'>

View File

@ -0,0 +1,13 @@
{% extends "InvenTree/settings/settings.html" %}
{% load i18n %}
{% block tabs %}
{% include "InvenTree/settings/tabs.html" with tab='po' %}
{% endblock %}
{% block subtitle %}
{% trans "Purchase Order Settings" %}
{% endblock %}
{% block settings %}
{% endblock %}

View File

@ -1,15 +1,16 @@
{% extends "base.html" %}
{% load i18n %}
{% load static %}
{% block page_title %}
InvenTree | Settings
InvenTree | {% trans "Settings" %}
{% endblock %}
{% block content %}
<div class='settings-container'>
<h3>InvenTree Settings</h3>
<h3>InvenTree {% trans "Settings" %}</h3>
<hr>
<div class='settings-nav'>
@ -19,6 +20,12 @@ InvenTree | Settings
</div>
<div class='settings-content'>
<h3>
{% block subtitle %}
SUBTITLE GOES HERE
{% endblock %}
</h3>
<hr>
{% block settings %}
{% endblock %}
</div>

View File

@ -0,0 +1,13 @@
{% extends "InvenTree/settings/settings.html" %}
{% load i18n %}
{% block tabs %}
{% include "InvenTree/settings/tabs.html" with tab='so' %}
{% endblock %}
{% block subtitle %}
{% trans "Sales Order Settings" %}
{% endblock %}
{% block settings %}
{% endblock %}

View File

@ -0,0 +1,13 @@
{% extends "InvenTree/settings/settings.html" %}
{% load i18n %}
{% block tabs %}
{% include "InvenTree/settings/tabs.html" with tab='stock' %}
{% endblock %}
{% block subtitle %}
{% trans "Stock Settings" %}
{% endblock %}
{% block settings %}
{% endblock %}

View File

@ -1,19 +1,32 @@
{% load i18n %}
<h4><span class='fas fa-user'></span> {% trans "User Settings" %}</h4>
<ul class='nav nav-pills nav-stacked'>
<li{% ifequal tab 'user' %} class='active'{% endifequal %}>
<a href="{% url 'settings-user' %}"><span class='fas fa-user'></span> User</a>
</li>
<li{% ifequal tab 'currency' %} class='active'{% endifequal %}>
<a href="{% url 'settings-currency' %}"><span class='fas fa-dollar-sign'></span> Currency</a>
</li>
<li{% ifequal tab 'part' %} class='active'{% endifequal %}>
<a href="{% url 'settings-part' %}"><span class='fas fa-shapes'></span> Part</a>
<a href="{% url 'settings-user' %}"><span class='fas fa-user'></span> {% trans "Account" %}</a>
</li>
<li{% ifequal tab 'theme' %} class='active'{% endifequal %}>
<a href="{% url 'settings-theme' %}"><span class='fas fa-fill'></span> Theme</a>
<a href="{% url 'settings-theme' %}"><span class='fas fa-fill'></span> {% trans "Theme" %}</a>
</li>
{% if user.is_staff %}
<li{% ifequal tab 'other' %} class='active'{% endifequal %}>
<a href="{% url 'settings-other' %}"><span class='fas fa-cogs'></span> Other</a>
</ul>
<h4><span class='fas fa-cogs'></span> {% trans "InvenTree Settings" %}</h4>
<ul class='nav nav-pills nav-stacked'>
<li{% ifequal tab 'currency' %} class='active'{% endifequal %}>
<a href="{% url 'settings-currency' %}"><span class='fas fa-dollar-sign'></span> {% trans "Currency" %}</a>
</li>
<li{% ifequal tab 'part' %} class='active'{% endifequal %}>
<a href="{% url 'settings-part' %}"><span class='fas fa-shapes'></span> {% trans "Parts" %}</a>
</li>
<li {% if tab == 'stock' %} class='active'{% endif %}>
<a href='{% url 'settings-stock' %}'><span class='fas fa-boxes'></span> {% trans "Stock" %}</a>
</li>
<li {% if tab == 'build' %} class='active'{% endif %}>
<a href="{% url 'settings-build' %}"><span class='fas fa-tools'></span> {% trans "Build Orders" %}</a>
</li>
<li {% if tab == 'po' %} class='active'{% endif %}>
<a href="{% url 'settings-po' %}"><span class='fas fa-shopping-cart'></span> {% trans "Purchase Orders" %}</a>
</li>
<li {% if tab == 'so' %} class='active'{% endif %}>
<a href="{% url 'settings-so' %}"><span class='fas fa-truck'></span> {% trans "Sales Orders" %}</a>
</li>
{% endif %}
</ul>

View File

@ -6,11 +6,15 @@
{% include "InvenTree/settings/tabs.html" with tab='theme' %}
{% endblock %}
{% block subtitle %}
{% trans "Theme Settings" %}
{% endblock %}
{% block settings %}
<div class='row'>
<div class='col-sm-6'>
<h4>Color Themes</h4>
<h4>{% trans "Color Themes" %}</h4>
</div>
</div>

View File

@ -1,41 +1,43 @@
{% extends "InvenTree/settings/settings.html" %}
{% load i18n %}
{% block tabs %}
{% include "InvenTree/settings/tabs.html" with tab='user' %}
{% endblock %}
{% block subtitle %}
{% trans "User Settings" %}
{% endblock %}
{% block settings %}
<div class='row'>
<div class='col-sm-6'>
<h4>User Information</h4>
<div class='container'>
<h4>{% trans "User Information" %}</h4>
<div class='btn-group' style='float: right;'>
<div class='btn btn-primary' type='button' id='edit-user' title='Edit User Information'>Edit</div>
<div class='btn btn-primary' type='button' id='edit-password' title='Change Password'>Set Password</div>
</div>
<div class='col-sm-6'>
<div class='btn-group' style='float: right;'>
<div class='btn btn-primary' type='button' id='edit-user' title='Edit User Information'>Edit</div>
<div class='btn btn-primary' type='button' id='edit-password' title='Change Password'>Set Password</div>
</div>
</div>
</div>
<table class='table table-striped table-condensed'>
<tr>
<td>Username</td>
<td>{{ user.username }}</td>
</tr>
<tr>
<td>First Name</td>
<td>{{ user.first_name }}</td>
</tr>
<tr>
<td>Last Name</td>
<td>{{ user.last_name }}</td>
</tr>
<tr>
<td>Email Address</td>
<td>{{ user.email }}</td>
</tr>
</table>
<table class='table table-striped table-condensed'>
<tr>
<td>{% trans "Username" %}</td>
<td>{{ user.username }}</td>
</tr>
<tr>
<td>{% trans "First Name" %}</td>
<td>{{ user.first_name }}</td>
</tr>
<tr>
<td>{% trans "Last Name" %}</td>
<td>{{ user.last_name }}</td>
</tr>
<tr>
<td>{% trans "Email Address" %}</td>
<td>{{ user.email }}</td>
</tr>
</table>
</div>
{% endblock %}

View File

@ -1,4 +1,5 @@
{% load i18n %}
{% load inventree_extras %}
function loadBuildTable(table, options) {
// Display a table of Build objects
@ -35,13 +36,26 @@ function loadBuildTable(table, options) {
switchable: false,
},
{
field: 'title',
field: 'reference',
title: '{% trans "Build" %}',
sortable: true,
switchable: false,
formatter: function(value, row, index, field) {
var prefix = "{% inventree_setting 'BUILDORDER_REFERENCE_PREFIX' %}";
if (prefix) {
value = `${prefix}${value}`;
}
return renderLink(value, '/build/' + row.pk + '/');
}
},
{
field: 'title',
title: '{% trans "Description" %}',
sortable: true,
},
{
field: 'part',
title: '{% trans "Part" %}',

View File

@ -1,4 +1,5 @@
{% load i18n %}
{% load inventree_extras %}
function removeOrderRowFromOrderWizard(e) {
/* Remove a part selection from an order form. */
@ -137,6 +138,13 @@ function loadPurchaseOrderTable(table, options) {
field: 'reference',
title: '{% trans "Purchase Order" %}',
formatter: function(value, row, index, field) {
var prefix = "{% inventree_setting 'PURCHASEORDER_REFERENCE_PREFIX' %}";
if (prefix) {
value = `${prefix}${value}`;
}
return renderLink(value, `/order/purchase-order/${row.pk}/`);
}
},
@ -212,6 +220,13 @@ function loadSalesOrderTable(table, options) {
field: 'reference',
title: '{% trans "Sales Order" %}',
formatter: function(value, row, index, field) {
var prefix = "{% inventree_setting 'SALESORDER_REFERENCE_PREFIX' %}";
if (prefix) {
value = `${prefix}${value}`;
}
return renderLink(value, `/order/sales-order/${row.pk}/`);
},
},