mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'master' of git://github.com/inventree/InvenTree into unique_email_company
This commit is contained in:
commit
7bc925d016
@ -19,9 +19,18 @@ from django.contrib.auth.models import Permission
|
|||||||
|
|
||||||
import InvenTree.version
|
import InvenTree.version
|
||||||
|
|
||||||
|
from common.models import InvenTreeSetting
|
||||||
from .settings import MEDIA_URL, STATIC_URL
|
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):
|
def generateTestKey(test_name):
|
||||||
"""
|
"""
|
||||||
Generate a test 'key' for a given test name.
|
Generate a test 'key' for a given test name.
|
||||||
|
@ -354,7 +354,7 @@ function renderErrorMessage(xhr) {
|
|||||||
|
|
||||||
var html = '<b>' + xhr.statusText + '</b><br>';
|
var html = '<b>' + xhr.statusText + '</b><br>';
|
||||||
|
|
||||||
html += '<b>Status Code - ' + xhr.status + '</b><br><hr>';
|
html += '<b>Error Code - ' + xhr.status + '</b><br><hr>';
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
<div class='panel-group'>
|
<div class='panel-group'>
|
||||||
@ -811,16 +811,37 @@ function launchModalForm(url, options = {}) {
|
|||||||
$(modal).modal('hide');
|
$(modal).modal('hide');
|
||||||
|
|
||||||
// Permission denied!
|
// Permission denied!
|
||||||
if (xhr.status == 403) {
|
if (xhr.status == 400) {
|
||||||
showAlertDialog(
|
showAlertDialog(
|
||||||
"Permission Denied",
|
"Error 400: Bad Request",
|
||||||
|
"Server returned error code 400"
|
||||||
|
);
|
||||||
|
} else if (xhr.status == 401) {
|
||||||
|
showAlertDialog(
|
||||||
|
"Error 401: Not Authenticated",
|
||||||
|
"Authentication credentials not supplied"
|
||||||
|
);
|
||||||
|
} else if (xhr.status == 403) {
|
||||||
|
showAlertDialog(
|
||||||
|
"Error 403: Permission Denied",
|
||||||
"You do not have the required permissions to access this function"
|
"You do not have the required permissions to access this function"
|
||||||
);
|
);
|
||||||
|
} else if (xhr.status == 404) {
|
||||||
return;
|
showAlertDialog(
|
||||||
|
"Error 404: Resource Not Found",
|
||||||
|
"The requested resource could not be located on the server"
|
||||||
|
);
|
||||||
|
} else if (xhr.status == 408) {
|
||||||
|
showAlertDialog(
|
||||||
|
"Error 408: Timeout",
|
||||||
|
"Connection timeout while requesting data from server"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showAlertDialog('Error requesting form data', renderErrorMessage(xhr));
|
||||||
}
|
}
|
||||||
|
|
||||||
showAlertDialog('Error requesting form data', renderErrorMessage(xhr));
|
console.log("Modal form error: " + xhr.status);
|
||||||
|
console.log("Message: " + xhr.responseText);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -181,8 +181,15 @@ function customGroupSorter(sortName, sortOrder, sortData) {
|
|||||||
sortData.sort(function(a, b) {
|
sortData.sort(function(a, b) {
|
||||||
|
|
||||||
// Extract default field values
|
// Extract default field values
|
||||||
var aa = a[sortName];
|
// Allow multi-level access if required
|
||||||
var bb = b[sortName];
|
// Ref: https://stackoverflow.com/a/6394168
|
||||||
|
|
||||||
|
function extract(obj, i) {
|
||||||
|
return obj[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
var aa = sortName.split('.').reduce(extract, a);
|
||||||
|
var bb = sortName.split('.').reduce(extract, b);
|
||||||
|
|
||||||
// Extract parent information
|
// Extract parent information
|
||||||
var aparent = a._data && a._data['parent-index'];
|
var aparent = a._data && a._data['parent-index'];
|
||||||
|
@ -69,10 +69,14 @@ apipatterns = [
|
|||||||
settings_urls = [
|
settings_urls = [
|
||||||
|
|
||||||
url(r'^user/?', SettingsView.as_view(template_name='InvenTree/settings/user.html'), name='settings-user'),
|
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'^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'^part/?', SettingsView.as_view(template_name='InvenTree/settings/part.html'), name='settings-part'),
|
||||||
url(r'^theme/?', ColorThemeSelectView.as_view(), name='settings-theme'),
|
url(r'^stock/?', SettingsView.as_view(template_name='InvenTree/settings/stock.html'), name='settings-stock'),
|
||||||
url(r'^other/?', SettingsView.as_view(template_name='InvenTree/settings/other.html'), name='settings-other'),
|
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
|
# Catch any other urls
|
||||||
url(r'^.*$', SettingsView.as_view(template_name='InvenTree/settings/user.html'), name='settings'),
|
url(r'^.*$', SettingsView.as_view(template_name='InvenTree/settings/user.html'), name='settings'),
|
||||||
|
@ -43,7 +43,7 @@ def validate_part_name(value):
|
|||||||
def validate_part_ipn(value):
|
def validate_part_ipn(value):
|
||||||
""" Validate the Part IPN against regex rule """
|
""" 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:
|
if pattern:
|
||||||
match = re.search(pattern, value)
|
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))
|
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):
|
def validate_tree_name(value):
|
||||||
""" Prevent illegal characters in tree item names """
|
""" Prevent illegal characters in tree item names """
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ INVENTREE_SW_VERSION = "0.1.4 pre"
|
|||||||
|
|
||||||
def inventreeInstanceName():
|
def inventreeInstanceName():
|
||||||
""" Returns the InstanceName settings for the current database """
|
""" 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():
|
def inventreeVersion():
|
||||||
|
@ -42,6 +42,8 @@ class InvenTreeBarcodePlugin(BarcodePlugin):
|
|||||||
elif type(self.data) is str:
|
elif type(self.data) is str:
|
||||||
try:
|
try:
|
||||||
self.data = json.loads(self.data)
|
self.data = json.loads(self.data)
|
||||||
|
if type(self.data) is not dict:
|
||||||
|
return False
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
|
@ -56,6 +56,34 @@ class BarcodeAPITest(APITestCase):
|
|||||||
self.assertIn('plugin', data)
|
self.assertIn('plugin', data)
|
||||||
self.assertIsNone(data['plugin'])
|
self.assertIsNone(data['plugin'])
|
||||||
|
|
||||||
|
def test_integer_barcode(self):
|
||||||
|
|
||||||
|
response = self.postBarcode(self.scan_url, '123456789')
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
data = response.data
|
||||||
|
self.assertIn('error', data)
|
||||||
|
|
||||||
|
self.assertIn('barcode_data', data)
|
||||||
|
self.assertIn('hash', data)
|
||||||
|
self.assertIn('plugin', data)
|
||||||
|
self.assertIsNone(data['plugin'])
|
||||||
|
|
||||||
|
def test_array_barcode(self):
|
||||||
|
|
||||||
|
response = self.postBarcode(self.scan_url, "['foo', 'bar']")
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
data = response.data
|
||||||
|
self.assertIn('error', data)
|
||||||
|
|
||||||
|
self.assertIn('barcode_data', data)
|
||||||
|
self.assertIn('hash', data)
|
||||||
|
self.assertIn('plugin', data)
|
||||||
|
self.assertIsNone(data['plugin'])
|
||||||
|
|
||||||
def test_barcode_generation(self):
|
def test_barcode_generation(self):
|
||||||
|
|
||||||
item = StockItem.objects.get(pk=522)
|
item = StockItem.objects.get(pk=522)
|
||||||
|
@ -10,17 +10,16 @@ from .models import Build, BuildItem
|
|||||||
class BuildAdmin(ImportExportModelAdmin):
|
class BuildAdmin(ImportExportModelAdmin):
|
||||||
|
|
||||||
list_display = (
|
list_display = (
|
||||||
|
'reference',
|
||||||
|
'title',
|
||||||
'part',
|
'part',
|
||||||
'status',
|
'status',
|
||||||
'batch',
|
'batch',
|
||||||
'quantity',
|
'quantity',
|
||||||
'creation_date',
|
|
||||||
'completion_date',
|
|
||||||
'title',
|
|
||||||
'notes',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
search_fields = [
|
search_fields = [
|
||||||
|
'reference',
|
||||||
'title',
|
'title',
|
||||||
'part__name',
|
'part__name',
|
||||||
'part__description',
|
'part__description',
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
fields:
|
fields:
|
||||||
part: 25
|
part: 25
|
||||||
batch: 'B1'
|
batch: 'B1'
|
||||||
|
reference: "0001"
|
||||||
title: 'Building 7 parts'
|
title: 'Building 7 parts'
|
||||||
quantity: 7
|
quantity: 7
|
||||||
notes: 'Some simple notes'
|
notes: 'Some simple notes'
|
||||||
@ -20,6 +21,7 @@
|
|||||||
pk: 2
|
pk: 2
|
||||||
fields:
|
fields:
|
||||||
part: 50
|
part: 50
|
||||||
|
reference: "0002"
|
||||||
title: 'Making things'
|
title: 'Making things'
|
||||||
batch: 'B2'
|
batch: 'B2'
|
||||||
status: 40 # COMPLETE
|
status: 40 # COMPLETE
|
||||||
|
@ -17,14 +17,26 @@ class EditBuildForm(HelperForm):
|
|||||||
""" Form for editing a Build object.
|
""" 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:
|
class Meta:
|
||||||
model = Build
|
model = Build
|
||||||
fields = [
|
fields = [
|
||||||
|
'reference',
|
||||||
'title',
|
'title',
|
||||||
'part',
|
'part',
|
||||||
|
'quantity',
|
||||||
'parent',
|
'parent',
|
||||||
'sales_order',
|
'sales_order',
|
||||||
'quantity',
|
|
||||||
'take_from',
|
'take_from',
|
||||||
'batch',
|
'batch',
|
||||||
'link',
|
'link',
|
||||||
|
64
InvenTree/build/migrations/0018_build_reference.py
Normal file
64
InvenTree/build/migrations/0018_build_reference.py
Normal 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'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]
|
23
InvenTree/build/migrations/0019_auto_20201019_1302.py
Normal file
23
InvenTree/build/migrations/0019_auto_20201019_1302.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
18
InvenTree/build/migrations/0020_auto_20201019_1325.py
Normal file
18
InvenTree/build/migrations/0020_auto_20201019_1325.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
@ -22,7 +22,9 @@ from markdownx.models import MarkdownxField
|
|||||||
from mptt.models import MPTTModel, TreeForeignKey
|
from mptt.models import MPTTModel, TreeForeignKey
|
||||||
|
|
||||||
from InvenTree.status_codes import BuildStatus
|
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
|
import InvenTree.fields
|
||||||
|
|
||||||
from stock import models as StockModels
|
from stock import models as StockModels
|
||||||
@ -34,6 +36,7 @@ class Build(MPTTModel):
|
|||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
part: The part to be built (from component BOM items)
|
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)
|
title: Brief title describing the build (required)
|
||||||
quantity: Number of units to be built
|
quantity: Number of units to be built
|
||||||
parent: Reference to a Build object for which this Build is required
|
parent: Reference to a Build object for which this Build is required
|
||||||
@ -47,8 +50,15 @@ class Build(MPTTModel):
|
|||||||
notes: Text notes
|
notes: Text notes
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Build Order")
|
||||||
|
verbose_name_plural = _("Build Orders")
|
||||||
|
|
||||||
def __str__(self):
|
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):
|
def get_absolute_url(self):
|
||||||
return reverse('build-detail', kwargs={'pk': self.id})
|
return reverse('build-detail', kwargs={'pk': self.id})
|
||||||
@ -69,8 +79,19 @@ class Build(MPTTModel):
|
|||||||
except PartModels.Part.DoesNotExist:
|
except PartModels.Part.DoesNotExist:
|
||||||
pass
|
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(
|
title = models.CharField(
|
||||||
verbose_name=_('Build Title'),
|
verbose_name=_('Description'),
|
||||||
blank=False,
|
blank=False,
|
||||||
max_length=100,
|
max_length=100,
|
||||||
help_text=_('Brief description of the build')
|
help_text=_('Brief description of the build')
|
||||||
@ -165,6 +186,38 @@ class Build(MPTTModel):
|
|||||||
def output_count(self):
|
def output_count(self):
|
||||||
return self.build_outputs.count()
|
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
|
@transaction.atomic
|
||||||
def cancelBuild(self, user):
|
def cancelBuild(self, user):
|
||||||
""" Mark the Build as CANCELLED
|
""" Mark the Build as CANCELLED
|
||||||
|
@ -41,6 +41,7 @@ class BuildSerializer(InvenTreeModelSerializer):
|
|||||||
'completion_date',
|
'completion_date',
|
||||||
'part',
|
'part',
|
||||||
'part_detail',
|
'part_detail',
|
||||||
|
'reference',
|
||||||
'sales_order',
|
'sales_order',
|
||||||
'quantity',
|
'quantity',
|
||||||
'status',
|
'status',
|
||||||
|
@ -5,18 +5,18 @@
|
|||||||
{% load status_codes %}
|
{% load status_codes %}
|
||||||
|
|
||||||
{% block page_title %}
|
{% block page_title %}
|
||||||
InvenTree | {% trans "Build" %} - {{ build }}
|
InvenTree | {% trans "Build Order" %} - {{ build }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block pre_content %}
|
{% block pre_content %}
|
||||||
{% if build.sales_order %}
|
{% if build.sales_order %}
|
||||||
<div class='alert alert-block alert-info'>
|
<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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if build.parent %}
|
{% if build.parent %}
|
||||||
<div class='alert alert-block alert-info'>
|
<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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -31,14 +31,15 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block page_data %}
|
{% block page_data %}
|
||||||
<h3>{% trans "Build" %} {% build_status_label build.status large=True %}</h3>
|
<h3>
|
||||||
<hr>
|
{% trans "Build Order" %} {{ build }}
|
||||||
<h4>
|
|
||||||
{{ build.quantity }} x {{ build.part.full_name }}
|
|
||||||
{% if user.is_staff and roles.build.change %}
|
{% 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>
|
<a href="{% url 'admin:build_build_change' build.pk %}"><span title="{% trans 'Admin view' %}" class='fas fa-user-shield'></span></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h4>
|
</h3>
|
||||||
|
<h3>{% build_status_label build.status large=True %}</h3>
|
||||||
|
<hr>
|
||||||
|
<p>{{ build.title }}</p>
|
||||||
<div class='btn-row'>
|
<div class='btn-row'>
|
||||||
<div class='btn-group action-buttons'>
|
<div class='btn-group action-buttons'>
|
||||||
{% if roles.build.change %}
|
{% if roles.build.change %}
|
||||||
@ -68,9 +69,9 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
<h4>{% trans "Build Details" %}</h4>
|
<h4>{% trans "Build Details" %}</h4>
|
||||||
<table class='table table-striped table-condensed'>
|
<table class='table table-striped table-condensed'>
|
||||||
<tr>
|
<tr>
|
||||||
<td></td>
|
<td><span class='fas fa-hashtag'></span></td>
|
||||||
<td>{% trans "Build Title" %}</td>
|
<td>{% trans "Build Order Reference" %}</td>
|
||||||
<td>{{ build.title }}</td>
|
<td>{{ build }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-shapes'></span></td>
|
<td><span class='fas fa-shapes'></span></td>
|
||||||
|
@ -54,7 +54,7 @@ class BuildTestSimple(TestCase):
|
|||||||
self.assertEqual(b.batch, 'B2')
|
self.assertEqual(b.batch, 'B2')
|
||||||
self.assertEqual(b.quantity, 21)
|
self.assertEqual(b.quantity, 21)
|
||||||
|
|
||||||
self.assertEqual(str(b), '21 x Orphan')
|
self.assertEqual(str(b), 'BO0002')
|
||||||
|
|
||||||
def test_url(self):
|
def test_url(self):
|
||||||
b1 = Build.objects.get(pk=1)
|
b1 = Build.objects.get(pk=1)
|
||||||
|
@ -400,7 +400,7 @@ class BuildCreate(AjaxCreateView):
|
|||||||
model = Build
|
model = Build
|
||||||
context_object_name = 'build'
|
context_object_name = 'build'
|
||||||
form_class = forms.EditBuildForm
|
form_class = forms.EditBuildForm
|
||||||
ajax_form_title = _('Start new Build')
|
ajax_form_title = _('New Build Order')
|
||||||
ajax_template_name = 'modal_form.html'
|
ajax_template_name = 'modal_form.html'
|
||||||
role_required = 'build.add'
|
role_required = 'build.add'
|
||||||
|
|
||||||
@ -415,6 +415,8 @@ class BuildCreate(AjaxCreateView):
|
|||||||
# User has provided a Part ID
|
# User has provided a Part ID
|
||||||
initials['part'] = self.request.GET.get('part', None)
|
initials['part'] = self.request.GET.get('part', None)
|
||||||
|
|
||||||
|
initials['reference'] = Build.getNextBuildNumber()
|
||||||
|
|
||||||
initials['parent'] = self.request.GET.get('parent', None)
|
initials['parent'] = self.request.GET.get('parent', None)
|
||||||
|
|
||||||
# User has provided a SalesOrder ID
|
# User has provided a SalesOrder ID
|
||||||
|
@ -14,7 +14,7 @@ class CurrencyAdmin(ImportExportModelAdmin):
|
|||||||
|
|
||||||
class SettingsAdmin(ImportExportModelAdmin):
|
class SettingsAdmin(ImportExportModelAdmin):
|
||||||
|
|
||||||
list_display = ('key', 'value', 'description')
|
list_display = ('key', 'value')
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Currency, CurrencyAdmin)
|
admin.site.register(Currency, CurrencyAdmin)
|
||||||
|
@ -1,9 +1,5 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.db.utils import OperationalError, ProgrammingError
|
from django.db.utils import OperationalError, ProgrammingError, IntegrityError
|
||||||
|
|
||||||
import os
|
|
||||||
import uuid
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
|
|
||||||
class CommonConfig(AppConfig):
|
class CommonConfig(AppConfig):
|
||||||
@ -12,45 +8,8 @@ class CommonConfig(AppConfig):
|
|||||||
def ready(self):
|
def ready(self):
|
||||||
|
|
||||||
""" Will be called when the Common app is first loaded """
|
""" Will be called when the Common app is first loaded """
|
||||||
self.populate_default_settings()
|
|
||||||
self.add_instance_name()
|
self.add_instance_name()
|
||||||
|
self.add_default_settings()
|
||||||
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
|
|
||||||
|
|
||||||
def add_instance_name(self):
|
def add_instance_name(self):
|
||||||
"""
|
"""
|
||||||
@ -61,20 +20,73 @@ class CommonConfig(AppConfig):
|
|||||||
# See note above
|
# See note above
|
||||||
from .models import InvenTreeSetting
|
from .models import InvenTreeSetting
|
||||||
|
|
||||||
|
"""
|
||||||
|
Note: The "old" instance name was stored under the key 'InstanceName',
|
||||||
|
but has now been renamed to 'INVENTREE_INSTANCE'.
|
||||||
|
"""
|
||||||
|
|
||||||
try:
|
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(
|
# Use the old name if it exists
|
||||||
key="InstanceName",
|
if InvenTreeSetting.objects.filter(key='InstanceName').exists():
|
||||||
value=val,
|
instance = InvenTreeSetting.objects.get(key='InstanceName')
|
||||||
description="Instance name for this InvenTree database installation."
|
instance_name = instance.value
|
||||||
)
|
|
||||||
|
|
||||||
name.save()
|
# Delete the legacy key
|
||||||
except (OperationalError, ProgrammingError):
|
instance.delete()
|
||||||
|
|
||||||
|
# Create new value
|
||||||
|
InvenTreeSetting.objects.create(
|
||||||
|
key='INVENTREE_INSTANCE',
|
||||||
|
value=instance_name
|
||||||
|
)
|
||||||
|
|
||||||
|
except (OperationalError, ProgrammingError, IntegrityError):
|
||||||
# Migrations have not yet been applied - table does not exist
|
# Migrations have not yet been applied - table does not exist
|
||||||
pass
|
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, IntegrityError):
|
||||||
|
# Table might not yet exist
|
||||||
|
pass
|
||||||
|
@ -7,7 +7,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from InvenTree.forms import HelperForm
|
from InvenTree.forms import HelperForm
|
||||||
|
|
||||||
from .models import Currency
|
from .models import Currency, InvenTreeSetting
|
||||||
|
|
||||||
|
|
||||||
class CurrencyEditForm(HelperForm):
|
class CurrencyEditForm(HelperForm):
|
||||||
@ -22,3 +22,17 @@ class CurrencyEditForm(HelperForm):
|
|||||||
'value',
|
'value',
|
||||||
'base'
|
'base'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class SettingEditForm(HelperForm):
|
||||||
|
"""
|
||||||
|
Form for creating / editing a settings object
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = InvenTreeSetting
|
||||||
|
|
||||||
|
fields = [
|
||||||
|
'key',
|
||||||
|
'value'
|
||||||
|
]
|
||||||
|
@ -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'
|
|
@ -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',
|
||||||
|
),
|
||||||
|
]
|
@ -27,6 +27,30 @@ class InvenTreeSetting(models.Model):
|
|||||||
even if that key does not exist.
|
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:
|
class Meta:
|
||||||
verbose_name = "InvenTree Setting"
|
verbose_name = "InvenTree Setting"
|
||||||
verbose_name_plural = "InvenTree Settings"
|
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 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:
|
try:
|
||||||
setting = InvenTreeSetting.objects.get(key__iexact=key)
|
settings = InvenTreeSetting.objects.filter(key__iexact=key)
|
||||||
return setting.value
|
|
||||||
|
if len(settings) > 0:
|
||||||
|
return settings[0].value
|
||||||
|
else:
|
||||||
|
return backup_value
|
||||||
except InvenTreeSetting.DoesNotExist:
|
except InvenTreeSetting.DoesNotExist:
|
||||||
return backup_value
|
return backup_value
|
||||||
|
|
||||||
@ -69,15 +101,13 @@ class InvenTreeSetting(models.Model):
|
|||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
|
||||||
setting.value = value
|
setting.value = str(value)
|
||||||
setting.save()
|
setting.save()
|
||||||
|
|
||||||
key = models.CharField(max_length=50, blank=False, unique=True, help_text=_('Settings key (must be unique - case insensitive'))
|
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'))
|
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):
|
def validate_unique(self, exclude=None):
|
||||||
""" Ensure that the key:value pair is unique.
|
""" Ensure that the key:value pair is unique.
|
||||||
In addition to the base validators, this ensures that the 'key'
|
In addition to the base validators, this ensures that the 'key'
|
||||||
|
@ -2,8 +2,9 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.test import TestCase
|
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):
|
class CurrencyTest(TestCase):
|
||||||
@ -17,3 +18,32 @@ class CurrencyTest(TestCase):
|
|||||||
# Simple test for now (improve this later!)
|
# Simple test for now (improve this later!)
|
||||||
|
|
||||||
self.assertEqual(Currency.objects.count(), 2)
|
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))
|
||||||
|
@ -35,3 +35,14 @@ class CurrencyDelete(AjaxDeleteView):
|
|||||||
model = models.Currency
|
model = models.Currency
|
||||||
ajax_form_title = _('Delete Currency')
|
ajax_form_title = _('Delete Currency')
|
||||||
ajax_template_name = "common/delete_currency.html"
|
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
|
||||||
|
@ -424,12 +424,16 @@ class SupplierPart(models.Model):
|
|||||||
return str(self)
|
return str(self)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
s = "{supplier} ({sku})".format(
|
s = ''
|
||||||
sku=self.SKU,
|
|
||||||
supplier=self.supplier.name)
|
if self.part.IPN:
|
||||||
|
s += f'{self.part.IPN}'
|
||||||
|
s += ' | '
|
||||||
|
|
||||||
|
s += f'{self.supplier.name} | {self.SKU}'
|
||||||
|
|
||||||
if self.manufacturer_string:
|
if self.manufacturer_string:
|
||||||
s = s + ' - ' + self.manufacturer_string
|
s = s + ' | ' + self.manufacturer_string
|
||||||
|
|
||||||
return s
|
return s
|
||||||
|
|
||||||
|
@ -94,7 +94,7 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
{% block js_ready %}
|
{% block js_ready %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
|
||||||
$('#order-part').click(function() {
|
$('#order-part, #order-part2').click(function() {
|
||||||
launchModalForm(
|
launchModalForm(
|
||||||
"{% url 'order-parts' %}",
|
"{% url 'order-parts' %}",
|
||||||
{
|
{
|
||||||
|
@ -6,18 +6,18 @@
|
|||||||
|
|
||||||
{% include "company/supplier_part_tabs.html" with tab='orders' %}
|
{% include "company/supplier_part_tabs.html" with tab='orders' %}
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h4>{% trans "Supplier Part Orders" %}</h4>
|
<h4>{% trans "Supplier Part Orders" %}</h4>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
<div id='button-bar'>
|
<div id='button-bar'>
|
||||||
<div class='btn-group'>
|
<div class='btn-group'>
|
||||||
<button class='btn btn-primary' type='button' id='part-order2' title='Order part'>Order Part</button>
|
<button class='btn btn-primary' type='button' id='order-part2' title='Order part'>Order Part</button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<table class='table table-striped table-condensed po-table' id='purchase-order-table' data-toolbar='#button-bar'>
|
<table class='table table-striped table-condensed po-table' id='purchase-order-table' data-toolbar='#button-bar'>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@ -7,10 +7,10 @@
|
|||||||
|
|
||||||
{% include "company/supplier_part_tabs.html" with tab='pricing' %}
|
{% include "company/supplier_part_tabs.html" with tab='pricing' %}
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h4>{% trans "Pricing Information" %}</h4>
|
<h4>{% trans "Pricing Information" %}</h4>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
<div id='price-break-toolbar' class='btn-group'>
|
<div id='price-break-toolbar' class='btn-group'>
|
||||||
<button class='btn btn-primary' id='new-price-break' type='button'>{% trans "Add Price Break" %}</button>
|
<button class='btn btn-primary' id='new-price-break' type='button'>{% trans "Add Price Break" %}</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,10 +6,10 @@
|
|||||||
|
|
||||||
{% include "company/supplier_part_tabs.html" with tab='stock' %}
|
{% include "company/supplier_part_tabs.html" with tab='stock' %}
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h4>{% trans "Supplier Part Stock" %}</h4>
|
<h4>{% trans "Supplier Part Stock" %}</h4>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
{% include "stock_table.html" %}
|
{% include "stock_table.html" %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
Binary file not shown.
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
@ -96,7 +96,7 @@ class EditPurchaseOrderForm(HelperForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.field_placeholder = {
|
self.field_placeholder = {
|
||||||
'reference': _('Enter purchase order number'),
|
'reference': _('Purchase Order reference'),
|
||||||
}
|
}
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
@ -20,46 +20,44 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block page_data %}
|
{% block page_data %}
|
||||||
<h3>{% trans "Purchase Order" %} {% purchase_order_status_label order.status large=True %}</h3>
|
<h3>
|
||||||
<hr>
|
{% trans "Purchase Order" %} {{ order.reference }}
|
||||||
<h4>
|
|
||||||
{{ order }}
|
|
||||||
{% if user.is_staff and roles.purchase_order.change %}
|
{% 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>
|
<a href="{% url 'admin:order_purchaseorder_change' order.pk %}"><span title='{% trans "Admin view" %}' class='fas fa-user-shield'></span></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h4>
|
</h3>
|
||||||
|
<h3>{% purchase_order_status_label order.status large=True %}</h3>
|
||||||
|
<hr>
|
||||||
<p>{{ order.description }}</p>
|
<p>{{ order.description }}</p>
|
||||||
<p>
|
<div class='btn-row'>
|
||||||
<div class='btn-row'>
|
<div class='btn-group action-buttons'>
|
||||||
<div class='btn-group action-buttons'>
|
{% if roles.purchase_order.change %}
|
||||||
{% if roles.purchase_order.change %}
|
<button type='button' class='btn btn-default' id='edit-order' title='{% trans "Edit order information" %}'>
|
||||||
<button type='button' class='btn btn-default' id='edit-order' title='{% trans "Edit order information" %}'>
|
<span class='fas fa-edit icon-green'></span>
|
||||||
<span class='fas fa-edit icon-green'></span>
|
</button>
|
||||||
</button>
|
{% if order.status == PurchaseOrderStatus.PENDING and order.lines.count > 0 %}
|
||||||
{% if order.status == PurchaseOrderStatus.PENDING and order.lines.count > 0 %}
|
<button type='button' class='btn btn-default' id='place-order' title='{% trans "Place order" %}'>
|
||||||
<button type='button' class='btn btn-default' id='place-order' title='{% trans "Place order" %}'>
|
<span class='fas fa-paper-plane icon-blue'></span>
|
||||||
<span class='fas fa-paper-plane icon-blue'></span>
|
</button>
|
||||||
</button>
|
{% elif order.status == PurchaseOrderStatus.PLACED %}
|
||||||
{% elif order.status == PurchaseOrderStatus.PLACED %}
|
<button type='button' class='btn btn-default' id='receive-order' title='{% trans "Receive items" %}'>
|
||||||
<button type='button' class='btn btn-default' id='receive-order' title='{% trans "Receive items" %}'>
|
<span class='fas fa-clipboard-check'></span>
|
||||||
<span class='fas fa-clipboard-check'></span>
|
</button>
|
||||||
</button>
|
<button type='button' class='btn btn-default' id='complete-order' title='{% trans "Mark order as complete" %}'>
|
||||||
<button type='button' class='btn btn-default' id='complete-order' title='{% trans "Mark order as complete" %}'>
|
<span class='fas fa-check-circle'></span>
|
||||||
<span class='fas fa-check-circle'></span>
|
</button>
|
||||||
</button>
|
{% endif %}
|
||||||
{% endif %}
|
{% if order.status == PurchaseOrderStatus.PENDING or order.status == PurchaseOrderStatus.PLACED %}
|
||||||
{% if order.status == PurchaseOrderStatus.PENDING or order.status == PurchaseOrderStatus.PLACED %}
|
<button type='button' class='btn btn-default' id='cancel-order' title='{% trans "Cancel order" %}'>
|
||||||
<button type='button' class='btn btn-default' id='cancel-order' title='{% trans "Cancel order" %}'>
|
<span class='fas fa-times-circle icon-red'></span>
|
||||||
<span class='fas fa-times-circle icon-red'></span>
|
</button>
|
||||||
</button>
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
<button type='button' class='btn btn-default' id='export-order' title='{% trans "Export order to file" %}'>
|
||||||
<button type='button' class='btn btn-default' id='export-order' title='{% trans "Export order to file" %}'>
|
<span class='fas fa-file-download'></span>
|
||||||
<span class='fas fa-file-download'></span>
|
</button>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</p>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block page_details %}
|
{% block page_details %}
|
||||||
|
34
InvenTree/order/templates/order/po_received_items.html
Normal file
34
InvenTree/order/templates/order/po_received_items.html
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{% extends "order/order_base.html" %}
|
||||||
|
|
||||||
|
{% load inventree_extras %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block details %}
|
||||||
|
|
||||||
|
{% include 'order/po_tabs.html' with tab='received' %}
|
||||||
|
|
||||||
|
<h4>{% trans "Received Items" %}</h4>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
{% include "stock_table.html" with read_only=True %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js_ready %}
|
||||||
|
{{ block.super }}
|
||||||
|
|
||||||
|
loadStockTable($("#stock-table"), {
|
||||||
|
params: {
|
||||||
|
purchase_order: {{ order.id }},
|
||||||
|
part_detail: true,
|
||||||
|
supplier_detail: true,
|
||||||
|
location_detail: true,
|
||||||
|
},
|
||||||
|
buttons: [
|
||||||
|
'#stock-options',
|
||||||
|
],
|
||||||
|
filterkey: "postock"
|
||||||
|
});
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -2,7 +2,10 @@
|
|||||||
|
|
||||||
<ul class='nav nav-tabs'>
|
<ul class='nav nav-tabs'>
|
||||||
<li{% ifequal tab 'details' %} class='active'{% endifequal %}>
|
<li{% ifequal tab 'details' %} class='active'{% endifequal %}>
|
||||||
<a href="{% url 'po-detail' order.id %}">{% trans "Items" %}</a>
|
<a href="{% url 'po-detail' order.id %}">{% trans "Line Items" %}</a>
|
||||||
|
</li>
|
||||||
|
<li {% if tab == 'received' %} class='active'{% endif %}>
|
||||||
|
<a href="{% url 'po-received' order.id %}">{% trans "Received Items" %}</a>
|
||||||
</li>
|
</li>
|
||||||
<li{% if tab == 'attachments' %} class='active'{% endif %}>
|
<li{% if tab == 'attachments' %} class='active'{% endif %}>
|
||||||
<a href="{% url 'po-attachments' order.id %}">{% trans "Attachments" %}
|
<a href="{% url 'po-attachments' order.id %}">{% trans "Attachments" %}
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
<div id='order-toolbar-buttons' class='btn-group' style='float: right;'>
|
<div id='order-toolbar-buttons' class='btn-group' style='float: right;'>
|
||||||
{% if order.status == PurchaseOrderStatus.PENDING and roles.purchase_order.change %}
|
{% if order.status == PurchaseOrderStatus.PENDING and roles.purchase_order.change %}
|
||||||
<button type='button' class='btn btn-default' id='new-po-line'>{% trans "Add Line Item" %}</button>
|
<button type='button' class='btn btn-primary' id='new-po-line'>{% trans "Add Line Item" %}</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -27,17 +27,18 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
/>
|
/>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block page_data %}
|
{% block page_data %}
|
||||||
|
|
||||||
<h3>{% trans "Sales Order" %} {% sales_order_status_label order.status large=True %}</h3>
|
<h3>
|
||||||
<hr>
|
{% trans "Sales Order" %} {{ order.reference }}
|
||||||
<h4>
|
|
||||||
{{ order }}
|
|
||||||
{% if user.is_staff and roles.sales_order.change %}
|
{% 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>
|
<a href="{% url 'admin:order_salesorder_change' order.pk %}"><span title='{% trans "Admin view" %}' class='fas fa-user-shield'></span></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h4>
|
</h3>
|
||||||
|
<h3>
|
||||||
|
{% sales_order_status_label order.status large=True %}
|
||||||
|
</h3>
|
||||||
|
<hr>
|
||||||
<p>{{ order.description }}</p>
|
<p>{{ order.description }}</p>
|
||||||
<div class='btn-row'>
|
<div class='btn-row'>
|
||||||
<div class='btn-group action-buttons'>
|
<div class='btn-group action-buttons'>
|
||||||
|
@ -21,6 +21,7 @@ purchase_order_detail_urls = [
|
|||||||
|
|
||||||
url(r'^notes/', views.PurchaseOrderNotes.as_view(), name='po-notes'),
|
url(r'^notes/', views.PurchaseOrderNotes.as_view(), name='po-notes'),
|
||||||
|
|
||||||
|
url(r'^received/', views.PurchaseOrderDetail.as_view(template_name='order/po_received_items.html'), name='po-received'),
|
||||||
url(r'^attachments/', views.PurchaseOrderDetail.as_view(template_name='order/po_attachments.html'), name='po-attachments'),
|
url(r'^attachments/', views.PurchaseOrderDetail.as_view(template_name='order/po_attachments.html'), name='po-attachments'),
|
||||||
url(r'^.*$', views.PurchaseOrderDetail.as_view(), name='po-detail'),
|
url(r'^.*$', views.PurchaseOrderDetail.as_view(), name='po-detail'),
|
||||||
]
|
]
|
||||||
|
@ -2,30 +2,20 @@
|
|||||||
JSON serializers for Part app
|
JSON serializers for Part app
|
||||||
"""
|
"""
|
||||||
import imghdr
|
import imghdr
|
||||||
|
|
||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
from .models import Part, PartStar
|
|
||||||
|
|
||||||
from .models import PartCategory
|
|
||||||
from .models import BomItem
|
|
||||||
from .models import PartParameter, PartParameterTemplate
|
|
||||||
from .models import PartAttachment
|
|
||||||
from .models import PartTestTemplate
|
|
||||||
from .models import PartSellPriceBreak
|
|
||||||
|
|
||||||
from stock.models import StockItem
|
|
||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from sql_util.utils import SubquerySum, SubqueryCount
|
|
||||||
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
|
from InvenTree.serializers import (InvenTreeAttachmentSerializerField,
|
||||||
|
InvenTreeModelSerializer)
|
||||||
|
from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus
|
||||||
|
from rest_framework import serializers
|
||||||
|
from sql_util.utils import SubqueryCount, SubquerySum
|
||||||
|
from stock.models import StockItem
|
||||||
|
|
||||||
from InvenTree.status_codes import PurchaseOrderStatus, BuildStatus
|
from .models import (BomItem, Part, PartAttachment, PartCategory,
|
||||||
from InvenTree.serializers import InvenTreeModelSerializer
|
PartParameter, PartParameterTemplate, PartSellPriceBreak,
|
||||||
from InvenTree.serializers import InvenTreeAttachmentSerializerField
|
PartStar, PartTestTemplate)
|
||||||
|
|
||||||
|
|
||||||
class CategorySerializer(InvenTreeModelSerializer):
|
class CategorySerializer(InvenTreeModelSerializer):
|
||||||
@ -41,6 +31,7 @@ class CategorySerializer(InvenTreeModelSerializer):
|
|||||||
'pk',
|
'pk',
|
||||||
'name',
|
'name',
|
||||||
'description',
|
'description',
|
||||||
|
'default_location',
|
||||||
'pathstring',
|
'pathstring',
|
||||||
'url',
|
'url',
|
||||||
'parent',
|
'parent',
|
||||||
@ -304,6 +295,7 @@ class PartSerializer(InvenTreeModelSerializer):
|
|||||||
'category_detail',
|
'category_detail',
|
||||||
'component',
|
'component',
|
||||||
'description',
|
'description',
|
||||||
|
'default_location',
|
||||||
'full_name',
|
'full_name',
|
||||||
'image',
|
'image',
|
||||||
'in_stock',
|
'in_stock',
|
||||||
|
@ -88,7 +88,7 @@ def inventree_docs_url(*args, **kwargs):
|
|||||||
|
|
||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
def inventree_setting(key, *args, **kwargs):
|
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()
|
@register.simple_tag()
|
||||||
|
@ -493,9 +493,9 @@ class PartDuplicate(AjaxCreateView):
|
|||||||
else:
|
else:
|
||||||
initials = super(AjaxCreateView, self).get_initial()
|
initials = super(AjaxCreateView, self).get_initial()
|
||||||
|
|
||||||
initials['bom_copy'] = str2bool(InvenTreeSetting.get_setting('part_deep_copy', True))
|
initials['bom_copy'] = str2bool(InvenTreeSetting.get_setting('PART_COPY_BOM', True))
|
||||||
# Create new entry in InvenTree/common/kvp.yaml?
|
|
||||||
initials['parameters_copy'] = str2bool(InvenTreeSetting.get_setting('part_deep_copy', True))
|
initials['parameters_copy'] = str2bool(InvenTreeSetting.get_setting('PART_COPY_PARAMETERS', True))
|
||||||
|
|
||||||
return initials
|
return initials
|
||||||
|
|
||||||
|
@ -324,8 +324,31 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
serializer_class = StockItemSerializer
|
serializer_class = StockItemSerializer
|
||||||
queryset = StockItem.objects.all()
|
queryset = StockItem.objects.all()
|
||||||
|
|
||||||
# TODO - Override the 'create' method for this view,
|
def create(self, request, *args, **kwargs):
|
||||||
# to allow the user to be recorded when a new StockItem object is created
|
"""
|
||||||
|
Create a new StockItem object via the API.
|
||||||
|
|
||||||
|
We override the default 'create' implementation.
|
||||||
|
|
||||||
|
If a location is *not* specified, but the linked *part* has a default location,
|
||||||
|
we can pre-fill the location automatically.
|
||||||
|
"""
|
||||||
|
|
||||||
|
serializer = self.get_serializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
item = serializer.save()
|
||||||
|
|
||||||
|
# A location was *not* specified - try to infer it
|
||||||
|
if 'location' not in request.data:
|
||||||
|
location = item.part.get_default_location()
|
||||||
|
if location is not None:
|
||||||
|
item.location = location
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
# Return a response
|
||||||
|
headers = self.get_success_headers(serializer.data)
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||||
|
|
||||||
def list(self, request, *args, **kwargs):
|
def list(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
@ -475,6 +498,11 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
if sales_order:
|
if sales_order:
|
||||||
queryset = queryset.filter(sales_order=sales_order)
|
queryset = queryset.filter(sales_order=sales_order)
|
||||||
|
|
||||||
|
purchase_order = params.get('purchase_order', None)
|
||||||
|
|
||||||
|
if purchase_order is not None:
|
||||||
|
queryset = queryset.filter(purchase_order=purchase_order)
|
||||||
|
|
||||||
# Filter stock items which are installed in another (specific) stock item
|
# Filter stock items which are installed in another (specific) stock item
|
||||||
installed_in = params.get('installed_in', None)
|
installed_in = params.get('installed_in', None)
|
||||||
|
|
||||||
|
@ -93,7 +93,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
|
|||||||
{% if item.uid %}
|
{% if item.uid %}
|
||||||
<li><a href='#' id='unlink-barcode'><span class='fas fa-unlink'></span> {% trans "Unlink Barcode" %}</a></li>
|
<li><a href='#' id='unlink-barcode'><span class='fas fa-unlink'></span> {% trans "Unlink Barcode" %}</a></li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li><a href='#' id='link-barcode'><span class='fas fa-link'></span> {% trans "Link Barcode" %}</a></li>\
|
<li><a href='#' id='link-barcode'><span class='fas fa-link'></span> {% trans "Link Barcode" %}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
@ -228,7 +228,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
|
|||||||
{% if item.uid %}
|
{% if item.uid %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-barcode'></span></td>
|
<td><span class='fas fa-barcode'></span></td>
|
||||||
<td>{% trans "Unique Identifier" %}</td>
|
<td>{% trans "Barcode Identifier" %}</td>
|
||||||
<td>{{ item.uid }}</td>
|
<td>{{ item.uid }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -97,6 +97,53 @@ class StockItemTest(StockAPITestCase):
|
|||||||
response = self.client.get(self.list_url, format='json')
|
response = self.client.get(self.list_url, format='json')
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def test_create_default_location(self):
|
||||||
|
"""
|
||||||
|
Test the default location functionality,
|
||||||
|
if a 'location' is not specified in the creation request.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# The part 'R_4K7_0603' (pk=4) has a default location specified
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
self.list_url,
|
||||||
|
data={
|
||||||
|
'part': 4,
|
||||||
|
'quantity': 10
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
self.assertEqual(response.data['location'], 2)
|
||||||
|
|
||||||
|
# What if we explicitly set the location to a different value?
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
self.list_url,
|
||||||
|
data={
|
||||||
|
'part': 4,
|
||||||
|
'quantity': 20,
|
||||||
|
'location': 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
self.assertEqual(response.data['location'], 1)
|
||||||
|
|
||||||
|
# And finally, what if we set the location explicitly to None?
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
self.list_url,
|
||||||
|
data={
|
||||||
|
'part': 4,
|
||||||
|
'quantity': 20,
|
||||||
|
'location': '',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
self.assertEqual(response.data['location'], None)
|
||||||
|
|
||||||
def test_stock_item_create(self):
|
def test_stock_item_create(self):
|
||||||
"""
|
"""
|
||||||
Test creation of a StockItem via the API
|
Test creation of a StockItem via the API
|
||||||
|
32
InvenTree/templates/InvenTree/settings/build.html
Normal file
32
InvenTree/templates/InvenTree/settings/build.html
Normal 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 %}
|
@ -1,4 +1,9 @@
|
|||||||
{% extends "InvenTree/settings/settings.html" %}
|
{% extends "InvenTree/settings/settings.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block subtitle %}
|
||||||
|
{% trans "General Settings" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block tabs %}
|
{% block tabs %}
|
||||||
{% include "InvenTree/settings/tabs.html" with tab='currency' %}
|
{% include "InvenTree/settings/tabs.html" with tab='currency' %}
|
||||||
@ -6,10 +11,10 @@
|
|||||||
|
|
||||||
{% block settings %}
|
{% block settings %}
|
||||||
|
|
||||||
<h4>Currencies</h4>
|
<h4>{% trans "Currencies" %}</h4>
|
||||||
|
|
||||||
<div id='currency-buttons'>
|
<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>
|
</div>
|
||||||
|
|
||||||
<table class='table table-striped table-condensed' id='currency-table' data-toolbar='#currency-buttons'>
|
<table class='table table-striped table-condensed' id='currency-table' data-toolbar='#currency-buttons'>
|
||||||
|
@ -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 %}
|
|
@ -5,7 +5,12 @@
|
|||||||
{% include "InvenTree/settings/tabs.html" with tab='part' %}
|
{% include "InvenTree/settings/tabs.html" with tab='part' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block subtitle %}
|
||||||
|
{% trans "Part Settings" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block settings %}
|
{% block settings %}
|
||||||
|
|
||||||
<h4>{% trans "Part Parameter Templates" %}</h4>
|
<h4>{% trans "Part Parameter Templates" %}</h4>
|
||||||
|
|
||||||
<div id='param-buttons'>
|
<div id='param-buttons'>
|
||||||
|
13
InvenTree/templates/InvenTree/settings/po.html
Normal file
13
InvenTree/templates/InvenTree/settings/po.html
Normal 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 %}
|
@ -1,15 +1,16 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block page_title %}
|
{% block page_title %}
|
||||||
InvenTree | Settings
|
InvenTree | {% trans "Settings" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class='settings-container'>
|
<div class='settings-container'>
|
||||||
|
|
||||||
<h3>InvenTree Settings</h3>
|
<h3>InvenTree {% trans "Settings" %}</h3>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<div class='settings-nav'>
|
<div class='settings-nav'>
|
||||||
@ -19,6 +20,12 @@ InvenTree | Settings
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class='settings-content'>
|
<div class='settings-content'>
|
||||||
|
<h3>
|
||||||
|
{% block subtitle %}
|
||||||
|
SUBTITLE GOES HERE
|
||||||
|
{% endblock %}
|
||||||
|
</h3>
|
||||||
|
<hr>
|
||||||
{% block settings %}
|
{% block settings %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
13
InvenTree/templates/InvenTree/settings/so.html
Normal file
13
InvenTree/templates/InvenTree/settings/so.html
Normal 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 %}
|
13
InvenTree/templates/InvenTree/settings/stock.html
Normal file
13
InvenTree/templates/InvenTree/settings/stock.html
Normal 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 %}
|
@ -1,19 +1,32 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<h4><span class='fas fa-user'></span> {% trans "User Settings" %}</h4>
|
||||||
<ul class='nav nav-pills nav-stacked'>
|
<ul class='nav nav-pills nav-stacked'>
|
||||||
<li{% ifequal tab 'user' %} class='active'{% endifequal %}>
|
<li{% ifequal tab 'user' %} class='active'{% endifequal %}>
|
||||||
<a href="{% url 'settings-user' %}"><span class='fas fa-user'></span> User</a>
|
<a href="{% url 'settings-user' %}"><span class='fas fa-user'></span> {% trans "Account" %}</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>
|
|
||||||
</li>
|
</li>
|
||||||
<li{% ifequal tab 'theme' %} class='active'{% endifequal %}>
|
<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>
|
</li>
|
||||||
{% if user.is_staff %}
|
</ul>
|
||||||
<li{% ifequal tab 'other' %} class='active'{% endifequal %}>
|
<h4><span class='fas fa-cogs'></span> {% trans "InvenTree Settings" %}</h4>
|
||||||
<a href="{% url 'settings-other' %}"><span class='fas fa-cogs'></span> Other</a>
|
<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>
|
</li>
|
||||||
{% endif %}
|
|
||||||
</ul>
|
</ul>
|
@ -6,11 +6,15 @@
|
|||||||
{% include "InvenTree/settings/tabs.html" with tab='theme' %}
|
{% include "InvenTree/settings/tabs.html" with tab='theme' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block subtitle %}
|
||||||
|
{% trans "Theme Settings" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block settings %}
|
{% block settings %}
|
||||||
|
|
||||||
<div class='row'>
|
<div class='row'>
|
||||||
<div class='col-sm-6'>
|
<div class='col-sm-6'>
|
||||||
<h4>Color Themes</h4>
|
<h4>{% trans "Color Themes" %}</h4>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,41 +1,43 @@
|
|||||||
{% extends "InvenTree/settings/settings.html" %}
|
{% extends "InvenTree/settings/settings.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
{% block tabs %}
|
{% block tabs %}
|
||||||
{% include "InvenTree/settings/tabs.html" with tab='user' %}
|
{% include "InvenTree/settings/tabs.html" with tab='user' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block subtitle %}
|
||||||
|
{% trans "User Settings" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block settings %}
|
{% block settings %}
|
||||||
|
|
||||||
<div class='row'>
|
<div class='container'>
|
||||||
<div class='col-sm-6'>
|
<h4>{% trans "User Information" %}</h4>
|
||||||
<h4>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>
|
||||||
<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'>
|
<table class='table table-striped table-condensed'>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Username</td>
|
<td>{% trans "Username" %}</td>
|
||||||
<td>{{ user.username }}</td>
|
<td>{{ user.username }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>First Name</td>
|
<td>{% trans "First Name" %}</td>
|
||||||
<td>{{ user.first_name }}</td>
|
<td>{{ user.first_name }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Last Name</td>
|
<td>{% trans "Last Name" %}</td>
|
||||||
<td>{{ user.last_name }}</td>
|
<td>{{ user.last_name }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Email Address</td>
|
<td>{% trans "Email Address" %}</td>
|
||||||
<td>{{ user.email }}</td>
|
<td>{{ user.email }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load inventree_extras %}
|
||||||
|
|
||||||
function loadBuildTable(table, options) {
|
function loadBuildTable(table, options) {
|
||||||
// Display a table of Build objects
|
// Display a table of Build objects
|
||||||
@ -35,13 +36,26 @@ function loadBuildTable(table, options) {
|
|||||||
switchable: false,
|
switchable: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'title',
|
field: 'reference',
|
||||||
title: '{% trans "Build" %}',
|
title: '{% trans "Build" %}',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
|
switchable: false,
|
||||||
formatter: function(value, row, index, field) {
|
formatter: function(value, row, index, field) {
|
||||||
|
|
||||||
|
var prefix = "{% inventree_setting 'BUILDORDER_REFERENCE_PREFIX' %}";
|
||||||
|
|
||||||
|
if (prefix) {
|
||||||
|
value = `${prefix}${value}`;
|
||||||
|
}
|
||||||
|
|
||||||
return renderLink(value, '/build/' + row.pk + '/');
|
return renderLink(value, '/build/' + row.pk + '/');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
field: 'title',
|
||||||
|
title: '{% trans "Description" %}',
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
field: 'part',
|
field: 'part',
|
||||||
title: '{% trans "Part" %}',
|
title: '{% trans "Part" %}',
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load inventree_extras %}
|
||||||
|
|
||||||
function removeOrderRowFromOrderWizard(e) {
|
function removeOrderRowFromOrderWizard(e) {
|
||||||
/* Remove a part selection from an order form. */
|
/* Remove a part selection from an order form. */
|
||||||
@ -137,6 +138,13 @@ function loadPurchaseOrderTable(table, options) {
|
|||||||
field: 'reference',
|
field: 'reference',
|
||||||
title: '{% trans "Purchase Order" %}',
|
title: '{% trans "Purchase Order" %}',
|
||||||
formatter: function(value, row, index, field) {
|
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}/`);
|
return renderLink(value, `/order/purchase-order/${row.pk}/`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -212,6 +220,13 @@ function loadSalesOrderTable(table, options) {
|
|||||||
field: 'reference',
|
field: 'reference',
|
||||||
title: '{% trans "Sales Order" %}',
|
title: '{% trans "Sales Order" %}',
|
||||||
formatter: function(value, row, index, field) {
|
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}/`);
|
return renderLink(value, `/order/sales-order/${row.pk}/`);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -262,7 +262,7 @@ function loadStockTable(table, options) {
|
|||||||
formatNoMatches: function() {
|
formatNoMatches: function() {
|
||||||
return '{% trans "No stock items matching query" %}';
|
return '{% trans "No stock items matching query" %}';
|
||||||
},
|
},
|
||||||
url: options.url,
|
url: options.url || "{% url 'api-stock-list' %}",
|
||||||
queryParams: filters,
|
queryParams: filters,
|
||||||
customSort: customGroupSorter,
|
customSort: customGroupSorter,
|
||||||
groupBy: true,
|
groupBy: true,
|
||||||
@ -274,16 +274,16 @@ function loadStockTable(table, options) {
|
|||||||
|
|
||||||
var row = data[0];
|
var row = data[0];
|
||||||
|
|
||||||
if (field == 'part_name') {
|
if (field == 'part_detail.name') {
|
||||||
|
|
||||||
var name = row.part_detail.full_name;
|
var name = row.part_detail.full_name;
|
||||||
|
|
||||||
return imageHoverIcon(row.part_detail.thumbnail) + name + ' <i>(' + data.length + ' items)</i>';
|
return imageHoverIcon(row.part_detail.thumbnail) + name + ' <i>(' + data.length + ' items)</i>';
|
||||||
}
|
}
|
||||||
else if (field == 'IPN') {
|
else if (field == 'part_detail.IPN') {
|
||||||
return row.part_detail.IPN;
|
return row.part_detail.IPN;
|
||||||
}
|
}
|
||||||
else if (field == 'part_description') {
|
else if (field == 'part_detail.description') {
|
||||||
return row.part_detail.description;
|
return row.part_detail.description;
|
||||||
}
|
}
|
||||||
else if (field == 'quantity') {
|
else if (field == 'quantity') {
|
||||||
@ -417,9 +417,10 @@ function loadStockTable(table, options) {
|
|||||||
switchable: false,
|
switchable: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'part_name',
|
field: 'part_detail.name',
|
||||||
title: '{% trans "Part" %}',
|
title: '{% trans "Part" %}',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
|
switchable: false,
|
||||||
formatter: function(value, row, index, field) {
|
formatter: function(value, row, index, field) {
|
||||||
|
|
||||||
var url = `/stock/item/${row.pk}/`;
|
var url = `/stock/item/${row.pk}/`;
|
||||||
@ -432,7 +433,7 @@ function loadStockTable(table, options) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'IPN',
|
field: 'part_detail.IPN',
|
||||||
title: 'IPN',
|
title: 'IPN',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
formatter: function(value, row, index, field) {
|
formatter: function(value, row, index, field) {
|
||||||
@ -440,7 +441,7 @@ function loadStockTable(table, options) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'part_description',
|
field: 'part_detail.description',
|
||||||
title: '{% trans "Description" %}',
|
title: '{% trans "Description" %}',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
formatter: function(value, row, index, field) {
|
formatter: function(value, row, index, field) {
|
||||||
|
Loading…
Reference in New Issue
Block a user