Compare commits

...

9 Commits

Author SHA1 Message Date
Oliver
26bf51c20a Back porting of security patches (#3197)
* Merge pull request from GHSA-fr2w-mp56-g4xp

* Enforce file download for attachments table(s)

* Enforce file download for attachment in 'StockItemTestResult' table

(cherry picked from commit 76aa3a75f2)

* Merge pull request from GHSA-7rq4-qcpw-74gq

* Merge pull request from GHSA-rm89-9g65-4ffr

* Enable HTML escaping for all tables by default

* Enable HTML escaping for all tables by default

* Adds automatic escaping for bootstrap tables where custom formatter function is specified

- Intercept the row data *before* it is provided to the renderer function
- Adds a function for sanitizing nested data structure

* Sanitize form data before processing

(cherry picked from commit cd418d6948)

* Increment version number for release

* Fix sanitization for array case - was missing a return value
2022-06-15 20:43:43 +10:00
Oliver
f9c28eedaf Add error handling for case where user does not have git installed (#3179) (#3198)
(cherry picked from commit 5ecba6b13c)
2022-06-15 18:52:10 +10:00
Oliver
9bdbb0137f Adds release.yml file for auto-generation of release notes (#3194) 2022-06-14 20:30:09 +10:00
Oliver
412b464b09 Prevent auto-creation of SalesOrderShipment if we are importing data (#3170)
- Fixes a bug which prevents importing of datasets
2022-06-10 11:26:16 +10:00
Oliver
f48bd62534 Bump version number 2022-06-02 16:35:00 +10:00
Oliver
bd92ff1290 Fix filtering for purchaseorder table on supplierpart page (#3115) 2022-06-02 14:25:28 +10:00
Oliver
3b3238f762 Check user permissions before performing search (#3083)
* Check user permissions before performing search

* JS linting

(cherry picked from commit 6c7a80c141)
2022-05-27 13:27:28 +10:00
Oliver
81d29efc12 Improve error management for order price calculation (#3075)
* Improve error management for order price calculation

- If there are missing exchange rates, it throws an error
- Very much an edge case

* Style fixes

* Add warning message if total order price cannot be calculated

* price -> cost

(cherry picked from commit 640a5d0f24)
2022-05-27 13:27:22 +10:00
Oliver Walters
044315afbe Bump version number 2022-05-24 20:33:48 +10:00
20 changed files with 387 additions and 83 deletions

31
.github/release.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
# .github/release.yml
changelog:
exclude:
labels:
- translation
categories:
- title: Breaking Changes
labels:
- Semver-Major
- breaking
- title: Security Patches
labels:
- security
- title: New Features
labels:
- Semver-Minor
- enhancement
- title: Bug Fixes
labels:
- Semver-Patch
- bug
- title: Devops / Setup Changes
labels:
- docker
- setup
- demo
- CI
- title: Other Changes
labels:
- "*"

View File

@@ -0,0 +1,33 @@
"""Admin classes"""
from import_export.resources import ModelResource
class InvenTreeResource(ModelResource):
"""Custom subclass of the ModelResource class provided by django-import-export"
Ensures that exported data are escaped to prevent malicious formula injection.
Ref: https://owasp.org/www-community/attacks/CSV_Injection
"""
def export_resource(self, obj):
"""Custom function to override default row export behaviour.
Specifically, strip illegal leading characters to prevent formula injection
"""
row = super().export_resource(obj)
illegal_start_vals = ['@', '=', '+', '-', '@', '\t', '\r', '\n']
for idx, val in enumerate(row):
if type(val) is str:
val = val.strip()
# If the value starts with certain 'suspicious' values, remove it!
while len(val) > 0 and val[0] in illegal_start_vals:
# Remove the first character
val = val[1:]
row[idx] = val
return row

View File

@@ -13,6 +13,7 @@
inventreeDocReady,
inventreeLoad,
inventreeSave,
sanitizeData,
*/
function attachClipboard(selector, containerselector, textElement) {
@@ -273,6 +274,42 @@ function loadBrandIcon(element, name) {
}
}
/*
* Function to sanitize a (potentially nested) object.
* Iterates through all levels, and sanitizes each primitive string.
*
* Note that this function effectively provides a "deep copy" of the provided data,
* and the original data structure is unaltered.
*/
function sanitizeData(data) {
if (data == null) {
return null;
} else if (Array.isArray(data)) {
// Handle arrays
var arr = [];
data.forEach(function(val) {
arr.push(sanitizeData(val));
});
return arr;
} else if (typeof(data) === 'object') {
// Handle nested structures
var nested = {};
$.each(data, function(k, v) {
nested[k] = sanitizeData(v);
});
return nested;
} else if (typeof(data) === 'string') {
// Perform string replacement
return data.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;').replace(/`/g, '&#x60;');
} else {
return data;
}
}
// Convenience function to determine if an element exists
$.fn.exists = function() {
return this.length !== 0;

View File

@@ -12,7 +12,7 @@ import common.models
from InvenTree.api_version import INVENTREE_API_VERSION
# InvenTree software version
INVENTREE_SW_VERSION = "0.7.0 dev"
INVENTREE_SW_VERSION = "0.7.2"
def inventreeInstanceName():

View File

@@ -2,16 +2,15 @@ from django.contrib import admin
from import_export.admin import ImportExportModelAdmin
from import_export.fields import Field
from import_export.resources import ModelResource
import import_export.widgets as widgets
from build.models import Build, BuildItem
from InvenTree.admin import InvenTreeResource
import part.models
class BuildResource(ModelResource):
"""Class for managing import/export of Build data"""
class BuildResource(InvenTreeResource):
"""Class for managing import/export of Build data."""
# For some reason, we need to specify the fields individually for this ModelResource,
# but we don't for other ones.
# TODO: 2022-05-12 - Need to investigate why this is the case!

View File

@@ -3,8 +3,8 @@ from django.contrib import admin
import import_export.widgets as widgets
from import_export.admin import ImportExportModelAdmin
from import_export.fields import Field
from import_export.resources import ModelResource
from InvenTree.admin import InvenTreeResource
from part.models import Part
from .models import (Company, ManufacturerPart, ManufacturerPartAttachment,
@@ -12,8 +12,8 @@ from .models import (Company, ManufacturerPart, ManufacturerPartAttachment,
SupplierPriceBreak)
class CompanyResource(ModelResource):
""" Class for managing Company data import/export """
class CompanyResource(InvenTreeResource):
"""Class for managing Company data import/export."""
class Meta:
model = Company
@@ -34,10 +34,8 @@ class CompanyAdmin(ImportExportModelAdmin):
]
class SupplierPartResource(ModelResource):
"""
Class for managing SupplierPart data import/export
"""
class SupplierPartResource(InvenTreeResource):
"""Class for managing SupplierPart data import/export."""
part = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part))
@@ -70,10 +68,8 @@ class SupplierPartAdmin(ImportExportModelAdmin):
autocomplete_fields = ('part', 'supplier', 'manufacturer_part',)
class ManufacturerPartResource(ModelResource):
"""
Class for managing ManufacturerPart data import/export
"""
class ManufacturerPartResource(InvenTreeResource):
"""Class for managing ManufacturerPart data import/export."""
part = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part))
@@ -118,10 +114,8 @@ class ManufacturerPartAttachmentAdmin(ImportExportModelAdmin):
autocomplete_fields = ('manufacturer_part',)
class ManufacturerPartParameterResource(ModelResource):
"""
Class for managing ManufacturerPartParameter data import/export
"""
class ManufacturerPartParameterResource(InvenTreeResource):
"""Class for managing ManufacturerPartParameter data import/export."""
class Meta:
model = ManufacturerPartParameter
@@ -148,8 +142,8 @@ class ManufacturerPartParameterAdmin(ImportExportModelAdmin):
autocomplete_fields = ('manufacturer_part',)
class SupplierPriceBreakResource(ModelResource):
""" Class for managing SupplierPriceBreak data import/export """
class SupplierPriceBreakResource(InvenTreeResource):
"""Class for managing SupplierPriceBreak data import/export."""
part = Field(attribute='part', widget=widgets.ForeignKeyWidget(SupplierPart))

View File

@@ -309,7 +309,9 @@ $('#new-price-break').click(function() {
});
loadPurchaseOrderTable($("#purchase-order-table"), {
url: "{% url 'api-po-list' %}?supplier_part={{ part.id }}",
params: {
supplier_part: {{ part.id }},
}
});
loadStockTable($("#stock-table"), {

View File

@@ -1,9 +1,12 @@
"""Admin functionality for the 'order' app"""
from django.contrib import admin
import import_export.widgets as widgets
from import_export.admin import ImportExportModelAdmin
from import_export.fields import Field
from import_export.resources import ModelResource
from InvenTree.admin import InvenTreeResource
from .models import (PurchaseOrder, PurchaseOrderExtraLine,
PurchaseOrderLineItem, SalesOrder, SalesOrderAllocation,
@@ -13,6 +16,7 @@ from .models import (PurchaseOrder, PurchaseOrderExtraLine,
# region general classes
class GeneralExtraLineAdmin:
"""Admin class template for the 'ExtraLineItem' models"""
list_display = (
'order',
'quantity',
@@ -29,6 +33,7 @@ class GeneralExtraLineAdmin:
class GeneralExtraLineMeta:
"""Metaclass template for the 'ExtraLineItem' models"""
skip_unchanged = True
report_skipped = False
clean_model_instances = True
@@ -36,11 +41,13 @@ class GeneralExtraLineMeta:
class PurchaseOrderLineItemInlineAdmin(admin.StackedInline):
"""Inline admin class for the PurchaseOrderLineItem model"""
model = PurchaseOrderLineItem
extra = 0
class PurchaseOrderAdmin(ImportExportModelAdmin):
"""Admin class for the PurchaseOrder model"""
exclude = [
'reference_int',
@@ -68,6 +75,7 @@ class PurchaseOrderAdmin(ImportExportModelAdmin):
class SalesOrderAdmin(ImportExportModelAdmin):
"""Admin class for the SalesOrder model"""
exclude = [
'reference_int',
@@ -90,10 +98,8 @@ class SalesOrderAdmin(ImportExportModelAdmin):
autocomplete_fields = ('customer',)
class PurchaseOrderResource(ModelResource):
"""
Class for managing import / export of PurchaseOrder data
"""
class PurchaseOrderResource(InvenTreeResource):
"""Class for managing import / export of PurchaseOrder data."""
# Add number of line items
line_items = Field(attribute='line_count', widget=widgets.IntegerWidget(), readonly=True)
@@ -102,6 +108,7 @@ class PurchaseOrderResource(ModelResource):
overdue = Field(attribute='is_overdue', widget=widgets.BooleanWidget(), readonly=True)
class Meta:
"""Metaclass"""
model = PurchaseOrder
skip_unchanged = True
clean_model_instances = True
@@ -110,8 +117,8 @@ class PurchaseOrderResource(ModelResource):
]
class PurchaseOrderLineItemResource(ModelResource):
""" Class for managing import / export of PurchaseOrderLineItem data """
class PurchaseOrderLineItemResource(InvenTreeResource):
"""Class for managing import / export of PurchaseOrderLineItem data."""
part_name = Field(attribute='part__part__name', readonly=True)
@@ -122,23 +129,24 @@ class PurchaseOrderLineItemResource(ModelResource):
SKU = Field(attribute='part__SKU', readonly=True)
class Meta:
"""Metaclass"""
model = PurchaseOrderLineItem
skip_unchanged = True
report_skipped = False
clean_model_instances = True
class PurchaseOrderExtraLineResource(ModelResource):
""" Class for managing import / export of PurchaseOrderExtraLine data """
class PurchaseOrderExtraLineResource(InvenTreeResource):
"""Class for managing import / export of PurchaseOrderExtraLine data."""
class Meta(GeneralExtraLineMeta):
"""Metaclass options."""
model = PurchaseOrderExtraLine
class SalesOrderResource(ModelResource):
"""
Class for managing import / export of SalesOrder data
"""
class SalesOrderResource(InvenTreeResource):
"""Class for managing import / export of SalesOrder data."""
# Add number of line items
line_items = Field(attribute='line_count', widget=widgets.IntegerWidget(), readonly=True)
@@ -147,6 +155,7 @@ class SalesOrderResource(ModelResource):
overdue = Field(attribute='is_overdue', widget=widgets.BooleanWidget(), readonly=True)
class Meta:
"""Metaclass options"""
model = SalesOrder
skip_unchanged = True
clean_model_instances = True
@@ -155,10 +164,8 @@ class SalesOrderResource(ModelResource):
]
class SalesOrderLineItemResource(ModelResource):
"""
Class for managing import / export of SalesOrderLineItem data
"""
class SalesOrderLineItemResource(InvenTreeResource):
"""Class for managing import / export of SalesOrderLineItem data."""
part_name = Field(attribute='part__name', readonly=True)
@@ -169,31 +176,34 @@ class SalesOrderLineItemResource(ModelResource):
fulfilled = Field(attribute='fulfilled_quantity', readonly=True)
def dehydrate_sale_price(self, item):
"""
Return a string value of the 'sale_price' field, rather than the 'Money' object.
"""Return a string value of the 'sale_price' field, rather than the 'Money' object.
Ref: https://github.com/inventree/InvenTree/issues/2207
"""
if item.sale_price:
return str(item.sale_price)
else:
return ''
class Meta:
"""Metaclass options"""
model = SalesOrderLineItem
skip_unchanged = True
report_skipped = False
clean_model_instances = True
class SalesOrderExtraLineResource(ModelResource):
""" Class for managing import / export of SalesOrderExtraLine data """
class SalesOrderExtraLineResource(InvenTreeResource):
"""Class for managing import / export of SalesOrderExtraLine data."""
class Meta(GeneralExtraLineMeta):
"""Metaclass options."""
model = SalesOrderExtraLine
class PurchaseOrderLineItemAdmin(ImportExportModelAdmin):
"""Admin class for the PurchaseOrderLine model"""
resource_class = PurchaseOrderLineItemResource
@@ -210,11 +220,12 @@ class PurchaseOrderLineItemAdmin(ImportExportModelAdmin):
class PurchaseOrderExtraLineAdmin(GeneralExtraLineAdmin, ImportExportModelAdmin):
"""Admin class for the PurchaseOrderExtraLine model"""
resource_class = PurchaseOrderExtraLineResource
class SalesOrderLineItemAdmin(ImportExportModelAdmin):
"""Admin class for the SalesOrderLine model"""
resource_class = SalesOrderLineItemResource
@@ -236,11 +247,12 @@ class SalesOrderLineItemAdmin(ImportExportModelAdmin):
class SalesOrderExtraLineAdmin(GeneralExtraLineAdmin, ImportExportModelAdmin):
"""Admin class for the SalesOrderExtraLine model"""
resource_class = SalesOrderExtraLineResource
class SalesOrderShipmentAdmin(ImportExportModelAdmin):
"""Admin class for the SalesOrderShipment model"""
list_display = [
'order',
@@ -258,6 +270,7 @@ class SalesOrderShipmentAdmin(ImportExportModelAdmin):
class SalesOrderAllocationAdmin(ImportExportModelAdmin):
"""Admin class for the SalesOrderAllocation model"""
list_display = (
'line',

View File

@@ -4,7 +4,10 @@ Order model definitions
# -*- coding: utf-8 -*-
import logging
import os
import sys
import traceback
from datetime import datetime
from decimal import Decimal
@@ -19,12 +22,15 @@ from django.dispatch.dispatcher import receiver
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from djmoney.contrib.exchange.exceptions import MissingRate
from djmoney.contrib.exchange.models import convert_money
from djmoney.money import Money
from error_report.models import Error
from markdownx.models import MarkdownxField
from mptt.models import TreeForeignKey
import InvenTree.helpers
import InvenTree.ready
from common.settings import currency_code_default
from company.models import Company, SupplierPart
from InvenTree.fields import InvenTreeModelMoneyField, RoundingDecimalField
@@ -38,6 +44,8 @@ from plugin.models import MetadataMixin
from stock import models as stock_models
from users import models as UserModels
logger = logging.getLogger('inventree')
def get_next_po_number():
"""
@@ -151,23 +159,74 @@ class Order(MetadataMixin, ReferenceIndexingMixin):
notes = MarkdownxField(blank=True, verbose_name=_('Notes'), help_text=_('Order notes'))
def get_total_price(self):
def get_total_price(self, target_currency=currency_code_default()):
"""
Calculates the total price of all order lines
Calculates the total price of all order lines, and converts to the specified target currency.
If not specified, the default system currency is used.
If currency conversion fails (e.g. there are no valid conversion rates),
then we simply return zero, rather than attempting some other calculation.
"""
target_currency = currency_code_default()
total = Money(0, target_currency)
# gather name reference
price_ref = 'sale_price' if isinstance(self, SalesOrder) else 'purchase_price'
# order items
total += sum(a.quantity * convert_money(getattr(a, price_ref), target_currency) for a in self.lines.all() if getattr(a, price_ref))
price_ref_tag = 'sale_price' if isinstance(self, SalesOrder) else 'purchase_price'
# extra lines
total += sum(a.quantity * convert_money(a.price, target_currency) for a in self.extra_lines.all() if a.price)
# order items
for line in self.lines.all():
price_ref = getattr(line, price_ref_tag)
if not price_ref:
continue
try:
total += line.quantity * convert_money(price_ref, target_currency)
except MissingRate:
# Record the error, try to press on
kind, info, data = sys.exc_info()
Error.objects.create(
kind=kind.__name__,
info=info,
data='\n'.join(traceback.format_exception(kind, info, data)),
path='order.get_total_price',
)
logger.error(f"Missing exchange rate for '{target_currency}'")
# Return None to indicate the calculated price is invalid
return None
# extra items
for line in self.extra_lines.all():
if not line.price:
continue
try:
total += line.quantity * convert_money(line.price, target_currency)
except MissingRate:
# Record the error, try to press on
kind, info, data = sys.exc_info()
Error.objects.create(
kind=kind.__name__,
info=info,
data='\n'.join(traceback.format_exception(kind, info, data)),
path='order.get_total_price',
)
logger.error(f"Missing exchange rate for '{target_currency}'")
# Return None to indicate the calculated price is invalid
return None
# set decimal-places
total.decimal_places = 4
return total
@@ -809,9 +868,19 @@ class SalesOrder(Order):
@receiver(post_save, sender=SalesOrder, dispatch_uid='build_post_save_log')
def after_save_sales_order(sender, instance: SalesOrder, created: bool, **kwargs):
"""Callback function to be executed after a SalesOrder instance is saved.
- If the SALESORDER_DEFAULT_SHIPMENT setting is enabled, create a default shipment
- Ignore if the database is not ready for access
- Ignore if data import is active
"""
Callback function to be executed after a SalesOrder instance is saved
"""
if not InvenTree.ready.canAppAccessDatabase(allow_test=True):
return
if InvenTree.ready.isImportingData():
return
if created and getSetting('SALESORDER_DEFAULT_SHIPMENT'):
# A new SalesOrder has just been created

View File

@@ -181,7 +181,15 @@ src="{% static 'img/blank_image.png' %}"
<tr>
<td><span class='fas fa-dollar-sign'></span></td>
<td>{% trans "Total cost" %}</td>
<td id="poTotalPrice">{{ order.get_total_price }}</td>
<td id="poTotalPrice">
{% with order.get_total_price as tp %}
{% if tp == None %}
<span class='badge bg-warning'>{% trans "Total cost could not be calculated" %}</span>
{% else %}
{{ tp }}
{% endif %}
{% endwith %}
</td>
</tr>
</table>
{% endblock %}

View File

@@ -188,7 +188,15 @@ src="{% static 'img/blank_image.png' %}"
<tr>
<td><span class='fas fa-dollar-sign'></span></td>
<td>{% trans "Total cost" %}</td>
<td id="soTotalPrice">{{ order.get_total_price }}</td>
<td id="soTotalPrice">
{% with order.get_total_price as tp %}
{% if tp == None %}
<span class='badge bg-warning'>{% trans "Total cost could not be calculated" %}</span>
{% else %}
{{ tp }}
{% endif %}
{% endwith %}
</td>
</tr>
</table>
{% endblock %}

View File

@@ -3,15 +3,15 @@ from django.contrib import admin
import import_export.widgets as widgets
from import_export.admin import ImportExportModelAdmin
from import_export.fields import Field
from import_export.resources import ModelResource
import part.models as models
from company.models import SupplierPart
from InvenTree.admin import InvenTreeResource
from stock.models import StockLocation
class PartResource(ModelResource):
""" Class for managing Part data import/export """
class PartResource(InvenTreeResource):
"""Class for managing Part data import/export."""
# ForeignKey fields
category = Field(attribute='category', widget=widgets.ForeignKeyWidget(models.PartCategory))
@@ -81,8 +81,8 @@ class PartAdmin(ImportExportModelAdmin):
]
class PartCategoryResource(ModelResource):
""" Class for managing PartCategory data import/export """
class PartCategoryResource(InvenTreeResource):
"""Class for managing PartCategory data import/export."""
parent = Field(attribute='parent', widget=widgets.ForeignKeyWidget(models.PartCategory))
@@ -157,8 +157,8 @@ class PartTestTemplateAdmin(admin.ModelAdmin):
autocomplete_fields = ('part',)
class BomItemResource(ModelResource):
""" Class for managing BomItem data import/export """
class BomItemResource(InvenTreeResource):
"""Class for managing BomItem data import/export."""
level = Field(attribute='level', readonly=True)
@@ -269,8 +269,8 @@ class ParameterTemplateAdmin(ImportExportModelAdmin):
search_fields = ('name', 'units')
class ParameterResource(ModelResource):
""" Class for managing PartParameter data import/export """
class ParameterResource(InvenTreeResource):
"""Class for managing PartParameter data import/export."""
part = Field(attribute='part', widget=widgets.ForeignKeyWidget(models.Part))

View File

@@ -114,6 +114,9 @@ def get_git_log(path):
output = output.split('\n')
except subprocess.CalledProcessError: # pragma: no cover
pass
except FileNotFoundError: # pragma: no cover
# Most likely the system does not have 'git' installed
pass
if not output:
output = 7 * [''] # pragma: no cover
@@ -129,6 +132,9 @@ def check_git_version():
output = str(subprocess.check_output(['git', '--version'], cwd=os.path.dirname(settings.BASE_DIR)), 'utf-8')
except subprocess.CalledProcessError: # pragma: no cover
return False
except FileNotFoundError: # pragma: no cover
# Most likely the system does not have 'git' installed
return False
# process version string
try:

View File

@@ -231,6 +231,9 @@ class PluginsRegistry:
except subprocess.CalledProcessError as error: # pragma: no cover
logger.error(f'Ran into error while trying to install plugins!\n{str(error)}')
return False
except FileNotFoundError: # pragma: no cover
# System most likely does not have 'git' installed
return False
logger.info(f'plugin requirements were run\n{output}')

View File

@@ -3,10 +3,10 @@ from django.contrib import admin
import import_export.widgets as widgets
from import_export.admin import ImportExportModelAdmin
from import_export.fields import Field
from import_export.resources import ModelResource
from build.models import Build
from company.models import Company, SupplierPart
from InvenTree.admin import InvenTreeResource
from order.models import PurchaseOrder, SalesOrder
from part.models import Part
@@ -14,8 +14,8 @@ from .models import (StockItem, StockItemAttachment, StockItemTestResult,
StockItemTracking, StockLocation)
class LocationResource(ModelResource):
""" Class for managing StockLocation data import/export """
class LocationResource(InvenTreeResource):
"""Class for managing StockLocation data import/export."""
parent = Field(attribute='parent', widget=widgets.ForeignKeyWidget(StockLocation))
@@ -65,8 +65,8 @@ class LocationAdmin(ImportExportModelAdmin):
]
class StockItemResource(ModelResource):
""" Class for managing StockItem data import/export """
class StockItemResource(InvenTreeResource):
"""Class for managing StockItem data import/export."""
# Custom managers for ForeignKey fields
part = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part))

View File

@@ -149,7 +149,7 @@ function loadAttachmentTable(url, options) {
var html = `<span class='fas ${icon}'></span> ${filename}`;
return renderLink(html, value);
return renderLink(html, value, {download: true});
} else if (row.link) {
var html = `<span class='fas fa-link'></span> ${row.link}`;
return renderLink(html, row.link);

View File

@@ -204,6 +204,9 @@ function constructChangeForm(fields, options) {
},
success: function(data) {
// Ensure the data are fully sanitized before we operate on it
data = sanitizeData(data);
// An optional function can be provided to process the returned results,
// before they are rendered to the form
if (options.processResults) {

View File

@@ -17,6 +17,41 @@ function closeSearchPanel() {
}
// Keep track of the roles / permissions available to the current user
var search_user_roles = null;
/*
* Check if the user has the specified role and permission
*/
function checkPermission(role, permission='view') {
if (!search_user_roles) {
return false;
}
if (!(role in search_user_roles)) {
return false;
}
var roles = search_user_roles[role];
if (!roles) {
return false;
}
var found = false;
search_user_roles[role].forEach(function(p) {
if (String(p).valueOf() == String(permission).valueOf()) {
found = true;
}
});
return found;
}
/*
* Callback when the search panel is opened.
* Ensure the panel is in a known state
@@ -27,6 +62,16 @@ function openSearchPanel() {
clearSearchResults();
// Request user roles if we do not have them
if (search_user_roles == null) {
inventreeGet('{% url "api-user-roles" %}', {}, {
success: function(response) {
search_user_roles = response.roles || {};
}
});
}
// Callback for text input changed
panel.find('#search-input').on('keyup change', searchTextChanged);
// Callback for "clear search" button
@@ -84,7 +129,7 @@ function updateSearch() {
// Show the "searching" text
$('#offcanvas-search').find('#search-pending').show();
if (user_settings.SEARCH_PREVIEW_SHOW_PARTS) {
if (checkPermission('part') && user_settings.SEARCH_PREVIEW_SHOW_PARTS) {
var params = {};
@@ -106,7 +151,7 @@ function updateSearch() {
);
}
if (user_settings.SEARCH_PREVIEW_SHOW_CATEGORIES) {
if (checkPermission('part_category') && user_settings.SEARCH_PREVIEW_SHOW_CATEGORIES) {
// Search for matching part categories
addSearchQuery(
'category',
@@ -120,7 +165,7 @@ function updateSearch() {
);
}
if (user_settings.SEARCH_PREVIEW_SHOW_STOCK) {
if (checkPermission('stock') && user_settings.SEARCH_PREVIEW_SHOW_STOCK) {
// Search for matching stock items
var filters = {
@@ -146,7 +191,7 @@ function updateSearch() {
);
}
if (user_settings.SEARCH_PREVIEW_SHOW_LOCATIONS) {
if (checkPermission('stock_location') && user_settings.SEARCH_PREVIEW_SHOW_LOCATIONS) {
// Search for matching stock locations
addSearchQuery(
'location',
@@ -160,7 +205,7 @@ function updateSearch() {
);
}
if (user_settings.SEARCH_PREVIEW_SHOW_COMPANIES) {
if ((checkPermission('sales_order') || checkPermission('purchase_order')) && user_settings.SEARCH_PREVIEW_SHOW_COMPANIES) {
// Search for matching companies
addSearchQuery(
'company',
@@ -174,7 +219,7 @@ function updateSearch() {
);
}
if (user_settings.SEARCH_PREVIEW_SHOW_PURCHASE_ORDERS) {
if (checkPermission('purchase_order') && user_settings.SEARCH_PREVIEW_SHOW_PURCHASE_ORDERS) {
var filters = {
supplier_detail: true,
@@ -197,7 +242,7 @@ function updateSearch() {
);
}
if (user_settings.SEARCH_PREVIEW_SHOW_SALES_ORDERS) {
if (checkPermission('sales_order') && user_settings.SEARCH_PREVIEW_SHOW_SALES_ORDERS) {
var filters = {
customer_detail: true,

View File

@@ -1306,7 +1306,8 @@ function loadStockTestResultsTable(table, options) {
var html = value;
if (row.attachment) {
html += `<a href='${row.attachment}'><span class='fas fa-file-alt float-right'></span></a>`;
var text = `<span class='fas fa-file-alt float-right'></span>`;
html += renderLink(text, row.attachment, {download: true});
}
return html;

View File

@@ -92,6 +92,13 @@ function renderLink(text, url, options={}) {
var max_length = options.max_length || -1;
var extra = '';
if (options.download) {
var fn = url.split('/').at(-1);
extra += ` download='${fn}'`;
}
// Shorten the displayed length if required
if ((max_length > 0) && (text.length > max_length)) {
var slice_length = (max_length - 3) / 2;
@@ -102,7 +109,7 @@ function renderLink(text, url, options={}) {
text = `${text_start}...${text_end}`;
}
return '<a href="' + url + '">' + text + '</a>';
return `<a href='${url}'${extra}>${text}</a>`;
}
@@ -282,6 +289,8 @@ $.fn.inventreeTable = function(options) {
// Extract query params
var filters = options.queryParams || options.filters || {};
options.escape = true;
// Store the total set of query params
options.query_params = filters;
@@ -468,6 +477,49 @@ function customGroupSorter(sortName, sortOrder, sortData) {
$.extend($.fn.bootstrapTable.defaults, $.fn.bootstrapTable.locales['en-US-custom']);
// Enable HTML escaping by default
$.fn.bootstrapTable.escape = true;
// Override the 'calculateObjectValue' function at bootstrap-table.js:3525
// Allows us to escape any nasty HTML tags which are rendered to the DOM
$.fn.bootstrapTable.utils._calculateObjectValue = $.fn.bootstrapTable.utils.calculateObjectValue;
$.fn.bootstrapTable.utils.calculateObjectValue = function escapeCellValue(self, name, args, defaultValue) {
var args_list = [];
if (args) {
args_list.push(args[0]);
if (name && typeof(name) === 'function' && name.name == 'formatter') {
/* This is a custom "formatter" function for a particular cell,
* which may side-step regular HTML escaping, and inject malicious code into the DOM.
*
* Here we have access to the 'args' supplied to the custom 'formatter' function,
* which are in the order:
* args = [value, row, index, field]
*
* 'row' is the one we are interested in
*/
var row = Object.assign({}, args[1]);
args_list.push(sanitizeData(row));
} else {
args_list.push(args[1]);
}
for (var ii = 2; ii < args.length; ii++) {
args_list.push(args[ii]);
}
}
var value = $.fn.bootstrapTable.utils._calculateObjectValue(self, name, args_list, defaultValue);
return value;
};
})(jQuery);
$.extend($.fn.treegrid.defaults, {