Merge branch 'inventree:master' into plugin-2037

This commit is contained in:
Matthias Mair 2021-10-29 00:21:07 +02:00 committed by GitHub
commit e8de149e2b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 154 additions and 49 deletions

View File

@ -266,7 +266,7 @@ class RegistratonMixin:
return False return False
def save_user(self, request, user, form, commit=True): def save_user(self, request, user, form, commit=True):
user = super().save_user(request, user, form, commit=commit) user = super().save_user(request, user, form)
start_group = InvenTreeSetting.get_setting('SIGNUP_GROUP') start_group = InvenTreeSetting.get_setting('SIGNUP_GROUP')
if start_group: if start_group:
try: try:

View File

@ -36,6 +36,13 @@ class InvenTreeMoneySerializer(MoneyField):
Ref: https://github.com/django-money/django-money/blob/master/djmoney/contrib/django_rest_framework/fields.py Ref: https://github.com/django-money/django-money/blob/master/djmoney/contrib/django_rest_framework/fields.py
""" """
def __init__(self, *args, **kwargs):
kwargs["max_digits"] = kwargs.get("max_digits", 19)
kwargs["decimal_places"] = kwargs.get("decimal_places", 4)
super().__init__(*args, **kwargs)
def get_value(self, data): def get_value(self, data):
""" """
Test that the returned amount is a valid Decimal Test that the returned amount is a valid Decimal
@ -52,7 +59,7 @@ class InvenTreeMoneySerializer(MoneyField):
amount = Decimal(amount) amount = Decimal(amount)
except: except:
raise ValidationError({ raise ValidationError({
self.field_name: _("Must be a valid number") self.field_name: [_("Must be a valid number")],
}) })
currency = data.get(get_currency_field_name(self.field_name), self.default_currency) currency = data.get(get_currency_field_name(self.field_name), self.default_currency)

View File

