mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 05:05:42 +00:00 
			
		
		
		
	Merge branch 'inventree:master' into plugin-2037
This commit is contained in:
		| @@ -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'>`; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user