From 1a22e8a617a624275cd04b1524fe3ac482062ef4 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 20 Sep 2024 01:42:08 +0000 Subject: [PATCH 01/44] Add model for recording barcode scan results --- .../common/migrations/0030_barcodescan.py | 35 +++++++++++ src/backend/InvenTree/common/models.py | 62 +++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 src/backend/InvenTree/common/migrations/0030_barcodescan.py diff --git a/src/backend/InvenTree/common/migrations/0030_barcodescan.py b/src/backend/InvenTree/common/migrations/0030_barcodescan.py new file mode 100644 index 000000000000..bcf506e169d8 --- /dev/null +++ b/src/backend/InvenTree/common/migrations/0030_barcodescan.py @@ -0,0 +1,35 @@ +# Generated by Django 4.2.15 on 2024-09-20 01:37 + +import InvenTree.models +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('common', '0029_inventreecustomuserstatemodel'), + ] + + operations = [ + migrations.CreateModel( + name='BarcodeScan', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('data', models.CharField(help_text='Barcode data', max_length=250, verbose_name='Data')), + ('timestamp', models.DateTimeField(auto_now_add=True, help_text='Date and time of the barcode scan', verbose_name='Timestamp')), + ('endpoint', models.CharField(blank=True, help_text='URL endpoint which processed the barcode', max_length=250, null=True, verbose_name='Path')), + ('plugin', models.CharField(blank=True, help_text='Plugin which processed the barcode', max_length=250, null=True, verbose_name='Plugin')), + ('status', models.IntegerField(blank=True, help_text='Response status code', null=True, verbose_name='Status')), + ('response', models.JSONField(blank=True, help_text='Response data from the barcode scan', null=True, verbose_name='Response')), + ('user', models.ForeignKey(blank=True, help_text='User who scanned the barcode', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='User')), + ], + options={ + 'abstract': False, + 'verbose_name': 'Barcode Scan', + }, + bases=(InvenTree.models.PluginValidationMixin, models.Model), + ), + ] diff --git a/src/backend/InvenTree/common/models.py b/src/backend/InvenTree/common/models.py index 03c6d8b0acae..f3874516d529 100644 --- a/src/backend/InvenTree/common/models.py +++ b/src/backend/InvenTree/common/models.py @@ -3445,3 +3445,65 @@ def clean(self) -> None: }) return super().clean() + + +class BarcodeScan(InvenTree.models.InvenTreeModel): + """Model for storing barcode scans results.""" + + class Meta: + """Model meta options.""" + + verbose_name = _('Barcode Scan') + + data = models.CharField( + max_length=250, + verbose_name=_('Data'), + help_text=_('Barcode data'), + blank=False, + null=False, + ) + + user = models.ForeignKey( + User, + on_delete=models.SET_NULL, + blank=True, + null=True, + verbose_name=_('User'), + help_text=_('User who scanned the barcode'), + ) + + timestamp = models.DateTimeField( + auto_now_add=True, + verbose_name=_('Timestamp'), + help_text=_('Date and time of the barcode scan'), + ) + + endpoint = models.CharField( + max_length=250, + verbose_name=_('Path'), + help_text=_('URL endpoint which processed the barcode'), + blank=True, + null=True, + ) + + plugin = models.CharField( + max_length=250, + verbose_name=_('Plugin'), + help_text=_('Plugin which processed the barcode'), + blank=True, + null=True, + ) + + status = models.IntegerField( + verbose_name=_('Status'), + help_text=_('Response status code'), + blank=True, + null=True, + ) + + response = models.JSONField( + verbose_name=_('Response'), + help_text=_('Response data from the barcode scan'), + blank=True, + null=True, + ) From 9411757213a17765847295b8efb9d8ffa0d4b1f3 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 20 Sep 2024 02:03:35 +0000 Subject: [PATCH 02/44] Add "admin" interface for new model --- src/backend/InvenTree/common/admin.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/backend/InvenTree/common/admin.py b/src/backend/InvenTree/common/admin.py index a0719f9ab463..05b919dc392d 100644 --- a/src/backend/InvenTree/common/admin.py +++ b/src/backend/InvenTree/common/admin.py @@ -35,6 +35,15 @@ def formfield_for_dbfield(self, db_field, request, **kwargs): search_fields = ('content_type', 'comment') +@admin.register(common.models.BarcodeScan) +class BarcodeScanAdmin(admin.ModelAdmin): + """Admin interface for BarcodeScan objects.""" + + list_display = ('data', 'timestamp', 'user', 'endpoint', 'plugin', 'status') + + list_filter = ('user', 'endpoint', 'plugin', 'status') + + @admin.register(common.models.ProjectCode) class ProjectCodeAdmin(ImportExportModelAdmin): """Admin settings for ProjectCode.""" From 3bc795419e15d8921681fa0de446289a42023169 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 20 Sep 2024 02:04:38 +0000 Subject: [PATCH 03/44] Add API endpoints for barcode scan history --- src/backend/InvenTree/InvenTree/mixins.py | 4 ++ .../InvenTree/plugin/base/barcodes/api.py | 45 ++++++++++++++++++- .../plugin/base/barcodes/serializers.py | 23 ++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) diff --git a/src/backend/InvenTree/InvenTree/mixins.py b/src/backend/InvenTree/InvenTree/mixins.py index b7233616e53a..0550986b09e5 100644 --- a/src/backend/InvenTree/InvenTree/mixins.py +++ b/src/backend/InvenTree/InvenTree/mixins.py @@ -180,5 +180,9 @@ class RetrieveUpdateDestroyAPI(CleanMixin, generics.RetrieveUpdateDestroyAPIView """View for retrieve, update and destroy API.""" +class RetrieveDestroyAPI(generics.RetrieveDestroyAPIView): + """View for retrieve and destroy API.""" + + class UpdateAPI(CleanMixin, generics.UpdateAPIView): """View for update API.""" diff --git a/src/backend/InvenTree/plugin/base/barcodes/api.py b/src/backend/InvenTree/plugin/base/barcodes/api.py index a497c79a62e9..65b4268f87fb 100644 --- a/src/backend/InvenTree/plugin/base/barcodes/api.py +++ b/src/backend/InvenTree/plugin/base/barcodes/api.py @@ -3,7 +3,7 @@ import logging from django.db.models import F -from django.urls import path +from django.urls import include, path from django.utils.translation import gettext_lazy as _ from drf_spectacular.utils import extend_schema, extend_schema_view @@ -12,10 +12,14 @@ from rest_framework.generics import CreateAPIView from rest_framework.response import Response +import common.models import order.models import plugin.base.barcodes.helper import stock.models +from InvenTree.filters import SEARCH_ORDER_FILTER from InvenTree.helpers import hash_barcode +from InvenTree.mixins import ListAPI, RetrieveDestroyAPI +from InvenTree.permissions import IsStaffOrReadOnly from plugin import registry from users.models import RuleSet @@ -608,7 +612,46 @@ def handle_barcode(self, barcode: str, request, **kwargs): raise ValidationError(response) +class BarcodeScanResultMixin: + """Mixin class for BarcodeScan API endpoints.""" + + queryset = common.models.BarcodeScan.objects.all() + serializer_class = barcode_serializers.BarcodeScanResultSerializer + + permission_classes = [permissions.IsAuthenticated, IsStaffOrReadOnly] + + +class BarcodeScanResultList(BarcodeScanResultMixin, ListAPI): + """List API endpoint for BarcodeScan objects.""" + + filter_backends = SEARCH_ORDER_FILTER + + ordering_fields = ['status', 'user', 'plugin', 'timestamp', 'endpoint'] + + ordering = '-timestamp' + + search_fields = ['plugin'] + + +class BarcodeScanResultDetail(BarcodeScanResultMixin, RetrieveDestroyAPI): + """Detail endpoint for a BarcodeScan object.""" + + barcode_api_urls = [ + # Barcode scan history + path( + 'history/', + include([ + path( + '/', + BarcodeScanResultDetail.as_view(), + name='api-barcode-scan-result-detail', + ), + path( + '', BarcodeScanResultList.as_view(), name='api-barcode-scan-result-list' + ), + ]), + ), # Generate a barcode for a database object path('generate/', BarcodeGenerate.as_view(), name='api-barcode-generate'), # Link a third-party barcode to an item (e.g. Part / StockItem / etc) diff --git a/src/backend/InvenTree/plugin/base/barcodes/serializers.py b/src/backend/InvenTree/plugin/base/barcodes/serializers.py index 99baebeff054..eb223139f9a5 100644 --- a/src/backend/InvenTree/plugin/base/barcodes/serializers.py +++ b/src/backend/InvenTree/plugin/base/barcodes/serializers.py @@ -5,12 +5,35 @@ from rest_framework import serializers +import common.models import order.models import plugin.base.barcodes.helper import stock.models from order.status_codes import PurchaseOrderStatus, SalesOrderStatus +class BarcodeScanResultSerializer(serializers.ModelSerializer): + """Serializer for barcode scan results.""" + + class Meta: + """Meta class for BarcodeScanResultSerializer.""" + + model = common.models.BarcodeScan + + fields = [ + 'id', + 'data', + 'timestamp', + 'endpoint', + 'plugin', + 'status', + 'response', + 'user', + ] + + read_only_fields = fields + + class BarcodeSerializer(serializers.Serializer): """Generic serializer for receiving barcode data.""" From a2513bb68fa733d16ebab71d3f6bafe898c8075e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 20 Sep 2024 02:13:18 +0000 Subject: [PATCH 04/44] Add global setting to control barcode result save --- src/backend/InvenTree/common/models.py | 6 ++++++ .../InvenTree/templates/InvenTree/settings/barcode.html | 1 + src/frontend/src/pages/Index/Settings/SystemSettings.tsx | 1 + 3 files changed, 8 insertions(+) diff --git a/src/backend/InvenTree/common/models.py b/src/backend/InvenTree/common/models.py index f3874516d529..f53bec253f3b 100644 --- a/src/backend/InvenTree/common/models.py +++ b/src/backend/InvenTree/common/models.py @@ -1398,6 +1398,12 @@ def save(self, *args, **kwargs): 'default': True, 'validator': bool, }, + 'BARCODE_STORE_RESULTS': { + 'name': _('Store Barcode Results'), + 'description': _('Store barcode scan results in the database'), + 'default': False, + 'validator': bool, + }, 'BARCODE_INPUT_DELAY': { 'name': _('Barcode Input Delay'), 'description': _('Barcode input processing delay time'), diff --git a/src/backend/InvenTree/templates/InvenTree/settings/barcode.html b/src/backend/InvenTree/templates/InvenTree/settings/barcode.html index 8da99c0a9a79..5aee711ba401 100644 --- a/src/backend/InvenTree/templates/InvenTree/settings/barcode.html +++ b/src/backend/InvenTree/templates/InvenTree/settings/barcode.html @@ -13,6 +13,7 @@ {% include "InvenTree/settings/setting.html" with key="BARCODE_ENABLE" icon="fa-qrcode" %} + {% include "InvenTree/settings/setting.html" with key="BARCODE_STORE_RESULTS" 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" %} {% include "InvenTree/settings/setting.html" with key="BARCODE_SHOW_TEXT" icon="fa-closed-captioning" %} diff --git a/src/frontend/src/pages/Index/Settings/SystemSettings.tsx b/src/frontend/src/pages/Index/Settings/SystemSettings.tsx index 22969847b7db..038b13eb86ce 100644 --- a/src/frontend/src/pages/Index/Settings/SystemSettings.tsx +++ b/src/frontend/src/pages/Index/Settings/SystemSettings.tsx @@ -96,6 +96,7 @@ export default function SystemSettings() { Date: Fri, 20 Sep 2024 03:00:59 +0000 Subject: [PATCH 05/44] Add frontend API endpoint --- src/frontend/src/components/items/QRCode.tsx | 2 +- src/frontend/src/enums/ApiEndpoints.tsx | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/frontend/src/components/items/QRCode.tsx b/src/frontend/src/components/items/QRCode.tsx index be8c573bb396..ade6ebac898c 100644 --- a/src/frontend/src/components/items/QRCode.tsx +++ b/src/frontend/src/components/items/QRCode.tsx @@ -75,7 +75,7 @@ export const InvenTreeQRCode = ({ const { data } = useQuery({ queryKey: ['qr-code', mdl_prop.model, mdl_prop.pk], queryFn: async () => { - const res = await api.post(apiUrl(ApiEndpoints.generate_barcode), { + const res = await api.post(apiUrl(ApiEndpoints.barcode_generate), { model: mdl_prop.model, pk: mdl_prop.pk }); diff --git a/src/frontend/src/enums/ApiEndpoints.tsx b/src/frontend/src/enums/ApiEndpoints.tsx index 1b574a65f4c7..e2460dbf1916 100644 --- a/src/frontend/src/enums/ApiEndpoints.tsx +++ b/src/frontend/src/enums/ApiEndpoints.tsx @@ -39,10 +39,6 @@ export enum ApiEndpoints { api_search = 'search/', settings_global_list = 'settings/global/', settings_user_list = 'settings/user/', - barcode = 'barcode/', - barcode_link = 'barcode/link/', - barcode_unlink = 'barcode/unlink/', - generate_barcode = 'barcode/generate/', news = 'news/', global_status = 'generic/status/', custom_state_list = 'generic/status/custom/', @@ -54,6 +50,13 @@ export enum ApiEndpoints { content_type_list = 'contenttype/', icons = 'icons/', + // Barcode API endpoints + barcode = 'barcode/', + barcode_history = 'barcode/history/', + barcode_link = 'barcode/link/', + barcode_unlink = 'barcode/unlink/', + barcode_generate = 'barcode/generate/', + // Data import endpoints import_session_list = 'importer/session/', import_session_accept_fields = 'importer/session/:id/accept_fields/', From ad6405a18bfe02515dde8457e306c0d81cd9e66c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 20 Sep 2024 03:10:48 +0000 Subject: [PATCH 06/44] Add PUI table in "admin center" --- .../InvenTree/plugin/base/barcodes/api.py | 10 ++- .../plugin/base/barcodes/serializers.py | 4 ++ .../Index/Settings/AdminCenter/Index.tsx | 11 ++++ .../settings/BarcodeScanHistoryTable.tsx | 65 +++++++++++++++++++ 4 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx diff --git a/src/backend/InvenTree/plugin/base/barcodes/api.py b/src/backend/InvenTree/plugin/base/barcodes/api.py index 65b4268f87fb..828cf5891540 100644 --- a/src/backend/InvenTree/plugin/base/barcodes/api.py +++ b/src/backend/InvenTree/plugin/base/barcodes/api.py @@ -617,9 +617,17 @@ class BarcodeScanResultMixin: queryset = common.models.BarcodeScan.objects.all() serializer_class = barcode_serializers.BarcodeScanResultSerializer - permission_classes = [permissions.IsAuthenticated, IsStaffOrReadOnly] + def get_queryset(self): + """Return the queryset for the BarcodeScan API.""" + queryset = super().get_queryset() + + # Pre-fetch user data + queryset = queryset.prefetch_related('user') + + return queryset + class BarcodeScanResultList(BarcodeScanResultMixin, ListAPI): """List API endpoint for BarcodeScan objects.""" diff --git a/src/backend/InvenTree/plugin/base/barcodes/serializers.py b/src/backend/InvenTree/plugin/base/barcodes/serializers.py index eb223139f9a5..b0785b0383bb 100644 --- a/src/backend/InvenTree/plugin/base/barcodes/serializers.py +++ b/src/backend/InvenTree/plugin/base/barcodes/serializers.py @@ -9,6 +9,7 @@ import order.models import plugin.base.barcodes.helper import stock.models +from InvenTree.serializers import UserSerializer from order.status_codes import PurchaseOrderStatus, SalesOrderStatus @@ -29,10 +30,13 @@ class Meta: 'status', 'response', 'user', + 'user_detail', ] read_only_fields = fields + user_detail = UserSerializer(source='user', read_only=True) + class BarcodeSerializer(serializers.Serializer): """Generic serializer for receiving barcode data.""" diff --git a/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx b/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx index a34e5e15ca55..3af0543bea49 100644 --- a/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx +++ b/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx @@ -18,6 +18,7 @@ import { IconListDetails, IconPackages, IconPlugConnected, + IconQrcode, IconReport, IconScale, IconSitemap, @@ -67,6 +68,10 @@ const ErrorReportTable = Loadable( lazy(() => import('../../../../tables/settings/ErrorTable')) ); +const BarcodeScanHistoryTable = Loadable( + lazy(() => import('../../../../tables/settings/BarcodeScanHistoryTable')) +); + const ImportSesssionTable = Loadable( lazy(() => import('../../../../tables/settings/ImportSessionTable')) ); @@ -108,6 +113,12 @@ export default function AdminCenter() { icon: , content: }, + { + name: 'barcode-history', + label: t`Barcode Scan History`, + icon: , + content: + }, { name: 'background', label: t`Background Tasks`, diff --git a/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx b/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx new file mode 100644 index 000000000000..18d82510cd30 --- /dev/null +++ b/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx @@ -0,0 +1,65 @@ +import { Badge, Group, Text } from '@mantine/core'; +import { useMemo } from 'react'; + +import { ApiEndpoints } from '../../enums/ApiEndpoints'; +import { useTable } from '../../hooks/UseTable'; +import { apiUrl } from '../../states/ApiState'; +import { useUserState } from '../../states/UserState'; +import { TableColumn } from '../Column'; +import { InvenTreeTable } from '../InvenTreeTable'; + +/* + * Display the barcode scan history table + */ +export default function BarcodeScanHistoryTable() { + const user = useUserState(); + const table = useTable('barcode-history'); + + const tableColumns: TableColumn[] = useMemo(() => { + return [ + { + accessor: 'timestamp', + sortable: true, + switchable: false, + render: (record: any) => { + return ( + + {record.timestamp} + {record.user_detail && ( + {record.user_detail.username} + )} + + ); + } + }, + { + accessor: 'data', + sortable: true, + switchable: false + }, + { + accessor: 'endpoint', + sortable: true + }, + { + accessor: 'plugin', + sortable: true + }, + { + accessor: 'status', + sortable: true + } + ]; + }, []); + + return ( + <> + + + ); +} From 48098773b99c003648266956100fae84e48d25cb Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 20 Sep 2024 03:13:35 +0000 Subject: [PATCH 07/44] Add API filter class --- src/backend/InvenTree/plugin/base/barcodes/api.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/backend/InvenTree/plugin/base/barcodes/api.py b/src/backend/InvenTree/plugin/base/barcodes/api.py index 828cf5891540..210edc0c82a4 100644 --- a/src/backend/InvenTree/plugin/base/barcodes/api.py +++ b/src/backend/InvenTree/plugin/base/barcodes/api.py @@ -6,6 +6,7 @@ from django.urls import include, path from django.utils.translation import gettext_lazy as _ +from django_filters import rest_framework as rest_filters from drf_spectacular.utils import extend_schema, extend_schema_view from rest_framework import permissions, status from rest_framework.exceptions import PermissionDenied, ValidationError @@ -629,9 +630,20 @@ def get_queryset(self): return queryset +class BarcodeScanResultFilter(rest_filters.FilterSet): + """Custom filterset for the BarcodeScanResult API.""" + + class Meta: + """Meta class for the BarcodeScanResultFilter.""" + + model = common.models.BarcodeScan + fields = ['user', 'plugin', 'status'] + + class BarcodeScanResultList(BarcodeScanResultMixin, ListAPI): """List API endpoint for BarcodeScan objects.""" + filterset_class = BarcodeScanResultFilter filter_backends = SEARCH_ORDER_FILTER ordering_fields = ['status', 'user', 'plugin', 'timestamp', 'endpoint'] From c56c077a74ebf12c523fedc2dceb50a7302f71cc Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 20 Sep 2024 03:16:09 +0000 Subject: [PATCH 08/44] Enable table filtering --- .../settings/BarcodeScanHistoryTable.tsx | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx b/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx index 18d82510cd30..ac0024d43795 100644 --- a/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx +++ b/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx @@ -1,11 +1,14 @@ +import { t } from '@lingui/macro'; import { Badge, Group, Text } from '@mantine/core'; import { useMemo } from 'react'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; +import { useUserFilters } from '../../hooks/UseFilter'; import { useTable } from '../../hooks/UseTable'; import { apiUrl } from '../../states/ApiState'; import { useUserState } from '../../states/UserState'; import { TableColumn } from '../Column'; +import { TableFilter } from '../Filter'; import { InvenTreeTable } from '../InvenTreeTable'; /* @@ -15,6 +18,8 @@ export default function BarcodeScanHistoryTable() { const user = useUserState(); const table = useTable('barcode-history'); + const userFilters = useUserFilters(); + const tableColumns: TableColumn[] = useMemo(() => { return [ { @@ -52,13 +57,26 @@ export default function BarcodeScanHistoryTable() { ]; }, []); + const filters: TableFilter[] = useMemo(() => { + return [ + { + name: 'user', + label: t`User`, + choices: userFilters.choices, + description: t`Filter by user` + } + ]; + }, [userFilters]); + return ( <> ); From 58d8b2892fc15a387e53939b2d7a62aeb3eb0280 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 20 Sep 2024 03:41:24 +0000 Subject: [PATCH 09/44] Update model definition --- src/backend/InvenTree/common/admin.py | 10 +++---- ...rcodescan.py => 0030_barcodescanresult.py} | 6 ++--- src/backend/InvenTree/common/models.py | 27 ++++++++++++------- .../InvenTree/plugin/base/barcodes/api.py | 7 ++--- .../plugin/base/barcodes/serializers.py | 3 +-- .../settings/BarcodeScanHistoryTable.tsx | 4 --- 6 files changed, 30 insertions(+), 27 deletions(-) rename src/backend/InvenTree/common/migrations/{0030_barcodescan.py => 0030_barcodescanresult.py} (85%) diff --git a/src/backend/InvenTree/common/admin.py b/src/backend/InvenTree/common/admin.py index 05b919dc392d..34c2c8651a11 100644 --- a/src/backend/InvenTree/common/admin.py +++ b/src/backend/InvenTree/common/admin.py @@ -35,13 +35,13 @@ def formfield_for_dbfield(self, db_field, request, **kwargs): search_fields = ('content_type', 'comment') -@admin.register(common.models.BarcodeScan) -class BarcodeScanAdmin(admin.ModelAdmin): - """Admin interface for BarcodeScan objects.""" +@admin.register(common.models.BarcodeScanResult) +class BarcodeScanResultAdmin(admin.ModelAdmin): + """Admin interface for BarcodeScanResult objects.""" - list_display = ('data', 'timestamp', 'user', 'endpoint', 'plugin', 'status') + list_display = ('data', 'timestamp', 'user', 'endpoint', 'status') - list_filter = ('user', 'endpoint', 'plugin', 'status') + list_filter = ('user', 'endpoint', 'status') @admin.register(common.models.ProjectCode) diff --git a/src/backend/InvenTree/common/migrations/0030_barcodescan.py b/src/backend/InvenTree/common/migrations/0030_barcodescanresult.py similarity index 85% rename from src/backend/InvenTree/common/migrations/0030_barcodescan.py rename to src/backend/InvenTree/common/migrations/0030_barcodescanresult.py index bcf506e169d8..55a1f9147e20 100644 --- a/src/backend/InvenTree/common/migrations/0030_barcodescan.py +++ b/src/backend/InvenTree/common/migrations/0030_barcodescanresult.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.15 on 2024-09-20 01:37 +# Generated by Django 4.2.15 on 2024-09-20 03:40 import InvenTree.models from django.conf import settings @@ -15,19 +15,17 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='BarcodeScan', + name='BarcodeScanResult', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('data', models.CharField(help_text='Barcode data', max_length=250, verbose_name='Data')), ('timestamp', models.DateTimeField(auto_now_add=True, help_text='Date and time of the barcode scan', verbose_name='Timestamp')), ('endpoint', models.CharField(blank=True, help_text='URL endpoint which processed the barcode', max_length=250, null=True, verbose_name='Path')), - ('plugin', models.CharField(blank=True, help_text='Plugin which processed the barcode', max_length=250, null=True, verbose_name='Plugin')), ('status', models.IntegerField(blank=True, help_text='Response status code', null=True, verbose_name='Status')), ('response', models.JSONField(blank=True, help_text='Response data from the barcode scan', null=True, verbose_name='Response')), ('user', models.ForeignKey(blank=True, help_text='User who scanned the barcode', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='User')), ], options={ - 'abstract': False, 'verbose_name': 'Barcode Scan', }, bases=(InvenTree.models.PluginValidationMixin, models.Model), diff --git a/src/backend/InvenTree/common/models.py b/src/backend/InvenTree/common/models.py index f53bec253f3b..684a9eb87ab4 100644 --- a/src/backend/InvenTree/common/models.py +++ b/src/backend/InvenTree/common/models.py @@ -3453,7 +3453,7 @@ def clean(self) -> None: return super().clean() -class BarcodeScan(InvenTree.models.InvenTreeModel): +class BarcodeScanResult(InvenTree.models.InvenTreeModel): """Model for storing barcode scans results.""" class Meta: @@ -3461,6 +3461,23 @@ class Meta: verbose_name = _('Barcode Scan') + @staticmethod + def log_scan_result(data, request, status, response): + """Log a barcode scan to the database.""" + # Exit if BARCODE_STORE_RESULTS is False + if not InvenTreeSetting.get_setting( + 'BARCODE_STORE_RESULTS', backup=False, create=False + ): + return + + # Extract information from the request + user = request.user if request.user and request.user.is_authenticated else None + endpoint = request.path + + BarcodeScanResult.objects.create( + data=data, user=user, endpoint=endpoint, status=status, response=response + ) + data = models.CharField( max_length=250, verbose_name=_('Data'), @@ -3492,14 +3509,6 @@ class Meta: null=True, ) - plugin = models.CharField( - max_length=250, - verbose_name=_('Plugin'), - help_text=_('Plugin which processed the barcode'), - blank=True, - null=True, - ) - status = models.IntegerField( verbose_name=_('Status'), help_text=_('Response status code'), diff --git a/src/backend/InvenTree/plugin/base/barcodes/api.py b/src/backend/InvenTree/plugin/base/barcodes/api.py index 210edc0c82a4..d7dc07142f8c 100644 --- a/src/backend/InvenTree/plugin/base/barcodes/api.py +++ b/src/backend/InvenTree/plugin/base/barcodes/api.py @@ -132,6 +132,7 @@ def handle_barcode(self, barcode: str, request, **kwargs): raise ValidationError(result) result['success'] = _('Match found for barcode data') + return Response(result) @@ -616,7 +617,7 @@ def handle_barcode(self, barcode: str, request, **kwargs): class BarcodeScanResultMixin: """Mixin class for BarcodeScan API endpoints.""" - queryset = common.models.BarcodeScan.objects.all() + queryset = common.models.BarcodeScanResult.objects.all() serializer_class = barcode_serializers.BarcodeScanResultSerializer permission_classes = [permissions.IsAuthenticated, IsStaffOrReadOnly] @@ -636,8 +637,8 @@ class BarcodeScanResultFilter(rest_filters.FilterSet): class Meta: """Meta class for the BarcodeScanResultFilter.""" - model = common.models.BarcodeScan - fields = ['user', 'plugin', 'status'] + model = common.models.BarcodeScanResult + fields = ['user', 'status'] class BarcodeScanResultList(BarcodeScanResultMixin, ListAPI): diff --git a/src/backend/InvenTree/plugin/base/barcodes/serializers.py b/src/backend/InvenTree/plugin/base/barcodes/serializers.py index b0785b0383bb..b5c3e6ab6afe 100644 --- a/src/backend/InvenTree/plugin/base/barcodes/serializers.py +++ b/src/backend/InvenTree/plugin/base/barcodes/serializers.py @@ -19,14 +19,13 @@ class BarcodeScanResultSerializer(serializers.ModelSerializer): class Meta: """Meta class for BarcodeScanResultSerializer.""" - model = common.models.BarcodeScan + model = common.models.BarcodeScanResult fields = [ 'id', 'data', 'timestamp', 'endpoint', - 'plugin', 'status', 'response', 'user', diff --git a/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx b/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx index ac0024d43795..b7a49e12614a 100644 --- a/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx +++ b/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx @@ -46,10 +46,6 @@ export default function BarcodeScanHistoryTable() { accessor: 'endpoint', sortable: true }, - { - accessor: 'plugin', - sortable: true - }, { accessor: 'status', sortable: true From d00160cacf5c287e71a7328a839c7ea4e3c915c6 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 20 Sep 2024 03:54:41 +0000 Subject: [PATCH 10/44] Allow more characters for barcode log --- .../migrations/0030_barcodescanresult.py | 4 +-- src/backend/InvenTree/common/models.py | 30 ++++++++++++++++--- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/backend/InvenTree/common/migrations/0030_barcodescanresult.py b/src/backend/InvenTree/common/migrations/0030_barcodescanresult.py index 55a1f9147e20..f025feb41430 100644 --- a/src/backend/InvenTree/common/migrations/0030_barcodescanresult.py +++ b/src/backend/InvenTree/common/migrations/0030_barcodescanresult.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.15 on 2024-09-20 03:40 +# Generated by Django 4.2.15 on 2024-09-20 03:53 import InvenTree.models from django.conf import settings @@ -22,7 +22,7 @@ class Migration(migrations.Migration): ('timestamp', models.DateTimeField(auto_now_add=True, help_text='Date and time of the barcode scan', verbose_name='Timestamp')), ('endpoint', models.CharField(blank=True, help_text='URL endpoint which processed the barcode', max_length=250, null=True, verbose_name='Path')), ('status', models.IntegerField(blank=True, help_text='Response status code', null=True, verbose_name='Status')), - ('response', models.JSONField(blank=True, help_text='Response data from the barcode scan', null=True, verbose_name='Response')), + ('response', models.JSONField(blank=True, help_text='Response data from the barcode scan', max_length=1000, null=True, verbose_name='Response')), ('user', models.ForeignKey(blank=True, help_text='User who scanned the barcode', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='User')), ], options={ diff --git a/src/backend/InvenTree/common/models.py b/src/backend/InvenTree/common/models.py index 684a9eb87ab4..434da231d502 100644 --- a/src/backend/InvenTree/common/models.py +++ b/src/backend/InvenTree/common/models.py @@ -43,6 +43,7 @@ import build.validators import common.currency import common.validators +import InvenTree.exceptions import InvenTree.fields import InvenTree.helpers import InvenTree.models @@ -3456,6 +3457,8 @@ def clean(self) -> None: class BarcodeScanResult(InvenTree.models.InvenTreeModel): """Model for storing barcode scans results.""" + BARCODE_SCAN_MAX_LEN = 250 + class Meta: """Model meta options.""" @@ -3474,12 +3477,30 @@ def log_scan_result(data, request, status, response): user = request.user if request.user and request.user.is_authenticated else None endpoint = request.path - BarcodeScanResult.objects.create( - data=data, user=user, endpoint=endpoint, status=status, response=response - ) + # Ensure that the response data is stringified first, otherwise cannot be JSON encoded + if type(response) is dict: + for key, value in response.items(): + if value is not None: + response[key] = str(value) + + # Ensure data is not too long + if len(data) > BarcodeScanResult.BARCODE_SCAN_MAX_LEN: + data = data[: BarcodeScanResult.BARCODE_SCAN_MAX_LEN] + + try: + BarcodeScanResult.objects.create( + data=data, + user=user, + endpoint=endpoint, + status=status, + response=response, + ) + except Exception: + # Gracefully log error to database + InvenTree.exceptions.log_error('barcode.log_scan_result') data = models.CharField( - max_length=250, + max_length=BARCODE_SCAN_MAX_LEN, verbose_name=_('Data'), help_text=_('Barcode data'), blank=False, @@ -3517,6 +3538,7 @@ def log_scan_result(data, request, status, response): ) response = models.JSONField( + max_length=1000, verbose_name=_('Response'), help_text=_('Response data from the barcode scan'), blank=True, From e6330bfc492bbdf1ec593b3c9df4d19d7fa3c240 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 20 Sep 2024 04:09:39 +0000 Subject: [PATCH 11/44] Log results to server --- src/backend/InvenTree/plugin/base/barcodes/api.py | 10 ++++++++++ .../src/tables/settings/BarcodeScanHistoryTable.tsx | 11 +++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/backend/InvenTree/plugin/base/barcodes/api.py b/src/backend/InvenTree/plugin/base/barcodes/api.py index d7dc07142f8c..29f14ce1aa66 100644 --- a/src/backend/InvenTree/plugin/base/barcodes/api.py +++ b/src/backend/InvenTree/plugin/base/barcodes/api.py @@ -129,10 +129,20 @@ def handle_barcode(self, barcode: str, request, **kwargs): if result['plugin'] is None: result['error'] = _('No match found for barcode data') + # Log the scan result + common.models.BarcodeScanResult.log_scan_result( + data=barcode, request=request, status=400, response=result + ) + raise ValidationError(result) result['success'] = _('Match found for barcode data') + # Log the scan result + common.models.BarcodeScanResult.log_scan_result( + data=barcode, request=request, status=200, response=result + ) + return Response(result) diff --git a/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx b/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx index b7a49e12614a..a5bcd24120e0 100644 --- a/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx +++ b/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx @@ -28,7 +28,7 @@ export default function BarcodeScanHistoryTable() { switchable: false, render: (record: any) => { return ( - + {record.timestamp} {record.user_detail && ( {record.user_detail.username} @@ -40,7 +40,14 @@ export default function BarcodeScanHistoryTable() { { accessor: 'data', sortable: true, - switchable: false + switchable: true, + render: (record: any) => { + return ( + + {record.data} + + ); + } }, { accessor: 'endpoint', From 5494fff41c34d5866befbeac4891fe6d9b70cbdb Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 20 Sep 2024 04:18:32 +0000 Subject: [PATCH 12/44] Add setting to control how long results are stored --- docs/docs/settings/global.md | 2 ++ src/backend/InvenTree/common/models.py | 7 +++++++ src/frontend/src/pages/Index/Settings/SystemSettings.tsx | 5 +++-- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/docs/settings/global.md b/docs/docs/settings/global.md index e59e535e41cb..de3c6f22d69c 100644 --- a/docs/docs/settings/global.md +++ b/docs/docs/settings/global.md @@ -90,6 +90,8 @@ Configuration of barcode functionality: {{ globalsetting("BARCODE_WEBCAM_SUPPORT") }} {{ globalsetting("BARCODE_SHOW_TEXT") }} {{ globalsetting("BARCODE_GENERATION_PLUGIN") }} +{{ globalsetting("BARCODE_STORE_RESULTS") }} +{{ globalsetting("BARCODE_RESULTS_MAX_AGE") }} ### Pricing and Currency diff --git a/src/backend/InvenTree/common/models.py b/src/backend/InvenTree/common/models.py index 434da231d502..e722a1c9fddb 100644 --- a/src/backend/InvenTree/common/models.py +++ b/src/backend/InvenTree/common/models.py @@ -1405,6 +1405,13 @@ def save(self, *args, **kwargs): 'default': False, 'validator': bool, }, + 'BARCODE_RESULTS_MAX_AGE': { + 'name': _('Barcode Results Max Age'), + 'description': _('Maximum age of barcode scan results to store'), + 'default': 30, + 'units': _('days'), + 'validator': [int, MinValueValidator(1)], + }, 'BARCODE_INPUT_DELAY': { 'name': _('Barcode Input Delay'), 'description': _('Barcode input processing delay time'), diff --git a/src/frontend/src/pages/Index/Settings/SystemSettings.tsx b/src/frontend/src/pages/Index/Settings/SystemSettings.tsx index 038b13eb86ce..dbd414003ac9 100644 --- a/src/frontend/src/pages/Index/Settings/SystemSettings.tsx +++ b/src/frontend/src/pages/Index/Settings/SystemSettings.tsx @@ -96,11 +96,12 @@ export default function SystemSettings() { ) From bdeef1fb5e434d95070974e959bae48465a1efd0 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 20 Sep 2024 04:31:06 +0000 Subject: [PATCH 13/44] Table updates --- .../InvenTree/plugin/base/barcodes/api.py | 3 +- .../plugin/base/barcodes/serializers.py | 2 +- src/frontend/src/tables/InvenTreeTable.tsx | 13 ++++--- .../settings/BarcodeScanHistoryTable.tsx | 38 ++++++++++++++++++- 4 files changed, 46 insertions(+), 10 deletions(-) diff --git a/src/backend/InvenTree/plugin/base/barcodes/api.py b/src/backend/InvenTree/plugin/base/barcodes/api.py index 29f14ce1aa66..88f86f23656d 100644 --- a/src/backend/InvenTree/plugin/base/barcodes/api.py +++ b/src/backend/InvenTree/plugin/base/barcodes/api.py @@ -17,6 +17,7 @@ import order.models import plugin.base.barcodes.helper import stock.models +from InvenTree.api import BulkDeleteMixin from InvenTree.filters import SEARCH_ORDER_FILTER from InvenTree.helpers import hash_barcode from InvenTree.mixins import ListAPI, RetrieveDestroyAPI @@ -651,7 +652,7 @@ class Meta: fields = ['user', 'status'] -class BarcodeScanResultList(BarcodeScanResultMixin, ListAPI): +class BarcodeScanResultList(BarcodeScanResultMixin, BulkDeleteMixin, ListAPI): """List API endpoint for BarcodeScan objects.""" filterset_class = BarcodeScanResultFilter diff --git a/src/backend/InvenTree/plugin/base/barcodes/serializers.py b/src/backend/InvenTree/plugin/base/barcodes/serializers.py index b5c3e6ab6afe..1ab4090719de 100644 --- a/src/backend/InvenTree/plugin/base/barcodes/serializers.py +++ b/src/backend/InvenTree/plugin/base/barcodes/serializers.py @@ -22,7 +22,7 @@ class Meta: model = common.models.BarcodeScanResult fields = [ - 'id', + 'pk', 'data', 'timestamp', 'endpoint', diff --git a/src/frontend/src/tables/InvenTreeTable.tsx b/src/frontend/src/tables/InvenTreeTable.tsx index 985a0422ad48..34383f6d61c0 100644 --- a/src/frontend/src/tables/InvenTreeTable.tsx +++ b/src/frontend/src/tables/InvenTreeTable.tsx @@ -244,6 +244,10 @@ export function InvenTreeTable>({ }; }, [props]); + const enableSelection: boolean = useMemo(() => { + return tableProps.enableSelection || tableProps.enableBulkDelete || false; + }, [tableProps]); + // Check if any columns are switchable (can be hidden) const hasSwitchableColumns: boolean = useMemo(() => { if (props.enableColumnSwitching == false) { @@ -310,7 +314,6 @@ export function InvenTreeTable>({ columns, fieldNames, tableProps.rowActions, - tableProps.enableSelection, tableState.hiddenColumns, tableState.selectedRecords ]); @@ -642,7 +645,7 @@ export function InvenTreeTable>({ actions={tableProps.barcodeActions ?? []} /> )} - {(tableProps.enableBulkDelete ?? false) && ( + {tableProps.enableBulkDelete && ( } @@ -733,12 +736,10 @@ export function InvenTreeTable>({ sortStatus={sortStatus} onSortStatusChange={handleSortStatusChange} selectedRecords={ - tableProps.enableSelection - ? tableState.selectedRecords - : undefined + enableSelection ? tableState.selectedRecords : undefined } onSelectedRecordsChange={ - tableProps.enableSelection ? onSelectedRecordsChange : undefined + enableSelection ? onSelectedRecordsChange : undefined } rowExpansion={tableProps.rowExpansion} rowStyle={tableProps.rowStyle} diff --git a/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx b/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx index a5bcd24120e0..77de1a42f6f2 100644 --- a/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx +++ b/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx @@ -1,15 +1,18 @@ import { t } from '@lingui/macro'; import { Badge, Group, Text } from '@mantine/core'; -import { useMemo } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; +import { UserRoles } from '../../enums/Roles'; import { useUserFilters } from '../../hooks/UseFilter'; +import { useDeleteApiFormModal } from '../../hooks/UseForm'; import { useTable } from '../../hooks/UseTable'; import { apiUrl } from '../../states/ApiState'; import { useUserState } from '../../states/UserState'; import { TableColumn } from '../Column'; import { TableFilter } from '../Filter'; import { InvenTreeTable } from '../InvenTreeTable'; +import { RowDeleteAction } from '../RowActions'; /* * Display the barcode scan history table @@ -71,14 +74,45 @@ export default function BarcodeScanHistoryTable() { ]; }, [userFilters]); + const canDelete: boolean = useMemo(() => { + return user.isStaff() && user.hasDeleteRole(UserRoles.admin); + }, [user]); + + const [selectedResult, setSelectedResult] = useState(0); + + const deleteResult = useDeleteApiFormModal({ + url: ApiEndpoints.barcode_history, + pk: selectedResult, + title: t`Delete Barcode Scan Record`, + table: table + }); + + const rowActions = useCallback( + (record: any) => { + return [ + RowDeleteAction({ + hidden: !canDelete, + onClick: () => { + setSelectedResult(record.pk); + deleteResult.open(); + } + }) + ]; + }, + [canDelete, user] + ); + return ( <> + {deleteResult.modal} From b0e67cf90df56f7b8e9380f434d2dd84bae42b12 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 20 Sep 2024 04:43:54 +0000 Subject: [PATCH 14/44] Add background task to delete old barcode scans --- src/backend/InvenTree/common/tasks.py | 16 ++++++++++++++++ .../pages/Index/Settings/AdminCenter/Index.tsx | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/backend/InvenTree/common/tasks.py b/src/backend/InvenTree/common/tasks.py index ffb67311b9f5..f9c76b871bba 100644 --- a/src/backend/InvenTree/common/tasks.py +++ b/src/backend/InvenTree/common/tasks.py @@ -13,6 +13,7 @@ import requests import InvenTree.helpers +from common.settings import get_global_setting from InvenTree.helpers_model import getModelsWithMixin from InvenTree.models import InvenTreeNotesMixin from InvenTree.tasks import ScheduledTask, scheduled_task @@ -20,6 +21,21 @@ logger = logging.getLogger('inventree') +@scheduled_task(ScheduledTask.DAILY) +def delete_old_barcode_results(): + """Remove old barcode scan results from the database.""" + try: + from common.models import BarcodeScanResult + except Exception: # pragma: no cover + return + + n_days = int(get_global_setting('BARCODE_RESULTS_MAX_AGE', 30)) + before = timezone.now() - timedelta(days=n_days) + + # Remove any barcode scan results older than the specified date + BarcodeScanResult.objects.filter(timestamp__lte=before).delete() + + @scheduled_task(ScheduledTask.DAILY) def delete_old_notifications(): """Remove old notifications from the database. diff --git a/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx b/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx index 3af0543bea49..1d7de700254c 100644 --- a/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx +++ b/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx @@ -115,7 +115,7 @@ export default function AdminCenter() { }, { name: 'barcode-history', - label: t`Barcode Scan History`, + label: t`Barcode Scans`, icon: , content: }, From 11ef9d0b6b83ad45cce492b5d3a86dd5d733a0e7 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 20 Sep 2024 06:36:23 +0000 Subject: [PATCH 15/44] Add detail drawer for barcode scan --- .../src/components/buttons/CopyButton.tsx | 11 +- src/frontend/src/functions/tables.tsx | 2 +- .../settings/BarcodeScanHistoryTable.tsx | 109 ++++++++++++++++-- 3 files changed, 111 insertions(+), 11 deletions(-) diff --git a/src/frontend/src/components/buttons/CopyButton.tsx b/src/frontend/src/components/buttons/CopyButton.tsx index 87d7586828f2..d60ad66bc933 100644 --- a/src/frontend/src/components/buttons/CopyButton.tsx +++ b/src/frontend/src/components/buttons/CopyButton.tsx @@ -3,6 +3,7 @@ import { ActionIcon, Button, CopyButton as MantineCopyButton, + MantineSize, Text, Tooltip } from '@mantine/core'; @@ -11,10 +12,14 @@ import { InvenTreeIcon } from '../../functions/icons'; export function CopyButton({ value, - label + label, + content, + size }: Readonly<{ value: any; label?: JSX.Element; + content?: JSX.Element; + size?: MantineSize; }>) { const ButtonComponent = label ? Button : ActionIcon; @@ -26,14 +31,14 @@ export function CopyButton({ color={copied ? 'teal' : 'gray'} onClick={copy} variant="transparent" - size="sm" + size={size ?? 'sm'} > {copied ? ( ) : ( )} - + {content} {label && {label}} diff --git a/src/frontend/src/functions/tables.tsx b/src/frontend/src/functions/tables.tsx index 73873a7bfa1e..6b3b9a4946a2 100644 --- a/src/frontend/src/functions/tables.tsx +++ b/src/frontend/src/functions/tables.tsx @@ -22,5 +22,5 @@ export function shortenString({ // Otherwise, shorten it let N = Math.floor(len / 2 - 1); - return str.slice(0, N) + '...' + str.slice(-N); + return str.slice(0, N) + ' ... ' + str.slice(-N); } diff --git a/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx b/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx index 77de1a42f6f2..a4a51c9a926d 100644 --- a/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx +++ b/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx @@ -1,9 +1,22 @@ import { t } from '@lingui/macro'; -import { Badge, Group, Text } from '@mantine/core'; +import { + Badge, + Divider, + Drawer, + Group, + Stack, + Table, + Text +} from '@mantine/core'; +import { useDisclosure } from '@mantine/hooks'; import { useCallback, useMemo, useState } from 'react'; +import { CopyButton } from '../../components/buttons/CopyButton'; +import { StylishText } from '../../components/items/StylishText'; +import { RenderUser } from '../../components/render/User'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { UserRoles } from '../../enums/Roles'; +import { shortenString } from '../../functions/tables'; import { useUserFilters } from '../../hooks/UseFilter'; import { useDeleteApiFormModal } from '../../hooks/UseForm'; import { useTable } from '../../hooks/UseTable'; @@ -14,6 +27,66 @@ import { TableFilter } from '../Filter'; import { InvenTreeTable } from '../InvenTreeTable'; import { RowDeleteAction } from '../RowActions'; +/* + * Render detail information for a particular barcode scan result. + */ +function BarcodeScanDetail({ scan }: { scan: any }) { + return ( + <> + +
+ + + {t`Timestamp`} + {scan.timestamp} + + + {t`User`} + + + + + + {t`Endpoint`} + {scan.endpoint} + + + {t`Status`} + {scan.status} + + + + {t`Response`} + + + {scan.response && + Object.keys(scan.response).map((key) => ( + + {key} + + + {scan.response[key]} + + + + + + + ))} + +
+ + + ); +} + /* * Display the barcode scan history table */ @@ -23,6 +96,8 @@ export default function BarcodeScanHistoryTable() { const userFilters = useUserFilters(); + const [opened, { open, close }] = useDisclosure(false); + const tableColumns: TableColumn[] = useMemo(() => { return [ { @@ -46,8 +121,15 @@ export default function BarcodeScanHistoryTable() { switchable: true, render: (record: any) => { return ( - - {record.data} + + {shortenString({ str: record.data, len: 100 })} ); } @@ -78,11 +160,11 @@ export default function BarcodeScanHistoryTable() { return user.isStaff() && user.hasDeleteRole(UserRoles.admin); }, [user]); - const [selectedResult, setSelectedResult] = useState(0); + const [selectedResult, setSelectedResult] = useState({}); const deleteResult = useDeleteApiFormModal({ url: ApiEndpoints.barcode_history, - pk: selectedResult, + pk: selectedResult.pk, title: t`Delete Barcode Scan Record`, table: table }); @@ -93,7 +175,7 @@ export default function BarcodeScanHistoryTable() { RowDeleteAction({ hidden: !canDelete, onClick: () => { - setSelectedResult(record.pk); + setSelectedResult(record); deleteResult.open(); } }) @@ -105,6 +187,15 @@ export default function BarcodeScanHistoryTable() { return ( <> {deleteResult.modal} + {t`Barcode Scan Details`}} + onClose={close} + > + + { + setSelectedResult(row); + open(); + } }} /> From 99fdd32ea1c2adc34de05265f4816c380e5ec148 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 20 Sep 2024 06:40:04 +0000 Subject: [PATCH 16/44] Log messages for BarcodePOReceive --- .../InvenTree/plugin/base/barcodes/api.py | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/backend/InvenTree/plugin/base/barcodes/api.py b/src/backend/InvenTree/plugin/base/barcodes/api.py index 88f86f23656d..67267a6e2611 100644 --- a/src/backend/InvenTree/plugin/base/barcodes/api.py +++ b/src/backend/InvenTree/plugin/base/barcodes/api.py @@ -444,6 +444,11 @@ def handle_barcode(self, barcode: str, request, **kwargs): if result := internal_barcode_plugin.scan(barcode): if 'stockitem' in result: response['error'] = _('Item has already been received') + + common.models.BarcodeScanResult.log_scan_result( + data=barcode, request=request, status=400, response=response + ) + raise ValidationError(response) # Now, look just for "supplier-barcode" plugins @@ -481,11 +486,18 @@ def handle_barcode(self, barcode: str, request, **kwargs): # A plugin has not been found! if plugin is None: response['error'] = _('No match for supplier barcode') + + if 'error' in response: + common.models.BarcodeScanResult.log_scan_result( + data=barcode, request=request, status=400, response=response + ) raise ValidationError(response) - elif 'error' in response: - raise ValidationError(response) - else: - return Response(response) + + common.models.BarcodeScanResult.log_scan_result( + data=barcode, request=request, status=200, response=response + ) + + return Response(response) class BarcodeSOAllocate(BarcodeView): From 56109bd95577a35c6c913493dd0c7a558853c1a7 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 20 Sep 2024 06:44:15 +0000 Subject: [PATCH 17/44] Add warning message if barcode logging is not enabled --- .../settings/BarcodeScanHistoryTable.tsx | 44 +++++++++++++------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx b/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx index a4a51c9a926d..1b7769382045 100644 --- a/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx +++ b/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx @@ -1,5 +1,6 @@ import { t } from '@lingui/macro'; import { + Alert, Badge, Divider, Drawer, @@ -9,6 +10,7 @@ import { Text } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; +import { IconExclamationCircle } from '@tabler/icons-react'; import { useCallback, useMemo, useState } from 'react'; import { CopyButton } from '../../components/buttons/CopyButton'; @@ -21,6 +23,7 @@ import { useUserFilters } from '../../hooks/UseFilter'; import { useDeleteApiFormModal } from '../../hooks/UseForm'; import { useTable } from '../../hooks/UseTable'; import { apiUrl } from '../../states/ApiState'; +import { useGlobalSettingsState } from '../../states/SettingsState'; import { useUserState } from '../../states/UserState'; import { TableColumn } from '../Column'; import { TableFilter } from '../Filter'; @@ -94,6 +97,8 @@ export default function BarcodeScanHistoryTable() { const user = useUserState(); const table = useTable('barcode-history'); + const globalSettings = useGlobalSettingsState(); + const userFilters = useUserFilters(); const [opened, { open, close }] = useDisclosure(false); @@ -196,20 +201,31 @@ export default function BarcodeScanHistoryTable() { > - { - setSelectedResult(row); - open(); - } - }} - /> + + {!globalSettings.isSet('BARCODE_STORE_RESULTS') && ( + } + title={t`Logging Disabled`} + > + {t`Barcode logging is not enabled`} + + )} + { + setSelectedResult(row); + open(); + } + }} + /> + ); } From 9a91827db4112cd7fafa2a2d4d4ddccc767d0b8e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 20 Sep 2024 10:17:08 +0000 Subject: [PATCH 18/44] Add "context" data to BarcodeScanResult --- .../migrations/0030_barcodescanresult.py | 3 ++- src/backend/InvenTree/common/models.py | 27 ++++++++++++++++--- .../plugin/base/barcodes/serializers.py | 1 + 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/backend/InvenTree/common/migrations/0030_barcodescanresult.py b/src/backend/InvenTree/common/migrations/0030_barcodescanresult.py index f025feb41430..fbf38ef98590 100644 --- a/src/backend/InvenTree/common/migrations/0030_barcodescanresult.py +++ b/src/backend/InvenTree/common/migrations/0030_barcodescanresult.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.15 on 2024-09-20 03:53 +# Generated by Django 4.2.15 on 2024-09-20 10:15 import InvenTree.models from django.conf import settings @@ -22,6 +22,7 @@ class Migration(migrations.Migration): ('timestamp', models.DateTimeField(auto_now_add=True, help_text='Date and time of the barcode scan', verbose_name='Timestamp')), ('endpoint', models.CharField(blank=True, help_text='URL endpoint which processed the barcode', max_length=250, null=True, verbose_name='Path')), ('status', models.IntegerField(blank=True, help_text='Response status code', null=True, verbose_name='Status')), + ('context', models.JSONField(blank=True, help_text='Context data for the barcode scan', max_length=1000, null=True, verbose_name='Context')), ('response', models.JSONField(blank=True, help_text='Response data from the barcode scan', max_length=1000, null=True, verbose_name='Response')), ('user', models.ForeignKey(blank=True, help_text='User who scanned the barcode', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='User')), ], diff --git a/src/backend/InvenTree/common/models.py b/src/backend/InvenTree/common/models.py index e722a1c9fddb..6c87fa99666a 100644 --- a/src/backend/InvenTree/common/models.py +++ b/src/backend/InvenTree/common/models.py @@ -3472,7 +3472,7 @@ class Meta: verbose_name = _('Barcode Scan') @staticmethod - def log_scan_result(data, request, status, response): + def log_scan_result(data, request, status, response=None, context=None): """Log a barcode scan to the database.""" # Exit if BARCODE_STORE_RESULTS is False if not InvenTreeSetting.get_setting( @@ -3486,9 +3486,19 @@ def log_scan_result(data, request, status, response): # Ensure that the response data is stringified first, otherwise cannot be JSON encoded if type(response) is dict: - for key, value in response.items(): - if value is not None: - response[key] = str(value) + response = {key: str(value) for key, value in response.items()} + elif response is None: + pass + else: + response = str(response) + + # Ensure that the context data is stringified first, otherwise cannot be JSON encoded + if type(context) is dict: + context = {key: str(value) for key, value in context.items()} + elif context is None: + pass + else: + context = str(context) # Ensure data is not too long if len(data) > BarcodeScanResult.BARCODE_SCAN_MAX_LEN: @@ -3501,6 +3511,7 @@ def log_scan_result(data, request, status, response): endpoint=endpoint, status=status, response=response, + context=context, ) except Exception: # Gracefully log error to database @@ -3544,6 +3555,14 @@ def log_scan_result(data, request, status, response): null=True, ) + context = models.JSONField( + max_length=1000, + verbose_name=_('Context'), + help_text=_('Context data for the barcode scan'), + blank=True, + null=True, + ) + response = models.JSONField( max_length=1000, verbose_name=_('Response'), diff --git a/src/backend/InvenTree/plugin/base/barcodes/serializers.py b/src/backend/InvenTree/plugin/base/barcodes/serializers.py index 1ab4090719de..5f3f107556cc 100644 --- a/src/backend/InvenTree/plugin/base/barcodes/serializers.py +++ b/src/backend/InvenTree/plugin/base/barcodes/serializers.py @@ -27,6 +27,7 @@ class Meta: 'timestamp', 'endpoint', 'status', + 'context', 'response', 'user', 'user_detail', From 09e2ed058d827bf3f2180661857011f6a25d66e9 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 20 Sep 2024 10:19:22 +0000 Subject: [PATCH 19/44] Display context data (if available) --- .../settings/BarcodeScanHistoryTable.tsx | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx b/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx index 1b7769382045..cf2ab321720c 100644 --- a/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx +++ b/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx @@ -57,11 +57,13 @@ function BarcodeScanDetail({ scan }: { scan: any }) { {t`Status`} {scan.status} - - - {t`Response`} - - + {scan.response && ( + + + {t`Response`} + + + )} {scan.response && Object.keys(scan.response).map((key) => ( @@ -83,6 +85,34 @@ function BarcodeScanDetail({ scan }: { scan: any }) { ))} + {scan.context && ( + + + {t`Context`} + + + )} + {scan.context && + Object.keys(scan.context).map((key) => ( + + {key} + + + {scan.context[key]} + + + + + + + ))} From d567f48f7dc4d19dcd0c9ec7a92ac18eddb97083 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 20 Sep 2024 10:29:49 +0000 Subject: [PATCH 20/44] Add context data when scanning --- src/backend/InvenTree/plugin/base/barcodes/api.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/backend/InvenTree/plugin/base/barcodes/api.py b/src/backend/InvenTree/plugin/base/barcodes/api.py index 67267a6e2611..c61c78538403 100644 --- a/src/backend/InvenTree/plugin/base/barcodes/api.py +++ b/src/backend/InvenTree/plugin/base/barcodes/api.py @@ -132,7 +132,7 @@ def handle_barcode(self, barcode: str, request, **kwargs): # Log the scan result common.models.BarcodeScanResult.log_scan_result( - data=barcode, request=request, status=400, response=result + barcode, request, 400, response=result ) raise ValidationError(result) @@ -141,7 +141,7 @@ def handle_barcode(self, barcode: str, request, **kwargs): # Log the scan result common.models.BarcodeScanResult.log_scan_result( - data=barcode, request=request, status=200, response=result + barcode, request, 200, response=result ) return Response(result) @@ -432,6 +432,8 @@ def handle_barcode(self, barcode: str, request, **kwargs): plugins = registry.with_mixin('barcode') + context = {'purchase_order': purchase_order, 'location': location} + # Look for a barcode plugin which knows how to deal with this barcode plugin = None @@ -446,7 +448,7 @@ def handle_barcode(self, barcode: str, request, **kwargs): response['error'] = _('Item has already been received') common.models.BarcodeScanResult.log_scan_result( - data=barcode, request=request, status=400, response=response + barcode, request, 400, context=context, response=response ) raise ValidationError(response) @@ -489,12 +491,12 @@ def handle_barcode(self, barcode: str, request, **kwargs): if 'error' in response: common.models.BarcodeScanResult.log_scan_result( - data=barcode, request=request, status=400, response=response + barcode, request, 400, context=context, response=response ) raise ValidationError(response) common.models.BarcodeScanResult.log_scan_result( - data=barcode, request=request, status=200, response=response + barcode, request, 200, context=context, response=response ) return Response(response) From 1449dab02edfe96824ba838e42726b4c5c0dfc5c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 20 Sep 2024 11:06:36 +0000 Subject: [PATCH 21/44] Simplify / refactor BarcodeSOAllocate --- .../InvenTree/plugin/base/barcodes/api.py | 116 ++++++++++++------ 1 file changed, 79 insertions(+), 37 deletions(-) diff --git a/src/backend/InvenTree/plugin/base/barcodes/api.py b/src/backend/InvenTree/plugin/base/barcodes/api.py index c61c78538403..13cf9b84bb39 100644 --- a/src/backend/InvenTree/plugin/base/barcodes/api.py +++ b/src/backend/InvenTree/plugin/base/barcodes/api.py @@ -520,7 +520,11 @@ class BarcodeSOAllocate(BarcodeView): serializer_class = barcode_serializers.BarcodeSOAllocateSerializer def get_line_item(self, stock_item, **kwargs): - """Return the matching line item for the provided stock item.""" + """Return the matching line item for the provided stock item. + + Raises: + ValidationError: If no single matching line item is found + """ # Extract sales order object (required field) sales_order = kwargs['sales_order'] @@ -537,22 +541,24 @@ def get_line_item(self, stock_item, **kwargs): ) if lines.count() > 1: - raise ValidationError({'error': _('Multiple matching line items found')}) + raise ValidationError(_('Multiple matching line items found')) if lines.count() == 0: - raise ValidationError({'error': _('No matching line item found')}) + raise ValidationError(_('No matching line item found')) return lines.first() def get_shipment(self, **kwargs): - """Extract the shipment from the provided kwargs, or guess.""" + """Extract the shipment from the provided kwargs, or guess. + + Raises: + ValidationError: If the shipment does not match the sales order + """ sales_order = kwargs['sales_order'] if shipment := kwargs.get('shipment', None): if shipment.order != sales_order: - raise ValidationError({ - 'error': _('Shipment does not match sales order') - }) + raise ValidationError(_('Shipment does not match sales order')) return shipment @@ -566,38 +572,64 @@ def get_shipment(self, **kwargs): # If shipment cannot be determined, return None return None - def handle_barcode(self, barcode: str, request, **kwargs): - """Handle barcode scan for sales order allocation.""" + def handle_barcode(self, barcode: str, request, *kwargs): + """Handle barcode scan for sales order allocation. + + Arguments: + barcode: Raw barcode data + request: HTTP request object + + kwargs: + sales_order: SalesOrder ID value (required) + line: SalesOrderLineItem ID value (optional) + shipment: SalesOrderShipment ID value (optional) + """ logger.debug("BarcodeSOAllocate: scanned barcode - '%s'", barcode) result = self.scan_barcode(barcode, request, **kwargs) - if result['plugin'] is None: - result['error'] = _('No match found for barcode data') + # Context for logging + context = {**kwargs} + + if 'sales_order' not in kwargs: + # SalesOrder ID *must* be provided + result['error'] = _('No sales order provided') + elif result['plugin'] is None: + # Check that the barcode at least matches a plugin + result['error'] = _('No matching plugin found for barcode data') + else: + try: + stock_item_id = result['stockitem'].get('pk', None) + stock_item = stock.models.StockItem.objects.get(pk=stock_item_id) + except (ValueError, stock.models.StockItem.DoesNotExist): + result['error'] = _('Barcode does not match an existing stock item') + + if 'error' in result: + common.models.BarcodeScanResult.log_scan_result( + barcode, request, 400, context=context, response=result + ) raise ValidationError(result) - # Check that the scanned barcode was a StockItem - if 'stockitem' not in result: - result['error'] = _('Barcode does not match an existing stock item') - raise ValidationError(result) + # At this stage, we have a valid StockItem object try: - stock_item_id = result['stockitem'].get('pk', None) - stock_item = stock.models.StockItem.objects.get(pk=stock_item_id) - except (ValueError, stock.models.StockItem.DoesNotExist): - result['error'] = _('Barcode does not match an existing stock item') - raise ValidationError(result) - - # At this stage, we have a valid StockItem object - # Extract any other data from the kwargs - line_item = self.get_line_item(stock_item, **kwargs) - sales_order = kwargs['sales_order'] - shipment = self.get_shipment(**kwargs) + # Extract any other data from the kwargs + # Note: This may raise a ValidationError at some point - we break on the first error + sales_order = kwargs['sales_order'] + line_item = self.get_line_item(stock_item, **kwargs) + shipment = self.get_shipment(**kwargs) + if stock_item is not None and line_item is not None: + if stock_item.part != line_item.part: + result['error'] = _('Stock item does not match line item') + except ValidationError as e: + result['error'] = str(e) + + if 'error' in result: + common.models.BarcodeScanResult.log_scan_result( + barcode, request, 400, context=context, response=result + ) - if stock_item is not None and line_item is not None: - if stock_item.part != line_item.part: - result['error'] = _('Stock item does not match line item') - raise ValidationError(result) + raise ValidationError(result) quantity = kwargs.get('quantity', None) @@ -605,11 +637,12 @@ def handle_barcode(self, barcode: str, request, **kwargs): if stock_item.serialized: quantity = 1 - if quantity is None: + elif quantity is None: quantity = line_item.quantity - line_item.shipped quantity = min(quantity, stock_item.unallocated_quantity()) response = { + **result, 'stock_item': stock_item.pk if stock_item else None, 'part': stock_item.part.pk if stock_item else None, 'sales_order': sales_order.pk if sales_order else None, @@ -621,22 +654,31 @@ def handle_barcode(self, barcode: str, request, **kwargs): if stock_item is not None and quantity is not None: if stock_item.unallocated_quantity() < quantity: response['error'] = _('Insufficient stock available') - raise ValidationError(response) # If we have sufficient information, we can allocate the stock item - if all(x is not None for x in [line_item, sales_order, shipment, quantity]): + elif all(x is not None for x in [line_item, sales_order, shipment, quantity]): order.models.SalesOrderAllocation.objects.create( line=line_item, shipment=shipment, item=stock_item, quantity=quantity ) response['success'] = _('Stock item allocated to sales order') - return Response(response) + else: + response['error'] = _('Not enough information') + response['action_required'] = True - response['error'] = _('Not enough information') - response['action_required'] = True + common.models.BarcodeScanResult.log_scan_result( + barcode, + request, + 200 if 'success' in response else 400, + context=context, + response=response, + ) - raise ValidationError(response) + if 'error' in response: + raise ValidationError(response) + else: + return Response(response) class BarcodeScanResultMixin: From 0d068ed45d061f1849b6ecfcb72e8076ccae69af Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 20 Sep 2024 11:18:56 +0000 Subject: [PATCH 22/44] Refactor BarcodePOAllocate --- .../InvenTree/plugin/base/barcodes/api.py | 62 +++++++++++-------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/src/backend/InvenTree/plugin/base/barcodes/api.py b/src/backend/InvenTree/plugin/base/barcodes/api.py index 13cf9b84bb39..d711be3c8dbb 100644 --- a/src/backend/InvenTree/plugin/base/barcodes/api.py +++ b/src/backend/InvenTree/plugin/base/barcodes/api.py @@ -350,7 +350,7 @@ def get_supplier_part( supplier_parts = company.models.SupplierPart.objects.filter(supplier=supplier) if not part and not supplier_part and not manufacturer_part: - raise ValidationError({'error': _('No matching part data found')}) + raise ValidationError(_('No matching part data found')) if part and (part_id := part.get('pk', None)): supplier_parts = supplier_parts.filter(part__pk=part_id) @@ -366,12 +366,10 @@ def get_supplier_part( ) if supplier_parts.count() == 0: - raise ValidationError({'error': _('No matching supplier parts found')}) + raise ValidationError(_('No matching supplier parts found')) if supplier_parts.count() > 1: - raise ValidationError({ - 'error': _('Multiple matching supplier parts found') - }) + raise ValidationError(_('Multiple matching supplier parts found')) # At this stage, we have a single matching supplier part return supplier_parts.first() @@ -384,21 +382,34 @@ def handle_barcode(self, barcode: str, request, **kwargs): result = self.scan_barcode(barcode, request, **kwargs) if result['plugin'] is None: - result['error'] = _('No match found for barcode data') - raise ValidationError(result) - - supplier_part = self.get_supplier_part( - purchase_order, - part=result.get('part', None), - supplier_part=result.get('supplierpart', None), - manufacturer_part=result.get('manufacturerpart', None), - ) + result['error'] = _('No matching plugin found for barcode data') - result['success'] = _('Matched supplier part') - result['supplierpart'] = supplier_part.format_matched_response() + else: + try: + supplier_part = self.get_supplier_part( + purchase_order, + part=result.get('part', None), + supplier_part=result.get('supplierpart', None), + manufacturer_part=result.get('manufacturerpart', None), + ) + result['success'] = _('Matched supplier part') + result['supplierpart'] = supplier_part.format_matched_response() + except ValidationError as e: + result['error'] = str(e) # TODO: Determine the 'quantity to order' for the supplier part + common.models.BarcodeScanResult.log_scan_result( + barcode, + request, + 400 if 'error' in result else 200, + response=result, + context={**kwargs}, + ) + + if 'error' in result: + raise ValidationError + return Response(result) @@ -432,7 +443,7 @@ def handle_barcode(self, barcode: str, request, **kwargs): plugins = registry.with_mixin('barcode') - context = {'purchase_order': purchase_order, 'location': location} + context = {**kwargs} # Look for a barcode plugin which knows how to deal with this barcode plugin = None @@ -489,16 +500,17 @@ def handle_barcode(self, barcode: str, request, **kwargs): if plugin is None: response['error'] = _('No match for supplier barcode') - if 'error' in response: - common.models.BarcodeScanResult.log_scan_result( - barcode, request, 400, context=context, response=response - ) - raise ValidationError(response) - common.models.BarcodeScanResult.log_scan_result( - barcode, request, 200, context=context, response=response + barcode, + request, + 400 if 'error' in response else 200, + context=context, + response=response, ) + if 'error' in response: + raise ValidationError(response) + return Response(response) @@ -670,7 +682,7 @@ def handle_barcode(self, barcode: str, request, *kwargs): common.models.BarcodeScanResult.log_scan_result( barcode, request, - 200 if 'success' in response else 400, + 400 if 'error' in response else 200, context=context, response=response, ) From 53ec320208acddc737e02b2f20b597778f34ef1d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 20 Sep 2024 11:51:43 +0000 Subject: [PATCH 23/44] Limit the number of saved scans --- docs/docs/settings/global.md | 2 +- src/backend/InvenTree/common/models.py | 19 ++++++++++++++----- src/backend/InvenTree/common/tasks.py | 16 ---------------- .../src/components/buttons/CopyButton.tsx | 8 ++++++-- .../pages/Index/Settings/SystemSettings.tsx | 2 +- 5 files changed, 22 insertions(+), 25 deletions(-) diff --git a/docs/docs/settings/global.md b/docs/docs/settings/global.md index de3c6f22d69c..241ac74f0d40 100644 --- a/docs/docs/settings/global.md +++ b/docs/docs/settings/global.md @@ -91,7 +91,7 @@ Configuration of barcode functionality: {{ globalsetting("BARCODE_SHOW_TEXT") }} {{ globalsetting("BARCODE_GENERATION_PLUGIN") }} {{ globalsetting("BARCODE_STORE_RESULTS") }} -{{ globalsetting("BARCODE_RESULTS_MAX_AGE") }} +{{ globalsetting("BARCODE_RESULTS_MAX_NUM") }} ### Pricing and Currency diff --git a/src/backend/InvenTree/common/models.py b/src/backend/InvenTree/common/models.py index 6c87fa99666a..a73617a45029 100644 --- a/src/backend/InvenTree/common/models.py +++ b/src/backend/InvenTree/common/models.py @@ -1405,11 +1405,10 @@ def save(self, *args, **kwargs): 'default': False, 'validator': bool, }, - 'BARCODE_RESULTS_MAX_AGE': { - 'name': _('Barcode Results Max Age'), - 'description': _('Maximum age of barcode scan results to store'), - 'default': 30, - 'units': _('days'), + 'BARCODE_RESULTS_MAX_NUM': { + 'name': _('Barcode Scans Maximum Count'), + 'description': _('Maximum number of barcode scan results to store'), + 'default': 100, 'validator': [int, MinValueValidator(1)], }, 'BARCODE_INPUT_DELAY': { @@ -3513,6 +3512,16 @@ def log_scan_result(data, request, status, response=None, context=None): response=response, context=context, ) + + # Ensure that we do not store too many scans + max_scans = int( + InvenTreeSetting.get_setting('BARCODE_RESULTS_MAX_NUM', create=False) + ) + num_scans = BarcodeScanResult.objects.count() + + if num_scans > max_scans: + n = num_scans - max_scans + BarcodeScanResult.objects.all().order_by('timestamp')[:n].delete() except Exception: # Gracefully log error to database InvenTree.exceptions.log_error('barcode.log_scan_result') diff --git a/src/backend/InvenTree/common/tasks.py b/src/backend/InvenTree/common/tasks.py index f9c76b871bba..ffb67311b9f5 100644 --- a/src/backend/InvenTree/common/tasks.py +++ b/src/backend/InvenTree/common/tasks.py @@ -13,7 +13,6 @@ import requests import InvenTree.helpers -from common.settings import get_global_setting from InvenTree.helpers_model import getModelsWithMixin from InvenTree.models import InvenTreeNotesMixin from InvenTree.tasks import ScheduledTask, scheduled_task @@ -21,21 +20,6 @@ logger = logging.getLogger('inventree') -@scheduled_task(ScheduledTask.DAILY) -def delete_old_barcode_results(): - """Remove old barcode scan results from the database.""" - try: - from common.models import BarcodeScanResult - except Exception: # pragma: no cover - return - - n_days = int(get_global_setting('BARCODE_RESULTS_MAX_AGE', 30)) - before = timezone.now() - timedelta(days=n_days) - - # Remove any barcode scan results older than the specified date - BarcodeScanResult.objects.filter(timestamp__lte=before).delete() - - @scheduled_task(ScheduledTask.DAILY) def delete_old_notifications(): """Remove old notifications from the database. diff --git a/src/frontend/src/components/buttons/CopyButton.tsx b/src/frontend/src/components/buttons/CopyButton.tsx index d60ad66bc933..6f2c733ec961 100644 --- a/src/frontend/src/components/buttons/CopyButton.tsx +++ b/src/frontend/src/components/buttons/CopyButton.tsx @@ -17,7 +17,7 @@ export function CopyButton({ size }: Readonly<{ value: any; - label?: JSX.Element; + label?: string; content?: JSX.Element; size?: MantineSize; }>) { @@ -39,7 +39,11 @@ export function CopyButton({ )} {content} - {label && {label}} + {label && ( + + {label} + + )} )} diff --git a/src/frontend/src/pages/Index/Settings/SystemSettings.tsx b/src/frontend/src/pages/Index/Settings/SystemSettings.tsx index dbd414003ac9..d8b931648b33 100644 --- a/src/frontend/src/pages/Index/Settings/SystemSettings.tsx +++ b/src/frontend/src/pages/Index/Settings/SystemSettings.tsx @@ -101,7 +101,7 @@ export default function SystemSettings() { 'BARCODE_SHOW_TEXT', 'BARCODE_GENERATION_PLUGIN', 'BARCODE_STORE_RESULTS', - 'BARCODE_RESULTS_MAX_AGE' + 'BARCODE_RESULTS_MAX_NUM' ]} /> ) From c7b60f064cb905d091e03b2c3ea68372d10fea58 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 20 Sep 2024 11:51:58 +0000 Subject: [PATCH 24/44] Improve error message display in PUI --- .../src/tables/settings/ErrorTable.tsx | 65 ++++++++++++++----- 1 file changed, 49 insertions(+), 16 deletions(-) diff --git a/src/frontend/src/tables/settings/ErrorTable.tsx b/src/frontend/src/tables/settings/ErrorTable.tsx index b3090e4d1cd4..781f57b3c09d 100644 --- a/src/frontend/src/tables/settings/ErrorTable.tsx +++ b/src/frontend/src/tables/settings/ErrorTable.tsx @@ -1,8 +1,9 @@ import { t } from '@lingui/macro'; -import { Drawer, Text } from '@mantine/core'; +import { Drawer, Group, Stack, Table, Text } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; import { useCallback, useMemo, useState } from 'react'; +import { CopyButton } from '../../components/buttons/CopyButton'; import { StylishText } from '../../components/items/StylishText'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { useDeleteApiFormModal } from '../../hooks/UseForm'; @@ -13,6 +14,48 @@ import { TableColumn } from '../Column'; import { InvenTreeTable } from '../InvenTreeTable'; import { RowAction, RowDeleteAction } from '../RowActions'; +function ErrorDetail({ error }: { error: any }) { + return ( + + + + + {t`Message`} + {error.info} + + + {t`Timestamp`} + {error.when} + + + {t`Path`} + {error.path} + + + {t`Traceback`} + + + + + + + + + + {error.data.split('\n').map((line: string, index: number) => ( + + {line} + + ))} + + + + +
+
+ ); +} + /* * Table for display server error information */ @@ -20,8 +63,6 @@ export default function ErrorReportTable() { const table = useTable('error-report'); const user = useUserState(); - const [error, setError] = useState(''); - const [opened, { open, close }] = useDisclosure(false); const columns: TableColumn[] = useMemo(() => { @@ -43,13 +84,11 @@ export default function ErrorReportTable() { ]; }, []); - const [selectedError, setSelectedError] = useState( - undefined - ); + const [selectedError, setSelectedError] = useState({}); const deleteErrorModal = useDeleteApiFormModal({ url: ApiEndpoints.error_report_list, - pk: selectedError, + pk: selectedError.pk, title: t`Delete Error Report`, preFormContent: ( {t`Are you sure you want to delete this error report?`} @@ -62,7 +101,7 @@ export default function ErrorReportTable() { return [ RowDeleteAction({ onClick: () => { - setSelectedError(record.pk); + setSelectedError(record); deleteErrorModal.open(); } }) @@ -79,13 +118,7 @@ export default function ErrorReportTable() { title={{t`Error Details`}} onClose={close} > - {error.split('\n').map((line: string) => { - return ( - - {line} - - ); - })} + { - setError(row.data); + setSelectedError(row); open(); } }} From 29b5ea39f29ff6244c1cadab7fb254735b1ca76d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 20 Sep 2024 13:16:57 +0000 Subject: [PATCH 25/44] Simplify barcode logging --- src/backend/InvenTree/common/admin.py | 4 +- .../migrations/0030_barcodescanresult.py | 3 +- src/backend/InvenTree/common/models.py | 63 --------- .../InvenTree/plugin/base/barcodes/api.py | 133 +++++++++++------- .../plugin/base/barcodes/serializers.py | 1 - .../settings/BarcodeScanHistoryTable.tsx | 19 +-- 6 files changed, 87 insertions(+), 136 deletions(-) diff --git a/src/backend/InvenTree/common/admin.py b/src/backend/InvenTree/common/admin.py index 34c2c8651a11..98e419388792 100644 --- a/src/backend/InvenTree/common/admin.py +++ b/src/backend/InvenTree/common/admin.py @@ -39,9 +39,9 @@ def formfield_for_dbfield(self, db_field, request, **kwargs): class BarcodeScanResultAdmin(admin.ModelAdmin): """Admin interface for BarcodeScanResult objects.""" - list_display = ('data', 'timestamp', 'user', 'endpoint', 'status') + list_display = ('data', 'timestamp', 'user', 'endpoint') - list_filter = ('user', 'endpoint', 'status') + list_filter = ('user', 'endpoint') @admin.register(common.models.ProjectCode) diff --git a/src/backend/InvenTree/common/migrations/0030_barcodescanresult.py b/src/backend/InvenTree/common/migrations/0030_barcodescanresult.py index fbf38ef98590..668a743188af 100644 --- a/src/backend/InvenTree/common/migrations/0030_barcodescanresult.py +++ b/src/backend/InvenTree/common/migrations/0030_barcodescanresult.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.15 on 2024-09-20 10:15 +# Generated by Django 4.2.15 on 2024-09-20 13:15 import InvenTree.models from django.conf import settings @@ -21,7 +21,6 @@ class Migration(migrations.Migration): ('data', models.CharField(help_text='Barcode data', max_length=250, verbose_name='Data')), ('timestamp', models.DateTimeField(auto_now_add=True, help_text='Date and time of the barcode scan', verbose_name='Timestamp')), ('endpoint', models.CharField(blank=True, help_text='URL endpoint which processed the barcode', max_length=250, null=True, verbose_name='Path')), - ('status', models.IntegerField(blank=True, help_text='Response status code', null=True, verbose_name='Status')), ('context', models.JSONField(blank=True, help_text='Context data for the barcode scan', max_length=1000, null=True, verbose_name='Context')), ('response', models.JSONField(blank=True, help_text='Response data from the barcode scan', max_length=1000, null=True, verbose_name='Response')), ('user', models.ForeignKey(blank=True, help_text='User who scanned the barcode', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='User')), diff --git a/src/backend/InvenTree/common/models.py b/src/backend/InvenTree/common/models.py index a73617a45029..9a7274eaedfc 100644 --- a/src/backend/InvenTree/common/models.py +++ b/src/backend/InvenTree/common/models.py @@ -3470,62 +3470,6 @@ class Meta: verbose_name = _('Barcode Scan') - @staticmethod - def log_scan_result(data, request, status, response=None, context=None): - """Log a barcode scan to the database.""" - # Exit if BARCODE_STORE_RESULTS is False - if not InvenTreeSetting.get_setting( - 'BARCODE_STORE_RESULTS', backup=False, create=False - ): - return - - # Extract information from the request - user = request.user if request.user and request.user.is_authenticated else None - endpoint = request.path - - # Ensure that the response data is stringified first, otherwise cannot be JSON encoded - if type(response) is dict: - response = {key: str(value) for key, value in response.items()} - elif response is None: - pass - else: - response = str(response) - - # Ensure that the context data is stringified first, otherwise cannot be JSON encoded - if type(context) is dict: - context = {key: str(value) for key, value in context.items()} - elif context is None: - pass - else: - context = str(context) - - # Ensure data is not too long - if len(data) > BarcodeScanResult.BARCODE_SCAN_MAX_LEN: - data = data[: BarcodeScanResult.BARCODE_SCAN_MAX_LEN] - - try: - BarcodeScanResult.objects.create( - data=data, - user=user, - endpoint=endpoint, - status=status, - response=response, - context=context, - ) - - # Ensure that we do not store too many scans - max_scans = int( - InvenTreeSetting.get_setting('BARCODE_RESULTS_MAX_NUM', create=False) - ) - num_scans = BarcodeScanResult.objects.count() - - if num_scans > max_scans: - n = num_scans - max_scans - BarcodeScanResult.objects.all().order_by('timestamp')[:n].delete() - except Exception: - # Gracefully log error to database - InvenTree.exceptions.log_error('barcode.log_scan_result') - data = models.CharField( max_length=BARCODE_SCAN_MAX_LEN, verbose_name=_('Data'), @@ -3557,13 +3501,6 @@ def log_scan_result(data, request, status, response=None, context=None): null=True, ) - status = models.IntegerField( - verbose_name=_('Status'), - help_text=_('Response status code'), - blank=True, - null=True, - ) - context = models.JSONField( max_length=1000, verbose_name=_('Context'), diff --git a/src/backend/InvenTree/plugin/base/barcodes/api.py b/src/backend/InvenTree/plugin/base/barcodes/api.py index d711be3c8dbb..aa16f6bbf792 100644 --- a/src/backend/InvenTree/plugin/base/barcodes/api.py +++ b/src/backend/InvenTree/plugin/base/barcodes/api.py @@ -17,7 +17,9 @@ import order.models import plugin.base.barcodes.helper import stock.models +from common.settings import get_global_setting from InvenTree.api import BulkDeleteMixin +from InvenTree.exceptions import log_error from InvenTree.filters import SEARCH_ORDER_FILTER from InvenTree.helpers import hash_barcode from InvenTree.mixins import ListAPI, RetrieveDestroyAPI @@ -36,6 +38,70 @@ class BarcodeView(CreateAPIView): # Default serializer class (can be overridden) serializer_class = barcode_serializers.BarcodeSerializer + def log_scan(self, request, response=None): + """Log a barcode scan to the database. + + Arguments: + request: HTTP request object + response: Optional response data + """ + from common.models import BarcodeScanResult + + # Extract context data from the request + context = {**request.GET.dict(), **request.POST.dict()} + + barcode = context.pop('barcode', None) + + # Exit if storing barcode scans is disabled + if not get_global_setting('BARCODE_STORE_RESULTS', backup=False, create=False): + return + + # Ensure that the response data is stringified first, otherwise cannot be JSON encoded + if type(response) is dict: + response = {key: str(value) for key, value in response.items()} + elif response is None: + pass + else: + response = str(response) + + # Ensure that the context data is stringified first, otherwise cannot be JSON encoded + if type(context) is dict: + context = {key: str(value) for key, value in context.items()} + elif context is None: + pass + else: + context = str(context) + + # Ensure data is not too long + if len(barcode) > BarcodeScanResult.BARCODE_SCAN_MAX_LEN: + barcode = barcode[: BarcodeScanResult.BARCODE_SCAN_MAX_LEN] + + try: + BarcodeScanResult.objects.create( + data=barcode, + user=request.user, + endpoint=request.path, + status=status, + response=response, + context=context, + ) + + # Ensure that we do not store too many scans + max_scans = int(get_global_setting('BARCODE_RESULTS_MAX_NUM', create=False)) + num_scans = BarcodeScanResult.objects.count() + + if num_scans > max_scans: + n = num_scans - max_scans + old_scan_ids = ( + BarcodeScanResult.objects.all() + .order_by('timestamp') + .values_list('pk', flat=True)[:n] + ) + BarcodeScanResult.objects.filter(pk__in=old_scan_ids).delete() + except Exception: + # Gracefully log error to database + log_error(f'{self.__class__.__name__}.log_scan') + def queryset(self): """This API view does not have a queryset.""" return None @@ -46,7 +112,13 @@ def queryset(self): def create(self, request, *args, **kwargs): """Handle create method - override default create.""" serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) + + try: + serializer.is_valid(raise_exception=True) + except Exception as exc: + self.log_scan(request, {'error': str(exc)}) + raise exc + data = serializer.validated_data barcode = str(data.pop('barcode')).strip() @@ -129,20 +201,13 @@ def handle_barcode(self, barcode: str, request, **kwargs): if result['plugin'] is None: result['error'] = _('No match found for barcode data') - - # Log the scan result - common.models.BarcodeScanResult.log_scan_result( - barcode, request, 400, response=result - ) - + self.log_scan(request, result) raise ValidationError(result) result['success'] = _('Match found for barcode data') # Log the scan result - common.models.BarcodeScanResult.log_scan_result( - barcode, request, 200, response=result - ) + self.log_scan(request, result) return Response(result) @@ -399,13 +464,7 @@ def handle_barcode(self, barcode: str, request, **kwargs): # TODO: Determine the 'quantity to order' for the supplier part - common.models.BarcodeScanResult.log_scan_result( - barcode, - request, - 400 if 'error' in result else 200, - response=result, - context={**kwargs}, - ) + self.log_scan(request, result) if 'error' in result: raise ValidationError @@ -443,8 +502,6 @@ def handle_barcode(self, barcode: str, request, **kwargs): plugins = registry.with_mixin('barcode') - context = {**kwargs} - # Look for a barcode plugin which knows how to deal with this barcode plugin = None @@ -457,11 +514,7 @@ def handle_barcode(self, barcode: str, request, **kwargs): if result := internal_barcode_plugin.scan(barcode): if 'stockitem' in result: response['error'] = _('Item has already been received') - - common.models.BarcodeScanResult.log_scan_result( - barcode, request, 400, context=context, response=response - ) - + self.log_scan(request, response) raise ValidationError(response) # Now, look just for "supplier-barcode" plugins @@ -500,13 +553,7 @@ def handle_barcode(self, barcode: str, request, **kwargs): if plugin is None: response['error'] = _('No match for supplier barcode') - common.models.BarcodeScanResult.log_scan_result( - barcode, - request, - 400 if 'error' in response else 200, - context=context, - response=response, - ) + self.log_scan(request, response) if 'error' in response: raise ValidationError(response) @@ -600,9 +647,6 @@ def handle_barcode(self, barcode: str, request, *kwargs): result = self.scan_barcode(barcode, request, **kwargs) - # Context for logging - context = {**kwargs} - if 'sales_order' not in kwargs: # SalesOrder ID *must* be provided result['error'] = _('No sales order provided') @@ -617,9 +661,7 @@ def handle_barcode(self, barcode: str, request, *kwargs): result['error'] = _('Barcode does not match an existing stock item') if 'error' in result: - common.models.BarcodeScanResult.log_scan_result( - barcode, request, 400, context=context, response=result - ) + self.log_scan(request, result) raise ValidationError(result) # At this stage, we have a valid StockItem object @@ -637,10 +679,7 @@ def handle_barcode(self, barcode: str, request, *kwargs): result['error'] = str(e) if 'error' in result: - common.models.BarcodeScanResult.log_scan_result( - barcode, request, 400, context=context, response=result - ) - + self.log_scan(request, result) raise ValidationError(result) quantity = kwargs.get('quantity', None) @@ -679,13 +718,7 @@ def handle_barcode(self, barcode: str, request, *kwargs): response['error'] = _('Not enough information') response['action_required'] = True - common.models.BarcodeScanResult.log_scan_result( - barcode, - request, - 400 if 'error' in response else 200, - context=context, - response=response, - ) + self.log_scan(request, response) if 'error' in response: raise ValidationError(response) @@ -717,7 +750,7 @@ class Meta: """Meta class for the BarcodeScanResultFilter.""" model = common.models.BarcodeScanResult - fields = ['user', 'status'] + fields = ['user'] class BarcodeScanResultList(BarcodeScanResultMixin, BulkDeleteMixin, ListAPI): @@ -726,7 +759,7 @@ class BarcodeScanResultList(BarcodeScanResultMixin, BulkDeleteMixin, ListAPI): filterset_class = BarcodeScanResultFilter filter_backends = SEARCH_ORDER_FILTER - ordering_fields = ['status', 'user', 'plugin', 'timestamp', 'endpoint'] + ordering_fields = ['user', 'plugin', 'timestamp', 'endpoint'] ordering = '-timestamp' diff --git a/src/backend/InvenTree/plugin/base/barcodes/serializers.py b/src/backend/InvenTree/plugin/base/barcodes/serializers.py index 5f3f107556cc..99176cb65da6 100644 --- a/src/backend/InvenTree/plugin/base/barcodes/serializers.py +++ b/src/backend/InvenTree/plugin/base/barcodes/serializers.py @@ -26,7 +26,6 @@ class Meta: 'data', 'timestamp', 'endpoint', - 'status', 'context', 'response', 'user', diff --git a/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx b/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx index cf2ab321720c..ba77ae265a19 100644 --- a/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx +++ b/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx @@ -1,14 +1,5 @@ import { t } from '@lingui/macro'; -import { - Alert, - Badge, - Divider, - Drawer, - Group, - Stack, - Table, - Text -} from '@mantine/core'; +import { Alert, Badge, Drawer, Group, Stack, Table, Text } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; import { IconExclamationCircle } from '@tabler/icons-react'; import { useCallback, useMemo, useState } from 'react'; @@ -53,10 +44,6 @@ function BarcodeScanDetail({ scan }: { scan: any }) { {t`Endpoint`} {scan.endpoint} - - {t`Status`} - {scan.status} - {scan.response && ( @@ -172,10 +159,6 @@ export default function BarcodeScanHistoryTable() { { accessor: 'endpoint', sortable: true - }, - { - accessor: 'status', - sortable: true } ]; }, []); From 2028bd2292ba33335cbcac7a2199886fddf74e26 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 20 Sep 2024 13:55:59 +0000 Subject: [PATCH 26/44] Improve table --- .../InvenTree/plugin/base/barcodes/api.py | 5 +- .../settings/BarcodeScanHistoryTable.tsx | 86 ++++++++++++------- 2 files changed, 59 insertions(+), 32 deletions(-) diff --git a/src/backend/InvenTree/plugin/base/barcodes/api.py b/src/backend/InvenTree/plugin/base/barcodes/api.py index aa16f6bbf792..126e36db5e23 100644 --- a/src/backend/InvenTree/plugin/base/barcodes/api.py +++ b/src/backend/InvenTree/plugin/base/barcodes/api.py @@ -48,9 +48,9 @@ def log_scan(self, request, response=None): from common.models import BarcodeScanResult # Extract context data from the request - context = {**request.GET.dict(), **request.POST.dict()} + context = {**request.GET.dict(), **request.POST.dict(), **request.data} - barcode = context.pop('barcode', None) + barcode = context.pop('barcode', '') # Exit if storing barcode scans is disabled if not get_global_setting('BARCODE_STORE_RESULTS', backup=False, create=False): @@ -81,7 +81,6 @@ def log_scan(self, request, response=None): data=barcode, user=request.user, endpoint=request.path, - status=status, response=response, context=context, ) diff --git a/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx b/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx index ba77ae265a19..adf3855bd936 100644 --- a/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx +++ b/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx @@ -1,8 +1,19 @@ import { t } from '@lingui/macro'; -import { Alert, Badge, Drawer, Group, Stack, Table, Text } from '@mantine/core'; +import { + Alert, + Badge, + Divider, + Drawer, + Group, + MantineStyleProp, + Stack, + Table, + Text +} from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; import { IconExclamationCircle } from '@tabler/icons-react'; import { useCallback, useMemo, useState } from 'react'; +import { text } from 'stream/consumers'; import { CopyButton } from '../../components/buttons/CopyButton'; import { StylishText } from '../../components/items/StylishText'; @@ -25,11 +36,42 @@ import { RowDeleteAction } from '../RowActions'; * Render detail information for a particular barcode scan result. */ function BarcodeScanDetail({ scan }: { scan: any }) { + const dataStyle: MantineStyleProp = { + textWrap: 'wrap', + lineBreak: 'auto', + wordBreak: 'break-word' + }; + + const hasResponseData = useMemo(() => { + return scan.response && Object.keys(scan.response).length > 0; + }, [scan.response]); + + const hasContextData = useMemo(() => { + return scan.context && Object.keys(scan.context).length > 0; + }, [scan.context]); + return ( <> + + + + {t`Barcode Information`} + + + + {t`Barcode`} + + + {scan.data} + + + + + + {t`Timestamp`} {scan.timestamp} @@ -44,59 +86,45 @@ function BarcodeScanDetail({ scan }: { scan: any }) { {t`Endpoint`} {scan.endpoint} - {scan.response && ( + {hasContextData && ( - {t`Response`} + {t`Context`} )} - {scan.response && - Object.keys(scan.response).map((key) => ( + {hasContextData && + Object.keys(scan.context).map((key) => ( {key} - - {scan.response[key]} + + {scan.context[key]} - + ))} - {scan.context && ( + {hasResponseData && ( - {t`Context`} + {t`Response`} )} - {scan.context && - Object.keys(scan.context).map((key) => ( + {hasResponseData && + Object.keys(scan.response).map((key) => ( {key} - - {scan.context[key]} + + {scan.response[key]} - + ))} From be9b2f1d1b29fe1c79cdb557cc2ce039a6e92217 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 20 Sep 2024 14:19:05 +0000 Subject: [PATCH 27/44] Updates --- src/frontend/src/components/modals/AboutInvenTreeModal.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/frontend/src/components/modals/AboutInvenTreeModal.tsx b/src/frontend/src/components/modals/AboutInvenTreeModal.tsx index aecbcb60ad17..8581506fac1c 100644 --- a/src/frontend/src/components/modals/AboutInvenTreeModal.tsx +++ b/src/frontend/src/components/modals/AboutInvenTreeModal.tsx @@ -1,4 +1,4 @@ -import { Trans } from '@lingui/macro'; +import { Trans, t } from '@lingui/macro'; import { Anchor, Badge, @@ -178,10 +178,7 @@ export function AboutInvenTreeModal({
- Copy version information} - /> +