2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-12-28 15:04:05 +00:00

Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters
2022-11-05 01:07:23 +11:00
72 changed files with 16379 additions and 15116 deletions

View File

@@ -134,7 +134,7 @@ class InvenTreeAPITestCase(UserMixin, APITestCase):
if expected_code is not None:
if response.status_code != expected_code:
print(f"Unexpected response at '{url}':")
print(f"Unexpected response at '{url}': status_code = {response.status_code}")
print(response.data)
self.assertEqual(response.status_code, expected_code)
@@ -143,11 +143,13 @@ class InvenTreeAPITestCase(UserMixin, APITestCase):
def post(self, url, data=None, expected_code=None, format='json'):
"""Issue a POST request."""
response = self.client.post(url, data=data, format=format)
# Set default value - see B006
if data is None:
data = {}
response = self.client.post(url, data=data, format=format)
if expected_code is not None:
if response.status_code != expected_code:

View File

@@ -2,11 +2,14 @@
# InvenTree API version
INVENTREE_API_VERSION = 78
INVENTREE_API_VERSION = 79
"""
Increment this API version number whenever there is a significant change to the API that any clients need to know about
v79 -> 2022-11-03 : https://github.com/inventree/InvenTree/pull/3895
- Add metadata to Company
v78 -> 2022-10-25 : https://github.com/inventree/InvenTree/pull/3854
- Make PartCategory to be filtered by name and description

View File

@@ -127,13 +127,6 @@ function inventreeDocReady() {
loadBrandIcon($(this), $(this).attr('brand_name'));
});
// Callback for "admin view" button
$('#admin-button, .admin-button').click(function() {
var url = $(this).attr('url');
location.href = url;
});
// Display any cached alert messages
showCachedAlerts();

View File

@@ -178,11 +178,15 @@ class APITests(InvenTreeAPITestCase):
def test_with_roles(self):
"""Assign some roles to the user."""
self.basicAuth()
response = self.get(reverse('api-user-roles'))
url = reverse('api-user-roles')
response = self.get(url)
self.assignRole('part.delete')
self.assignRole('build.change')
response = self.get(reverse('api-user-roles'))
response = self.get(url)
roles = response.data['roles']

View File

@@ -213,7 +213,7 @@ class BuildTest(BuildAPITest):
"location": 1,
"status": 50, # Item requires attention
},
expected_code=201
expected_code=201,
)
self.assertEqual(self.build.incomplete_outputs.count(), 0)

View File

@@ -933,6 +933,17 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'validator': bool,
},
'BARCODE_INPUT_DELAY': {
'name': _('Barcode Input Delay'),
'description': _('Barcode input processing delay time'),
'default': 50,
'validator': [
int,
MinValueValidator(1),
],
'units': 'ms',
},
'BARCODE_WEBCAM_SUPPORT': {
'name': _('Barcode Webcam Support'),
'description': _('Allow barcode scanning via webcam in browser'),
@@ -1321,7 +1332,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'PLUGIN_ON_STARTUP': {
'name': _('Check plugins on startup'),
'description': _('Check that all plugins are installed on startup - enable in container enviroments'),
'description': _('Check that all plugins are installed on startup - enable in container environments'),
'default': False,
'validator': bool,
'requires_restart': True,

View File

@@ -10,7 +10,9 @@ from rest_framework import filters
from InvenTree.api import AttachmentMixin, ListCreateDestroyAPIView
from InvenTree.filters import InvenTreeOrderingFilter
from InvenTree.helpers import str2bool
from InvenTree.mixins import ListCreateAPI, RetrieveUpdateDestroyAPI
from InvenTree.mixins import (ListCreateAPI, RetrieveUpdateAPI,
RetrieveUpdateDestroyAPI)
from plugin.serializers import MetadataSerializer
from .models import (Company, ManufacturerPart, ManufacturerPartAttachment,
ManufacturerPartParameter, SupplierPart,
@@ -83,6 +85,16 @@ class CompanyDetail(RetrieveUpdateDestroyAPI):
return queryset
class CompanyMetadata(RetrieveUpdateAPI):
"""API endpoint for viewing / updating Company metadata."""
def get_serializer(self, *args, **kwargs):
"""Return MetadataSerializer instance for a Company"""
return MetadataSerializer(Company, *args, **kwargs)
queryset = Company.objects.all()
class ManufacturerPartFilter(rest_filters.FilterSet):
"""Custom API filters for the ManufacturerPart list endpoint."""
@@ -460,7 +472,11 @@ company_api_urls = [
re_path(r'^.*$', SupplierPriceBreakList.as_view(), name='api-part-supplier-price-list'),
])),
re_path(r'^(?P<pk>\d+)/?', CompanyDetail.as_view(), name='api-company-detail'),
re_path(r'^(?P<pk>\d+)/?', include([
re_path(r'^metadata/', CompanyMetadata.as_view(), name='api-company-metadata'),
re_path(r'^.*$', CompanyDetail.as_view(), name='api-company-detail'),
])),
re_path(r'^.*$', CompanyList.as_view(), name='api-company-list'),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.16 on 2022-11-02 17:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('company', '0048_auto_20220913_0312'),
]
operations = [
migrations.AddField(
model_name='company',
name='metadata',
field=models.JSONField(blank=True, help_text='JSON metadata field, for use by external plugins', null=True, verbose_name='Plugin Metadata'),
),
]

View File

@@ -23,6 +23,7 @@ from common.settings import currency_code_default
from InvenTree.fields import InvenTreeURLField, RoundingDecimalField
from InvenTree.models import InvenTreeAttachment, InvenTreeBarcodeMixin
from InvenTree.status_codes import PurchaseOrderStatus
from plugin.models import MetadataMixin
def rename_company_image(instance, filename):
@@ -50,7 +51,7 @@ def rename_company_image(instance, filename):
return os.path.join(base, fn)
class Company(models.Model):
class Company(MetadataMixin, models.Model):
"""A Company object represents an external company.
It may be a supplier or a customer or a manufacturer (or a combination)

View File

