mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
commit
19329a9627
@ -163,7 +163,7 @@ LOGGING = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = CONFIG.get('middleware', [
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'django.middleware.locale.LocaleMiddleware',
|
'django.middleware.locale.LocaleMiddleware',
|
||||||
@ -173,9 +173,12 @@ MIDDLEWARE = [
|
|||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
|
||||||
'InvenTree.middleware.AuthRequiredMiddleware'
|
'InvenTree.middleware.AuthRequiredMiddleware'
|
||||||
]
|
])
|
||||||
|
|
||||||
|
AUTHENTICATION_BACKENDS = CONFIG.get('authentication_backends', [
|
||||||
|
'django.contrib.auth.backends.ModelBackend'
|
||||||
|
])
|
||||||
|
|
||||||
# If the debug toolbar is enabled, add the modules
|
# If the debug toolbar is enabled, add the modules
|
||||||
if DEBUG and CONFIG.get('debug_toolbar', False):
|
if DEBUG and CONFIG.get('debug_toolbar', False):
|
||||||
|
@ -1,7 +1,136 @@
|
|||||||
function makeOption(id, title) {
|
function makeOption(text, value, title) {
|
||||||
/* Format an option for a select element
|
/* Format an option for a select element
|
||||||
*/
|
*/
|
||||||
return "<option value='" + id + "'>" + title + "</option>";
|
|
||||||
|
var html = `<option value='${value || text}'`;
|
||||||
|
|
||||||
|
if (title) {
|
||||||
|
html += ` title='${title}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `>${text}</option>`;
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeOptionsList(elements, textFunc, valueFunc, titleFunc) {
|
||||||
|
/*
|
||||||
|
* Programatically generate a list of <option> elements,
|
||||||
|
* from the (assumed array) of elements.
|
||||||
|
* For each element, we pass the element to the supplied functions,
|
||||||
|
* which (in turn) generate display / value / title values.
|
||||||
|
*
|
||||||
|
* Args:
|
||||||
|
* - elements: List of elements
|
||||||
|
* - textFunc: Function which takes an element and generates the text to be displayed
|
||||||
|
* - valueFunc: optional function which takes an element and generates the value
|
||||||
|
* - titleFunc: optional function which takes an element and generates a title
|
||||||
|
*/
|
||||||
|
|
||||||
|
var options = [];
|
||||||
|
|
||||||
|
elements.forEach(function(element) {
|
||||||
|
|
||||||
|
var text = textFunc(element);
|
||||||
|
var value = null;
|
||||||
|
var title = null;
|
||||||
|
|
||||||
|
if (valueFunc) {
|
||||||
|
value = valueFunc(element);
|
||||||
|
} else {
|
||||||
|
value = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (titleFunc) {
|
||||||
|
title = titleFunc(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
options.push(makeOption(text, value, title));
|
||||||
|
});
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function setFieldOptions(fieldName, optionList, options={}) {
|
||||||
|
/* Set the options for a <select> field.
|
||||||
|
*
|
||||||
|
* Args:
|
||||||
|
* - fieldName: The name of the target field
|
||||||
|
* - Options: List of formatted <option> strings
|
||||||
|
* - append: If true, options will be appended, otherwise will replace existing options.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
var append = options.append || false;
|
||||||
|
|
||||||
|
var modal = options.modal || '#modal-form';
|
||||||
|
|
||||||
|
var field = getFieldByName(modal, fieldName);
|
||||||
|
|
||||||
|
var addEmptyOption = options.addEmptyOption || true;
|
||||||
|
|
||||||
|
// If not appending, clear out the field...
|
||||||
|
if (!append) {
|
||||||
|
field.find('option').remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addEmptyOption) {
|
||||||
|
// Add an 'empty' option at the top of the list
|
||||||
|
field.append(makeOption('---------', '', '---------'));
|
||||||
|
}
|
||||||
|
|
||||||
|
optionList.forEach(function(option) {
|
||||||
|
field.append(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function reloadFieldOptions(fieldName, options) {
|
||||||
|
/* Reload the options for a given field,
|
||||||
|
* using an AJAX request.
|
||||||
|
*
|
||||||
|
* Args:
|
||||||
|
* - fieldName: The name of the field
|
||||||
|
* - options:
|
||||||
|
* -- url: Query url
|
||||||
|
* -- params: Query params
|
||||||
|
* -- value: A function which takes a returned option and returns the 'value' (if not specified, the `pk` field is used)
|
||||||
|
* -- text: A function which takes a returned option and returns the 'text'
|
||||||
|
* -- title: A function which takes a returned option and returns the 'title' (optional!)
|
||||||
|
*/
|
||||||
|
|
||||||
|
inventreeGet(options.url, options.params, {
|
||||||
|
success: function(response) {
|
||||||
|
var opts = makeOptionsList(response,
|
||||||
|
function(item) {
|
||||||
|
return options.text(item);
|
||||||
|
},
|
||||||
|
function(item) {
|
||||||
|
if (options.value) {
|
||||||
|
return options.value(item);
|
||||||
|
} else {
|
||||||
|
// Fallback is to use the 'pk' field
|
||||||
|
return item.pk;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
function(item) {
|
||||||
|
if (options.title) {
|
||||||
|
return options.title(item);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update the target field with the new options
|
||||||
|
setFieldOptions(fieldName, opts);
|
||||||
|
},
|
||||||
|
error: function(response) {
|
||||||
|
console.log("Error GETting field options");
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -397,6 +526,13 @@ function injectModalForm(modal, form_html) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getFieldByName(modal, name) {
|
||||||
|
/* Find the field (with the given name) within the modal */
|
||||||
|
|
||||||
|
return $(modal).find(`#id_${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function insertNewItemButton(modal, options) {
|
function insertNewItemButton(modal, options) {
|
||||||
/* Insert a button into a modal form, after a field label.
|
/* Insert a button into a modal form, after a field label.
|
||||||
* Looks for a <label> tag inside the form with the attribute "for='id_<field>'"
|
* Looks for a <label> tag inside the form with the attribute "for='id_<field>'"
|
||||||
@ -476,6 +612,39 @@ function attachSecondaries(modal, secondaries) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function attachFieldCallback(modal, callback) {
|
||||||
|
/* Attach a 'callback' function to a given field in the modal form.
|
||||||
|
* When the value of that field is changed, the callback function is performed.
|
||||||
|
*
|
||||||
|
* options:
|
||||||
|
* - field: The name of the field to attach to
|
||||||
|
* - action: A function to perform
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Find the field input in the form
|
||||||
|
var field = getFieldByName(modal, callback.field);
|
||||||
|
|
||||||
|
field.change(function() {
|
||||||
|
|
||||||
|
if (callback.action) {
|
||||||
|
// Run the callback function with the new value of the field!
|
||||||
|
callback.action(field.val(), field);
|
||||||
|
} else {
|
||||||
|
console.log(`Value changed for field ${callback.field} - ${field.val()}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function attachCallbacks(modal, callbacks) {
|
||||||
|
/* Attach a provided list of callback functions */
|
||||||
|
|
||||||
|
for (var i = 0; i < callbacks.length; i++) {
|
||||||
|
attachFieldCallback(modal, callbacks[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function handleModalForm(url, options) {
|
function handleModalForm(url, options) {
|
||||||
/* Update a modal form after data are received from the server.
|
/* Update a modal form after data are received from the server.
|
||||||
* Manages POST requests until the form is successfully submitted.
|
* Manages POST requests until the form is successfully submitted.
|
||||||
@ -575,6 +744,7 @@ function launchModalForm(url, options = {}) {
|
|||||||
* no_post - If true, only display form data, hide submit button, and disallow POST
|
* no_post - If true, only display form data, hide submit button, and disallow POST
|
||||||
* after_render - Callback function to run after form is rendered
|
* after_render - Callback function to run after form is rendered
|
||||||
* secondary - List of secondary modals to attach
|
* secondary - List of secondary modals to attach
|
||||||
|
* callback - List of callback functions to attach to inputs
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var modal = options.modal || '#modal-form';
|
var modal = options.modal || '#modal-form';
|
||||||
@ -615,6 +785,10 @@ function launchModalForm(url, options = {}) {
|
|||||||
attachSecondaries(modal, options.secondary);
|
attachSecondaries(modal, options.secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.callback) {
|
||||||
|
attachCallbacks(modal, options.callback);
|
||||||
|
}
|
||||||
|
|
||||||
if (options.no_post) {
|
if (options.no_post) {
|
||||||
modalShowSubmitButton(modal, false);
|
modalShowSubmitButton(modal, false);
|
||||||
} else {
|
} else {
|
||||||
|
@ -104,12 +104,41 @@ class SupplierPartList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
queryset = super().get_queryset()
|
queryset = super().get_queryset()
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
def filter_queryset(self, queryset):
|
||||||
|
"""
|
||||||
|
Custom filtering for the queryset.
|
||||||
|
"""
|
||||||
|
|
||||||
|
queryset = super().filter_queryset(queryset)
|
||||||
|
|
||||||
|
params = self.request.query_params
|
||||||
|
|
||||||
|
# Filter by manufacturer
|
||||||
|
manufacturer = params.get('manufacturer', None)
|
||||||
|
|
||||||
|
if manufacturer is not None:
|
||||||
|
queryset = queryset.filter(manufacturer=manufacturer)
|
||||||
|
|
||||||
|
# Filter by supplier
|
||||||
|
supplier = params.get('supplier', None)
|
||||||
|
|
||||||
|
if supplier is not None:
|
||||||
|
queryset = queryset.filter(supplier=supplier)
|
||||||
|
|
||||||
# Filter by EITHER manufacturer or supplier
|
# Filter by EITHER manufacturer or supplier
|
||||||
company = self.request.query_params.get('company', None)
|
company = params.get('company', None)
|
||||||
|
|
||||||
if company is not None:
|
if company is not None:
|
||||||
queryset = queryset.filter(Q(manufacturer=company) | Q(supplier=company))
|
queryset = queryset.filter(Q(manufacturer=company) | Q(supplier=company))
|
||||||
|
|
||||||
|
# Filter by parent part?
|
||||||
|
part = params.get('part', None)
|
||||||
|
|
||||||
|
if part is not None:
|
||||||
|
queryset = queryset.filter(part=part)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def get_serializer(self, *args, **kwargs):
|
def get_serializer(self, *args, **kwargs):
|
||||||
@ -130,6 +159,11 @@ class SupplierPartList(generics.ListCreateAPIView):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
kwargs['pretty'] = str2bool(self.request.query_params.get('pretty', None))
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
kwargs['context'] = self.get_serializer_context()
|
kwargs['context'] = self.get_serializer_context()
|
||||||
|
|
||||||
return self.serializer_class(*args, **kwargs)
|
return self.serializer_class(*args, **kwargs)
|
||||||
@ -147,9 +181,6 @@ class SupplierPartList(generics.ListCreateAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
filter_fields = [
|
filter_fields = [
|
||||||
'part',
|
|
||||||
'supplier',
|
|
||||||
'manufacturer',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
search_fields = [
|
search_fields = [
|
||||||
|
@ -418,6 +418,10 @@ class SupplierPart(models.Model):
|
|||||||
|
|
||||||
return [line.order for line in self.purchase_order_line_items.all().prefetch_related('order')]
|
return [line.order for line in self.purchase_order_line_items.all().prefetch_related('order')]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pretty_name(self):
|
||||||
|
return str(self)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
s = "{supplier} ({sku})".format(
|
s = "{supplier} ({sku})".format(
|
||||||
sku=self.SKU,
|
sku=self.SKU,
|
||||||
|
@ -80,13 +80,17 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
|
|||||||
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
|
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
|
||||||
|
|
||||||
supplier_detail = CompanyBriefSerializer(source='supplier', many=False, read_only=True)
|
supplier_detail = CompanyBriefSerializer(source='supplier', many=False, read_only=True)
|
||||||
|
|
||||||
manufacturer_detail = CompanyBriefSerializer(source='manufacturer', many=False, read_only=True)
|
manufacturer_detail = CompanyBriefSerializer(source='manufacturer', many=False, read_only=True)
|
||||||
|
|
||||||
|
pretty_name = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
part_detail = kwargs.pop('part_detail', False)
|
part_detail = kwargs.pop('part_detail', False)
|
||||||
supplier_detail = kwargs.pop('supplier_detail', False)
|
supplier_detail = kwargs.pop('supplier_detail', False)
|
||||||
manufacturer_detail = kwargs.pop('manufacturer_detail', False)
|
manufacturer_detail = kwargs.pop('manufacturer_detail', False)
|
||||||
|
prettify = kwargs.pop('pretty', False)
|
||||||
|
|
||||||
super(SupplierPartSerializer, self).__init__(*args, **kwargs)
|
super(SupplierPartSerializer, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
@ -99,6 +103,9 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
|
|||||||
if manufacturer_detail is not True:
|
if manufacturer_detail is not True:
|
||||||
self.fields.pop('manufacturer_detail')
|
self.fields.pop('manufacturer_detail')
|
||||||
|
|
||||||
|
if prettify is not True:
|
||||||
|
self.fields.pop('pretty_name')
|
||||||
|
|
||||||
supplier = serializers.PrimaryKeyRelatedField(queryset=Company.objects.filter(is_supplier=True))
|
supplier = serializers.PrimaryKeyRelatedField(queryset=Company.objects.filter(is_supplier=True))
|
||||||
|
|
||||||
manufacturer = serializers.PrimaryKeyRelatedField(queryset=Company.objects.filter(is_manufacturer=True))
|
manufacturer = serializers.PrimaryKeyRelatedField(queryset=Company.objects.filter(is_manufacturer=True))
|
||||||
@ -109,6 +116,7 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
|
|||||||
'pk',
|
'pk',
|
||||||
'part',
|
'part',
|
||||||
'part_detail',
|
'part_detail',
|
||||||
|
'pretty_name',
|
||||||
'supplier',
|
'supplier',
|
||||||
'supplier_detail',
|
'supplier_detail',
|
||||||
'SKU',
|
'SKU',
|
||||||
|
@ -44,23 +44,13 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$("#item-create").click(function() {
|
$("#item-create").click(function() {
|
||||||
launchModalForm("{% url 'stock-item-create' %}", {
|
createNewStockItem({
|
||||||
reload: true,
|
|
||||||
data: {
|
data: {
|
||||||
part: {{ part.part.id }},
|
part: {{ part.part.id }},
|
||||||
supplier_part: {{ part.id }},
|
supplier_part: {{ part.id }},
|
||||||
},
|
},
|
||||||
secondary: [
|
reload: true,
|
||||||
{
|
|
||||||
field: 'location',
|
|
||||||
label: '{% trans "New Location" %}',
|
|
||||||
title: '{% trans "Create New Location" %}',
|
|
||||||
url: "{% url 'stock-location-create' %}",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@ -87,3 +87,20 @@ latex:
|
|||||||
interpreter: pdflatex
|
interpreter: pdflatex
|
||||||
# Extra options to pass through to the LaTeX interpreter
|
# Extra options to pass through to the LaTeX interpreter
|
||||||
options: ''
|
options: ''
|
||||||
|
|
||||||
|
# Permit custom authentication backends
|
||||||
|
#authentication_backends:
|
||||||
|
# - 'django.contrib.auth.backends.ModelBackend'
|
||||||
|
|
||||||
|
# Custom middleware, sometimes needed alongside an authentication backend change.
|
||||||
|
#middleware:
|
||||||
|
# - 'django.middleware.security.SecurityMiddleware'
|
||||||
|
# - 'django.contrib.sessions.middleware.SessionMiddleware'
|
||||||
|
# - 'django.middleware.locale.LocaleMiddleware'
|
||||||
|
# - 'django.middleware.common.CommonMiddleware'
|
||||||
|
# - 'django.middleware.csrf.CsrfViewMiddleware'
|
||||||
|
# - 'corsheaders.middleware.CorsMiddleware'
|
||||||
|
# - 'django.contrib.auth.middleware.AuthenticationMiddleware'
|
||||||
|
# - 'django.contrib.messages.middleware.MessageMiddleware'
|
||||||
|
# - 'django.middleware.clickjacking.XFrameOptionsMiddleware'
|
||||||
|
# - 'InvenTree.middleware.AuthRequiredMiddleware'
|
@ -7,6 +7,8 @@ from rapidfuzz import fuzz
|
|||||||
import tablib
|
import tablib
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
@ -47,7 +49,7 @@ def MakeBomTemplate(fmt):
|
|||||||
return DownloadFile(data, filename)
|
return DownloadFile(data, filename)
|
||||||
|
|
||||||
|
|
||||||
def ExportBom(part, fmt='csv', cascade=False, max_levels=None, supplier_data=False):
|
def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=False, stock_data=False, supplier_data=False):
|
||||||
""" Export a BOM (Bill of Materials) for a given part.
|
""" Export a BOM (Bill of Materials) for a given part.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -92,9 +94,75 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, supplier_data=Fal
|
|||||||
|
|
||||||
dataset = BomItemResource().export(queryset=bom_items, cascade=cascade)
|
dataset = BomItemResource().export(queryset=bom_items, cascade=cascade)
|
||||||
|
|
||||||
|
def add_columns_to_dataset(columns, column_size):
|
||||||
|
try:
|
||||||
|
for header, column_dict in columns.items():
|
||||||
|
# Construct column tuple
|
||||||
|
col = tuple(column_dict.get(c_idx, '') for c_idx in range(column_size))
|
||||||
|
# Add column to dataset
|
||||||
|
dataset.append_col(col, header=header)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if parameter_data:
|
||||||
|
"""
|
||||||
|
If requested, add extra columns for each PartParameter associated with each line item
|
||||||
|
"""
|
||||||
|
|
||||||
|
parameter_cols = {}
|
||||||
|
|
||||||
|
for b_idx, bom_item in enumerate(bom_items):
|
||||||
|
# Get part parameters
|
||||||
|
parameters = bom_item.sub_part.get_parameters()
|
||||||
|
# Add parameters to columns
|
||||||
|
if parameters:
|
||||||
|
for parameter in parameters:
|
||||||
|
name = parameter.template.name
|
||||||
|
value = parameter.data
|
||||||
|
|
||||||
|
try:
|
||||||
|
parameter_cols[name].update({b_idx: value})
|
||||||
|
except KeyError:
|
||||||
|
parameter_cols[name] = {b_idx: value}
|
||||||
|
|
||||||
|
# Add parameter columns to dataset
|
||||||
|
parameter_cols_ordered = OrderedDict(sorted(parameter_cols.items(), key=lambda x: x[0]))
|
||||||
|
add_columns_to_dataset(parameter_cols_ordered, len(bom_items))
|
||||||
|
|
||||||
|
if stock_data:
|
||||||
|
"""
|
||||||
|
If requested, add extra columns for stock data associated with each line item
|
||||||
|
"""
|
||||||
|
|
||||||
|
stock_headers = [
|
||||||
|
_('Default Location'),
|
||||||
|
_('Available Stock'),
|
||||||
|
]
|
||||||
|
|
||||||
|
stock_cols = {}
|
||||||
|
|
||||||
|
for b_idx, bom_item in enumerate(bom_items):
|
||||||
|
stock_data = []
|
||||||
|
# Get part default location
|
||||||
|
try:
|
||||||
|
stock_data.append(bom_item.sub_part.get_default_location().name)
|
||||||
|
except AttributeError:
|
||||||
|
stock_data.append('')
|
||||||
|
# Get part current stock
|
||||||
|
stock_data.append(bom_item.sub_part.available_stock)
|
||||||
|
|
||||||
|
for s_idx, header in enumerate(stock_headers):
|
||||||
|
try:
|
||||||
|
stock_cols[header].update({b_idx: stock_data[s_idx]})
|
||||||
|
except KeyError:
|
||||||
|
stock_cols[header] = {b_idx: stock_data[s_idx]}
|
||||||
|
|
||||||
|
# Add stock columns to dataset
|
||||||
|
add_columns_to_dataset(stock_cols, len(bom_items))
|
||||||
|
|
||||||
if supplier_data:
|
if supplier_data:
|
||||||
"""
|
"""
|
||||||
If requested, add extra columns for each SupplierPart associated with the each line item
|
If requested, add extra columns for each SupplierPart associated with each line item
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Expand dataset with manufacturer parts
|
# Expand dataset with manufacturer parts
|
||||||
@ -150,11 +218,7 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, supplier_data=Fal
|
|||||||
manufacturer_cols[k_mpn] = {b_idx: manufacturer_mpn}
|
manufacturer_cols[k_mpn] = {b_idx: manufacturer_mpn}
|
||||||
|
|
||||||
# Add manufacturer columns to dataset
|
# Add manufacturer columns to dataset
|
||||||
for header, col_dict in manufacturer_cols.items():
|
add_columns_to_dataset(manufacturer_cols, len(bom_items))
|
||||||
# Construct column tuple
|
|
||||||
col = tuple(col_dict.get(c_idx, '') for c_idx in range(len(bom_items)))
|
|
||||||
# Add column to dataset
|
|
||||||
dataset.append_col(col, header=header)
|
|
||||||
|
|
||||||
data = dataset.export(fmt)
|
data = dataset.export(fmt)
|
||||||
|
|
||||||
|
@ -58,7 +58,11 @@ class BomExportForm(forms.Form):
|
|||||||
|
|
||||||
levels = forms.IntegerField(label=_("Levels"), required=True, initial=0, help_text=_("Select maximum number of BOM levels to export (0 = all levels)"))
|
levels = forms.IntegerField(label=_("Levels"), required=True, initial=0, help_text=_("Select maximum number of BOM levels to export (0 = all levels)"))
|
||||||
|
|
||||||
supplier_data = forms.BooleanField(label=_("Include Supplier Data"), required=False, initial=True, help_text=_("Include supplier part data in exported BOM"))
|
parameter_data = forms.BooleanField(label=_("Include Parameter Data"), required=False, initial=False, help_text=_("Include part parameters data in exported BOM"))
|
||||||
|
|
||||||
|
stock_data = forms.BooleanField(label=_("Include Stock Data"), required=False, initial=False, help_text=_("Include part stock data in exported BOM"))
|
||||||
|
|
||||||
|
supplier_data = forms.BooleanField(label=_("Include Supplier Data"), required=False, initial=True, help_text=_("Include part supplier data in exported BOM"))
|
||||||
|
|
||||||
def get_choices(self):
|
def get_choices(self):
|
||||||
""" BOM export format choices """
|
""" BOM export format choices """
|
||||||
@ -196,11 +200,19 @@ class EditCategoryForm(HelperForm):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class PartModelChoiceField(forms.ModelChoiceField):
|
||||||
|
""" Extending string representation of Part instance with available stock """
|
||||||
|
def label_from_instance(self, part):
|
||||||
|
return f'{part} - {part.available_stock}'
|
||||||
|
|
||||||
|
|
||||||
class EditBomItemForm(HelperForm):
|
class EditBomItemForm(HelperForm):
|
||||||
""" Form for editing a BomItem object """
|
""" Form for editing a BomItem object """
|
||||||
|
|
||||||
quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5)
|
quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5)
|
||||||
|
|
||||||
|
sub_part = PartModelChoiceField(queryset=Part.objects.all())
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = BomItem
|
model = BomItem
|
||||||
fields = [
|
fields = [
|
||||||
|
@ -268,7 +268,7 @@ class Part(MPTTModel):
|
|||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "{n} - {d}".format(n=self.full_name, d=self.description)
|
return f"{self.full_name} - {self.description}"
|
||||||
|
|
||||||
def checkAddToBOM(self, parent):
|
def checkAddToBOM(self, parent):
|
||||||
"""
|
"""
|
||||||
|
@ -63,7 +63,7 @@
|
|||||||
<option value=''>--- {% trans "Select Part" %} ---</option>
|
<option value=''>--- {% trans "Select Part" %} ---</option>
|
||||||
{% for part in row.part_options %}
|
{% for part in row.part_options %}
|
||||||
<option value='{{ part.id }}' {% if part.id == row.part.id %} selected='selected' {% elif part.id == row.part_match.id %} selected='selected' {% endif %}>
|
<option value='{{ part.id }}' {% if part.id == row.part.id %} selected='selected' {% elif part.id == row.part_match.id %} selected='selected' {% endif %}>
|
||||||
{{ part.full_name }} - {{ part.description }}
|
{{ part }} - {{ part.available_stock }}
|
||||||
</option>
|
</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
|
@ -25,12 +25,10 @@
|
|||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
|
||||||
$('#add-stock-item').click(function () {
|
$('#add-stock-item').click(function () {
|
||||||
launchModalForm(
|
createNewStockItem({
|
||||||
"{% url 'stock-item-create' %}",
|
|
||||||
{
|
|
||||||
reload: true,
|
reload: true,
|
||||||
data: {
|
data: {
|
||||||
part: {{ part.id }}
|
part: {{ part.id }},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -64,37 +62,12 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$('#item-create').click(function () {
|
$('#item-create').click(function () {
|
||||||
launchModalForm("{% url 'stock-item-create' %}", {
|
createNewStockItem({
|
||||||
reload: true,
|
reload: true,
|
||||||
data: {
|
data: {
|
||||||
part: {{ part.id }}
|
part: {{ part.id }},
|
||||||
},
|
|
||||||
secondary: [
|
|
||||||
{
|
|
||||||
field: 'part',
|
|
||||||
label: '{% trans "New Part" %}',
|
|
||||||
title: '{% trans "Create New Part" %}',
|
|
||||||
url: "{% url 'part-create' %}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'supplier_part',
|
|
||||||
label: '{% trans "New Supplier Part" %}',
|
|
||||||
title: '{% trans "Create new Supplier Part" %}',
|
|
||||||
url: "{% url 'supplier-part-create' %}",
|
|
||||||
data: {
|
|
||||||
part: {{ part.id }}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'location',
|
|
||||||
label: '{% trans "New Location" %}',
|
|
||||||
title: '{% trans "Create New Location" %}',
|
|
||||||
url: "{% url 'stock-location-create' %}",
|
|
||||||
}
|
}
|
||||||
]
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -1499,6 +1499,10 @@ class BomDownload(AjaxView):
|
|||||||
|
|
||||||
cascade = str2bool(request.GET.get('cascade', False))
|
cascade = str2bool(request.GET.get('cascade', False))
|
||||||
|
|
||||||
|
parameter_data = str2bool(request.GET.get('parameter_data', False))
|
||||||
|
|
||||||
|
stock_data = str2bool(request.GET.get('stock_data', False))
|
||||||
|
|
||||||
supplier_data = str2bool(request.GET.get('supplier_data', False))
|
supplier_data = str2bool(request.GET.get('supplier_data', False))
|
||||||
|
|
||||||
levels = request.GET.get('levels', None)
|
levels = request.GET.get('levels', None)
|
||||||
@ -1516,7 +1520,13 @@ class BomDownload(AjaxView):
|
|||||||
if not IsValidBOMFormat(export_format):
|
if not IsValidBOMFormat(export_format):
|
||||||
export_format = 'csv'
|
export_format = 'csv'
|
||||||
|
|
||||||
return ExportBom(part, fmt=export_format, cascade=cascade, max_levels=levels, supplier_data=supplier_data)
|
return ExportBom(part,
|
||||||
|
fmt=export_format,
|
||||||
|
cascade=cascade,
|
||||||
|
max_levels=levels,
|
||||||
|
parameter_data=parameter_data,
|
||||||
|
stock_data=stock_data,
|
||||||
|
supplier_data=supplier_data)
|
||||||
|
|
||||||
def get_data(self):
|
def get_data(self):
|
||||||
return {
|
return {
|
||||||
@ -1541,6 +1551,8 @@ class BomExport(AjaxView):
|
|||||||
fmt = request.POST.get('file_format', 'csv').lower()
|
fmt = request.POST.get('file_format', 'csv').lower()
|
||||||
cascade = str2bool(request.POST.get('cascading', False))
|
cascade = str2bool(request.POST.get('cascading', False))
|
||||||
levels = request.POST.get('levels', None)
|
levels = request.POST.get('levels', None)
|
||||||
|
parameter_data = str2bool(request.POST.get('parameter_data', False))
|
||||||
|
stock_data = str2bool(request.POST.get('stock_data', False))
|
||||||
supplier_data = str2bool(request.POST.get('supplier_data', False))
|
supplier_data = str2bool(request.POST.get('supplier_data', False))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -1556,6 +1568,8 @@ class BomExport(AjaxView):
|
|||||||
|
|
||||||
url += '?file_format=' + fmt
|
url += '?file_format=' + fmt
|
||||||
url += '&cascade=' + str(cascade)
|
url += '&cascade=' + str(cascade)
|
||||||
|
url += '¶meter_data=' + str(parameter_data)
|
||||||
|
url += '&stock_data=' + str(stock_data)
|
||||||
url += '&supplier_data=' + str(supplier_data)
|
url += '&supplier_data=' + str(supplier_data)
|
||||||
|
|
||||||
if levels:
|
if levels:
|
||||||
|
@ -180,7 +180,7 @@ class TestReport(ReportTemplateBase):
|
|||||||
Test if this report template matches a given StockItem objects
|
Test if this report template matches a given StockItem objects
|
||||||
"""
|
"""
|
||||||
|
|
||||||
filters = validateFilterString(self.part_filters)
|
filters = validateFilterString(self.filters)
|
||||||
|
|
||||||
items = StockItem.objects.filter(**filters)
|
items = StockItem.objects.filter(**filters)
|
||||||
|
|
||||||
|
@ -237,7 +237,7 @@ class TestReportFormatForm(HelperForm):
|
|||||||
|
|
||||||
for template in templates:
|
for template in templates:
|
||||||
if template.matches_stock_item(self.stock_item):
|
if template.matches_stock_item(self.stock_item):
|
||||||
choices.append(template)
|
choices.append((template.pk, template))
|
||||||
|
|
||||||
return choices
|
return choices
|
||||||
|
|
||||||
|
@ -320,15 +320,12 @@ $("#print-label").click(function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$("#stock-duplicate").click(function() {
|
$("#stock-duplicate").click(function() {
|
||||||
launchModalForm(
|
createNewStockItem({
|
||||||
"{% url 'stock-item-create' %}",
|
|
||||||
{
|
|
||||||
follow: true,
|
follow: true,
|
||||||
data: {
|
data: {
|
||||||
copy: {{ item.id }},
|
copy: {{ item.id }},
|
||||||
},
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#stock-edit").click(function () {
|
$("#stock-edit").click(function () {
|
||||||
|
@ -204,38 +204,13 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
$('#item-create').click(function () {
|
$('#item-create').click(function () {
|
||||||
launchModalForm("{% url 'stock-item-create' %}",
|
createNewStockItem({
|
||||||
{
|
|
||||||
follow: true,
|
|
||||||
data: {
|
data: {
|
||||||
{% if location %}
|
{% if location %}
|
||||||
location: {{ location.id }}
|
location: {{ location.id }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
},
|
|
||||||
secondary: [
|
|
||||||
{
|
|
||||||
field: 'part',
|
|
||||||
label: 'New Part',
|
|
||||||
title: 'Create New Part',
|
|
||||||
url: "{% url 'part-create' %}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'supplier_part',
|
|
||||||
label: 'New Supplier Part',
|
|
||||||
title: 'Create new Supplier Part',
|
|
||||||
url: "{% url 'supplier-part-create' %}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'location',
|
|
||||||
label: 'New Location',
|
|
||||||
title: 'Create New Location',
|
|
||||||
url: "{% url 'stock-location-create' %}",
|
|
||||||
}
|
}
|
||||||
]
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
loadStockTable($("#stock-table"), {
|
loadStockTable($("#stock-table"), {
|
||||||
|
@ -1120,6 +1120,7 @@ class StockItemSerialize(AjaxUpdateView):
|
|||||||
|
|
||||||
initials['quantity'] = item.quantity
|
initials['quantity'] = item.quantity
|
||||||
initials['serial_numbers'] = item.part.getSerialNumberString(item.quantity)
|
initials['serial_numbers'] = item.part.getSerialNumberString(item.quantity)
|
||||||
|
if item.location is not None:
|
||||||
initials['destination'] = item.location.pk
|
initials['destination'] = item.location.pk
|
||||||
|
|
||||||
return initials
|
return initials
|
||||||
@ -1239,7 +1240,7 @@ class StockItemCreate(AjaxCreateView):
|
|||||||
form.rebuild_layout()
|
form.rebuild_layout()
|
||||||
|
|
||||||
# Hide the 'part' field (as a valid part is selected)
|
# Hide the 'part' field (as a valid part is selected)
|
||||||
form.fields['part'].widget = HiddenInput()
|
# form.fields['part'].widget = HiddenInput()
|
||||||
|
|
||||||
# trackable parts get special consideration
|
# trackable parts get special consideration
|
||||||
if part.trackable:
|
if part.trackable:
|
||||||
@ -1266,6 +1267,11 @@ class StockItemCreate(AjaxCreateView):
|
|||||||
# TODO - This does NOT work for some reason? Ref build.views.BuildItemCreate
|
# TODO - This does NOT work for some reason? Ref build.views.BuildItemCreate
|
||||||
form.fields['supplier_part'].initial = all_parts[0].id
|
form.fields['supplier_part'].initial = all_parts[0].id
|
||||||
|
|
||||||
|
else:
|
||||||
|
# No Part has been selected!
|
||||||
|
# We must not provide *any* options for SupplierPart
|
||||||
|
form.fields['supplier_part'].queryset = SupplierPart.objects.none()
|
||||||
|
|
||||||
# Otherwise if the user has selected a SupplierPart, we know what Part they meant!
|
# Otherwise if the user has selected a SupplierPart, we know what Part they meant!
|
||||||
if form['supplier_part'].value() is not None:
|
if form['supplier_part'].value() is not None:
|
||||||
pass
|
pass
|
||||||
|
@ -735,3 +735,59 @@ function loadStockTrackingTable(table, options) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function createNewStockItem(options) {
|
||||||
|
/* Launch a modal form to create a new stock item.
|
||||||
|
*
|
||||||
|
* This is really just a helper function which calls launchModalForm,
|
||||||
|
* but it does get called a lot, so here we are ...
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Add in some funky options
|
||||||
|
|
||||||
|
options.callback = [
|
||||||
|
{
|
||||||
|
field: 'part',
|
||||||
|
action: function(value) {
|
||||||
|
|
||||||
|
reloadFieldOptions(
|
||||||
|
'supplier_part',
|
||||||
|
{
|
||||||
|
url: "{% url 'api-supplier-part-list' %}",
|
||||||
|
params: {
|
||||||
|
part: value,
|
||||||
|
pretty: true,
|
||||||
|
},
|
||||||
|
text: function(item) {
|
||||||
|
return item.pretty_name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
options.secondary = [
|
||||||
|
{
|
||||||
|
field: 'part',
|
||||||
|
label: '{% trans "New Part" %}',
|
||||||
|
title: '{% trans "Create New Part" %}',
|
||||||
|
url: "{% url 'part-create' %}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'supplier_part',
|
||||||
|
label: '{% trans "New Supplier Part" %}',
|
||||||
|
title: '{% trans "Create new Supplier Part" %}',
|
||||||
|
url: "{% url 'supplier-part-create' %}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'location',
|
||||||
|
label: '{% trans "New Location" %}',
|
||||||
|
title: '{% trans "Create New Location" %}',
|
||||||
|
url: "{% url 'stock-location-create' %}",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
launchModalForm("{% url 'stock-item-create' %}", options);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user