mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'inventree:master' into plugin-2037
This commit is contained in:
commit
e8de149e2b
@ -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:
|
||||||
|
@ -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)
|
||||||
|
@ -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(),
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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'>
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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 -->
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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() {
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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'>`;
|
||||||
|
Loading…
Reference in New Issue
Block a user