@@ -67,9 +67,7 @@ class LabelPrintMixin:
plugin = registry.get_plugin(plugin_key)
if plugin:
config = plugin.plugin_config()
if config and config.active:
if plugin.is_active():
# Only return the plugin if it is enabled!
return plugin
else:

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-10-25 10:54+0000\n"
"POT-Creation-Date: 2022-10-28 07:18+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -59,35 +59,35 @@ msgstr ""
msgid "Provided value does not match required pattern: "
msgstr ""
#: InvenTree/forms.py:133
#: InvenTree/forms.py:134
msgid "Enter password"
msgstr ""
#: InvenTree/forms.py:134
#: InvenTree/forms.py:135
msgid "Enter new password"
msgstr ""
#: InvenTree/forms.py:143
#: InvenTree/forms.py:144
msgid "Confirm password"
msgstr ""
#: InvenTree/forms.py:144
#: InvenTree/forms.py:145
msgid "Confirm new password"
msgstr ""
#: InvenTree/forms.py:148
#: InvenTree/forms.py:149
msgid "Old password"
msgstr ""
#: InvenTree/forms.py:177
#: InvenTree/forms.py:178
msgid "Email (again)"
msgstr ""
#: InvenTree/forms.py:181
#: InvenTree/forms.py:182
msgid "Email address confirmation"
msgstr ""
#: InvenTree/forms.py:202
#: InvenTree/forms.py:203
msgid "You must type the same email each time."
msgstr ""
@@ -131,30 +131,35 @@ msgstr ""
msgid "Empty serial number string"
msgstr ""
#: InvenTree/helpers.py:633
#: InvenTree/helpers.py:640
msgid "Duplicate serial"
msgstr ""
#: InvenTree/helpers.py:662
#: InvenTree/helpers.py:673 InvenTree/helpers.py:708
#, python-brace-format
msgid "Invalid group range: {g}"
msgstr ""
#: InvenTree/helpers.py:714 InvenTree/helpers.py:721 InvenTree/helpers.py:736
#: InvenTree/helpers.py:702
#, python-brace-format
msgid "Group range {g} exceeds allowed quantity ({q})"
msgstr ""
#: InvenTree/helpers.py:726 InvenTree/helpers.py:733 InvenTree/helpers.py:748
#, python-brace-format
msgid "Invalid group sequence: {g}"
msgstr ""
#: InvenTree/helpers.py:746
#: InvenTree/helpers.py:758
msgid "No serial numbers found"
msgstr ""
#: InvenTree/helpers.py:749
#: InvenTree/helpers.py:761
#, python-brace-format
msgid "Number of unique serial numbers ({s}) must match quantity ({q})"
msgstr ""
#: InvenTree/helpers.py:948
#: InvenTree/helpers.py:960
msgid "Remove HTML tags from this value"
msgstr ""
@@ -224,9 +229,9 @@ msgstr ""
msgid "File comment"
msgstr ""
#: InvenTree/models.py:422 InvenTree/models.py:423 common/models.py:1726
#: common/models.py:1727 common/models.py:1950 common/models.py:1951
#: common/models.py:2213 common/models.py:2214 part/models.py:2254
#: InvenTree/models.py:422 InvenTree/models.py:423 common/models.py:1733
#: common/models.py:1734 common/models.py:1957 common/models.py:1958
#: common/models.py:2220 common/models.py:2221 part/models.py:2254
#: part/models.py:2274 plugin/models.py:260 plugin/models.py:261
#: report/templates/report/inventree_test_report_base.html:96
#: templates/js/translated/stock.js:2649
@@ -266,7 +271,7 @@ msgstr ""
msgid "Invalid choice"
msgstr ""
#: InvenTree/models.py:557 InvenTree/models.py:558 common/models.py:1936
#: InvenTree/models.py:557 InvenTree/models.py:558 common/models.py:1943
#: company/models.py:358 label/models.py:101 part/models.py:760
#: part/models.py:2432 plugin/models.py:94 report/models.py:152
#: templates/InvenTree/settings/mixins/urls.html:13
@@ -678,24 +683,24 @@ msgstr ""
msgid "Production"
msgstr ""
#: InvenTree/validators.py:19
#: InvenTree/validators.py:20
msgid "Not a valid currency code"
msgstr ""
#: InvenTree/validators.py:90
#: InvenTree/validators.py:91
#, python-brace-format
msgid "IPN must match regex pattern {pat}"
msgstr ""
#: InvenTree/validators.py:132 InvenTree/validators.py:148
#: InvenTree/validators.py:133 InvenTree/validators.py:149
msgid "Overage value must not be negative"
msgstr ""
#: InvenTree/validators.py:150
#: InvenTree/validators.py:151
msgid "Overage must not exceed 100%"
msgstr ""
#: InvenTree/validators.py:157
#: InvenTree/validators.py:158
msgid "Invalid value for overage"
msgstr ""
@@ -746,7 +751,8 @@ msgstr ""
#: order/templates/order/so_sidebar.html:13
#: part/templates/part/part_sidebar.html:22 templates/InvenTree/index.html:221
#: templates/InvenTree/search.html:141
#: templates/InvenTree/settings/sidebar.html:47 users/models.py:41
#: templates/InvenTree/settings/sidebar.html:47
#: templates/js/translated/search.js:254 users/models.py:41
msgid "Build Orders"
msgstr ""
@@ -1010,7 +1016,7 @@ msgstr ""
#: build/models.py:1359 build/serializers.py:192
#: build/templates/build/build_base.html:85
#: build/templates/build/detail.html:34 common/models.py:1758
#: build/templates/build/detail.html:34 common/models.py:1765
#: company/templates/company/supplier_part.html:341 order/models.py:911
#: order/models.py:1437 order/serializers.py:1213
#: order/templates/order/order_wizard/match_parts.html:30 part/forms.py:40
@@ -2321,7 +2327,7 @@ msgstr ""
msgid "Enable plugins to respond to internal events"
msgstr ""
#: common/models.py:1391 common/models.py:1719
#: common/models.py:1391 common/models.py:1726
msgid "Settings key (must be unique - case insensitive"
msgstr ""
@@ -2558,120 +2564,128 @@ msgid "Display companies in search preview window"
msgstr ""
#: common/models.py:1616
msgid "Search Purchase Orders"
msgid "Search Build Orders"
msgstr ""
#: common/models.py:1617
msgid "Display purchase orders in search preview window"
msgid "Display build orders in search preview window"
msgstr ""
#: common/models.py:1623
msgid "Exclude Inactive Purchase Orders"
msgid "Search Purchase Orders"
msgstr ""
#: common/models.py:1624
msgid "Exclude inactive purchase orders from search preview window"
msgid "Display purchase orders in search preview window"
msgstr ""
#: common/models.py:1630
msgid "Search Sales Orders"
msgid "Exclude Inactive Purchase Orders"
msgstr ""
#: common/models.py:1631
msgid "Display sales orders in search preview window"
msgid "Exclude inactive purchase orders from search preview window"
msgstr ""
#: common/models.py:1637
msgid "Exclude Inactive Sales Orders"
msgid "Search Sales Orders"
msgstr ""
#: common/models.py:1638
msgid "Exclude inactive sales orders from search preview window"
msgid "Display sales orders in search preview window"
msgstr ""
#: common/models.py:1644
msgid "Search Preview Results"
msgid "Exclude Inactive Sales Orders"
msgstr ""
#: common/models.py:1645
msgid "Number of results to show in each section of the search preview window"
msgid "Exclude inactive sales orders from search preview window"
msgstr ""
#: common/models.py:1651
msgid "Show Quantity in Forms"
msgid "Search Preview Results"
msgstr ""
#: common/models.py:1652
msgid "Display available part quantity in some forms"
msgid "Number of results to show in each section of the search preview window"
msgstr ""
#: common/models.py:1658
msgid "Escape Key Closes Forms"
msgid "Show Quantity in Forms"
msgstr ""
#: common/models.py:1659
msgid "Use the escape key to close modal forms"
msgid "Display available part quantity in some forms"
msgstr ""
#: common/models.py:1665
msgid "Fixed Navbar"
msgid "Escape Key Closes Forms"
msgstr ""
#: common/models.py:1666
msgid "The navbar position is fixed to the top of the screen"
msgid "Use the escape key to close modal forms"
msgstr ""
#: common/models.py:1672
msgid "Date Format"
msgid "Fixed Navbar"
msgstr ""
#: common/models.py:1673
msgid "The navbar position is fixed to the top of the screen"
msgstr ""
#: common/models.py:1679
msgid "Date Format"
msgstr ""
#: common/models.py:1680
msgid "Preferred format for displaying dates"
msgstr ""
#: common/models.py:1687 part/templates/part/detail.html:41
#: common/models.py:1694 part/templates/part/detail.html:41
msgid "Part Scheduling"
msgstr ""
#: common/models.py:1688
#: common/models.py:1695
msgid "Display part scheduling information"
msgstr ""
#: common/models.py:1694
#: common/models.py:1701
msgid "Table String Length"
msgstr ""
#: common/models.py:1695
#: common/models.py:1702
msgid "Maximimum length limit for strings displayed in table views"
msgstr ""
#: common/models.py:1759
#: common/models.py:1766
msgid "Price break quantity"
msgstr ""
#: common/models.py:1766 company/serializers.py:372
#: common/models.py:1773 company/serializers.py:372
#: company/templates/company/supplier_part.html:346 order/models.py:952
#: templates/js/translated/part.js:1103 templates/js/translated/part.js:2223
msgid "Price"
msgstr ""
#: common/models.py:1767
#: common/models.py:1774
msgid "Unit price at specified quantity"
msgstr ""
#: common/models.py:1927 common/models.py:2105
#: common/models.py:1934 common/models.py:2112
msgid "Endpoint"
msgstr ""
#: common/models.py:1928
#: common/models.py:1935
msgid "Endpoint at which this webhook is received"
msgstr ""
#: common/models.py:1937
#: common/models.py:1944
msgid "Name for this webhook"
msgstr ""
#: common/models.py:1942 part/models.py:935 plugin/models.py:100
#: common/models.py:1949 part/models.py:935 plugin/models.py:100
#: templates/js/translated/table_filters.js:34
#: templates/js/translated/table_filters.js:112
#: templates/js/translated/table_filters.js:324
@@ -2679,67 +2693,67 @@ msgstr ""
msgid "Active"
msgstr ""
#: common/models.py:1943
#: common/models.py:1950
msgid "Is this webhook active"
msgstr ""
#: common/models.py:1957
#: common/models.py:1964
msgid "Token"
msgstr ""
#: common/models.py:1958
#: common/models.py:1965
msgid "Token for access"
msgstr ""
#: common/models.py:1965
#: common/models.py:1972
msgid "Secret"
msgstr ""
#: common/models.py:1966
#: common/models.py:1973
msgid "Shared secret for HMAC"
msgstr ""
#: common/models.py:2072
#: common/models.py:2079
msgid "Message ID"
msgstr ""
#: common/models.py:2073
#: common/models.py:2080
msgid "Unique identifier for this message"
msgstr ""
#: common/models.py:2081
#: common/models.py:2088
msgid "Host"
msgstr ""
#: common/models.py:2082
#: common/models.py:2089
msgid "Host from which this message was received"
msgstr ""
#: common/models.py:2089
#: common/models.py:2096
msgid "Header"
msgstr ""
#: common/models.py:2090
#: common/models.py:2097
msgid "Header of this message"
msgstr ""
#: common/models.py:2096
#: common/models.py:2103
msgid "Body"
msgstr ""
#: common/models.py:2097
#: common/models.py:2104
msgid "Body of this message"
msgstr ""
#: common/models.py:2106
#: common/models.py:2113
msgid "Endpoint on which this message was received"
msgstr ""
#: common/models.py:2111
#: common/models.py:2118
msgid "Worked on"
msgstr ""
#: common/models.py:2112
#: common/models.py:2119
msgid "Was the work on this message finished?"
msgstr ""
@@ -3228,7 +3242,7 @@ msgstr ""
#: part/templates/part/detail.html:84 part/templates/part/part_sidebar.html:37
#: templates/InvenTree/index.html:252 templates/InvenTree/search.html:200
#: templates/InvenTree/settings/sidebar.html:49
#: templates/js/translated/search.js:277 templates/navbar.html:50
#: templates/js/translated/search.js:293 templates/navbar.html:50
#: users/models.py:42
msgid "Purchase Orders"
msgstr ""
@@ -3251,7 +3265,7 @@ msgstr ""
#: part/templates/part/detail.html:107 part/templates/part/part_sidebar.html:41
#: templates/InvenTree/index.html:283 templates/InvenTree/search.html:220
#: templates/InvenTree/settings/sidebar.html:51
#: templates/js/translated/search.js:301 templates/navbar.html:61
#: templates/js/translated/search.js:317 templates/navbar.html:61
#: users/models.py:43
msgid "Sales Orders"
msgstr ""
@@ -3321,7 +3335,7 @@ msgstr ""
#: company/templates/company/manufacturer_part.html:136
#: company/templates/company/manufacturer_part.html:183
#: part/templates/part/detail.html:371 part/templates/part/detail.html:401
#: templates/js/translated/forms.js:458 templates/js/translated/helpers.js:36
#: templates/js/translated/forms.js:455 templates/js/translated/helpers.js:36
#: users/models.py:222
msgid "Delete"
msgstr ""
@@ -3570,7 +3584,7 @@ msgstr ""
msgid "New Customer"
msgstr ""
#: company/views.py:52 templates/js/translated/search.js:254
#: company/views.py:52 templates/js/translated/search.js:270
msgid "Companies"
msgstr ""
@@ -4379,47 +4393,47 @@ msgstr ""
msgid "Updated {part} unit-price to {price} and quantity to {qty}"
msgstr ""
#: part/api.py:514
#: part/api.py:516
msgid "Incoming Purchase Order"
msgstr ""
#: part/api.py:534
#: part/api.py:536
msgid "Outgoing Sales Order"
msgstr ""
#: part/api.py:552
#: part/api.py:554
msgid "Stock produced by Build Order"
msgstr ""
#: part/api.py:638
#: part/api.py:640
msgid "Stock required for Build Order"
msgstr ""
#: part/api.py:775
#: part/api.py:777
msgid "Valid"
msgstr ""
#: part/api.py:776
#: part/api.py:778
msgid "Validate entire Bill of Materials"
msgstr ""
#: part/api.py:782
#: part/api.py:784
msgid "This option must be selected"
msgstr ""
#: part/api.py:1205
#: part/api.py:1207
msgid "Must be greater than zero"
msgstr ""
#: part/api.py:1209
#: part/api.py:1211
msgid "Must be a valid quantity"
msgstr ""
#: part/api.py:1224
#: part/api.py:1226
msgid "Specify location for initial part stock"
msgstr ""
#: part/api.py:1255 part/api.py:1259 part/api.py:1274 part/api.py:1278
#: part/api.py:1257 part/api.py:1261 part/api.py:1276 part/api.py:1280
msgid "This field is required"
msgstr ""
@@ -5755,23 +5769,23 @@ msgstr ""
msgid "No matching action found"
msgstr ""
#: plugin/base/barcodes/api.py:52 plugin/base/barcodes/api.py:110
msgid "Must provide barcode_data parameter"
#: plugin/base/barcodes/api.py:54 plugin/base/barcodes/api.py:113
msgid "Missing barcode data"
msgstr ""
#: plugin/base/barcodes/api.py:82
#: plugin/base/barcodes/api.py:83
msgid "No match found for barcode data"
msgstr ""
#: plugin/base/barcodes/api.py:86
#: plugin/base/barcodes/api.py:87
msgid "Match found for barcode data"
msgstr ""
#: plugin/base/barcodes/api.py:125
#: plugin/base/barcodes/api.py:126
msgid "Barcode matches existing item"
msgstr ""
#: plugin/base/barcodes/api.py:222
#: plugin/base/barcodes/api.py:223
msgid "No match found for provided value"
msgstr ""
@@ -7603,7 +7617,7 @@ msgstr ""
msgid "Please confirm that <a href=\"mailto:%(email)s\">%(email)s</a> is an email address for user %(user_display)s."
msgstr ""
#: templates/account/email_confirm.html:22 templates/js/translated/forms.js:649
#: templates/account/email_confirm.html:22 templates/js/translated/forms.js:646
msgid "Confirm"
msgstr ""
@@ -8653,61 +8667,61 @@ msgstr ""
msgid "Create filter"
msgstr ""
#: templates/js/translated/forms.js:372 templates/js/translated/forms.js:387
#: templates/js/translated/forms.js:401 templates/js/translated/forms.js:415
#: templates/js/translated/forms.js:369 templates/js/translated/forms.js:384
#: templates/js/translated/forms.js:398 templates/js/translated/forms.js:412
msgid "Action Prohibited"
msgstr ""
#: templates/js/translated/forms.js:374
#: templates/js/translated/forms.js:371
msgid "Create operation not allowed"
msgstr ""
#: templates/js/translated/forms.js:389
#: templates/js/translated/forms.js:386
msgid "Update operation not allowed"
msgstr ""
#: templates/js/translated/forms.js:403
#: templates/js/translated/forms.js:400
msgid "Delete operation not allowed"
msgstr ""
#: templates/js/translated/forms.js:417
#: templates/js/translated/forms.js:414
msgid "View operation not allowed"
msgstr ""
#: templates/js/translated/forms.js:675
#: templates/js/translated/forms.js:672
msgid "Keep this form open"
msgstr ""
#: templates/js/translated/forms.js:776
#: templates/js/translated/forms.js:773
msgid "Enter a valid number"
msgstr ""
#: templates/js/translated/forms.js:1269 templates/modals.html:19
#: templates/js/translated/forms.js:1266 templates/modals.html:19
#: templates/modals.html:43
msgid "Form errors exist"
msgstr ""
#: templates/js/translated/forms.js:1706
#: templates/js/translated/forms.js:1703
msgid "No results found"
msgstr ""
#: templates/js/translated/forms.js:1922 templates/search.html:29
#: templates/js/translated/forms.js:1919 templates/search.html:29
msgid "Searching"
msgstr ""
#: templates/js/translated/forms.js:2175
#: templates/js/translated/forms.js:2172
msgid "Clear input"
msgstr ""
#: templates/js/translated/forms.js:2641
#: templates/js/translated/forms.js:2638
msgid "File Column"
msgstr ""
#: templates/js/translated/forms.js:2641
#: templates/js/translated/forms.js:2638
msgid "Field Name"
msgstr ""
#: templates/js/translated/forms.js:2653
#: templates/js/translated/forms.js:2650
msgid "Select Columns"
msgstr ""
@@ -9737,11 +9751,11 @@ msgstr ""
msgid "Sales Order(s) must be selected before printing report"
msgstr ""
#: templates/js/translated/search.js:394
#: templates/js/translated/search.js:410
msgid "Minimize results"
msgstr ""
#: templates/js/translated/search.js:397
#: templates/js/translated/search.js:413
msgid "Remove results"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-10-25 10:54+0000\n"
"POT-Creation-Date: 2022-10-28 07:18+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -59,35 +59,35 @@ msgstr ""
msgid "Provided value does not match required pattern: "
msgstr ""
#: InvenTree/forms.py:133
#: InvenTree/forms.py:134
msgid "Enter password"
msgstr ""
#: InvenTree/forms.py:134
#: InvenTree/forms.py:135
msgid "Enter new password"
msgstr ""
#: InvenTree/forms.py:143
#: InvenTree/forms.py:144
msgid "Confirm password"
msgstr ""
#: InvenTree/forms.py:144
#: InvenTree/forms.py:145
msgid "Confirm new password"
msgstr ""
#: InvenTree/forms.py:148
#: InvenTree/forms.py:149
msgid "Old password"
msgstr ""
#: InvenTree/forms.py:177
#: InvenTree/forms.py:178
msgid "Email (again)"
msgstr ""
#: InvenTree/forms.py:181
#: InvenTree/forms.py:182
msgid "Email address confirmation"
msgstr ""
#: InvenTree/forms.py:202
#: InvenTree/forms.py:203
msgid "You must type the same email each time."
msgstr ""
@@ -131,30 +131,35 @@ msgstr ""
msgid "Empty serial number string"
msgstr ""
#: InvenTree/helpers.py:633
#: InvenTree/helpers.py:640
msgid "Duplicate serial"
msgstr ""
#: InvenTree/helpers.py:662
#: InvenTree/helpers.py:673 InvenTree/helpers.py:708
#, python-brace-format
msgid "Invalid group range: {g}"
msgstr ""
#: InvenTree/helpers.py:714 InvenTree/helpers.py:721 InvenTree/helpers.py:736
#: InvenTree/helpers.py:702
#, python-brace-format
msgid "Group range {g} exceeds allowed quantity ({q})"
msgstr ""
#: InvenTree/helpers.py:726 InvenTree/helpers.py:733 InvenTree/helpers.py:748
#, python-brace-format
msgid "Invalid group sequence: {g}"
msgstr ""
#: InvenTree/helpers.py:746
#: InvenTree/helpers.py:758
msgid "No serial numbers found"
msgstr ""
#: InvenTree/helpers.py:749
#: InvenTree/helpers.py:761
#, python-brace-format
msgid "Number of unique serial numbers ({s}) must match quantity ({q})"
msgstr ""
#: InvenTree/helpers.py:948
#: InvenTree/helpers.py:960
msgid "Remove HTML tags from this value"
msgstr ""
@@ -224,9 +229,9 @@ msgstr ""
msgid "File comment"
msgstr ""
#: InvenTree/models.py:422 InvenTree/models.py:423 common/models.py:1726
#: common/models.py:1727 common/models.py:1950 common/models.py:1951
#: common/models.py:2213 common/models.py:2214 part/models.py:2254
#: InvenTree/models.py:422 InvenTree/models.py:423 common/models.py:1733
#: common/models.py:1734 common/models.py:1957 common/models.py:1958
#: common/models.py:2220 common/models.py:2221 part/models.py:2254
#: part/models.py:2274 plugin/models.py:260 plugin/models.py:261
#: report/templates/report/inventree_test_report_base.html:96
#: templates/js/translated/stock.js:2649
@@ -266,7 +271,7 @@ msgstr ""
msgid "Invalid choice"
msgstr ""
#: InvenTree/models.py:557 InvenTree/models.py:558 common/models.py:1936
#: InvenTree/models.py:557 InvenTree/models.py:558 common/models.py:1943
#: company/models.py:358 label/models.py:101 part/models.py:760
#: part/models.py:2432 plugin/models.py:94 report/models.py:152
#: templates/InvenTree/settings/mixins/urls.html:13
@@ -678,24 +683,24 @@ msgstr ""
msgid "Production"
msgstr ""
#: InvenTree/validators.py:19
#: InvenTree/validators.py:20
msgid "Not a valid currency code"
msgstr ""
#: InvenTree/validators.py:90
#: InvenTree/validators.py:91
#, python-brace-format
msgid "IPN must match regex pattern {pat}"
msgstr ""
#: InvenTree/validators.py:132 InvenTree/validators.py:148
#: InvenTree/validators.py:133 InvenTree/validators.py:149
msgid "Overage value must not be negative"
msgstr ""
#: InvenTree/validators.py:150
#: InvenTree/validators.py:151
msgid "Overage must not exceed 100%"
msgstr ""
#: InvenTree/validators.py:157
#: InvenTree/validators.py:158
msgid "Invalid value for overage"
msgstr ""
@@ -746,7 +751,8 @@ msgstr ""
#: order/templates/order/so_sidebar.html:13
#: part/templates/part/part_sidebar.html:22 templates/InvenTree/index.html:221
#: templates/InvenTree/search.html:141
#: templates/InvenTree/settings/sidebar.html:47 users/models.py:41
#: templates/InvenTree/settings/sidebar.html:47
#: templates/js/translated/search.js:254 users/models.py:41
msgid "Build Orders"
msgstr ""
@@ -1010,7 +1016,7 @@ msgstr ""
#: build/models.py:1359 build/serializers.py:192
#: build/templates/build/build_base.html:85
#: build/templates/build/detail.html:34 common/models.py:1758
#: build/templates/build/detail.html:34 common/models.py:1765
#: company/templates/company/supplier_part.html:341 order/models.py:911
#: order/models.py:1437 order/serializers.py:1213
#: order/templates/order/order_wizard/match_parts.html:30 part/forms.py:40
@@ -2321,7 +2327,7 @@ msgstr ""
msgid "Enable plugins to respond to internal events"
msgstr ""
#: common/models.py:1391 common/models.py:1719
#: common/models.py:1391 common/models.py:1726
msgid "Settings key (must be unique - case insensitive"
msgstr ""
@@ -2558,120 +2564,128 @@ msgid "Display companies in search preview window"
msgstr ""
#: common/models.py:1616
msgid "Search Purchase Orders"
msgid "Search Build Orders"
msgstr ""
#: common/models.py:1617
msgid "Display purchase orders in search preview window"
msgid "Display build orders in search preview window"
msgstr ""
#: common/models.py:1623
msgid "Exclude Inactive Purchase Orders"
msgid "Search Purchase Orders"
msgstr ""
#: common/models.py:1624
msgid "Exclude inactive purchase orders from search preview window"
msgid "Display purchase orders in search preview window"
msgstr ""
#: common/models.py:1630
msgid "Search Sales Orders"
msgid "Exclude Inactive Purchase Orders"
msgstr ""
#: common/models.py:1631
msgid "Display sales orders in search preview window"
msgid "Exclude inactive purchase orders from search preview window"
msgstr ""
#: common/models.py:1637
msgid "Exclude Inactive Sales Orders"
msgid "Search Sales Orders"
msgstr ""
#: common/models.py:1638
msgid "Exclude inactive sales orders from search preview window"
msgid "Display sales orders in search preview window"
msgstr ""
#: common/models.py:1644
msgid "Search Preview Results"
msgid "Exclude Inactive Sales Orders"
msgstr ""
#: common/models.py:1645
msgid "Number of results to show in each section of the search preview window"
msgid "Exclude inactive sales orders from search preview window"
msgstr ""
#: common/models.py:1651
msgid "Show Quantity in Forms"
msgid "Search Preview Results"
msgstr ""
#: common/models.py:1652
msgid "Display available part quantity in some forms"
msgid "Number of results to show in each section of the search preview window"
msgstr ""
#: common/models.py:1658
msgid "Escape Key Closes Forms"
msgid "Show Quantity in Forms"
msgstr ""
#: common/models.py:1659
msgid "Use the escape key to close modal forms"
msgid "Display available part quantity in some forms"
msgstr ""
#: common/models.py:1665
msgid "Fixed Navbar"
msgid "Escape Key Closes Forms"
msgstr ""
#: common/models.py:1666
msgid "The navbar position is fixed to the top of the screen"
msgid "Use the escape key to close modal forms"
msgstr ""
#: common/models.py:1672
msgid "Date Format"
msgid "Fixed Navbar"
msgstr ""
#: common/models.py:1673
msgid "The navbar position is fixed to the top of the screen"
msgstr ""
#: common/models.py:1679
msgid "Date Format"
msgstr ""
#: common/models.py:1680
msgid "Preferred format for displaying dates"
msgstr ""
#: common/models.py:1687 part/templates/part/detail.html:41
#: common/models.py:1694 part/templates/part/detail.html:41
msgid "Part Scheduling"
msgstr ""
#: common/models.py:1688
#: common/models.py:1695
msgid "Display part scheduling information"
msgstr ""
#: common/models.py:1694
#: common/models.py:1701
msgid "Table String Length"
msgstr ""
#: common/models.py:1695
#: common/models.py:1702
msgid "Maximimum length limit for strings displayed in table views"
msgstr ""
#: common/models.py:1759
#: common/models.py:1766
msgid "Price break quantity"
msgstr ""
#: common/models.py:1766 company/serializers.py:372
#: common/models.py:1773 company/serializers.py:372
#: company/templates/company/supplier_part.html:346 order/models.py:952
#: templates/js/translated/part.js:1103 templates/js/translated/part.js:2223
msgid "Price"
msgstr ""
#: common/models.py:1767
#: common/models.py:1774
msgid "Unit price at specified quantity"
msgstr ""
#: common/models.py:1927 common/models.py:2105
#: common/models.py:1934 common/models.py:2112
msgid "Endpoint"
msgstr ""
#: common/models.py:1928
#: common/models.py:1935
msgid "Endpoint at which this webhook is received"
msgstr ""
#: common/models.py:1937
#: common/models.py:1944
msgid "Name for this webhook"
msgstr ""
#: common/models.py:1942 part/models.py:935 plugin/models.py:100
#: common/models.py:1949 part/models.py:935 plugin/models.py:100
#: templates/js/translated/table_filters.js:34
#: templates/js/translated/table_filters.js:112
#: templates/js/translated/table_filters.js:324
@@ -2679,67 +2693,67 @@ msgstr ""
msgid "Active"
msgstr ""
#: common/models.py:1943
#: common/models.py:1950
msgid "Is this webhook active"
msgstr ""
#: common/models.py:1957
#: common/models.py:1964
msgid "Token"
msgstr ""
#: common/models.py:1958
#: common/models.py:1965
msgid "Token for access"
msgstr ""
#: common/models.py:1965
#: common/models.py:1972
msgid "Secret"
msgstr ""
#: common/models.py:1966
#: common/models.py:1973
msgid "Shared secret for HMAC"
msgstr ""
#: common/models.py:2072
#: common/models.py:2079
msgid "Message ID"
msgstr ""
#: common/models.py:2073
#: common/models.py:2080
msgid "Unique identifier for this message"
msgstr ""
#: common/models.py:2081
#: common/models.py:2088
msgid "Host"
msgstr ""
#: common/models.py:2082
#: common/models.py:2089
msgid "Host from which this message was received"
msgstr ""
#: common/models.py:2089
#: common/models.py:2096
msgid "Header"
msgstr ""
#: common/models.py:2090
#: common/models.py:2097
msgid "Header of this message"
msgstr ""
#: common/models.py:2096
#: common/models.py:2103
msgid "Body"
msgstr ""
#: common/models.py:2097
#: common/models.py:2104
msgid "Body of this message"
msgstr ""
#: common/models.py:2106
#: common/models.py:2113
msgid "Endpoint on which this message was received"
msgstr ""
#: common/models.py:2111
#: common/models.py:2118
msgid "Worked on"
msgstr ""
#: common/models.py:2112
#: common/models.py:2119
msgid "Was the work on this message finished?"
msgstr ""
@@ -3228,7 +3242,7 @@ msgstr ""
#: part/templates/part/detail.html:84 part/templates/part/part_sidebar.html:37
#: templates/InvenTree/index.html:252 templates/InvenTree/search.html:200
#: templates/InvenTree/settings/sidebar.html:49
#: templates/js/translated/search.js:277 templates/navbar.html:50
#: templates/js/translated/search.js:293 templates/navbar.html:50
#: users/models.py:42
msgid "Purchase Orders"
msgstr ""
@@ -3251,7 +3265,7 @@ msgstr ""
#: part/templates/part/detail.html:107 part/templates/part/part_sidebar.html:41
#: templates/InvenTree/index.html:283 templates/InvenTree/search.html:220
#: templates/InvenTree/settings/sidebar.html:51
#: templates/js/translated/search.js:301 templates/navbar.html:61
#: templates/js/translated/search.js:317 templates/navbar.html:61
#: users/models.py:43
msgid "Sales Orders"
msgstr ""
@@ -3321,7 +3335,7 @@ msgstr ""
#: company/templates/company/manufacturer_part.html:136
#: company/templates/company/manufacturer_part.html:183
#: part/templates/part/detail.html:371 part/templates/part/detail.html:401
#: templates/js/translated/forms.js:458 templates/js/translated/helpers.js:36
#: templates/js/translated/forms.js:455 templates/js/translated/helpers.js:36
#: users/models.py:222
msgid "Delete"
msgstr ""
@@ -3570,7 +3584,7 @@ msgstr ""
msgid "New Customer"
msgstr ""
#: company/views.py:52 templates/js/translated/search.js:254
#: company/views.py:52 templates/js/translated/search.js:270
msgid "Companies"
msgstr ""
@@ -4379,47 +4393,47 @@ msgstr ""
msgid "Updated {part} unit-price to {price} and quantity to {qty}"
msgstr ""
#: part/api.py:514
#: part/api.py:516
msgid "Incoming Purchase Order"
msgstr ""
#: part/api.py:534
#: part/api.py:536
msgid "Outgoing Sales Order"
msgstr ""
#: part/api.py:552
#: part/api.py:554
msgid "Stock produced by Build Order"
msgstr ""
#: part/api.py:638
#: part/api.py:640
msgid "Stock required for Build Order"
msgstr ""
#: part/api.py:775
#: part/api.py:777
msgid "Valid"
msgstr ""
#: part/api.py:776
#: part/api.py:778
msgid "Validate entire Bill of Materials"
msgstr ""
#: part/api.py:782
#: part/api.py:784
msgid "This option must be selected"
msgstr ""
#: part/api.py:1205
#: part/api.py:1207
msgid "Must be greater than zero"
msgstr ""
#: part/api.py:1209
#: part/api.py:1211
msgid "Must be a valid quantity"
msgstr ""
#: part/api.py:1224
#: part/api.py:1226
msgid "Specify location for initial part stock"
msgstr ""
#: part/api.py:1255 part/api.py:1259 part/api.py:1274 part/api.py:1278
#: part/api.py:1257 part/api.py:1261 part/api.py:1276 part/api.py:1280
msgid "This field is required"
msgstr ""
@@ -5755,23 +5769,23 @@ msgstr ""
msgid "No matching action found"
msgstr ""
#: plugin/base/barcodes/api.py:52 plugin/base/barcodes/api.py:110
msgid "Must provide barcode_data parameter"
#: plugin/base/barcodes/api.py:54 plugin/base/barcodes/api.py:113
msgid "Missing barcode data"
msgstr ""
#: plugin/base/barcodes/api.py:82
#: plugin/base/barcodes/api.py:83
msgid "No match found for barcode data"
msgstr ""
#: plugin/base/barcodes/api.py:86
#: plugin/base/barcodes/api.py:87
msgid "Match found for barcode data"
msgstr ""
#: plugin/base/barcodes/api.py:125
#: plugin/base/barcodes/api.py:126
msgid "Barcode matches existing item"
msgstr ""
#: plugin/base/barcodes/api.py:222
#: plugin/base/barcodes/api.py:223
msgid "No match found for provided value"
msgstr ""
@@ -7603,7 +7617,7 @@ msgstr ""
msgid "Please confirm that <a href=\"mailto:%(email)s\">%(email)s</a> is an email address for user %(user_display)s."
msgstr ""
#: templates/account/email_confirm.html:22 templates/js/translated/forms.js:649
#: templates/account/email_confirm.html:22 templates/js/translated/forms.js:646
msgid "Confirm"
msgstr ""
@@ -8653,61 +8667,61 @@ msgstr ""
msgid "Create filter"
msgstr ""
#: templates/js/translated/forms.js:372 templates/js/translated/forms.js:387
#: templates/js/translated/forms.js:401 templates/js/translated/forms.js:415
#: templates/js/translated/forms.js:369 templates/js/translated/forms.js:384
#: templates/js/translated/forms.js:398 templates/js/translated/forms.js:412
msgid "Action Prohibited"
msgstr ""
#: templates/js/translated/forms.js:374
#: templates/js/translated/forms.js:371
msgid "Create operation not allowed"
msgstr ""
#: templates/js/translated/forms.js:389
#: templates/js/translated/forms.js:386
msgid "Update operation not allowed"
msgstr ""
#: templates/js/translated/forms.js:403
#: templates/js/translated/forms.js:400
msgid "Delete operation not allowed"
msgstr ""
#: templates/js/translated/forms.js:417
#: templates/js/translated/forms.js:414
msgid "View operation not allowed"
msgstr ""
#: templates/js/translated/forms.js:675
#: templates/js/translated/forms.js:672
msgid "Keep this form open"
msgstr ""
#: templates/js/translated/forms.js:776
#: templates/js/translated/forms.js:773
msgid "Enter a valid number"
msgstr ""
#: templates/js/translated/forms.js:1269 templates/modals.html:19
#: templates/js/translated/forms.js:1266 templates/modals.html:19
#: templates/modals.html:43
msgid "Form errors exist"
msgstr ""
#: templates/js/translated/forms.js:1706
#: templates/js/translated/forms.js:1703
msgid "No results found"
msgstr ""
#: templates/js/translated/forms.js:1922 templates/search.html:29
#: templates/js/translated/forms.js:1919 templates/search.html:29
msgid "Searching"
msgstr ""
#: templates/js/translated/forms.js:2175
#: templates/js/translated/forms.js:2172
msgid "Clear input"
msgstr ""
#: templates/js/translated/forms.js:2641
#: templates/js/translated/forms.js:2638
msgid "File Column"
msgstr ""
#: templates/js/translated/forms.js:2641
#: templates/js/translated/forms.js:2638
msgid "Field Name"
msgstr ""
#: templates/js/translated/forms.js:2653
#: templates/js/translated/forms.js:2650
msgid "Select Columns"
msgstr ""
@@ -9737,11 +9751,11 @@ msgstr ""
msgid "Sales Order(s) must be selected before printing report"
msgstr ""
#: templates/js/translated/search.js:394
#: templates/js/translated/search.js:410
msgid "Minimize results"
msgstr ""
#: templates/js/translated/search.js:397
#: templates/js/translated/search.js:413
msgid "Remove results"
msgstr ""

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-10-25 10:54+0000\n"
"POT-Creation-Date: 2022-10-28 07:18+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -59,35 +59,35 @@ msgstr ""
msgid "Provided value does not match required pattern: "
msgstr ""
#: InvenTree/forms.py:133
#: InvenTree/forms.py:134
msgid "Enter password"
msgstr ""
#: InvenTree/forms.py:134
#: InvenTree/forms.py:135
msgid "Enter new password"
msgstr ""
#: InvenTree/forms.py:143
#: InvenTree/forms.py:144
msgid "Confirm password"
msgstr ""
#: InvenTree/forms.py:144
#: InvenTree/forms.py:145
msgid "Confirm new password"
msgstr ""
#: InvenTree/forms.py:148
#: InvenTree/forms.py:149
msgid "Old password"
msgstr ""
#: InvenTree/forms.py:177
#: InvenTree/forms.py:178
msgid "Email (again)"
msgstr ""
#: InvenTree/forms.py:181
#: InvenTree/forms.py:182
msgid "Email address confirmation"
msgstr ""
#: InvenTree/forms.py:202
#: InvenTree/forms.py:203
msgid "You must type the same email each time."
msgstr ""
@@ -131,30 +131,35 @@ msgstr ""
msgid "Empty serial number string"
msgstr ""
#: InvenTree/helpers.py:633
#: InvenTree/helpers.py:640
msgid "Duplicate serial"
msgstr ""
#: InvenTree/helpers.py:662
#: InvenTree/helpers.py:673 InvenTree/helpers.py:708
#, python-brace-format
msgid "Invalid group range: {g}"
msgstr ""
#: InvenTree/helpers.py:714 InvenTree/helpers.py:721 InvenTree/helpers.py:736
#: InvenTree/helpers.py:702
#, python-brace-format
msgid "Group range {g} exceeds allowed quantity ({q})"
msgstr ""
#: InvenTree/helpers.py:726 InvenTree/helpers.py:733 InvenTree/helpers.py:748
#, python-brace-format
msgid "Invalid group sequence: {g}"
msgstr ""
#: InvenTree/helpers.py:746
#: InvenTree/helpers.py:758
msgid "No serial numbers found"
msgstr ""
#: InvenTree/helpers.py:749
#: InvenTree/helpers.py:761
#, python-brace-format
msgid "Number of unique serial numbers ({s}) must match quantity ({q})"
msgstr ""
#: InvenTree/helpers.py:948
#: InvenTree/helpers.py:960
msgid "Remove HTML tags from this value"
msgstr ""
@@ -224,9 +229,9 @@ msgstr ""
msgid "File comment"
msgstr ""
#: InvenTree/models.py:422 InvenTree/models.py:423 common/models.py:1726
#: common/models.py:1727 common/models.py:1950 common/models.py:1951
#: common/models.py:2213 common/models.py:2214 part/models.py:2254
#: InvenTree/models.py:422 InvenTree/models.py:423 common/models.py:1733
#: common/models.py:1734 common/models.py:1957 common/models.py:1958
#: common/models.py:2220 common/models.py:2221 part/models.py:2254
#: part/models.py:2274 plugin/models.py:260 plugin/models.py:261
#: report/templates/report/inventree_test_report_base.html:96
#: templates/js/translated/stock.js:2649
@@ -266,7 +271,7 @@ msgstr ""
msgid "Invalid choice"
msgstr ""
#: InvenTree/models.py:557 InvenTree/models.py:558 common/models.py:1936
#: InvenTree/models.py:557 InvenTree/models.py:558 common/models.py:1943
#: company/models.py:358 label/models.py:101 part/models.py:760
#: part/models.py:2432 plugin/models.py:94 report/models.py:152
#: templates/InvenTree/settings/mixins/urls.html:13
@@ -678,24 +683,24 @@ msgstr ""
msgid "Production"
msgstr ""
#: InvenTree/validators.py:19
#: InvenTree/validators.py:20
msgid "Not a valid currency code"
msgstr ""
#: InvenTree/validators.py:90
#: InvenTree/validators.py:91
#, python-brace-format
msgid "IPN must match regex pattern {pat}"
msgstr ""
#: InvenTree/validators.py:132 InvenTree/validators.py:148
#: InvenTree/validators.py:133 InvenTree/validators.py:149
msgid "Overage value must not be negative"
msgstr ""
#: InvenTree/validators.py:150
#: InvenTree/validators.py:151
msgid "Overage must not exceed 100%"
msgstr ""
#: InvenTree/validators.py:157
#: InvenTree/validators.py:158
msgid "Invalid value for overage"
msgstr ""
@@ -746,7 +751,8 @@ msgstr ""
#: order/templates/order/so_sidebar.html:13
#: part/templates/part/part_sidebar.html:22 templates/InvenTree/index.html:221
#: templates/InvenTree/search.html:141
#: templates/InvenTree/settings/sidebar.html:47 users/models.py:41
#: templates/InvenTree/settings/sidebar.html:47
#: templates/js/translated/search.js:254 users/models.py:41
msgid "Build Orders"
msgstr ""
@@ -1010,7 +1016,7 @@ msgstr ""
#: build/models.py:1359 build/serializers.py:192
#: build/templates/build/build_base.html:85
#: build/templates/build/detail.html:34 common/models.py:1758
#: build/templates/build/detail.html:34 common/models.py:1765
#: company/templates/company/supplier_part.html:341 order/models.py:911
#: order/models.py:1437 order/serializers.py:1213
#: order/templates/order/order_wizard/match_parts.html:30 part/forms.py:40
@@ -2321,7 +2327,7 @@ msgstr ""
msgid "Enable plugins to respond to internal events"
msgstr ""
#: common/models.py:1391 common/models.py:1719
#: common/models.py:1391 common/models.py:1726
msgid "Settings key (must be unique - case insensitive"
msgstr ""
@@ -2558,120 +2564,128 @@ msgid "Display companies in search preview window"
msgstr ""
#: common/models.py:1616
msgid "Search Purchase Orders"
msgid "Search Build Orders"
msgstr ""
#: common/models.py:1617
msgid "Display purchase orders in search preview window"
msgid "Display build orders in search preview window"
msgstr ""
#: common/models.py:1623
msgid "Exclude Inactive Purchase Orders"
msgid "Search Purchase Orders"
msgstr ""
#: common/models.py:1624
msgid "Exclude inactive purchase orders from search preview window"
msgid "Display purchase orders in search preview window"
msgstr ""
#: common/models.py:1630
msgid "Search Sales Orders"
msgid "Exclude Inactive Purchase Orders"
msgstr ""
#: common/models.py:1631
msgid "Display sales orders in search preview window"
msgid "Exclude inactive purchase orders from search preview window"
msgstr ""
#: common/models.py:1637
msgid "Exclude Inactive Sales Orders"
msgid "Search Sales Orders"
msgstr ""
#: common/models.py:1638
msgid "Exclude inactive sales orders from search preview window"
msgid "Display sales orders in search preview window"
msgstr ""
#: common/models.py:1644
msgid "Search Preview Results"
msgid "Exclude Inactive Sales Orders"
msgstr ""
#: common/models.py:1645
msgid "Number of results to show in each section of the search preview window"
msgid "Exclude inactive sales orders from search preview window"
msgstr ""
#: common/models.py:1651
msgid "Show Quantity in Forms"
msgid "Search Preview Results"
msgstr ""
#: common/models.py:1652
msgid "Display available part quantity in some forms"
msgid "Number of results to show in each section of the search preview window"
msgstr ""
#: common/models.py:1658
msgid "Escape Key Closes Forms"
msgid "Show Quantity in Forms"
msgstr ""
#: common/models.py:1659
msgid "Use the escape key to close modal forms"
msgid "Display available part quantity in some forms"
msgstr ""
#: common/models.py:1665
msgid "Fixed Navbar"
msgid "Escape Key Closes Forms"
msgstr ""
#: common/models.py:1666
msgid "The navbar position is fixed to the top of the screen"
msgid "Use the escape key to close modal forms"
msgstr ""
#: common/models.py:1672
msgid "Date Format"
msgid "Fixed Navbar"
msgstr ""
#: common/models.py:1673
msgid "The navbar position is fixed to the top of the screen"
msgstr ""
#: common/models.py:1679
msgid "Date Format"
msgstr ""
#: common/models.py:1680
msgid "Preferred format for displaying dates"
msgstr ""
#: common/models.py:1687 part/templates/part/detail.html:41
#: common/models.py:1694 part/templates/part/detail.html:41
msgid "Part Scheduling"
msgstr ""
#: common/models.py:1688
#: common/models.py:1695
msgid "Display part scheduling information"
msgstr ""
#: common/models.py:1694
#: common/models.py:1701
msgid "Table String Length"
msgstr ""
#: common/models.py:1695
#: common/models.py:1702
msgid "Maximimum length limit for strings displayed in table views"
msgstr ""
#: common/models.py:1759
#: common/models.py:1766
msgid "Price break quantity"
msgstr ""
#: common/models.py:1766 company/serializers.py:372
#: common/models.py:1773 company/serializers.py:372
#: company/templates/company/supplier_part.html:346 order/models.py:952
#: templates/js/translated/part.js:1103 templates/js/translated/part.js:2223
msgid "Price"
msgstr ""
#: common/models.py:1767
#: common/models.py:1774
msgid "Unit price at specified quantity"
msgstr ""
#: common/models.py:1927 common/models.py:2105
#: common/models.py:1934 common/models.py:2112
msgid "Endpoint"
msgstr ""
#: common/models.py:1928
#: common/models.py:1935
msgid "Endpoint at which this webhook is received"
msgstr ""
#: common/models.py:1937
#: common/models.py:1944
msgid "Name for this webhook"
msgstr ""
#: common/models.py:1942 part/models.py:935 plugin/models.py:100
#: common/models.py:1949 part/models.py:935 plugin/models.py:100
#: templates/js/translated/table_filters.js:34
#: templates/js/translated/table_filters.js:112
#: templates/js/translated/table_filters.js:324
@@ -2679,67 +2693,67 @@ msgstr ""
msgid "Active"
msgstr ""
#: common/models.py:1943
#: common/models.py:1950
msgid "Is this webhook active"
msgstr ""
#: common/models.py:1957
#: common/models.py:1964
msgid "Token"
msgstr ""
#: common/models.py:1958
#: common/models.py:1965
msgid "Token for access"
msgstr ""
#: common/models.py:1965
#: common/models.py:1972
msgid "Secret"
msgstr ""
#: common/models.py:1966
#: common/models.py:1973
msgid "Shared secret for HMAC"
msgstr ""
#: common/models.py:2072
#: common/models.py:2079
msgid "Message ID"
msgstr ""
#: common/models.py:2073
#: common/models.py:2080
msgid "Unique identifier for this message"
msgstr ""
#: common/models.py:2081
#: common/models.py:2088
msgid "Host"
msgstr ""
#: common/models.py:2082
#: common/models.py:2089
msgid "Host from which this message was received"
msgstr ""
#: common/models.py:2089
#: common/models.py:2096
msgid "Header"
msgstr ""
#: common/models.py:2090
#: common/models.py:2097
msgid "Header of this message"
msgstr ""
#: common/models.py:2096
#: common/models.py:2103
msgid "Body"
msgstr ""
#: common/models.py:2097
#: common/models.py:2104
msgid "Body of this message"
msgstr ""
#: common/models.py:2106
#: common/models.py:2113
msgid "Endpoint on which this message was received"
msgstr ""
#: common/models.py:2111
#: common/models.py:2118
msgid "Worked on"
msgstr ""
#: common/models.py:2112
#: common/models.py:2119
msgid "Was the work on this message finished?"
msgstr ""
@@ -3228,7 +3242,7 @@ msgstr ""
#: part/templates/part/detail.html:84 part/templates/part/part_sidebar.html:37
#: templates/InvenTree/index.html:252 templates/InvenTree/search.html:200
#: templates/InvenTree/settings/sidebar.html:49
#: templates/js/translated/search.js:277 templates/navbar.html:50
#: templates/js/translated/search.js:293 templates/navbar.html:50
#: users/models.py:42
msgid "Purchase Orders"
msgstr ""
@@ -3251,7 +3265,7 @@ msgstr ""
#: part/templates/part/detail.html:107 part/templates/part/part_sidebar.html:41
#: templates/InvenTree/index.html:283 templates/InvenTree/search.html:220
#: templates/InvenTree/settings/sidebar.html:51
#: templates/js/translated/search.js:301 templates/navbar.html:61
#: templates/js/translated/search.js:317 templates/navbar.html:61
#: users/models.py:43
msgid "Sales Orders"
msgstr ""
@@ -3321,7 +3335,7 @@ msgstr ""
#: company/templates/company/manufacturer_part.html:136
#: company/templates/company/manufacturer_part.html:183
#: part/templates/part/detail.html:371 part/templates/part/detail.html:401
#: templates/js/translated/forms.js:458 templates/js/translated/helpers.js:36
#: templates/js/translated/forms.js:455 templates/js/translated/helpers.js:36
#: users/models.py:222
msgid "Delete"
msgstr ""
@@ -3570,7 +3584,7 @@ msgstr ""
msgid "New Customer"
msgstr ""
#: company/views.py:52 templates/js/translated/search.js:254
#: company/views.py:52 templates/js/translated/search.js:270
msgid "Companies"
msgstr ""
@@ -4379,47 +4393,47 @@ msgstr ""
msgid "Updated {part} unit-price to {price} and quantity to {qty}"
msgstr ""
#: part/api.py:514
#: part/api.py:516
msgid "Incoming Purchase Order"
msgstr ""
#: part/api.py:534
#: part/api.py:536
msgid "Outgoing Sales Order"
msgstr ""
#: part/api.py:552
#: part/api.py:554
msgid "Stock produced by Build Order"
msgstr ""
#: part/api.py:638
#: part/api.py:640
msgid "Stock required for Build Order"
msgstr ""
#: part/api.py:775
#: part/api.py:777
msgid "Valid"
msgstr ""
#: part/api.py:776
#: part/api.py:778
msgid "Validate entire Bill of Materials"
msgstr ""
#: part/api.py:782
#: part/api.py:784
msgid "This option must be selected"
msgstr ""
#: part/api.py:1205
#: part/api.py:1207
msgid "Must be greater than zero"
msgstr ""
#: part/api.py:1209
#: part/api.py:1211
msgid "Must be a valid quantity"
msgstr ""
#: part/api.py:1224
#: part/api.py:1226
msgid "Specify location for initial part stock"
msgstr ""
#: part/api.py:1255 part/api.py:1259 part/api.py:1274 part/api.py:1278
#: part/api.py:1257 part/api.py:1261 part/api.py:1276 part/api.py:1280
msgid "This field is required"
msgstr ""
@@ -5755,23 +5769,23 @@ msgstr ""
msgid "No matching action found"
msgstr ""
#: plugin/base/barcodes/api.py:52 plugin/base/barcodes/api.py:110
msgid "Must provide barcode_data parameter"
#: plugin/base/barcodes/api.py:54 plugin/base/barcodes/api.py:113
msgid "Missing barcode data"
msgstr ""
#: plugin/base/barcodes/api.py:82
#: plugin/base/barcodes/api.py:83
msgid "No match found for barcode data"
msgstr ""
#: plugin/base/barcodes/api.py:86
#: plugin/base/barcodes/api.py:87
msgid "Match found for barcode data"
msgstr ""
#: plugin/base/barcodes/api.py:125
#: plugin/base/barcodes/api.py:126
msgid "Barcode matches existing item"
msgstr ""
#: plugin/base/barcodes/api.py:222
#: plugin/base/barcodes/api.py:223
msgid "No match found for provided value"
msgstr ""
@@ -7603,7 +7617,7 @@ msgstr ""
msgid "Please confirm that <a href=\"mailto:%(email)s\">%(email)s</a> is an email address for user %(user_display)s."
msgstr ""
#: templates/account/email_confirm.html:22 templates/js/translated/forms.js:649
#: templates/account/email_confirm.html:22 templates/js/translated/forms.js:646
msgid "Confirm"
msgstr ""
@@ -8653,61 +8667,61 @@ msgstr ""
msgid "Create filter"
msgstr ""
#: templates/js/translated/forms.js:372 templates/js/translated/forms.js:387
#: templates/js/translated/forms.js:401 templates/js/translated/forms.js:415
#: templates/js/translated/forms.js:369 templates/js/translated/forms.js:384
#: templates/js/translated/forms.js:398 templates/js/translated/forms.js:412
msgid "Action Prohibited"
msgstr ""
#: templates/js/translated/forms.js:374
#: templates/js/translated/forms.js:371
msgid "Create operation not allowed"
msgstr ""
#: templates/js/translated/forms.js:389
#: templates/js/translated/forms.js:386
msgid "Update operation not allowed"
msgstr ""
#: templates/js/translated/forms.js:403
#: templates/js/translated/forms.js:400
msgid "Delete operation not allowed"
msgstr ""
#: templates/js/translated/forms.js:417
#: templates/js/translated/forms.js:414
msgid "View operation not allowed"
msgstr ""
#: templates/js/translated/forms.js:675
#: templates/js/translated/forms.js:672
msgid "Keep this form open"
msgstr ""
#: templates/js/translated/forms.js:776
#: templates/js/translated/forms.js:773
msgid "Enter a valid number"
msgstr ""
#: templates/js/translated/forms.js:1269 templates/modals.html:19
#: templates/js/translated/forms.js:1266 templates/modals.html:19
#: templates/modals.html:43
msgid "Form errors exist"
msgstr ""
#: templates/js/translated/forms.js:1706
#: templates/js/translated/forms.js:1703
msgid "No results found"
msgstr ""
#: templates/js/translated/forms.js:1922 templates/search.html:29
#: templates/js/translated/forms.js:1919 templates/search.html:29
msgid "Searching"
msgstr ""
#: templates/js/translated/forms.js:2175
#: templates/js/translated/forms.js:2172
msgid "Clear input"
msgstr ""
#: templates/js/translated/forms.js:2641
#: templates/js/translated/forms.js:2638
msgid "File Column"
msgstr ""
#: templates/js/translated/forms.js:2641
#: templates/js/translated/forms.js:2638
msgid "Field Name"
msgstr ""
#: templates/js/translated/forms.js:2653
#: templates/js/translated/forms.js:2650
msgid "Select Columns"
msgstr ""
@@ -9737,11 +9751,11 @@ msgstr ""
msgid "Sales Order(s) must be selected before printing report"
msgstr ""
#: templates/js/translated/search.js:394
#: templates/js/translated/search.js:410
msgid "Minimize results"
msgstr ""
#: templates/js/translated/search.js:397
#: templates/js/translated/search.js:413
msgid "Remove results"
msgstr ""

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -48,7 +48,7 @@
<h4>{% trans "Extra Lines" %}</h4>
{% include "spacer.html" %}
<div class='btn-group' role='group'>
{% if roles.purchase_order.change and order.status == PurchaseOrderStatus.PENDING %}
{% if roles.purchase_order.change %}
<button type='button' class='btn btn-success' id='new-po-extra-line'>
<span class='fas fa-plus-circle'></span> {% trans "Add Extra Line" %}
</button>

