mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #1956 from SchrodingersGat/supplier-part-from-form
Supplier part from form
This commit is contained in:
commit
1db654e990
@ -730,6 +730,13 @@
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.form-panel {
|
||||
border-radius: 5px;
|
||||
border: 1px solid #ccc;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
|
||||
.modal input {
|
||||
width: 100%;
|
||||
}
|
||||
|
@ -9,12 +9,14 @@ from django.conf.urls import url, include
|
||||
from django.urls import reverse
|
||||
from django.http import JsonResponse
|
||||
from django.db.models import Q, F, Count, Min, Max, Avg
|
||||
from django.db import transaction
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import filters, serializers
|
||||
from rest_framework import generics
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from django_filters import rest_framework as rest_filters
|
||||
@ -23,7 +25,7 @@ from djmoney.money import Money
|
||||
from djmoney.contrib.exchange.models import convert_money
|
||||
from djmoney.contrib.exchange.exceptions import MissingRate
|
||||
|
||||
from decimal import Decimal
|
||||
from decimal import Decimal, InvalidOperation
|
||||
|
||||
from .models import Part, PartCategory, BomItem
|
||||
from .models import PartParameter, PartParameterTemplate
|
||||
@ -31,7 +33,10 @@ from .models import PartAttachment, PartTestTemplate
|
||||
from .models import PartSellPriceBreak, PartInternalPriceBreak
|
||||
from .models import PartCategoryParameterTemplate
|
||||
|
||||
from stock.models import StockItem
|
||||
from company.models import Company, ManufacturerPart, SupplierPart
|
||||
|
||||
from stock.models import StockItem, StockLocation
|
||||
|
||||
from common.models import InvenTreeSetting
|
||||
from build.models import Build
|
||||
|
||||
@ -630,6 +635,7 @@ class PartList(generics.ListCreateAPIView):
|
||||
else:
|
||||
return Response(data)
|
||||
|
||||
@transaction.atomic
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""
|
||||
We wish to save the user who created this part!
|
||||
@ -637,6 +643,8 @@ class PartList(generics.ListCreateAPIView):
|
||||
Note: Implementation copied from DRF class CreateModelMixin
|
||||
"""
|
||||
|
||||
# TODO: Unit tests for this function!
|
||||
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
@ -680,21 +688,97 @@ class PartList(generics.ListCreateAPIView):
|
||||
pass
|
||||
|
||||
# Optionally create initial stock item
|
||||
try:
|
||||
initial_stock = Decimal(request.data.get('initial_stock', 0))
|
||||
initial_stock = str2bool(request.data.get('initial_stock', False))
|
||||
|
||||
if initial_stock > 0 and part.default_location is not None:
|
||||
if initial_stock:
|
||||
try:
|
||||
|
||||
stock_item = StockItem(
|
||||
initial_stock_quantity = Decimal(request.data.get('initial_stock_quantity', ''))
|
||||
|
||||
if initial_stock_quantity <= 0:
|
||||
raise ValidationError({
|
||||
'initial_stock_quantity': [_('Must be greater than zero')],
|
||||
})
|
||||
except (ValueError, InvalidOperation): # Invalid quantity provided
|
||||
raise ValidationError({
|
||||
'initial_stock_quantity': [_('Must be a valid quantity')],
|
||||
})
|
||||
|
||||
initial_stock_location = request.data.get('initial_stock_location', None)
|
||||
|
||||
try:
|
||||
initial_stock_location = StockLocation.objects.get(pk=initial_stock_location)
|
||||
except (ValueError, StockLocation.DoesNotExist):
|
||||
initial_stock_location = None
|
||||
|
||||
if initial_stock_location is None:
|
||||
if part.default_location is not None:
|
||||
initial_stock_location = part.default_location
|
||||
else:
|
||||
raise ValidationError({
|
||||
'initial_stock_location': [_('Specify location for initial part stock')],
|
||||
})
|
||||
|
||||
stock_item = StockItem(
|
||||
part=part,
|
||||
quantity=initial_stock_quantity,
|
||||
location=initial_stock_location,
|
||||
)
|
||||
|
||||
stock_item.save(user=request.user)
|
||||
|
||||
# Optionally add manufacturer / supplier data to the part
|
||||
if part.purchaseable and str2bool(request.data.get('add_supplier_info', False)):
|
||||
|
||||
try:
|
||||
manufacturer = Company.objects.get(pk=request.data.get('manufacturer', None))
|
||||
except:
|
||||
manufacturer = None
|
||||
|
||||
try:
|
||||
supplier = Company.objects.get(pk=request.data.get('supplier', None))
|
||||
except:
|
||||
supplier = None
|
||||
|
||||
mpn = str(request.data.get('MPN', '')).strip()
|
||||
sku = str(request.data.get('SKU', '')).strip()
|
||||
|
||||
# Construct a manufacturer part
|
||||
if manufacturer or mpn:
|
||||
if not manufacturer:
|
||||
raise ValidationError({
|
||||
'manufacturer': [_("This field is required")]
|
||||
})
|
||||
if not mpn:
|
||||
raise ValidationError({
|
||||
'MPN': [_("This field is required")]
|
||||
})
|
||||
|
||||
manufacturer_part = ManufacturerPart.objects.create(
|
||||
part=part,
|
||||
quantity=initial_stock,
|
||||
location=part.default_location,
|
||||
manufacturer=manufacturer,
|
||||
MPN=mpn
|
||||
)
|
||||
else:
|
||||
# No manufacturer part data specified
|
||||
manufacturer_part = None
|
||||
|
||||
stock_item.save(user=request.user)
|
||||
if supplier or sku:
|
||||
if not supplier:
|
||||
raise ValidationError({
|
||||
'supplier': [_("This field is required")]
|
||||
})
|
||||
if not sku:
|
||||
raise ValidationError({
|
||||
'SKU': [_("This field is required")]
|
||||
})
|
||||
|
||||
except:
|
||||
pass
|
||||
SupplierPart.objects.create(
|
||||
part=part,
|
||||
supplier=supplier,
|
||||
SKU=sku,
|
||||
manufacturer_part=manufacturer_part,
|
||||
)
|
||||
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
|
||||
|
@ -276,6 +276,7 @@
|
||||
constructForm('{% url "api-part-list" %}', {
|
||||
method: 'POST',
|
||||
fields: fields,
|
||||
groups: partGroups(),
|
||||
title: '{% trans "Create Part" %}',
|
||||
onSuccess: function(data) {
|
||||
// Follow the new part
|
||||
|
@ -129,6 +129,7 @@ class PartAPITest(InvenTreeAPITestCase):
|
||||
'location',
|
||||
'bom',
|
||||
'test_templates',
|
||||
'company',
|
||||
]
|
||||
|
||||
roles = [
|
||||
@ -465,6 +466,128 @@ class PartAPITest(InvenTreeAPITestCase):
|
||||
self.assertFalse(response.data['active'])
|
||||
self.assertFalse(response.data['purchaseable'])
|
||||
|
||||
def test_initial_stock(self):
|
||||
"""
|
||||
Tests for initial stock quantity creation
|
||||
"""
|
||||
|
||||
url = reverse('api-part-list')
|
||||
|
||||
# Track how many parts exist at the start of this test
|
||||
n = Part.objects.count()
|
||||
|
||||
# Set up required part data
|
||||
data = {
|
||||
'category': 1,
|
||||
'name': "My lil' test part",
|
||||
'description': 'A part with which to test',
|
||||
}
|
||||
|
||||
# Signal that we want to add initial stock
|
||||
data['initial_stock'] = True
|
||||
|
||||
# Post without a quantity
|
||||
response = self.post(url, data, expected_code=400)
|
||||
self.assertIn('initial_stock_quantity', response.data)
|
||||
|
||||
# Post with an invalid quantity
|
||||
data['initial_stock_quantity'] = "ax"
|
||||
response = self.post(url, data, expected_code=400)
|
||||
self.assertIn('initial_stock_quantity', response.data)
|
||||
|
||||
# Post with a negative quantity
|
||||
data['initial_stock_quantity'] = -1
|
||||
response = self.post(url, data, expected_code=400)
|
||||
self.assertIn('Must be greater than zero', response.data['initial_stock_quantity'])
|
||||
|
||||
# Post with a valid quantity
|
||||
data['initial_stock_quantity'] = 12345
|
||||
|
||||
response = self.post(url, data, expected_code=400)
|
||||
self.assertIn('initial_stock_location', response.data)
|
||||
|
||||
# Check that the number of parts has not increased (due to form failures)
|
||||
self.assertEqual(Part.objects.count(), n)
|
||||
|
||||
# Now, set a location
|
||||
data['initial_stock_location'] = 1
|
||||
|
||||
response = self.post(url, data, expected_code=201)
|
||||
|
||||
# Check that the part has been created
|
||||
self.assertEqual(Part.objects.count(), n + 1)
|
||||
|
||||
pk = response.data['pk']
|
||||
|
||||
new_part = Part.objects.get(pk=pk)
|
||||
|
||||
self.assertEqual(new_part.total_stock, 12345)
|
||||
|
||||
def test_initial_supplier_data(self):
|
||||
"""
|
||||
Tests for initial creation of supplier / manufacturer data
|
||||
"""
|
||||
|
||||
url = reverse('api-part-list')
|
||||
|
||||
n = Part.objects.count()
|
||||
|
||||
# Set up initial part data
|
||||
data = {
|
||||
'category': 1,
|
||||
'name': 'Buy Buy Buy',
|
||||
'description': 'A purchaseable part',
|
||||
'purchaseable': True,
|
||||
}
|
||||
|
||||
# Signal that we wish to create initial supplier data
|
||||
data['add_supplier_info'] = True
|
||||
|
||||
# Specify MPN but not manufacturer
|
||||
data['MPN'] = 'MPN-123'
|
||||
|
||||
response = self.post(url, data, expected_code=400)
|
||||
self.assertIn('manufacturer', response.data)
|
||||
|
||||
# Specify manufacturer but not MPN
|
||||
del data['MPN']
|
||||
data['manufacturer'] = 1
|
||||
response = self.post(url, data, expected_code=400)
|
||||
self.assertIn('MPN', response.data)
|
||||
|
||||
# Specify SKU but not supplier
|
||||
del data['manufacturer']
|
||||
data['SKU'] = 'SKU-123'
|
||||
response = self.post(url, data, expected_code=400)
|
||||
self.assertIn('supplier', response.data)
|
||||
|
||||
# Specify supplier but not SKU
|
||||
del data['SKU']
|
||||
data['supplier'] = 1
|
||||
response = self.post(url, data, expected_code=400)
|
||||
self.assertIn('SKU', response.data)
|
||||
|
||||
# Check that no new parts have been created
|
||||
self.assertEqual(Part.objects.count(), n)
|
||||
|
||||
# Now, fully specify the details
|
||||
data['SKU'] = 'SKU-123'
|
||||
data['supplier'] = 3
|
||||
data['MPN'] = 'MPN-123'
|
||||
data['manufacturer'] = 6
|
||||
|
||||
response = self.post(url, data, expected_code=201)
|
||||
|
||||
self.assertEqual(Part.objects.count(), n + 1)
|
||||
|
||||
pk = response.data['pk']
|
||||
|
||||
new_part = Part.objects.get(pk=pk)
|
||||
|
||||
# Check that there is a new manufacturer part *and* a new supplier part
|
||||
self.assertEqual(new_part.supplier_parts.count(), 1)
|
||||
self.assertEqual(new_part.manufacturer_parts.count(), 1)
|
||||
|
||||
|
||||
class PartDetailTests(InvenTreeAPITestCase):
|
||||
"""
|
||||
|
@ -264,6 +264,10 @@ function constructForm(url, options) {
|
||||
// Default HTTP method
|
||||
options.method = options.method || 'PATCH';
|
||||
|
||||
// Default "groups" definition
|
||||
options.groups = options.groups || {};
|
||||
options.current_group = null;
|
||||
|
||||
// Construct an "empty" data object if not provided
|
||||
if (!options.data) {
|
||||
options.data = {};
|
||||
@ -362,6 +366,14 @@ function constructFormBody(fields, options) {
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize an "empty" field for each specified field
|
||||
for (field in displayed_fields) {
|
||||
if (!(field in fields)) {
|
||||
console.log("adding blank field for ", field);
|
||||
fields[field] = {};
|
||||
}
|
||||
}
|
||||
|
||||
// Provide each field object with its own name
|
||||
for(field in fields) {
|
||||
fields[field].name = field;
|
||||
@ -379,52 +391,18 @@ function constructFormBody(fields, options) {
|
||||
// Override existing query filters (if provided!)
|
||||
fields[field].filters = Object.assign(fields[field].filters || {}, field_options.filters);
|
||||
|
||||
// TODO: Refactor the following code with Object.assign (see above)
|
||||
for (var opt in field_options) {
|
||||
|
||||
// "before" and "after" renders
|
||||
fields[field].before = field_options.before;
|
||||
fields[field].after = field_options.after;
|
||||
var val = field_options[opt];
|
||||
|
||||
// Secondary modal options
|
||||
fields[field].secondary = field_options.secondary;
|
||||
|
||||
// Edit callback
|
||||
fields[field].onEdit = field_options.onEdit;
|
||||
|
||||
fields[field].multiline = field_options.multiline;
|
||||
|
||||
// Custom help_text
|
||||
if (field_options.help_text) {
|
||||
fields[field].help_text = field_options.help_text;
|
||||
}
|
||||
|
||||
// Custom label
|
||||
if (field_options.label) {
|
||||
fields[field].label = field_options.label;
|
||||
}
|
||||
|
||||
// Custom placeholder
|
||||
if (field_options.placeholder) {
|
||||
fields[field].placeholder = field_options.placeholder;
|
||||
}
|
||||
|
||||
// Choices
|
||||
if (field_options.choices) {
|
||||
fields[field].choices = field_options.choices;
|
||||
}
|
||||
|
||||
// Field prefix
|
||||
if (field_options.prefix) {
|
||||
fields[field].prefix = field_options.prefix;
|
||||
} else if (field_options.icon) {
|
||||
// Specify icon like 'fa-user'
|
||||
fields[field].prefix = `<span class='fas ${field_options.icon}'></span>`;
|
||||
}
|
||||
|
||||
fields[field].hidden = field_options.hidden;
|
||||
|
||||
if (field_options.read_only != null) {
|
||||
fields[field].read_only = field_options.read_only;
|
||||
if (opt == 'filters') {
|
||||
// ignore filters (see above)
|
||||
} else if (opt == 'icon') {
|
||||
// Specify custom icon
|
||||
fields[field].prefix = `<span class='fas ${val}'></span>`;
|
||||
} else {
|
||||
fields[field][opt] = field_options[opt];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -465,8 +443,10 @@ function constructFormBody(fields, options) {
|
||||
html += constructField(name, field, options);
|
||||
}
|
||||
|
||||
// TODO: Dynamically create the modals,
|
||||
// so that we can have an infinite number of stacks!
|
||||
if (options.current_group) {
|
||||
// Close out the current group
|
||||
html += `</div></div>`;
|
||||
}
|
||||
|
||||
// Create a new modal if one does not exists
|
||||
if (!options.modal) {
|
||||
@ -535,6 +515,8 @@ function constructFormBody(fields, options) {
|
||||
submitFormData(fields, options);
|
||||
}
|
||||
});
|
||||
|
||||
initializeGroups(fields, options);
|
||||
}
|
||||
|
||||
|
||||
@ -860,9 +842,12 @@ function handleFormErrors(errors, fields, options) {
|
||||
|
||||
var non_field_errors = $(options.modal).find('#non-field-errors');
|
||||
|
||||
// TODO: Display the JSON error text when hovering over the "info" icon
|
||||
non_field_errors.append(
|
||||
`<div class='alert alert-block alert-danger'>
|
||||
<b>{% trans "Form errors exist" %}</b>
|
||||
<span id='form-errors-info' class='float-right fas fa-info-circle icon-red'>
|
||||
</span>
|
||||
</div>`
|
||||
);
|
||||
|
||||
@ -932,7 +917,10 @@ function addFieldCallbacks(fields, options) {
|
||||
function addFieldCallback(name, field, options) {
|
||||
|
||||
$(options.modal).find(`#id_${name}`).change(function() {
|
||||
field.onEdit(name, field, options);
|
||||
|
||||
var value = getFormFieldValue(name, field, options);
|
||||
|
||||
field.onEdit(value, name, field, options);
|
||||
});
|
||||
}
|
||||
|
||||
@ -960,6 +948,71 @@ function addClearCallback(name, field, options) {
|
||||
}
|
||||
|
||||
|
||||
// Initialize callbacks and initial states for groups
|
||||
function initializeGroups(fields, options) {
|
||||
|
||||
var modal = options.modal;
|
||||
|
||||
// Callback for when the group is expanded
|
||||
$(modal).find('.form-panel-content').on('show.bs.collapse', function() {
|
||||
|
||||
var panel = $(this).closest('.form-panel');
|
||||
var group = panel.attr('group');
|
||||
|
||||
var icon = $(modal).find(`#group-icon-${group}`);
|
||||
|
||||
icon.removeClass('fa-angle-right');
|
||||
icon.addClass('fa-angle-up');
|
||||
});
|
||||
|
||||
// Callback for when the group is collapsed
|
||||
$(modal).find('.form-panel-content').on('hide.bs.collapse', function() {
|
||||
|
||||
var panel = $(this).closest('.form-panel');
|
||||
var group = panel.attr('group');
|
||||
|
||||
var icon = $(modal).find(`#group-icon-${group}`);
|
||||
|
||||
icon.removeClass('fa-angle-up');
|
||||
icon.addClass('fa-angle-right');
|
||||
});
|
||||
|
||||
// Set initial state of each specified group
|
||||
for (var group in options.groups) {
|
||||
|
||||
var group_options = options.groups[group];
|
||||
|
||||
if (group_options.collapsed) {
|
||||
$(modal).find(`#form-panel-content-${group}`).collapse("hide");
|
||||
} else {
|
||||
$(modal).find(`#form-panel-content-${group}`).collapse("show");
|
||||
}
|
||||
|
||||
if (group_options.hidden) {
|
||||
hideFormGroup(group, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hide a form group
|
||||
function hideFormGroup(group, options) {
|
||||
$(options.modal).find(`#form-panel-${group}`).hide();
|
||||
}
|
||||
|
||||
// Show a form group
|
||||
function showFormGroup(group, options) {
|
||||
$(options.modal).find(`#form-panel-${group}`).show();
|
||||
}
|
||||
|
||||
function setFormGroupVisibility(group, vis, options) {
|
||||
if (vis) {
|
||||
showFormGroup(group, options);
|
||||
} else {
|
||||
hideFormGroup(group, options);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function initializeRelatedFields(fields, options) {
|
||||
|
||||
var field_names = options.field_names;
|
||||
@ -1353,6 +1406,8 @@ function renderModelData(name, model, data, parameters, options) {
|
||||
*/
|
||||
function constructField(name, parameters, options) {
|
||||
|
||||
var html = '';
|
||||
|
||||
// Shortcut for simple visual fields
|
||||
if (parameters.type == 'candy') {
|
||||
return constructCandyInput(name, parameters, options);
|
||||
@ -1365,13 +1420,58 @@ function constructField(name, parameters, options) {
|
||||
return constructHiddenInput(name, parameters, options);
|
||||
}
|
||||
|
||||
// Are we ending a group?
|
||||
if (options.current_group && parameters.group != options.current_group) {
|
||||
html += `</div></div>`;
|
||||
|
||||
// Null out the current "group" so we can start a new one
|
||||
options.current_group = null;
|
||||
}
|
||||
|
||||
// Are we starting a new group?
|
||||
if (parameters.group) {
|
||||
|
||||
var group = parameters.group;
|
||||
|
||||
var group_options = options.groups[group] || {};
|
||||
|
||||
// Are we starting a new group?
|
||||
// Add HTML for the start of a separate panel
|
||||
if (parameters.group != options.current_group) {
|
||||
|
||||
html += `
|
||||
<div class='panel form-panel' id='form-panel-${group}' group='${group}'>
|
||||
<div class='panel-heading form-panel-heading' id='form-panel-heading-${group}'>`;
|
||||
if (group_options.collapsible) {
|
||||
html += `
|
||||
<div data-toggle='collapse' data-target='#form-panel-content-${group}'>
|
||||
<a href='#'><span id='group-icon-${group}' class='fas fa-angle-up'></span>
|
||||
`;
|
||||
} else {
|
||||
html += `<div>`;
|
||||
}
|
||||
|
||||
html += `<h4 style='display: inline;'>${group_options.title || group}</h4>`;
|
||||
|
||||
if (group_options.collapsible) {
|
||||
html += `</a>`;
|
||||
}
|
||||
|
||||
html += `
|
||||
</div></div>
|
||||
<div class='panel-content form-panel-content' id='form-panel-content-${group}'>
|
||||
`;
|
||||
}
|
||||
|
||||
// Keep track of the group we are in
|
||||
options.current_group = group;
|
||||
}
|
||||
|
||||
var form_classes = 'form-group';
|
||||
|
||||
if (parameters.errors) {
|
||||
form_classes += ' has-error';
|
||||
}
|
||||
|
||||
var html = '';
|
||||
|
||||
// Optional content to render before the field
|
||||
if (parameters.before) {
|
||||
@ -1428,13 +1528,14 @@ function constructField(name, parameters, options) {
|
||||
html += `</div>`; // input-group
|
||||
}
|
||||
|
||||
// Div for error messages
|
||||
html += `<div id='errors-${name}'></div>`;
|
||||
|
||||
if (parameters.help_text) {
|
||||
html += constructHelpText(name, parameters, options);
|
||||
}
|
||||
|
||||
// Div for error messages
|
||||
html += `<div id='errors-${name}'></div>`;
|
||||
|
||||
|
||||
html += `</div>`; // controls
|
||||
html += `</div>`; // form-group
|
||||
|
||||
@ -1599,6 +1700,10 @@ function constructInputOptions(name, classes, type, parameters) {
|
||||
opts.push(`placeholder='${parameters.placeholder}'`);
|
||||
}
|
||||
|
||||
if (parameters.type == 'boolean') {
|
||||
opts.push(`style='display: inline-block; width: 20px; margin-right: 20px;'`);
|
||||
}
|
||||
|
||||
if (parameters.multiline) {
|
||||
return `<textarea ${opts.join(' ')}></textarea>`;
|
||||
} else {
|
||||
@ -1772,7 +1877,13 @@ function constructCandyInput(name, parameters, options) {
|
||||
*/
|
||||
function constructHelpText(name, parameters, options) {
|
||||
|
||||
var html = `<div id='hint_id_${name}' class='help-block'><i>${parameters.help_text}</i></div>`;
|
||||
var style = '';
|
||||
|
||||
if (parameters.type == 'boolean') {
|
||||
style = `style='display: inline-block; margin-left: 25px' `;
|
||||
}
|
||||
|
||||
var html = `<div id='hint_id_${name}' ${style}class='help-block'><i>${parameters.help_text}</i></div>`;
|
||||
|
||||
return html;
|
||||
}
|
@ -13,6 +13,31 @@ function yesNoLabel(value) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function partGroups(options={}) {
|
||||
|
||||
return {
|
||||
attributes: {
|
||||
title: '{% trans "Part Attributes" %}',
|
||||
collapsible: true,
|
||||
},
|
||||
create: {
|
||||
title: '{% trans "Part Creation Options" %}',
|
||||
collapsible: true,
|
||||
},
|
||||
duplicate: {
|
||||
title: '{% trans "Part Duplication Options" %}',
|
||||
collapsible: true,
|
||||
},
|
||||
supplier: {
|
||||
title: '{% trans "Supplier Options" %}',
|
||||
collapsible: true,
|
||||
hidden: !global_settings.PART_PURCHASEABLE,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Construct fieldset for part forms
|
||||
function partFields(options={}) {
|
||||
|
||||
@ -48,36 +73,44 @@ function partFields(options={}) {
|
||||
minimum_stock: {
|
||||
icon: 'fa-boxes',
|
||||
},
|
||||
attributes: {
|
||||
type: 'candy',
|
||||
html: `<hr><h4><i>{% trans "Part Attributes" %}</i></h4><hr>`
|
||||
},
|
||||
component: {
|
||||
value: global_settings.PART_COMPONENT,
|
||||
group: 'attributes',
|
||||
},
|
||||
assembly: {
|
||||
value: global_settings.PART_ASSEMBLY,
|
||||
group: 'attributes',
|
||||
},
|
||||
is_template: {
|
||||
value: global_settings.PART_TEMPLATE,
|
||||
group: 'attributes',
|
||||
},
|
||||
trackable: {
|
||||
value: global_settings.PART_TRACKABLE,
|
||||
group: 'attributes',
|
||||
},
|
||||
purchaseable: {
|
||||
value: global_settings.PART_PURCHASEABLE,
|
||||
group: 'attributes',
|
||||
onEdit: function(value, name, field, options) {
|
||||
setFormGroupVisibility('supplier', value, options);
|
||||
}
|
||||
},
|
||||
salable: {
|
||||
value: global_settings.PART_SALABLE,
|
||||
group: 'attributes',
|
||||
},
|
||||
virtual: {
|
||||
value: global_settings.PART_VIRTUAL,
|
||||
group: 'attributes',
|
||||
},
|
||||
};
|
||||
|
||||
// If editing a part, we can set the "active" status
|
||||
if (options.edit) {
|
||||
fields.active = {};
|
||||
fields.active = {
|
||||
group: 'attributes'
|
||||
};
|
||||
}
|
||||
|
||||
// Pop expiry field
|
||||
@ -91,16 +124,32 @@ function partFields(options={}) {
|
||||
// No supplier parts available yet
|
||||
delete fields["default_supplier"];
|
||||
|
||||
fields.create = {
|
||||
type: 'candy',
|
||||
html: `<hr><h4><i>{% trans "Part Creation Options" %}</i></h4><hr>`,
|
||||
};
|
||||
|
||||
if (global_settings.PART_CREATE_INITIAL) {
|
||||
|
||||
fields.initial_stock = {
|
||||
type: 'boolean',
|
||||
label: '{% trans "Create Initial Stock" %}',
|
||||
help_text: '{% trans "Create an initial stock item for this part" %}',
|
||||
group: 'create',
|
||||
};
|
||||
|
||||
fields.initial_stock_quantity = {
|
||||
type: 'decimal',
|
||||
value: 1,
|
||||
label: '{% trans "Initial Stock Quantity" %}',
|
||||
help_text: '{% trans "Initialize part stock with specified quantity" %}',
|
||||
help_text: '{% trans "Specify initial stock quantity for this part" %}',
|
||||
group: 'create',
|
||||
};
|
||||
|
||||
// TODO - Allow initial location of stock to be specified
|
||||
fields.initial_stock_location = {
|
||||
label: '{% trans "Location" %}',
|
||||
help_text: '{% trans "Select destination stock location" %}',
|
||||
type: 'related field',
|
||||
required: true,
|
||||
api_url: `/api/stock/location/`,
|
||||
model: 'stocklocation',
|
||||
group: 'create',
|
||||
};
|
||||
}
|
||||
|
||||
@ -109,21 +158,65 @@ function partFields(options={}) {
|
||||
label: '{% trans "Copy Category Parameters" %}',
|
||||
help_text: '{% trans "Copy parameter templates from selected part category" %}',
|
||||
value: global_settings.PART_CATEGORY_PARAMETERS,
|
||||
group: 'create',
|
||||
};
|
||||
|
||||
// Supplier options
|
||||
fields.add_supplier_info = {
|
||||
type: 'boolean',
|
||||
label: '{% trans "Add Supplier Data" %}',
|
||||
help_text: '{% trans "Create initial supplier data for this part" %}',
|
||||
group: 'supplier',
|
||||
};
|
||||
|
||||
fields.supplier = {
|
||||
type: 'related field',
|
||||
model: 'company',
|
||||
label: '{% trans "Supplier" %}',
|
||||
help_text: '{% trans "Select supplier" %}',
|
||||
filters: {
|
||||
'is_supplier': true,
|
||||
},
|
||||
api_url: '{% url "api-company-list" %}',
|
||||
group: 'supplier',
|
||||
};
|
||||
|
||||
fields.SKU = {
|
||||
type: 'string',
|
||||
label: '{% trans "SKU" %}',
|
||||
help_text: '{% trans "Supplier stock keeping unit" %}',
|
||||
group: 'supplier',
|
||||
};
|
||||
|
||||
fields.manufacturer = {
|
||||
type: 'related field',
|
||||
model: 'company',
|
||||
label: '{% trans "Manufacturer" %}',
|
||||
help_text: '{% trans "Select manufacturer" %}',
|
||||
filters: {
|
||||
'is_manufacturer': true,
|
||||
},
|
||||
api_url: '{% url "api-company-list" %}',
|
||||
group: 'supplier',
|
||||
};
|
||||
|
||||
fields.MPN = {
|
||||
type: 'string',
|
||||
label: '{% trans "MPN" %}',
|
||||
help_text: '{% trans "Manufacturer Part Number" %}',
|
||||
group: 'supplier',
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
// Additional fields when "duplicating" a part
|
||||
if (options.duplicate) {
|
||||
|
||||
fields.duplicate = {
|
||||
type: 'candy',
|
||||
html: `<hr><h4><i>{% trans "Part Duplication Options" %}</i></h4><hr>`,
|
||||
};
|
||||
|
||||
fields.copy_from = {
|
||||
type: 'integer',
|
||||
hidden: true,
|
||||
value: options.duplicate,
|
||||
group: 'duplicate',
|
||||
},
|
||||
|
||||
fields.copy_image = {
|
||||
@ -131,6 +224,7 @@ function partFields(options={}) {
|
||||
label: '{% trans "Copy Image" %}',
|
||||
help_text: '{% trans "Copy image from original part" %}',
|
||||
value: true,
|
||||
group: 'duplicate',
|
||||
},
|
||||
|
||||
fields.copy_bom = {
|
||||
@ -138,6 +232,7 @@ function partFields(options={}) {
|
||||
label: '{% trans "Copy BOM" %}',
|
||||
help_text: '{% trans "Copy bill of materials from original part" %}',
|
||||
value: global_settings.PART_COPY_BOM,
|
||||
group: 'duplicate',
|
||||
};
|
||||
|
||||
fields.copy_parameters = {
|
||||
@ -145,6 +240,7 @@ function partFields(options={}) {
|
||||
label: '{% trans "Copy Parameters" %}',
|
||||
help_text: '{% trans "Copy parameter data from original part" %}',
|
||||
value: global_settings.PART_COPY_PARAMETERS,
|
||||
group: 'duplicate',
|
||||
};
|
||||
}
|
||||
|
||||
@ -191,8 +287,11 @@ function editPart(pk, options={}) {
|
||||
edit: true
|
||||
});
|
||||
|
||||
var groups = partGroups({});
|
||||
|
||||
constructForm(url, {
|
||||
fields: fields,
|
||||
groups: partGroups(),
|
||||
title: '{% trans "Edit Part" %}',
|
||||
reload: true,
|
||||
});
|
||||
@ -221,6 +320,7 @@ function duplicatePart(pk, options={}) {
|
||||
constructForm('{% url "api-part-list" %}', {
|
||||
method: 'POST',
|
||||
fields: fields,
|
||||
groups: partGroups(),
|
||||
title: '{% trans "Duplicate Part" %}',
|
||||
data: data,
|
||||
onSuccess: function(data) {
|
||||
|
Loading…
Reference in New Issue
Block a user