@ -9,6 +9,7 @@ from rest_framework import serializers
from sql_util.utils import SubqueryCount from sql_util.utils import SubqueryCount
from InvenTree.serializers import InvenTreeModelSerializer from InvenTree.serializers import InvenTreeModelSerializer
from InvenTree.serializers import InvenTreeMoneySerializer
from InvenTree.serializers import InvenTreeImageSerializerField from InvenTree.serializers import InvenTreeImageSerializerField
from part.serializers import PartBriefSerializer from part.serializers import PartBriefSerializer
@ -256,7 +257,11 @@ class SupplierPriceBreakSerializer(InvenTreeModelSerializer):
quantity = serializers.FloatField() quantity = serializers.FloatField()
price = serializers.CharField() price = InvenTreeMoneySerializer(
allow_null=True,
required=True,
label=_('Price'),
)
price_currency = serializers.ChoiceField( price_currency = serializers.ChoiceField(
choices=currency_code_mappings(), choices=currency_code_mappings(),

View File

@ -154,7 +154,6 @@ class POLineItemSerializer(InvenTreeModelSerializer):
supplier_part_detail = SupplierPartSerializer(source='part', many=False, read_only=True) supplier_part_detail = SupplierPartSerializer(source='part', many=False, read_only=True)
purchase_price = InvenTreeMoneySerializer( purchase_price = InvenTreeMoneySerializer(
max_digits=19, decimal_places=4,
allow_null=True allow_null=True
) )
@ -557,8 +556,6 @@ class SOLineItemSerializer(InvenTreeModelSerializer):
fulfilled = serializers.FloatField(source='fulfilled_quantity', read_only=True) fulfilled = serializers.FloatField(source='fulfilled_quantity', read_only=True)
sale_price = InvenTreeMoneySerializer( sale_price = InvenTreeMoneySerializer(
max_digits=19,
decimal_places=4,
allow_null=True allow_null=True
) )

View File

@ -19,9 +19,13 @@
<div class='panel-content'> <div class='panel-content'>
{% if roles.sales_order.change %} {% if roles.sales_order.change %}
<div id='order-toolbar-buttons' class='btn-group' style='float: right;'> <div id='order-toolbar-buttons' class='btn-group' style='float: right;'>
<button type='button' class='btn btn-success' id='new-so-line'> <div class='btn-group'>
<span class='fas fa-plus-circle'></span> {% trans "Add Line Item" %} <button type='button' class='btn btn-success' id='new-so-line'>
</button> <span class='fas fa-plus-circle'></span> {% trans "Add Line Item" %}
</button>
<div class='filter-list' id='filter-list-sales-order-lines'>
</div>
</div>
</div> </div>
{% endif %} {% endif %}
<table class='table table-striped table-condensed' id='so-lines-table' data-toolbar='#order-toolbar-buttons'> <table class='table table-striped table-condensed' id='so-lines-table' data-toolbar='#order-toolbar-buttons'>

View File

@ -109,7 +109,6 @@ class PartSalePriceSerializer(InvenTreeModelSerializer):
quantity = serializers.FloatField() quantity = serializers.FloatField()
price = InvenTreeMoneySerializer( price = InvenTreeMoneySerializer(
max_digits=19, decimal_places=4,
allow_null=True allow_null=True
) )
@ -134,7 +133,6 @@ class PartInternalPriceSerializer(InvenTreeModelSerializer):
quantity = serializers.FloatField() quantity = serializers.FloatField()
price = InvenTreeMoneySerializer( price = InvenTreeMoneySerializer(
max_digits=19, decimal_places=4,
allow_null=True allow_null=True
) )

View File

@ -128,6 +128,13 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
<p>
<!-- Details show/hide button -->
<button id="toggle-part-details" class="btn btn-primary" data-toggle="collapse" data-target="#collapsible-part-details" value="show">
<span class="fas fa-chevron-down"></span> {% trans "Show Part Details" %}
</button>
</p>
</div> </div>
</div> </div>
<div class='info-messages'> <div class='info-messages'>
@ -208,13 +215,6 @@
</div> </div>
</div> </div>
<p>
<!-- Details show/hide button -->
<button id="toggle-part-details" class="btn btn-primary" data-toggle="collapse" data-target="#collapsible-part-details" value="show">
<span class="fas fa-chevron-down"></span> {% trans "Show Part Details" %}
</button>
</p>
<div class="collapse" id="collapsible-part-details"> <div class="collapse" id="collapsible-part-details">
<div class="card card-body"> <div class="card card-body">
<!-- Details Table --> <!-- Details Table -->

View File

@ -148,7 +148,6 @@ class StockItemSerializer(InvenTreeModelSerializer):
purchase_price = InvenTreeMoneySerializer( purchase_price = InvenTreeMoneySerializer(
label=_('Purchase Price'), label=_('Purchase Price'),
max_digits=19, decimal_places=4,
allow_null=True allow_null=True
) )

View File

@ -283,6 +283,11 @@ function setupFilterList(tableKey, table, target) {
element.append(`<button id='reload-${tableKey}' title='{% trans "Reload data" %}' class='btn btn-default filter-tag'><span class='fas fa-redo-alt'></span></button>`); element.append(`<button id='reload-${tableKey}' title='{% trans "Reload data" %}' class='btn btn-default filter-tag'><span class='fas fa-redo-alt'></span></button>`);
// Callback for reloading the table
element.find(`#reload-${tableKey}`).click(function() {
$(table).bootstrapTable('refresh');
});
// If there are no filters defined for this table, exit now // If there are no filters defined for this table, exit now
if (jQuery.isEmptyObject(getAvailableTableFilters(tableKey))) { if (jQuery.isEmptyObject(getAvailableTableFilters(tableKey))) {
return; return;
@ -303,11 +308,6 @@ function setupFilterList(tableKey, table, target) {
element.append(`<div title='${description}' class='filter-tag'>${title} = ${value}<span ${tag}='${key}' class='close'>x</span></div>`); element.append(`<div title='${description}' class='filter-tag'>${title} = ${value}<span ${tag}='${key}' class='close'>x</span></div>`);
} }
// Callback for reloading the table
element.find(`#reload-${tableKey}`).click(function() {
$(table).bootstrapTable('refresh');
});
// Add a callback for adding a new filter // Add a callback for adding a new filter
element.find(`#${add}`).click(function clicked() { element.find(`#${add}`).click(function clicked() {

View File

@ -621,6 +621,10 @@ function submitFormData(fields, options) {
var has_files = false; var has_files = false;
var data_valid = true;
var data_errors = {};
// Extract values for each field // Extract values for each field
for (var idx = 0; idx < options.field_names.length; idx++) { for (var idx = 0; idx < options.field_names.length; idx++) {
@ -633,6 +637,21 @@ function submitFormData(fields, options) {
if (field) { if (field) {
switch (field.type) {
// Ensure numerical fields are "valid"
case 'integer':
case 'float':
case 'decimal':
if (!validateFormField(name, options)) {
data_valid = false;
data_errors[name] = ['{% trans "Enter a valid number" %}'];
}
break;
default:
break;
}
var value = getFormFieldValue(name, field, options); var value = getFormFieldValue(name, field, options);
// Handle file inputs // Handle file inputs
@ -662,6 +681,11 @@ function submitFormData(fields, options) {
} }
} }
if (!data_valid) {
handleFormErrors(data_errors, fields, options);
return;
}
var upload_func = inventreePut; var upload_func = inventreePut;
if (has_files) { if (has_files) {
@ -732,7 +756,8 @@ function updateFieldValues(fields, options) {
* Update the value of a named field * Update the value of a named field
*/ */
function updateFieldValue(name, value, field, options) { function updateFieldValue(name, value, field, options) {
var el = $(options.modal).find(`#id_${name}`);
var el = getFormFieldElement(name, options);
if (!el) { if (!el) {
console.log(`WARNING: updateFieldValue could not find field '${name}'`); console.log(`WARNING: updateFieldValue could not find field '${name}'`);
@ -760,6 +785,46 @@ function updateFieldValue(name, value, field, options) {
} }
// Find the named field element in the modal DOM
function getFormFieldElement(name, options) {
var el = $(options.modal).find(`#id_${name}`);
if (!el.exists) {
console.log(`ERROR: Could not find form element for field '${name}'`);
}
return el;
}
/*
* Check that a "numerical" input field has a valid number in it.
* An invalid number is expunged at the client side by the getFormFieldValue() function,
* which means that an empty string '' is sent to the server if the number is not valud.
* This can result in confusing error messages displayed under the form field.
*
* So, we can invalid numbers and display errors *before* the form is submitted!
*/
function validateFormField(name, options) {
if (getFormFieldElement(name, options)) {
var el = document.getElementById(`id_${name}`);
if (el.validity.valueMissing) {
// Accept empty strings (server will validate)
return true;
} else {
return el.validity.valid;
}
} else {
return false;
}
}
/* /*
* Extract and field value before sending back to the server * Extract and field value before sending back to the server
* *
@ -771,7 +836,7 @@ function updateFieldValue(name, value, field, options) {
function getFormFieldValue(name, field, options) { function getFormFieldValue(name, field, options) {
// Find the HTML element // Find the HTML element
var el = $(options.modal).find(`#id_${name}`); var el = getFormFieldElement(name, options);
if (!el) { if (!el) {
return null; return null;
@ -1086,7 +1151,9 @@ function addFieldCallbacks(fields, options) {
function addFieldCallback(name, field, options) { function addFieldCallback(name, field, options) {
$(options.modal).find(`#id_${name}`).change(function() { var el = getFormFieldElement(name, options);
el.change(function() {
var value = getFormFieldValue(name, field, options); var value = getFormFieldValue(name, field, options);
@ -1299,7 +1366,7 @@ function initializeRelatedField(field, fields, options) {
} }
// Find the select element and attach a select2 to it // Find the select element and attach a select2 to it
var select = $(options.modal).find(`#id_${name}`); var select = getFormFieldElement(name, options);
// Add a button to launch a 'secondary' modal // Add a button to launch a 'secondary' modal
if (field.secondary != null) { if (field.secondary != null) {
@ -1492,7 +1559,7 @@ function initializeRelatedField(field, fields, options) {
*/ */
function setRelatedFieldData(name, data, options) { function setRelatedFieldData(name, data, options) {
var select = $(options.modal).find(`#id_${name}`); var select = getFormFieldElement(name, options);
var option = new Option(name, data.pk, true, true); var option = new Option(name, data.pk, true, true);
@ -1513,9 +1580,7 @@ function setRelatedFieldData(name, data, options) {
function initializeChoiceField(field, fields, options) { function initializeChoiceField(field, fields, options) {
var name = field.name; var select = getFormFieldElement(field.name, options);
var select = $(options.modal).find(`#id_${name}`);
select.select2({ select.select2({
dropdownAutoWidth: false, dropdownAutoWidth: false,
@ -1926,8 +1991,17 @@ function constructInputOptions(name, classes, type, parameters) {
opts.push(`placeholder='${parameters.placeholder}'`); opts.push(`placeholder='${parameters.placeholder}'`);
} }
if (parameters.type == 'boolean') { switch (parameters.type) {
case 'boolean':
opts.push(`style='display: inline-block; width: 20px; margin-right: 20px;'`); opts.push(`style='display: inline-block; width: 20px; margin-right: 20px;'`);
break;
case 'integer':
case 'float':
case 'decimal':
opts.push(`step='any'`);
break;
default:
break;
} }
if (parameters.multiline) { if (parameters.multiline) {

View File

@ -864,6 +864,7 @@ function loadPurchaseOrderLineItemTable(table, options={}) {
}, },
{ {
sortable: true, sortable: true,
switchable: false,
field: 'quantity', field: 'quantity',
title: '{% trans "Quantity" %}', title: '{% trans "Quantity" %}',
footerFormatter: function(data) { footerFormatter: function(data) {
@ -879,18 +880,29 @@ function loadPurchaseOrderLineItemTable(table, options={}) {
field: 'purchase_price', field: 'purchase_price',
title: '{% trans "Unit Price" %}', title: '{% trans "Unit Price" %}',
formatter: function(value, row) { formatter: function(value, row) {
return row.purchase_price_string || row.purchase_price; var formatter = new Intl.NumberFormat(
'en-US',
{
style: 'currency',
currency: row.purchase_price_currency
}
);
return formatter.format(row.purchase_price);
} }
}, },
{ {
field: 'total_price', field: 'total_price',
sortable: true, sortable: true,
field: 'total_price', title: '{% trans "Total Price" %}',
title: '{% trans "Total price" %}',
formatter: function(value, row) { formatter: function(value, row) {
var total = row.purchase_price * row.quantity; var formatter = new Intl.NumberFormat(
var formatter = new Intl.NumberFormat('en-US', {style: 'currency', currency: row.purchase_price_currency}); 'en-US',
return formatter.format(total); {
style: 'currency',
currency: row.purchase_price_currency
}
);
return formatter.format(row.purchase_price * row.quantity);
}, },
footerFormatter: function(data) { footerFormatter: function(data) {
var total = data.map(function(row) { var total = data.map(function(row) {
@ -1436,7 +1448,7 @@ function loadSalesOrderLineItemTable(table, options={}) {
sortable: true, sortable: true,
field: 'reference', field: 'reference',
title: '{% trans "Reference" %}', title: '{% trans "Reference" %}',
switchable: false, switchable: true,
}, },
{ {
sortable: true, sortable: true,
@ -1456,14 +1468,6 @@ function loadSalesOrderLineItemTable(table, options={}) {
field: 'sale_price', field: 'sale_price',
title: '{% trans "Unit Price" %}', title: '{% trans "Unit Price" %}',
formatter: function(value, row) { formatter: function(value, row) {
return row.sale_price_string || row.sale_price;
}
},
{
sortable: true,
title: '{% trans "Total price" %}',
formatter: function(value, row) {
var total = row.sale_price * row.quantity;
var formatter = new Intl.NumberFormat( var formatter = new Intl.NumberFormat(
'en-US', 'en-US',
{ {
@ -1472,7 +1476,23 @@ function loadSalesOrderLineItemTable(table, options={}) {
} }
); );
return formatter.format(total); return formatter.format(row.sale_price);
}
},
{
field: 'total_price',
sortable: true,
title: '{% trans "Total Price" %}',
formatter: function(value, row) {
var formatter = new Intl.NumberFormat(
'en-US',
{
style: 'currency',
currency: row.sale_price_currency
}
);
return formatter.format(row.sale_price * row.quantity);
}, },
footerFormatter: function(data) { footerFormatter: function(data) {
var total = data.map(function(row) { var total = data.map(function(row) {
@ -1544,6 +1564,7 @@ function loadSalesOrderLineItemTable(table, options={}) {
if (pending) { if (pending) {
columns.push({ columns.push({
field: 'buttons', field: 'buttons',
switchable: false,
formatter: function(value, row, index, field) { formatter: function(value, row, index, field) {
var html = `<div class='btn-group float-right' role='group'>`; var html = `<div class='btn-group float-right' role='group'>`;