View File

@@ -42,7 +42,7 @@
<h4>{% trans "Extra Lines" %}</h4>
{% include "spacer.html" %}
<div class='btn-group' role='group'>
{% if roles.sales_order.change and order.is_pending %}
{% if roles.sales_order.change %}
<button type='button' class='btn btn-success' id='new-so-extra-line'>
<span class='fas fa-plus-circle'></span> {% trans "Add Extra Line" %}
</button>

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.16 on 2022-10-30 23:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('part', '0087_bomitem_consumable'),
]
operations = [
migrations.AlterField(
model_name='partparametertemplate',
name='name',
field=models.CharField(help_text='Parameter Name', max_length=100, unique=True, verbose_name='Name'),
),
]

View File

@@ -2383,10 +2383,7 @@ class PartTestTemplate(models.Model):
def validate_template_name(name):
"""Prevent illegal characters in "name" field for PartParameterTemplate."""
for c in "\"\'`!?|": # noqa: P103
if c in str(name):
raise ValidationError(_(f"Illegal character in template name ({c})"))
"""Placeholder for legacy function used in migrations."""
class PartParameterTemplate(models.Model):
@@ -2431,10 +2428,7 @@ class PartParameterTemplate(models.Model):
max_length=100,
verbose_name=_('Name'),
help_text=_('Parameter Name'),
unique=True,
validators=[
validate_template_name,
]
unique=True
)
units = models.CharField(max_length=25, verbose_name=_('Units'), help_text=_('Parameter Units'), blank=True)

