mirror of
https://github.com/inventree/InvenTree.git
synced 2025-05-03 22:08:49 +00:00
Enable and disable plugins via the API (#4964)
* Cleanup plugin settings page - Template adjustments * Activate plugin directly via API * Update plugin activate endpoint - Allow plugin to be deactivated also - Default value = True if not provided * Update front-end / js - Allow same JS method to either enable or disable a plugin * Hide info for plugins which are not active * remove duplicated column * Tweak serializer docstring * Fix typo * Add extra data to plugin serializer - is_builtin - is_sample * Some backend cleanup - Don't stringify null values - Don't replace None with "Unavailable" * front-end table for rendering plugins * Change default sorting - Show active plugins first * Fix button callback * Remove old template * JS linting * More linting
This commit is contained in:
parent
0c47552199
commit
45ec7b9728
@ -70,6 +70,8 @@ class PluginList(ListAPI):
|
|||||||
]
|
]
|
||||||
|
|
||||||
ordering = [
|
ordering = [
|
||||||
|
'-active',
|
||||||
|
'name',
|
||||||
'key',
|
'key',
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -120,10 +122,17 @@ class PluginInstall(CreateAPI):
|
|||||||
|
|
||||||
|
|
||||||
class PluginActivate(UpdateAPI):
|
class PluginActivate(UpdateAPI):
|
||||||
"""Endpoint for activating a plugin."""
|
"""Endpoint for activating a plugin.
|
||||||
|
|
||||||
|
- PATCH: Activate a plugin
|
||||||
|
|
||||||
|
Pass a boolean value for the 'active' field.
|
||||||
|
If not provided, it is assumed to be True,
|
||||||
|
and the plugin will be activated.
|
||||||
|
"""
|
||||||
|
|
||||||
queryset = PluginConfig.objects.all()
|
queryset = PluginConfig.objects.all()
|
||||||
serializer_class = PluginSerializers.PluginConfigEmptySerializer
|
serializer_class = PluginSerializers.PluginActivateSerializer
|
||||||
permission_classes = [IsSuperuser, ]
|
permission_classes = [IsSuperuser, ]
|
||||||
|
|
||||||
def get_object(self):
|
def get_object(self):
|
||||||
@ -134,9 +143,8 @@ class PluginActivate(UpdateAPI):
|
|||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
"""Activate the plugin."""
|
"""Activate the plugin."""
|
||||||
instance = serializer.instance
|
|
||||||
instance.active = True
|
serializer.save()
|
||||||
instance.save()
|
|
||||||
|
|
||||||
|
|
||||||
class PluginSettingList(ListAPI):
|
class PluginSettingList(ListAPI):
|
||||||
|
@ -76,11 +76,22 @@ class PluginConfig(models.Model):
|
|||||||
plugin = registry.plugins_full.get(self.key, None)
|
plugin = registry.plugins_full.get(self.key, None)
|
||||||
|
|
||||||
def get_plugin_meta(name):
|
def get_plugin_meta(name):
|
||||||
|
"""Return a meta-value associated with this plugin"""
|
||||||
|
|
||||||
|
# Ignore if the plugin config is not defined
|
||||||
if not plugin:
|
if not plugin:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Ignore if the plugin is not active
|
||||||
if not self.active:
|
if not self.active:
|
||||||
return _('Unvailable')
|
return None
|
||||||
return str(getattr(plugin, name, None))
|
|
||||||
|
result = getattr(plugin, name, None)
|
||||||
|
|
||||||
|
if result is not None:
|
||||||
|
result = str(result)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
self.meta = {
|
self.meta = {
|
||||||
key: get_plugin_meta(key) for key in ['slug', 'human_name', 'description', 'author',
|
key: get_plugin_meta(key) for key in ['slug', 'human_name', 'description', 'author',
|
||||||
|
@ -275,8 +275,7 @@ class InvenTreePlugin(VersionMixin, MixinBase, MetaBase):
|
|||||||
pub_date = self.package.get('date')
|
pub_date = self.package.get('date')
|
||||||
else:
|
else:
|
||||||
pub_date = datetime.fromisoformat(str(pub_date))
|
pub_date = datetime.fromisoformat(str(pub_date))
|
||||||
if not pub_date:
|
|
||||||
pub_date = _('No date found') # pragma: no cover
|
|
||||||
return pub_date
|
return pub_date
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -53,11 +53,20 @@ class PluginConfigSerializer(serializers.ModelSerializer):
|
|||||||
"""Meta for serializer."""
|
"""Meta for serializer."""
|
||||||
model = PluginConfig
|
model = PluginConfig
|
||||||
fields = [
|
fields = [
|
||||||
|
'pk',
|
||||||
'key',
|
'key',
|
||||||
'name',
|
'name',
|
||||||
'active',
|
'active',
|
||||||
'meta',
|
'meta',
|
||||||
'mixins',
|
'mixins',
|
||||||
|
'is_builtin',
|
||||||
|
'is_sample',
|
||||||
|
]
|
||||||
|
|
||||||
|
read_only_fields = [
|
||||||
|
'key',
|
||||||
|
'is_builtin',
|
||||||
|
'is_sample',
|
||||||
]
|
]
|
||||||
|
|
||||||
meta = serializers.DictField(read_only=True)
|
meta = serializers.DictField(read_only=True)
|
||||||
@ -171,6 +180,26 @@ class PluginConfigInstallSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
class PluginConfigEmptySerializer(serializers.Serializer):
|
class PluginConfigEmptySerializer(serializers.Serializer):
|
||||||
"""Serializer for a PluginConfig."""
|
"""Serializer for a PluginConfig."""
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class PluginActivateSerializer(serializers.Serializer):
|
||||||
|
"""Serializer for activating or deactivating a plugin"""
|
||||||
|
|
||||||
|
model = PluginConfig
|
||||||
|
|
||||||
|
active = serializers.BooleanField(
|
||||||
|
required=False, default=True,
|
||||||
|
label=_('Activate Plugin'),
|
||||||
|
help_text=_('Activate this plugin')
|
||||||
|
)
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
"""Apply the new 'active' value to the plugin instance"""
|
||||||
|
|
||||||
|
instance.active = validated_data.get('active', True)
|
||||||
|
instance.save()
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
class PluginSettingSerializer(GenericReferencedSettingSerializer):
|
class PluginSettingSerializer(GenericReferencedSettingSerializer):
|
||||||
|
@ -52,37 +52,15 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<div id='plugin-button-toolbar'>
|
||||||
|
<div class='button-toolbar container-fluid'>
|
||||||
|
<div class='btn-group' role='group'>
|
||||||
|
{% include "filter_list.html" with id="plugins" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class='table-responsive'>
|
<div class='table-responsive'>
|
||||||
<table class='table table-striped table-condensed'>
|
<table class='table table-striped table-condensed' id='plugin-table' data-toolbar='#plugin-button-toolbar'></table>
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "Name" %}</th>
|
|
||||||
<th>{% trans "Key" %}</th>
|
|
||||||
<th>{% trans "Author" %}</th>
|
|
||||||
<th>{% trans "Date" %}</th>
|
|
||||||
<th>{% trans "Version" %}</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
{% plugin_list as pl_list %}
|
|
||||||
{% if pl_list %}
|
|
||||||
<tr><td colspan="6"><h6>{% trans 'Active plugins' %}</h6></td></tr>
|
|
||||||
{% for plugin_key, plugin in pl_list.items %}
|
|
||||||
{% include "InvenTree/settings/plugin_details.html" with plugin=plugin plugin_key=plugin_key %}
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% inactive_plugin_list as in_pl_list %}
|
|
||||||
{% if in_pl_list %}
|
|
||||||
<tr><td colspan="6"><h6>{% trans 'Inactive plugins' %}</h6></td></tr>
|
|
||||||
{% for plugin_key, plugin in in_pl_list.items %}
|
|
||||||
{% include "InvenTree/settings/plugin_details.html" with plugin=plugin plugin_key=plugin_key %}
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% plugin_errors as pl_errors %}
|
{% plugin_errors as pl_errors %}
|
||||||
|
@ -1,75 +0,0 @@
|
|||||||
{% load inventree_extras %}
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
{% if plugin.is_active %}
|
|
||||||
<span class='fas fa-check-circle icon-green'></span>
|
|
||||||
{% else %}
|
|
||||||
<span class='fas fa-times-circle icon-red'></span>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if plugin.human_name %}
|
|
||||||
{{ plugin.human_name }}
|
|
||||||
{% elif plugin.title %}
|
|
||||||
{{ plugin.title }}
|
|
||||||
{% elif plugin.name %}
|
|
||||||
{{ plugin.name }}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% define plugin.registered_mixins as mixin_list %}
|
|
||||||
|
|
||||||
{% if mixin_list %}
|
|
||||||
{% for mixin in mixin_list %}
|
|
||||||
<a class='sidebar-selector' id='select-plugin-{{ plugin_key }}' data-bs-parent="#sidebar">
|
|
||||||
<span class='badge bg-dark badge-right rounded-pill'>{{ mixin.human_name }}</span>
|
|
||||||
</a>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if plugin.is_builtin %}
|
|
||||||
<a class='sidebar-selector' id='select-plugin-{{ plugin_key }}' data-bs-parent='#sidebar'>
|
|
||||||
<span class='badge bg-success rounded-pill badge-right'>{% trans "Builtin" %}</span>
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if plugin.is_sample %}
|
|
||||||
<a class='sidebar-selector' id='select-plugin-{{ plugin_key }}' data-bs-parent="#sidebar">
|
|
||||||
<span class='badge bg-info rounded-pill badge-right'>{% trans "Sample" %}</span>
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if plugin.website %}
|
|
||||||
<a href="{{ plugin.website }}"><span class="fas fa-globe"></span></a>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>{{ plugin_key }}</td>
|
|
||||||
{% trans "Unavailable" as no_info %}
|
|
||||||
<td>
|
|
||||||
{% if plugin.author %}
|
|
||||||
{{ plugin.author }}
|
|
||||||
{% else %}
|
|
||||||
<em>{{ no_info }}</em>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if plugin.pub_date %}
|
|
||||||
{% render_date plugin.pub_date %}
|
|
||||||
{% else %}
|
|
||||||
<em>{{ no_info }}</em>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if plugin.version %}
|
|
||||||
{{ plugin.version }}
|
|
||||||
{% else %}
|
|
||||||
<em>{{ no_info }}</em>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if user.is_staff and perms.plugin.change_pluginconfig %}
|
|
||||||
{% url 'admin:plugin_pluginconfig_change' plugin.pk as url %}
|
|
||||||
{% include "admin_button.html" with url=url %}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
@ -69,12 +69,6 @@
|
|||||||
|
|
||||||
{% if user.is_staff %}
|
{% if user.is_staff %}
|
||||||
{% include "InvenTree/settings/settings_staff_js.html" %}
|
{% include "InvenTree/settings/settings_staff_js.html" %}
|
||||||
{% plugins_enabled as plug %}
|
|
||||||
{% if plug %}
|
|
||||||
$("#install-plugin").click(function() {
|
|
||||||
installPlugin();
|
|
||||||
});
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
enableSidebar('settings');
|
enableSidebar('settings');
|
||||||
|
@ -385,3 +385,21 @@ onPanelLoad('stocktake', function() {
|
|||||||
});
|
});
|
||||||
{% endif %}
|
{% endif %}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Javascript for plugins panel
|
||||||
|
onPanelLoad('plugin', function() {
|
||||||
|
|
||||||
|
{% plugins_enabled as plug %}
|
||||||
|
|
||||||
|
loadPluginTable('#plugin-table', {
|
||||||
|
custom: {% js_bool plug %},
|
||||||
|
});
|
||||||
|
|
||||||
|
{% if plug %}
|
||||||
|
// Callback to install new plugin
|
||||||
|
$("#install-plugin").click(function() {
|
||||||
|
installPlugin();
|
||||||
|
});
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
});
|
||||||
|
@ -2,17 +2,149 @@
|
|||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
|
|
||||||
/* globals
|
/* globals
|
||||||
|
addCachedAlert,
|
||||||
constructForm,
|
constructForm,
|
||||||
showMessage,
|
showMessage,
|
||||||
inventreeGet,
|
inventreeGet,
|
||||||
inventreePut,
|
inventreePut,
|
||||||
|
loadTableFilters,
|
||||||
|
makeIconButton,
|
||||||
|
renderDate,
|
||||||
|
setupFilterList,
|
||||||
|
showApiError,
|
||||||
|
showModalSpinner,
|
||||||
|
wrapButtons,
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* exported
|
/* exported
|
||||||
|
activatePlugin,
|
||||||
installPlugin,
|
installPlugin,
|
||||||
|
loadPluginTable,
|
||||||
locateItemOrLocation
|
locateItemOrLocation
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Load the plugin table
|
||||||
|
*/
|
||||||
|
function loadPluginTable(table, options={}) {
|
||||||
|
|
||||||
|
options.params = options.params || {};
|
||||||
|
|
||||||
|
let filters = loadTableFilters('plugins', options.params);
|
||||||
|
|
||||||
|
setupFilterList('plugins', $(table), '#filter-list-plugins');
|
||||||
|
|
||||||
|
$(table).inventreeTable({
|
||||||
|
url: '{% url "api-plugin-list" %}',
|
||||||
|
name: 'plugins',
|
||||||
|
original: options.params,
|
||||||
|
queryParams: filters,
|
||||||
|
sortable: true,
|
||||||
|
formatNoMatches: function() {
|
||||||
|
return '{% trans "No plugins found" %}';
|
||||||
|
},
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
field: 'active',
|
||||||
|
title: '',
|
||||||
|
sortable: true,
|
||||||
|
formatter: function(value, row) {
|
||||||
|
if (row.active) {
|
||||||
|
return `<span class='fa fa-check-circle icon-green' title='{% trans "This plugin is active" %}'></span>`;
|
||||||
|
} else {
|
||||||
|
return `<span class='fa fa-times-circle icon-red' title ='{% trans "This plugin is not active" %}'></span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
title: '{% trans "Plugin Description" %}',
|
||||||
|
sortable: true,
|
||||||
|
formatter: function(value, row) {
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
if (row.active) {
|
||||||
|
html += `<strong>${value}</strong>`;
|
||||||
|
if (row.meta && row.meta.description) {
|
||||||
|
html += ` - <small>${row.meta.description}</small>`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html += `<em>${value}</em>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row.is_builtin) {
|
||||||
|
html += `<span class='badge bg-success rounded-pill badge-right'>{% trans "Builtin" %}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row.is_sample) {
|
||||||
|
html += `<span class='badge bg-info rounded-pill badge-right'>{% trans "Sample" %}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'meta.version',
|
||||||
|
title: '{% trans "Version" %}',
|
||||||
|
formatter: function(value, row) {
|
||||||
|
if (value) {
|
||||||
|
let html = value;
|
||||||
|
|
||||||
|
if (row.meta.pub_date) {
|
||||||
|
html += `<span class='badge rounded-pill bg-dark float-right'>${renderDate(row.meta.pub_date)}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
} else {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'meta.author',
|
||||||
|
title: '{% trans "Author" %}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'actions',
|
||||||
|
title: '',
|
||||||
|
formatter: function(value, row) {
|
||||||
|
let buttons = '';
|
||||||
|
|
||||||
|
// Check if custom plugins are enabled for this instance
|
||||||
|
if (options.custom && !row.is_builtin) {
|
||||||
|
if (row.active) {
|
||||||
|
buttons += makeIconButton('fa-stop-circle icon-red', 'btn-plugin-disable', row.pk, '{% trans "Disable Plugin" %}');
|
||||||
|
} else {
|
||||||
|
buttons += makeIconButton('fa-play-circle icon-green', 'btn-plugin-enable', row.pk, '{% trans "Enable Plugin" %}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrapButtons(buttons);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.custom) {
|
||||||
|
// Callback to activate a plugin
|
||||||
|
$(table).on('click', '.btn-plugin-enable', function() {
|
||||||
|
let pk = $(this).attr('pk');
|
||||||
|
activatePlugin(pk, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Callback to deactivate a plugin
|
||||||
|
$(table).on('click', '.btn-plugin-disable', function() {
|
||||||
|
let pk = $(this).attr('pk');
|
||||||
|
activatePlugin(pk, false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Install a new plugin via the API
|
||||||
|
*/
|
||||||
function installPlugin() {
|
function installPlugin() {
|
||||||
constructForm(`/api/plugins/install/`, {
|
constructForm(`/api/plugins/install/`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -30,6 +162,55 @@ function installPlugin() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Activate a specific plugin via the API
|
||||||
|
*/
|
||||||
|
function activatePlugin(plugin_id, active=true) {
|
||||||
|
|
||||||
|
let url = `{% url "api-plugin-list" %}${plugin_id}/activate/`;
|
||||||
|
|
||||||
|
let html = active ? `
|
||||||
|
<span class='alert alert-block alert-info'>
|
||||||
|
{% trans "Are you sure you want to enable this plugin?" %}
|
||||||
|
</span>
|
||||||
|
` : `
|
||||||
|
<span class='alert alert-block alert-danger'>
|
||||||
|
{% trans "Are you sure you want to disable this plugin?" %}
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
constructForm(null, {
|
||||||
|
title: active ? '{% trans "Enable Plugin" %}' : '{% trans "Disable Plugin" %}',
|
||||||
|
preFormContent: html,
|
||||||
|
confirm: true,
|
||||||
|
submitText: active ? '{% trans "Enable" %}' : '{% trans "Disable" %}',
|
||||||
|
submitClass: active ? 'success' : 'danger',
|
||||||
|
onSubmit: function(_fields, opts) {
|
||||||
|
showModalSpinner(opts.modal);
|
||||||
|
|
||||||
|
inventreePut(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
active: active,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: 'PATCH',
|
||||||
|
success: function() {
|
||||||
|
$(opts.modal).modal('hide');
|
||||||
|
addCachedAlert('{% trans "Plugin updated" %}', {style: 'success'});
|
||||||
|
location.reload();
|
||||||
|
},
|
||||||
|
error: function(xhr) {
|
||||||
|
$(opts.modal).modal('hide');
|
||||||
|
showApiError(xhr, url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function locateItemOrLocation(options={}) {
|
function locateItemOrLocation(options={}) {
|
||||||
|
|
||||||
if (!options.item && !options.location) {
|
if (!options.item && !options.location) {
|
||||||
|
@ -426,6 +426,17 @@ function getPartTestTemplateFilters() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Return a dictionary of filters for the "plugins" table
|
||||||
|
function getPluginTableFilters() {
|
||||||
|
return {
|
||||||
|
active: {
|
||||||
|
type: 'bool',
|
||||||
|
title: '{% trans "Active" %}',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Return a dictionary of filters for the "build" table
|
// Return a dictionary of filters for the "build" table
|
||||||
function getBuildTableFilters() {
|
function getBuildTableFilters() {
|
||||||
|
|
||||||
@ -774,6 +785,8 @@ function getAvailableTableFilters(tableKey) {
|
|||||||
return getPartTableFilters();
|
return getPartTableFilters();
|
||||||
case 'parttests':
|
case 'parttests':
|
||||||
return getPartTestTemplateFilters();
|
return getPartTestTemplateFilters();
|
||||||
|
case 'plugins':
|
||||||
|
return getPluginTableFilters();
|
||||||
case 'purchaseorder':
|
case 'purchaseorder':
|
||||||
return getPurchaseOrderFilters();
|
return getPurchaseOrderFilters();
|
||||||
case 'purchaseorderlineitem':
|
case 'purchaseorderlineitem':
|
||||||
|
Loading…
x
Reference in New Issue
Block a user