View File

@@ -63,11 +63,6 @@
</div>
{% endif %}
{% endif %}
{% if roles.part_category.add %}
<button class='btn btn-success' id='cat-create' title='{% trans "Create new part category" %}'>
<span class='fas fa-plus-circle'></span> {% trans "New Category" %}
</button>
{% endif %}
{% endblock %}
{% block details_left %}
@@ -225,7 +220,17 @@
<div class='panel panel-hidden' id='panel-subcategories'>
<div class='panel-heading'>
<h4>{% trans "Subcategories" %}</h4>
<div class='d-flex flex-wrap'>
<h4>{% trans "Subcategories" %}</h4>
{% include "spacer.html" %}
<div class='btn-group' role='group'>
{% if roles.part_category.add %}
<button class='btn btn-success' id='cat-create' title='{% trans "Create new part category" %}'>
<span class='fas fa-plus-circle'></span> {% trans "New Category" %}
</button>
{% endif %}
</div>
</div>
</div>
<div class='panel-content'>
<div id='subcategory-button-toolbar'>

View File

@@ -593,7 +593,7 @@ class PartAPITest(InvenTreeAPITestCase):
{
'convert_from': variant.pk,
},
expected_code=200
expected_code=200,
)
# There should be the same number of results for each request
@@ -1854,7 +1854,7 @@ class BomItemTest(InvenTreeAPITestCase):
data={
'validated': True,
},
expected_code=200
expected_code=200,
)
# Check that the expected response is returned

View File

@@ -52,7 +52,7 @@ class PluginConfigAdmin(admin.ModelAdmin):
"""Custom admin with restricted id fields."""
readonly_fields = ["key", "name", ]
list_display = ['name', 'key', '__str__', 'active', 'is_sample']
list_display = ['name', 'key', '__str__', 'active', 'is_builtin', 'is_sample']
list_filter = ['active']
actions = [plugin_activate, plugin_deactivate, ]
inlines = [PluginSettingInline, ]

View File

@@ -7,7 +7,6 @@ The main code for plugin special sauce is in the plugin registry in `InvenTree/p
import logging
from django.apps import AppConfig
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from maintenance_mode.core import set_maintenance_mode
@@ -26,34 +25,34 @@ class PluginAppConfig(AppConfig):
def ready(self):
"""The ready method is extended to initialize plugins."""
if settings.PLUGINS_ENABLED:
if not canAppAccessDatabase(allow_test=True, allow_plugins=True):
logger.info("Skipping plugin loading sequence") # pragma: no cover
else:
logger.info('Loading InvenTree plugins')
if not canAppAccessDatabase(allow_test=True, allow_plugins=True):
logger.info("Skipping plugin loading sequence") # pragma: no cover
else:
logger.info('Loading InvenTree plugins')
if not registry.is_loading:
# this is the first startup
try:
from common.models import InvenTreeSetting
if InvenTreeSetting.get_setting('PLUGIN_ON_STARTUP', create=False, cache=False):
# make sure all plugins are installed
registry.install_plugin_file()
except Exception: # pragma: no cover
pass
if not registry.is_loading:
# this is the first startup
try:
from common.models import InvenTreeSetting
if InvenTreeSetting.get_setting('PLUGIN_ON_STARTUP', create=False, cache=False):
# make sure all plugins are installed
registry.install_plugin_file()
except Exception: # pragma: no cover
pass
# get plugins and init them
registry.plugin_modules = registry.collect_plugins()
registry.load_plugins()
# get plugins and init them
registry.plugin_modules = registry.collect_plugins()
registry.load_plugins()
# drop out of maintenance
# makes sure we did not have an error in reloading and maintenance is still active
set_maintenance_mode(False)
# drop out of maintenance
# makes sure we did not have an error in reloading and maintenance is still active
set_maintenance_mode(False)
# check git version
registry.git_is_modern = check_git_version()
if not registry.git_is_modern: # pragma: no cover # simulating old git seems not worth it for coverage
log_error(_('Your enviroment has an outdated git version. This prevents InvenTree from loading plugin details.'), 'load')
# check git version
registry.git_is_modern = check_git_version()
if not registry.git_is_modern: # pragma: no cover # simulating old git seems not worth it for coverage
log_error(_('Your environment has an outdated git version. This prevents InvenTree from loading plugin details.'), 'load')
else:
logger.info("Plugins not enabled - skipping loading sequence") # pragma: no cover

View File

@@ -11,8 +11,8 @@ from rest_framework.views import APIView
from InvenTree.helpers import hash_barcode
from plugin import registry
from plugin.builtin.barcodes.inventree_barcode import (
InvenTreeExternalBarcodePlugin, InvenTreeInternalBarcodePlugin)
from plugin.builtin.barcodes.inventree_barcode import \
InvenTreeInternalBarcodePlugin
from users.models import RuleSet
@@ -53,11 +53,8 @@ class BarcodeScan(APIView):
if not barcode_data:
raise ValidationError({'barcode': _('Missing barcode data')})
# Ensure that the default barcode handlers are run first
plugins = [
InvenTreeInternalBarcodePlugin(),
InvenTreeExternalBarcodePlugin(),
] + registry.with_mixin('barcode')
# Note: the default barcode handlers are loaded (and thus run) first
plugins = registry.with_mixin('barcode')
barcode_hash = hash_barcode(barcode_data)
@@ -113,10 +110,7 @@ class BarcodeAssign(APIView):
raise ValidationError({'barcode': _('Missing barcode data')})
# Here we only check against 'InvenTree' plugins
plugins = [
InvenTreeInternalBarcodePlugin(),
InvenTreeExternalBarcodePlugin(),
]
plugins = registry.with_mixin('barcode', builtin=True)
# First check if the provided barcode matches an existing database entry
for plugin in plugins:
@@ -133,7 +127,7 @@ class BarcodeAssign(APIView):
valid_labels = []
for model in InvenTreeExternalBarcodePlugin.get_supported_barcode_models():
for model in InvenTreeInternalBarcodePlugin.get_supported_barcode_models():
label = model.barcode_model_type()
valid_labels.append(label)
@@ -188,7 +182,7 @@ class BarcodeUnassign(APIView):
"""Respond to a barcode unassign POST request"""
# The following database models support assignment of third-party barcodes
supported_models = InvenTreeExternalBarcodePlugin.get_supported_barcode_models()
supported_models = InvenTreeInternalBarcodePlugin.get_supported_barcode_models()
supported_labels = [model.barcode_model_type() for model in supported_models]
model_names = ', '.join(supported_labels)

View File

@@ -58,9 +58,8 @@ def register_event(event, *args, **kwargs):
if plugin.mixin_enabled('events'):
config = plugin.plugin_config()
if config and config.active:
if plugin.is_active():
# Only allow event registering for 'active' plugins
logger.debug(f"Registering callback for plugin '{slug}'")

View File

@@ -30,7 +30,7 @@ def print_label(plugin_slug: str, pdf_data, filename=None, label_instance=None,
"""
logger.info(f"Plugin '{plugin_slug}' is printing a label '{filename}'")
plugin = registry.plugins.get(plugin_slug, None)
plugin = registry.get_plugin(plugin_slug)
if plugin is None: # pragma: no cover
logger.error(f"Could not find matching plugin for '{plugin_slug}'")

View File

@@ -9,6 +9,8 @@ references model objects actually exist in the database.
import json
from django.utils.translation import gettext_lazy as _
from company.models import SupplierPart
from InvenTree.helpers import hash_barcode
from part.models import Part
@@ -17,8 +19,14 @@ from plugin.mixins import BarcodeMixin
from stock.models import StockItem, StockLocation
class InvenTreeBarcodePlugin(BarcodeMixin, InvenTreePlugin):
"""Generic base class for handling InvenTree barcodes"""
class InvenTreeInternalBarcodePlugin(BarcodeMixin, InvenTreePlugin):
"""Builtin BarcodePlugin for matching and generating internal barcodes."""
NAME = "InvenTreeBarcode"
TITLE = _("Inventree Barcodes")
DESCRIPTION = _("Provides native support for barcodes")
VERSION = "2.0.0"
AUTHOR = _("InvenTree contributors")
@staticmethod
def get_supported_barcode_models():
@@ -58,57 +66,42 @@ class InvenTreeBarcodePlugin(BarcodeMixin, InvenTreePlugin):
return response
class InvenTreeInternalBarcodePlugin(InvenTreeBarcodePlugin):
"""Builtin BarcodePlugin for matching and generating internal barcodes."""
NAME = "InvenTreeInternalBarcode"
def scan(self, barcode_data):
"""Scan a barcode against this plugin.
Here we are looking for a dict object which contains a reference to a particular InvenTree database object
"""
# Create hash from raw barcode data
barcode_hash = hash_barcode(barcode_data)
# Attempt to coerce the barcode data into a dict object
# This is the internal barcode representation that InvenTree uses
barcode_dict = None
if type(barcode_data) is dict:
pass
barcode_dict = barcode_data
elif type(barcode_data) is str:
try:
barcode_data = json.loads(barcode_data)
barcode_dict = json.loads(barcode_data)
except json.JSONDecodeError:
return None
else:
return None
pass
if type(barcode_data) is not dict:
return None
if barcode_dict is not None and type(barcode_dict) is dict:
# Look for various matches. First good match will be returned
for model in self.get_supported_barcode_models():
label = model.barcode_model_type()
# Look for various matches. First good match will be returned
if label in barcode_dict:
try:
instance = model.objects.get(pk=barcode_dict[label])
return self.format_matched_response(label, model, instance)
except (ValueError, model.DoesNotExist):
pass
# If no "direct" hits are found, look for assigned third-party barcodes
for model in self.get_supported_barcode_models():
label = model.barcode_model_type()
if label in barcode_data:
try:
instance = model.objects.get(pk=barcode_data[label])
return self.format_matched_response(label, model, instance)
except (ValueError, model.DoesNotExist):
pass
class InvenTreeExternalBarcodePlugin(InvenTreeBarcodePlugin):
"""Builtin BarcodePlugin for matching arbitrary external barcodes."""
NAME = "InvenTreeExternalBarcode"
def scan(self, barcode_data):
"""Scan a barcode against this plugin.
Here we are looking for a dict object which contains a reference to a particular InvenTree databse object
"""
for model in self.get_supported_barcode_models():
label = model.barcode_model_type()
barcode_hash = hash_barcode(barcode_data)
instance = model.lookup_barcode(barcode_hash)

View File

@@ -29,7 +29,7 @@ class TestInvenTreeBarcode(InvenTreeAPITestCase):
'barcode': barcode_data,
'stockitem': 521
},
expected_code=400
expected_code=400,
)
self.assertIn('error', response.data)
@@ -250,7 +250,7 @@ class TestInvenTreeBarcode(InvenTreeAPITestCase):
)
self.assertIn('success', response.data)
self.assertEqual(response.data['plugin'], 'InvenTreeExternalBarcode')
self.assertEqual(response.data['plugin'], 'InvenTreeBarcode')
self.assertEqual(response.data['part']['pk'], 1)
# Attempting to assign the same barcode to a different part should result in an error
@@ -347,7 +347,7 @@ class TestInvenTreeBarcode(InvenTreeAPITestCase):
response = self.scan({'barcode': 'blbla=10004'}, expected_code=200)
self.assertEqual(response.data['barcode_data'], 'blbla=10004')
self.assertEqual(response.data['plugin'], 'InvenTreeExternalBarcode')
self.assertEqual(response.data['plugin'], 'InvenTreeBarcode')
# Scan for a StockItem instance
si = stock.models.StockItem.objects.get(pk=1)
@@ -402,7 +402,7 @@ class TestInvenTreeBarcode(InvenTreeAPITestCase):
self.assertEqual(response.data['stocklocation']['pk'], 5)
self.assertEqual(response.data['stocklocation']['api_url'], '/api/stock/location/5/')
self.assertEqual(response.data['stocklocation']['web_url'], '/stock/location/5/')
self.assertEqual(response.data['plugin'], 'InvenTreeInternalBarcode')
self.assertEqual(response.data['plugin'], 'InvenTreeBarcode')
# Scan a Part object
response = self.scan(
@@ -423,7 +423,7 @@ class TestInvenTreeBarcode(InvenTreeAPITestCase):
)
self.assertEqual(response.data['supplierpart']['pk'], 1)
self.assertEqual(response.data['plugin'], 'InvenTreeInternalBarcode')
self.assertEqual(response.data['plugin'], 'InvenTreeBarcode')
self.assertIn('success', response.data)
self.assertIn('barcode_data', response.data)

View File

@@ -27,8 +27,10 @@ class CoreNotificationsPlugin(SettingsMixin, InvenTreePlugin):
"""Core notification methods for InvenTree."""
NAME = "CoreNotificationsPlugin"
TITLE = _("InvenTree Notifications")
AUTHOR = _('InvenTree contributors')
DESCRIPTION = _('Integrated outgoing notificaton methods')
VERSION = "1.0.0"
SETTINGS = {
'ENABLE_NOTIFICATION_EMAILS': {

View File

@@ -158,16 +158,20 @@ class PluginConfig(models.Model):
@admin.display(boolean=True, description=_('Sample plugin'))
def is_sample(self) -> bool:
"""Is this plugin a sample app?"""
# Loaded and active plugin
if isinstance(self.plugin, InvenTreePlugin):
return self.plugin.check_is_sample()
# If no plugin_class is available it can not be a sample
if not self.plugin:
return False
# Not loaded plugin
return self.plugin.check_is_sample() # pragma: no cover
return self.plugin.check_is_sample()
@admin.display(boolean=True, description=_('Builtin Plugin'))
def is_builtin(self) -> bool:
"""Return True if this is a 'builtin' plugin"""
if not self.plugin:
return False
return self.plugin.check_is_builtin()
class PluginSetting(common.models.BaseInvenTreeSetting):

View File

@@ -106,10 +106,15 @@ class MetaBase:
def is_active(self):
"""Return True if this plugin is currently active."""
cfg = self.plugin_config()
if cfg:
return cfg.active
# Builtin plugins are always considered "active"
if self.is_builtin:
return True
config = self.plugin_config()
if config:
return config.active
else:
return False # pragma: no cover
@@ -300,6 +305,16 @@ class InvenTreePlugin(VersionMixin, MixinBase, MetaBase):
"""Is this plugin part of the samples?"""
return self.check_is_sample()
@classmethod
def check_is_builtin(cls) -> bool:
"""Determine if a particular plugin class is a 'builtin' plugin"""
return str(cls.check_package_path()).startswith('plugin/builtin')
@property
def is_builtin(self) -> bool:
"""Is this plugin is builtin"""
return self.check_is_builtin()
@classmethod
def check_package_path(cls):
"""Path to the plugin."""

View File

@@ -108,9 +108,6 @@ class PluginsRegistry:
Args:
full_reload (bool, optional): Reload everything - including plugin mechanism. Defaults to False.
"""
if not settings.PLUGINS_ENABLED:
# Plugins not enabled, do nothing
return # pragma: no cover
logger.info('Start loading plugins')
@@ -167,9 +164,6 @@ class PluginsRegistry:
def unload_plugins(self):
"""Unload and deactivate all IntegrationPlugins."""
if not settings.PLUGINS_ENABLED:
# Plugins not enabled, do nothing
return # pragma: no cover
logger.info('Start unloading plugins')
@@ -187,6 +181,7 @@ class PluginsRegistry:
# remove maintenance
if not _maintenance:
set_maintenance_mode(False) # pragma: no cover
logger.info('Finished unloading plugins')
def reload_plugins(self, full_reload: bool = False):
@@ -210,62 +205,63 @@ class PluginsRegistry:
def plugin_dirs(self):
"""Construct a list of directories from where plugins can be loaded"""
# Builtin plugins are *always* loaded
dirs = ['plugin.builtin', ]
if settings.TESTING or settings.DEBUG:
# If in TEST or DEBUG mode, load plugins from the 'samples' directory
dirs.append('plugin.samples')
if settings.PLUGINS_ENABLED:
# Any 'external' plugins are only loaded if PLUGINS_ENABLED is set to True
if settings.TESTING:
custom_dirs = os.getenv('INVENTREE_PLUGIN_TEST_DIR', None)
else: # pragma: no cover
custom_dirs = get_setting('INVENTREE_PLUGIN_DIR', 'plugin_dir')
if settings.TESTING or settings.DEBUG:
# If in TEST or DEBUG mode, load plugins from the 'samples' directory
dirs.append('plugin.samples')
# Load from user specified directories (unless in testing mode)
dirs.append('plugins')
if settings.TESTING:
custom_dirs = os.getenv('INVENTREE_PLUGIN_TEST_DIR', None)
else: # pragma: no cover
custom_dirs = get_setting('INVENTREE_PLUGIN_DIR', 'plugin_dir')
if custom_dirs is not None:
# Allow multiple plugin directories to be specified
for pd_text in custom_dirs.split(','):
pd = Path(pd_text.strip()).absolute()
# Load from user specified directories (unless in testing mode)
dirs.append('plugins')
# Attempt to create the directory if it does not already exist
if not pd.exists():
try:
pd.mkdir(exist_ok=True)
except Exception: # pragma: no cover
logger.error(f"Could not create plugin directory '{pd}'")
continue
if custom_dirs is not None:
# Allow multiple plugin directories to be specified
for pd_text in custom_dirs.split(','):
pd = Path(pd_text.strip()).absolute()
# Ensure the directory has an __init__.py file
init_filename = pd.joinpath('__init__.py')
# Attempt to create the directory if it does not already exist
if not pd.exists():
try:
pd.mkdir(exist_ok=True)
except Exception: # pragma: no cover
logger.error(f"Could not create plugin directory '{pd}'")
continue
if not init_filename.exists():
try:
init_filename.write_text("# InvenTree plugin directory\n")
except Exception: # pragma: no cover
logger.error(f"Could not create file '{init_filename}'")
continue
# Ensure the directory has an __init__.py file
init_filename = pd.joinpath('__init__.py')
# By this point, we have confirmed that the directory at least exists
if pd.exists() and pd.is_dir():
# Convert to python dot-path
if pd.is_relative_to(settings.BASE_DIR):
pd_path = '.'.join(pd.relative_to(settings.BASE_DIR).parts)
else:
pd_path = str(pd)
if not init_filename.exists():
try:
init_filename.write_text("# InvenTree plugin directory\n")
except Exception: # pragma: no cover
logger.error(f"Could not create file '{init_filename}'")
continue
# Add path
dirs.append(pd_path)
logger.info(f"Added plugin directory: '{pd}' as '{pd_path}'")
# By this point, we have confirmed that the directory at least exists
if pd.exists() and pd.is_dir():
# Convert to python dot-path
if pd.is_relative_to(settings.BASE_DIR):
pd_path = '.'.join(pd.relative_to(settings.BASE_DIR).parts)
else:
pd_path = str(pd)
# Add path
dirs.append(pd_path)
logger.info(f"Added plugin directory: '{pd}' as '{pd_path}'")
return dirs
def collect_plugins(self):
"""Collect plugins from all possible ways of loading. Returned as list."""
if not settings.PLUGINS_ENABLED:
# Plugins not enabled, do nothing
return # pragma: no cover
collected_plugins = []
@@ -293,17 +289,20 @@ class PluginsRegistry:
if modules:
[collected_plugins.append(item) for item in modules]
# Check if not running in testing mode and apps should be loaded from hooks
if (not settings.PLUGIN_TESTING) or (settings.PLUGIN_TESTING and settings.PLUGIN_TESTING_SETUP):
# Collect plugins from setup entry points
for entry in get_entrypoints():
try:
plugin = entry.load()
plugin.is_package = True
plugin._get_package_metadata()
collected_plugins.append(plugin)
except Exception as error: # pragma: no cover
handle_error(error, do_raise=False, log_name='discovery')
# From this point any plugins are considered "external" and only loaded if plugins are explicitly enabled
if settings.PLUGINS_ENABLED:
# Check if not running in testing mode and apps should be loaded from hooks
if (not settings.PLUGIN_TESTING) or (settings.PLUGIN_TESTING and settings.PLUGIN_TESTING_SETUP):
# Collect plugins from setup entry points
for entry in get_entrypoints():
try:
plugin = entry.load()
plugin.is_package = True
plugin._get_package_metadata()
collected_plugins.append(plugin)
except Exception as error: # pragma: no cover
handle_error(error, do_raise=False, log_name='discovery')
# Log collected plugins
logger.info(f'Collected {len(collected_plugins)} plugins!')
@@ -335,7 +334,7 @@ class PluginsRegistry:
# endregion
# region registry functions
def with_mixin(self, mixin: str, active=None):
def with_mixin(self, mixin: str, active=None, builtin=None):
"""Returns reference to all plugins that have a specified mixin enabled."""
result = []
@@ -343,10 +342,13 @@ class PluginsRegistry:
if plugin.mixin_enabled(mixin):
if active is not None:
# Filter by 'enabled' status
config = plugin.plugin_config()
# Filter by 'active' status of plugin
if active != plugin.is_active():
continue
if config.active != active:
if builtin is not None:
# Filter by 'builtin' status of plugin
if builtin != plugin.is_builtin:
continue
result.append(plugin)
@@ -403,8 +405,14 @@ class PluginsRegistry:
# Append reference to plugin
plg.db = plg_db
# Always activate if testing
if settings.PLUGIN_TESTING or (plg_db and plg_db.active):
# Check if this is a 'builtin' plugin
builtin = plg.check_is_builtin()
# Determine if this plugin should be loaded:
# - If PLUGIN_TESTING is enabled
# - If this is a 'builtin' plugin
# - If this plugin has been explicitly enabled by the user
if settings.PLUGIN_TESTING or builtin or (plg_db and plg_db.active):
# Check if the plugin was blocked -> threw an error; option1: package, option2: file-based
if disabled and ((plg.__name__ == disabled) or (plg.__module__ == disabled)):
safe_reference(plugin=plg, key=plg_key, active=False)
@@ -498,10 +506,9 @@ class PluginsRegistry:
for _key, plugin in plugins:
if plugin.mixin_enabled('schedule'):
config = plugin.plugin_config()
# Only active tasks for plugins which are enabled
if config and config.active:
if plugin.is_active():
# Only active tasks for plugins which are enabled
plugin.register_tasks()
task_keys += plugin.get_task_names()

View File

@@ -1,7 +1,7 @@
"""Unit tests for action plugins."""
from InvenTree.helpers import InvenTreeTestCase
from plugin.builtin.action.simpleactionplugin import SimpleActionPlugin
from plugin.samples.integration.simpleactionplugin import SimpleActionPlugin
class SimpleActionPluginTests(InvenTreeTestCase):

View File

@@ -28,25 +28,38 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
url = reverse('api-plugin-install')
# valid - Pypi
data = self.post(url, {
'confirm': True,
'packagename': self.PKG_NAME
}, expected_code=201).data
data = self.post(
url,
{
'confirm': True,
'packagename': self.PKG_NAME
},
expected_code=201,
).data
self.assertEqual(data['success'], True)
# valid - github url
data = self.post(url, {
'confirm': True,
'url': self.PKG_URL
}, expected_code=201).data
data = self.post(
url,
{
'confirm': True,
'url': self.PKG_URL
},
expected_code=201,
).data
self.assertEqual(data['success'], True)
# valid - github url and packagename
data = self.post(url, {
'confirm': True,
'url': self.PKG_URL,
'packagename': 'minimal',
}, expected_code=201).data
data = self.post(
url,
{
'confirm': True,
'url': self.PKG_URL,
'packagename': 'minimal',
},
expected_code=201,
).data
self.assertEqual(data['success'], True)
# invalid tries
@@ -57,17 +70,20 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
data = self.post(url, {
'confirm': True,
}, expected_code=400).data
self.assertEqual(data['url'][0].title().upper(), self.MSG_NO_PKG.upper())
self.assertEqual(data['packagename'][0].title().upper(), self.MSG_NO_PKG.upper())
# not confirmed
self.post(url, {
'packagename': self.PKG_NAME
}, expected_code=400).data
}, expected_code=400)
data = self.post(url, {
'packagename': self.PKG_NAME,
'confirm': False,
}, expected_code=400).data
self.assertEqual(data['confirm'][0].title().upper(), 'Installation not confirmed'.upper())
def test_admin_action(self):

View File

@@ -305,6 +305,7 @@ class TestReportTest(ReportTest):
InvenTreeSetting.set_setting('REPORT_ATTACH_TEST_REPORT', True, None)
response = self.get(url, {'item': item.pk}, expected_code=200)
headers = response.headers
self.assertEqual(headers['Content-Type'], 'application/pdf')

View File

@@ -53,12 +53,21 @@
{% else %}
<li><a class='dropdown-item' href='#' id='barcode-link'><span class='fas fa-link'></span> {% trans "Link Barcode" %}</a></li>
{% endif %}
{% if labels_enabled %}
<li><a class='dropdown-item' href='#' id='print-label'><span class='fas fa-tag'></span> {% trans "Print Label" %}</a></li>
{% endif %}
<li><a class='dropdown-item' href='#' id='barcode-check-in'><span class='fas fa-arrow-right'></span> {% trans "Check-in Items" %}</a></li>
<li><a class='dropdown-item' href='#' id='barcode-scan-in-items' title='{% trans "Scan stock items into this location" %}'><span class='fas fa-boxes'></span> {% trans "Scan In Stock Items" %}</a></li>
<li><a class='dropdown-item' href='#' id='barcode-scan-in-containers' title='{% trans "Scan stock container into this location" %}'><span class='fas fa-sitemap'></span> {% trans "Scan In Container" %}</a></li>
</ul>
</div>
<!-- Printing action -->
{% if labels_enabled %}
<div class='btn-group' role='group'>
<button id='printing-options' title='{% trans "Printing actions" %}' class='btn btn-outline-secondary dropdown-toggle' type='button' data-bs-toggle='dropdown'>
<span class='fas fa-print'></span> <span class='caret'></span>
</button>
<ul class='dropdown-menu'>
<li><a class='dropdown-item' href='#' id='print-label'><span class='fas fa-print'></span> {% trans "Print Label" %}</a>
</ul>
</div>
{% endif %}
<!-- Check permissions and owner -->
{% if user_owns_location %}
{% if roles.stock.change %}
@@ -96,11 +105,6 @@
{% endif %}
{% endif %}
{% endif %}
{% if user_owns_location and roles.stock_location.add %}
<button class='btn btn-success' id='location-create' type='button' title='{% trans "Create new stock location" %}'>
<span class='fas fa-plus-circle'></span> {% trans "New Location" %}
</button>
{% endif %}
{% endblock %}
{% block details_left %}
@@ -203,7 +207,17 @@
<div class='panel panel-hidden' id='panel-sublocations'>
<div class='panel-heading'>
<h4>{% trans "Sublocations" %}</h4>
<div class='d-flex flex-wrap'>
<h4>{% trans "Sublocations" %}</h4>
{% include "spacer.html" %}
<div class='btn-group' role='group'>
{% if user_owns_location and roles.stock_location.add %}
<button class='btn btn-success' id='location-create' type='button' title='{% trans "Create new stock location" %}'>
<span class='fas fa-plus-circle'></span> {% trans "New Location" %}
</button>
{% endif %}
</div>
</div>
</div>
<div class='panel-content'>
<div id='sublocation-button-toolbar'>
@@ -284,13 +298,29 @@
{% endif %}
{% if location %}
$("#barcode-check-in").click(function() {
barcodeCheckIn({{ location.id }});
$("#barcode-scan-in-items").click(function() {
barcodeCheckInStockItems({{ location.id }});
});
$('#barcode-scan-in-containers').click(function() {
barcodeCheckInStockLocations({{ location.id }},
{
onSuccess: function() {
showMessage(
'{% trans "Scanned stock container into this location" %}',
{
style: 'success',
}
);
$('#sublocation-table').bootstrapTable('refresh');
}
}
);
});
{% endif %}
$('#location-create').click(function () {
createStockLocation({
{% if location %}
parent: {{ location.pk }},

View File

@@ -13,6 +13,7 @@
<table class='table table-striped table-condensed'>
<tbody>
{% include "InvenTree/settings/setting.html" with key="BARCODE_ENABLE" icon="fa-qrcode" %}
{% include "InvenTree/settings/setting.html" with key="BARCODE_INPUT_DELAY" icon="fa-hourglass-half" %}
{% include "InvenTree/settings/setting.html" with key="BARCODE_WEBCAM_SUPPORT" icon="fa-video" %}
</tbody>
</table>

View File

@@ -31,6 +31,8 @@
</table>
</div>
{% plugins_enabled as plug %}
<div class='panel-heading'>
<div class='d-flex flex-wrap'>
<h4>{% trans "Plugins" %}</h4>
@@ -38,78 +40,46 @@
<div class='btn-group' role='group'>
{% url 'admin:plugin_pluginconfig_changelist' as url %}
{% include "admin_button.html" with url=url %}
{% if plug %}
<button class="btn btn-success" id="install-plugin" title="{% trans 'Install Plugin' %}"><span class='fas fa-plus-circle'></span> {% trans "Install Plugin" %}</button>
{% endif %}
</div>
</div>
</div>
{% if not plug %}
<div class='alert alert-warning alert-block'>
{% trans "External plugins are not enabled for this InvenTree installation" %}<br>
</div>
{% endif %}
<div class='table-responsive'>
<table class='table table-striped table-condensed'>
<thead>
<tr>
<th>{% trans "Admin" %}</th>
<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 %}
{% mixin_enabled plugin 'urls' as urls %}
{% mixin_enabled plugin 'settings' as settings %}
<tr>
<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>
<td>{{ plugin.human_name }}<span class="text-muted"> - {{plugin_key}}</span>
{% define plugin.registered_mixins as mixin_list %}
{% 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 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.website %}
<a href="{{ plugin.website }}"><span class="fas fa-globe"></span></a>
{% endif %}
</td>
<td>{{ plugin.author }}</td>
<td>{% render_date plugin.pub_date %}</td>
<td>{% if plugin.version %}{{ plugin.version }}{% endif %}</td>
</tr>
{% 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="5"></td></tr>
<tr><td colspan="5"><h6>{% trans 'Inactive plugins' %}</h6></td></tr>
<tr><td colspan="6"><h6>{% trans 'Inactive plugins' %}</h6></td></tr>
{% for plugin_key, plugin in in_pl_list.items %}
<tr>
<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>
<td>{{plugin.name}}<span class="text-muted"> - {{plugin.key}}</span></td>
<td colspan="3"></td>
</tr>
{% include "InvenTree/settings/plugin_details.html" with plugin=plugin plugin_key=plugin_key %}
{% endfor %}
{% endif %}
</tbody>

View File

@@ -0,0 +1,75 @@
{% 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 "Unvailable" 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>

View File

@@ -23,16 +23,16 @@
<td>{% trans "Name" %}</td>
<td>{{ plugin.human_name }}{% include "clip.html" %}</td>
</tr>
<tr>
<td><span class='fas fa-user'></span></span></td>
<td>{% trans "Author" %}</td>
<td>{{ plugin.author }}{% include "clip.html" %}</td>
</tr>
<tr>
<td></td>
<td>{% trans "Description" %}</td>
<td>{{ plugin.description }}{% include "clip.html" %}</td>
</tr>
<tr>
<td><span class='fas fa-user'></span></span></td>
<td>{% trans "Author" %}</td>
<td>{{ plugin.author }}{% include "clip.html" %}</td>
</tr>
<tr>
<td><span class='fas fa-calendar-alt'></span></td>
<td>{% trans "Date" %}</td>
@@ -94,7 +94,14 @@
<td>{% trans "Installation path" %}</td>
<td>{{ plugin.package_path }}</td>
</tr>
{% if plugin.is_package == False %}
{% if plugin.is_package %}
{% elif plugin.is_builtin %}
<tr>
<td><span class='fas fa-check-circle icon-green'></span></td>
<td>{% trans "Builtin" %}</td>
<td>{% trans "This is a builtin plugin which cannot be disabled" %}</td>
</tr>
{% else %}
<tr>
<td><span class='fas fa-user'></span></td>
<td>{% trans "Commit Author" %}</td><td>{{ plugin.package.author }} - {{ plugin.package.mail }}{% include "clip.html" %}</td>

View File

@@ -42,8 +42,6 @@
{% include "InvenTree/settings/po.html" %}
{% include "InvenTree/settings/so.html" %}
{% plugins_enabled as plug %}
{% if plug %}
{% include "InvenTree/settings/plugin.html" %}
{% plugin_list as pl_list %}
{% for plugin_key, plugin in pl_list.items %}
@@ -51,7 +49,6 @@
{% include "InvenTree/settings/plugin_settings.html" %}
{% endif %}
{% endfor %}
{% endif %}
{% endif %}

View File

@@ -51,10 +51,11 @@
{% trans "Sales Orders" as text %}
{% include "sidebar_item.html" with label='sales-order' text=text icon="fa-truck" %}
{% plugins_enabled as plug %}
{% if plug %}
{% include "sidebar_header.html" with text="Plugin Settings" %}
{% include "sidebar_item.html" with label='plugin' text="Plugins" icon="fa-plug" %}
{% trans "Plugin Settings" as text %}
{% include "sidebar_header.html" with text=text %}
{% trans "Plugins" as text %}
{% include "sidebar_item.html" with label='plugin' text=text icon="fa-plug" %}
{% plugin_list as pl_list %}
{% for plugin_key, plugin in pl_list.items %}
@@ -62,6 +63,5 @@
{% include "sidebar_item.html" with label='plugin-'|add:plugin_key text=plugin.human_name %}
{% endif %}
{% endfor %}
{% endif %}
{% endif %}

View File

@@ -1,4 +1,12 @@
{% load inventree_extras %}
{% load i18n %}
<button id='admin-button' title='{% trans "View in administration panel" %}' type='button' class='btn btn-primary admin-button' url='{{ url }}'>
<span class='fas fa-user-shield'></span>
</button>
{% inventree_customize 'hide_admin_link' as hidden %}
{% if not hidden and user.is_staff %}
<a href='{{ url }}'>
<button id='admin-button' href='{{ url }}' title='{% trans "View in administration panel" %}' type='button' class='btn btn-primary admin-button'>
<span class='fas fa-user-shield'></span>
</button>
</a>
{% endif %}

View File

@@ -14,7 +14,8 @@
*/
/* exported
barcodeCheckIn,
barcodeCheckInStockItems,
barcodeCheckInStockLocations,
barcodeScanDialog,
linkBarcodeDialog,
scanItemsIntoLocation,
@@ -22,12 +23,14 @@
onBarcodeScanClicked,
*/
function makeBarcodeInput(placeholderText='', hintText='') {
/*
* Generate HTML for a barcode input
*/
var barcodeInputTimer = null;
placeholderText = placeholderText || '{% trans "Scan barcode data here using wedge scanner" %}';
/*
* Generate HTML for a barcode scan input
*/
function makeBarcodeInput(placeholderText='', hintText='') {
placeholderText = placeholderText || '{% trans "Scan barcode data here using barcode scanner" %}';
hintText = hintText || '{% trans "Enter barcode data" %}';
@@ -43,7 +46,7 @@ function makeBarcodeInput(placeholderText='', hintText='') {
<span class='fas fa-qrcode'></span>
</span>
<input id='barcode' class='textinput textInput form-control' type='text' name='barcode' placeholder='${placeholderText}'>
<button id='barcode_scan_btn' type='button' class='btn btn-secondary' onclick='onBarcodeScanClicked()' style='display: none;'>
<button title='{% trans "Scan barcode using connected webcam" %}' id='barcode_scan_btn' type='button' class='btn btn-secondary' onclick='onBarcodeScanClicked()' style='display: none;'>
<span class='fas fa-camera'></span>
</button>
</div>
@@ -92,6 +95,9 @@ function onBarcodeScanCompleted(result, options) {
postBarcodeData(result.data, options);
}
/*
* Construct a generic "notes" field for barcode scanning operations
*/
function makeNotesField(options={}) {
var tooltip = options.tooltip || '{% trans "Enter optional notes for stock transfer" %}';
@@ -199,6 +205,9 @@ function showBarcodeMessage(modal, message, style='danger') {
}
/*
* Display an error message when the server indicates an error
*/
function showInvalidResponseError(modal, response, status) {
showBarcodeMessage(
modal,
@@ -207,6 +216,9 @@ function showInvalidResponseError(modal, response, status) {
}
/*
* Enable (or disable) the barcode scanning input
*/
function enableBarcodeInput(modal, enabled=true) {
var barcode = $(modal + ' #barcode');
@@ -218,6 +230,10 @@ function enableBarcodeInput(modal, enabled=true) {
barcode.focus();
}
/*
* Extract scanned data from the barcode input
*/
function getBarcodeData(modal) {
modal = modal || '#modal-form';
@@ -233,10 +249,10 @@ function getBarcodeData(modal) {
}
/*
* Handle a barcode display dialog.
*/
function barcodeDialog(title, options={}) {
/*
* Handle a barcode display dialog.
*/
var modal = '#modal-form';
@@ -244,7 +260,6 @@ function barcodeDialog(title, options={}) {
var barcode = getBarcodeData(modal);
if (barcode && barcode.length > 0) {
postBarcodeData(barcode, options);
}
}
@@ -264,7 +279,15 @@ function barcodeDialog(title, options={}) {
event.preventDefault();
if (event.which == 10 || event.which == 13) {
clearTimeout(barcodeInputTimer);
sendBarcode();
} else {
// Start a timer to automatically send barcode after input is complete
clearTimeout(barcodeInputTimer);
barcodeInputTimer = setTimeout(function() {
sendBarcode();
}, global_settings.BARCODE_INPUT_DELAY);
}
});
@@ -305,9 +328,11 @@ function barcodeDialog(title, options={}) {
modalShowSubmitButton(modal, false);
}
var details = options.details || '{% trans "Scan barcode data" %}';
var content = '';
content += `<div class='alert alert-info alert-block'>{% trans "Scan barcode data below" %}</div>`;
content += `<div class='alert alert-info alert-block'>${details}</div>`;
content += `<div id='barcode-error-message'></div>`;
content += `<form class='js-modal-form' method='post'>`;
@@ -431,7 +456,7 @@ function unlinkBarcode(data, options={}) {
/*
* Display dialog to check multiple stock items in to a stock location.
*/
function barcodeCheckIn(location_id, options={}) {
function barcodeCheckInStockItems(location_id, options={}) {
var modal = '#modal-form';
@@ -486,6 +511,7 @@ function barcodeCheckIn(location_id, options={}) {
$(modal + ' #barcode').focus();
// Callback to remove the scanned item from the table
$(modal + ' .button-item-remove').unbind('click').on('mouseup', function() {
var pk = $(this).attr('pk');
@@ -514,8 +540,9 @@ function barcodeCheckIn(location_id, options={}) {
var extra = makeNotesField();
barcodeDialog(
'{% trans "Check Stock Items into Location" %}',
'{% trans "Scan Stock Items Into Location" %}',
{
details: '{% trans "Scan stock item barcode to check in to this location" %}',
headerContent: table,
preShow: function() {
modalSetSubmitText(modal, '{% trans "Check In" %}');
@@ -609,7 +636,7 @@ function barcodeCheckIn(location_id, options={}) {
);
} else {
// Barcode does not match a stock item
showBarcodeMessage(modal, '{% trans "Barcode does not match Stock Item" %}', 'warning');
showBarcodeMessage(modal, '{% trans "Barcode does not match valid stock item" %}', 'warning');
}
},
}
@@ -617,6 +644,59 @@ function barcodeCheckIn(location_id, options={}) {
}
/*
* Display dialog to scan stock locations into the current location
*/
function barcodeCheckInStockLocations(location_id, options={}) {
var modal = '#modal-form';
var header = '';
barcodeDialog(
'{% trans "Scan Stock Container Into Location" %}',
{
details: '{% trans "Scan stock container barcode to check in to this location" %}',
headerContent: header,
preShow: function() {
modalEnable(modal, false);
},
onShow: function() {
// TODO
},
onScan: function(response) {
if ('stocklocation' in response) {
var pk = response.stocklocation.pk;
var url = `/api/stock/location/${pk}/`;
// Move the scanned location into *this* location
inventreePut(
url,
{
parent: location_id,
},
{
method: 'PATCH',
success: function(response) {
$(modal).modal('hide');
handleFormSuccess(response, options);
},
error: function(xhr) {
$(modal).modal('hide');
showApiError(xhr, url);
},
}
);
} else {
// Barcode does not match a valid stock location
showBarcodeMessage(modal, '{% trans "Barcode does not match valid stock location" %}', 'warning');
}
}
}
);
}
/*
* Display dialog to check a single stock item into a stock location
*/

View File

@@ -2473,9 +2473,6 @@ function loadPurchaseOrderExtraLineTable(table, options={}) {
setupFilterList('purchaseorderextraline', $(table), filter_target);
// Is the order pending?
var pending = options.status == {{ SalesOrderStatus.PENDING }};
// Table columns to display
var columns = [
{
@@ -2555,26 +2552,24 @@ function loadPurchaseOrderExtraLineTable(table, options={}) {
title: '{% trans "Notes" %}',
});
if (pending) {
columns.push({
field: 'buttons',
switchable: false,
formatter: function(value, row, index, field) {
columns.push({
field: 'buttons',
switchable: false,
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'>`;
var pk = row.pk;
var pk = row.pk;
html += makeIconButton('fa-clone', 'button-duplicate', pk, '{% trans "Duplicate line" %}');
html += makeIconButton('fa-edit icon-blue', 'button-edit', pk, '{% trans "Edit line" %}');
html += makeIconButton('fa-trash-alt icon-red', 'button-delete', pk, '{% trans "Delete line" %}', );
html += makeIconButton('fa-clone', 'button-duplicate', pk, '{% trans "Duplicate line" %}');
html += makeIconButton('fa-edit icon-blue', 'button-edit', pk, '{% trans "Edit line" %}');
html += makeIconButton('fa-trash-alt icon-red', 'button-delete', pk, '{% trans "Delete line" %}', );
html += `</div>`;
html += `</div>`;
return html;
}
});
}
return html;
}
});
function reloadTable() {
$(table).bootstrapTable('refresh');
@@ -4320,9 +4315,6 @@ function loadSalesOrderExtraLineTable(table, options={}) {
setupFilterList('salesorderextraline', $(table), filter_target);
// Is the order pending?
var pending = options.status == {{ SalesOrderStatus.PENDING }};
// Table columns to display
var columns = [
{
@@ -4402,26 +4394,24 @@ function loadSalesOrderExtraLineTable(table, options={}) {
title: '{% trans "Notes" %}',
});
if (pending) {
columns.push({
field: 'buttons',
switchable: false,
formatter: function(value, row, index, field) {
columns.push({
field: 'buttons',
switchable: false,
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'>`;
var pk = row.pk;
var pk = row.pk;
html += makeIconButton('fa-clone', 'button-duplicate', pk, '{% trans "Duplicate line" %}');
html += makeIconButton('fa-edit icon-blue', 'button-edit', pk, '{% trans "Edit line" %}');
html += makeIconButton('fa-trash-alt icon-red', 'button-delete', pk, '{% trans "Delete line" %}', );
html += makeIconButton('fa-clone', 'button-duplicate', pk, '{% trans "Duplicate line" %}');
html += makeIconButton('fa-edit icon-blue', 'button-edit', pk, '{% trans "Edit line" %}');
html += makeIconButton('fa-trash-alt icon-red', 'button-delete', pk, '{% trans "Delete line" %}', );
html += `</div>`;
html += `</div>`;
return html;
}
});
}
return html;
}
});
function reloadTable() {
$(table).bootstrapTable('refresh');

View File

@@ -1878,15 +1878,16 @@ function loadPartCategoryTable(table, options) {
},
event: () => {
inventreeSave('category-tree-view', 0);
table.bootstrapTable(
'refreshOptions',
{
treeEnable: false,
serverSort: true,
search: true,
pagination: true,
}
);
// Adjust table options
options.treeEnable = false;
options.serverSort = false;
options.search = true;
options.pagination = true;
// Destroy and re-create the table
table.bootstrapTable('destroy');
loadPartCategoryTable(table, options);
}
},
{
@@ -1897,15 +1898,16 @@ function loadPartCategoryTable(table, options) {
},
event: () => {
inventreeSave('category-tree-view', 1);
table.bootstrapTable(
'refreshOptions',
{
treeEnable: true,
serverSort: false,
search: false,
pagination: false,
}
);
// Adjust table options
options.treeEnable = true;
options.serverSort = false;
options.search = false;
options.pagination = false;
// Destroy and re-create the table
table.bootstrapTable('destroy');
loadPartCategoryTable(table, options);
}
}
] : [],

View File

@@ -2351,15 +2351,16 @@ function loadStockLocationTable(table, options) {
},
event: () => {
inventreeSave('location-tree-view', 0);
table.bootstrapTable(
'refreshOptions',
{
treeEnable: false,
serverSort: true,
search: true,
pagination: true,
}
);
// Adjust table options
options.treeEnable = false;
options.serverSort = true;
options.search = true;
options.pagination = true;
// Destroy and re-create the table
table.bootstrapTable('destroy');
loadStockLocationTable(table, options);
}
},
{
@@ -2370,15 +2371,16 @@ function loadStockLocationTable(table, options) {
},
event: () => {
inventreeSave('location-tree-view', 1);
table.bootstrapTable(
'refreshOptions',
{
treeEnable: true,
serverSort: false,
search: false,
pagination: false,
}
);
// Adjust table options
options.treeEnable = true;
options.serverSort = false;
options.search = false;
options.pagination = false;
// Destroy and re-create the table
table.bootstrapTable('destroy');
loadStockLocationTable(table, options);
}
}
] : [],

View File

@@ -14,5 +14,5 @@ INVENTREE_DB_PORT=5432
INVENTREE_DB_USER=pguser
INVENTREE_DB_PASSWORD=pgpassword
# Enable plugins?
# Enable custom plugins?
INVENTREE_PLUGINS_ENABLED=True

View File

@@ -41,7 +41,7 @@ INVENTREE_DB_PORT=5432
#INVENTREE_CACHE_HOST=inventree-cache
#INVENTREE_CACHE_PORT=6379
# Enable plugins?
# Enable custom plugins?
INVENTREE_PLUGINS_ENABLED=False
# Image tag that should be used