diff --git a/.github/workflows/qc_checks.yaml b/.github/workflows/qc_checks.yaml index be5522f39a..78d979af3b 100644 --- a/.github/workflows/qc_checks.yaml +++ b/.github/workflows/qc_checks.yaml @@ -165,7 +165,7 @@ jobs: pip install --require-hashes -r docs/requirements.txt python docs/ci/check_mkdocs_config.py - name: Check Links - uses: gaurav-nelson/github-action-markdown-link-check@5c5dfc0ac2e225883c0e5f03a85311ec2830d368 # pin@v1 + uses: tcort/github-action-markdown-link-check@e7c7a18363c842693fadde5d41a3bd3573a7a225 # pin@v1 with: folder-path: docs config-file: docs/mlc_config.json @@ -726,6 +726,11 @@ jobs: - name: Install Playwright OS dependencies if: steps.playwright-cache.outputs.cache-hit == 'true' run: cd src/frontend && npx playwright install-deps + - name: Install Sample Plugin + run: | + pip install -U inventree-plugin-creator + create-inventree-plugin --default + cd MyCustomPlugin && pip install -e . && cd frontend && npm install && npm run translate && npm run build - name: Run Playwright tests id: tests run: | diff --git a/contrib/container/gunicorn.conf.py b/contrib/container/gunicorn.conf.py index 682f75b50d..9f95c0e53a 100644 --- a/contrib/container/gunicorn.conf.py +++ b/contrib/container/gunicorn.conf.py @@ -19,7 +19,7 @@ threads = 4 # Worker timeout (default = 90 seconds) -timeout = os.environ.get('INVENTREE_GUNICORN_TIMEOUT', '90') +timeout = int(os.environ.get('INVENTREE_GUNICORN_TIMEOUT', '90')) # Number of worker processes workers = os.environ.get('INVENTREE_GUNICORN_WORKERS', None) @@ -43,7 +43,13 @@ preload_app = True def post_fork(server, worker): - """Post-fork hook to set up logging for each worker.""" + """Post-fork hook called after each worker process is forked.""" + from django.db import connections + + # Close any DB connections inherited from the master process — PostgreSQL + # connections are not fork-safe, so each worker must open its own. + connections.close_all() + from django.conf import settings if not settings.TRACING_ENABLED: diff --git a/contrib/dev_reqs/requirements.txt b/contrib/dev_reqs/requirements.txt index 1c07b0fd79..507269542f 100644 --- a/contrib/dev_reqs/requirements.txt +++ b/contrib/dev_reqs/requirements.txt @@ -1,8 +1,8 @@ # This file was autogenerated by uv via the following command: # uv pip compile contrib/dev_reqs/requirements.in -o contrib/dev_reqs/requirements.txt -c src/backend/requirements.txt -certifi==2026.4.22 \ - --hash=sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a \ - --hash=sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580 +certifi==2026.5.20 \ + --hash=sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897 \ + --hash=sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d # via # -c src/backend/requirements.txt # requests @@ -139,9 +139,9 @@ charset-normalizer==3.4.7 \ # via # -c src/backend/requirements.txt # requests -idna==3.15 \ - --hash=sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8 \ - --hash=sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc +idna==3.16 \ + --hash=sha256:cc246e3a3f89580c3a951b5ad298ca4638078b2cdd4f115654332b5c26daded5 \ + --hash=sha256:d7a6da03db833450fca25d2358ac9ff06cd624577a4aea3a596d5c0f77b8e03d # via # -c src/backend/requirements.txt # requests diff --git a/docs/docs/app/barcode.md b/docs/docs/app/barcode.md index 2645287f76..21eebb315f 100644 --- a/docs/docs/app/barcode.md +++ b/docs/docs/app/barcode.md @@ -67,6 +67,8 @@ Transfer the currently selected stock location into another location. Scanning a Receive incoming purchase order items into the selected location. Scanning a *new* barcode which is associated with an item in an incoming purchase order will receive the item into the selected location. +*Note: Both purchase order number and supplier SKU's are required to be found on the barcode for this function to find the associated line item. Missing one will lead to an error.* + #### Scan Items Into Location the *Scan Items Into Location* action allows you to scan items into the selected location. Scanning a valid barcode associated with a stock item (already in the database) will result in that item being transferred to the selected location. @@ -105,6 +107,8 @@ From the [Purchase Order detail page](./po.md#purchase-order-detail) page, the f Receive incoming purchase order items against the selected purchase order. Scanning a *new* barcode which is associated with an item in an incoming purchase order will receive the item into stock. +*Note: supplier SKU's are required to be found on the barcode for this function to find the associated line item.* + ### Sales Order Actions The following barcode actions are available for [Sales Orders](./so.md): diff --git a/docs/docs/settings/user.md b/docs/docs/settings/user.md index fc8a1664cb..bc6919c780 100644 --- a/docs/docs/settings/user.md +++ b/docs/docs/settings/user.md @@ -29,6 +29,7 @@ The *Display Settings* screen shows general display configuration options: {{ usersetting("SHOW_FULL_CATEGORY_IN_TABLES")}} {{ usersetting("SHOW_BOM_SUBASSEMBLY_LEVELS")}} {{ usersetting("ENABLE_LAST_BREADCRUMB") }} +{{ usersetting("SHOW_EXTRA_MODEL_INFO") }} {{ usersetting("SHOW_FULL_LOCATION_IN_TABLES") }} {{ usersetting("DISPLAY_ITEMS_FINAL_LEVEL") }} diff --git a/docs/docs/start/config.md b/docs/docs/start/config.md index 25b7fd6639..ffff05e39d 100644 --- a/docs/docs/start/config.md +++ b/docs/docs/start/config.md @@ -256,6 +256,9 @@ The following database options can be configured: {{ configsetting("INVENTREE_DB_PASSWORD") }} Database password (if required) | {{ configsetting("INVENTREE_DB_HOST") }} Database host address (if required) | {{ configsetting("INVENTREE_DB_PORT") }} Database host port (if required) | +{{ configsetting("INVENTREE_DB_CONN_MAX_AGE") }} Database connection max age (s) | +{{ configsetting("INVENTREE_DB_CONN_HEALTH_CHECKS") }} Enable database connection health checks | + {{ configsetting("INVENTREE_DB_OPTIONS") }} Additional database options (as a JSON object) | !!! tip "Database Password" @@ -266,11 +269,12 @@ The following database options can be configured: If running with a PostgreSQL database backend, the following additional options are available: {{ configtable() }} -{{ configsetting("INVENTREE_DB_TIMEOUT", default="2") }} Database connection timeout (s) | +{{ configsetting("INVENTREE_DB_TIMEOUT", default="10") }} Database connection timeout (s) | | `INVENTREE_DB_TCP_KEEPALIVES` | database.tcp_keepalives | 1 | TCP keepalive | -| `INVENTREE_DB_TCP_KEEPALIVES_IDLE` | database.tcp_keepalives_idle | 1 | Idle TCP keepalive | -| `INVENTREE_DB_TCP_KEEPALIVES_INTERVAL` | database.tcp_keepalives_interval | 1| TCP keepalive interval | +| `INVENTREE_DB_TCP_KEEPALIVES_IDLE` | database.tcp_keepalives_idle | 5 | Idle TCP keepalive | +| `INVENTREE_DB_TCP_KEEPALIVES_INTERVAL` | database.tcp_keepalives_interval | 5 | TCP keepalive interval | | `INVENTREE_DB_TCP_KEEPALIVES_COUNT` | database.tcp_keepalives_count | 5 | TCP keepalive count | +| `INVENTREE_DB_TCP_USER_TIMEOUT` | database.tcp_user_timeout | 2000 | TCP user timeout (ms) | | `INVENTREE_DB_ISOLATION_SERIALIZABLE` | database.serializable | False | Database isolation level configured to "serializable" | ### MySQL Settings @@ -278,6 +282,7 @@ If running with a PostgreSQL database backend, the following additional options If running with a MySQL database backend, the following additional options are available: {{ configtable() }} +{{ configsetting("INVENTREE_DB_TIMEOUT", default="10") }} Database connection timeout (s) | | `INVENTREE_DB_ISOLATION_SERIALIZABLE` | database.serializable | False | Database isolation level configured to "serializable" | ### SQLite Settings diff --git a/docs/requirements.txt b/docs/requirements.txt index 98c237c7e4..7417a624c0 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -28,9 +28,9 @@ bracex==2.6 \ --hash=sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952 \ --hash=sha256:98f1347cd77e22ee8d967a30ad4e310b233f7754dbf31ff3fceb76145ba47dc7 # via wcmatch -certifi==2026.4.22 \ - --hash=sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a \ - --hash=sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580 +certifi==2026.5.20 \ + --hash=sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897 \ + --hash=sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d # via # -c src/backend/requirements.txt # httpcore @@ -169,9 +169,9 @@ charset-normalizer==3.4.7 \ # via # -c src/backend/requirements.txt # requests -click==8.3.3 \ - --hash=sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2 \ - --hash=sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613 +click==8.4.1 \ + --hash=sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2 \ + --hash=sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96 # via # mkdocs # neoteroi-mkdocs @@ -227,9 +227,9 @@ httpx==0.28.1 \ --hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \ --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad # via neoteroi-mkdocs -idna==3.15 \ - --hash=sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8 \ - --hash=sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc +idna==3.16 \ + --hash=sha256:cc246e3a3f89580c3a951b5ad298ca4638078b2cdd4f115654332b5c26daded5 \ + --hash=sha256:d7a6da03db833450fca25d2358ac9ff06cd624577a4aea3a596d5c0f77b8e03d # via # -c src/backend/requirements.txt # anyio diff --git a/src/backend/InvenTree/InvenTree/admin.py b/src/backend/InvenTree/InvenTree/admin.py index 2655794159..637e77a10f 100644 --- a/src/backend/InvenTree/InvenTree/admin.py +++ b/src/backend/InvenTree/InvenTree/admin.py @@ -2,7 +2,12 @@ from django.contrib import admin from django.http.request import HttpRequest +from django.utils import timezone +from allauth.usersessions.admin import UserSessionAdmin +from allauth.usersessions.models import UserSession +from django_q.admin import ScheduleAdmin +from django_q.models import Schedule from djmoney.contrib.exchange.admin import RateAdmin from djmoney.contrib.exchange.models import Rate @@ -17,3 +22,51 @@ class CustomRateAdmin(RateAdmin): admin.site.unregister(Rate) admin.site.register(Rate, CustomRateAdmin) + + +def run_schedule_now(modeladmin, request, queryset): + """Immediately queue the selected scheduled tasks for execution.""" + queryset.update(next_run=timezone.now()) + count = queryset.count() + modeladmin.message_user(request, f'{count} task(s) queued for immediate execution.') + + +run_schedule_now.short_description = 'Run selected tasks now' + + +class ReadOnlyScheduleAdmin(ScheduleAdmin): + """Read-only admin interface for django-q Schedule objects.""" + + actions = [run_schedule_now] + + def has_add_permission(self, request: HttpRequest) -> bool: + """Prevent adding new Schedule objects.""" + return False + + def has_change_permission(self, request: HttpRequest, obj=None) -> bool: + """Prevent changing existing Schedule objects.""" + return False + + def has_delete_permission(self, request: HttpRequest, obj=None) -> bool: + """Prevent deleting Schedule objects.""" + return False + + +admin.site.unregister(Schedule) +admin.site.register(Schedule, ReadOnlyScheduleAdmin) + + +class InvenTreeUserSessionAdmin(UserSessionAdmin): + """Admin interface for UserSession - view and delete only, no add or edit.""" + + def has_add_permission(self, request: HttpRequest) -> bool: + """Prevent creating sessions via admin.""" + return False + + def has_change_permission(self, request: HttpRequest, obj=None) -> bool: + """Prevent editing sessions via admin.""" + return False + + +admin.site.unregister(UserSession) +admin.site.register(UserSession, InvenTreeUserSessionAdmin) diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 061c60ea48..4fcc2b9b2b 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,16 +1,23 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 498 +INVENTREE_API_VERSION = 500 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ -v498 -> 2026-06-01 : https://github.com/inventree/InvenTree/pull/12022 + +v500 -> 2026-06-01 : https://github.com/inventree/InvenTree/pull/12022 - Adds optional "merge" field to each item in the Stock Transfer API endpoint - When merge is enabled, transferred stock is combined into compatible existing stock at the destination - Stock merge tracking entries now include an "added" delta field +v499 -> 2026-06-01 : https://github.com/inventree/InvenTree/pull/12057 + - Fixes search field issues on the BarcodeScanHistory API endpoint + +v498 -> 2026-05-31 : https://github.com/inventree/InvenTree/pull/12055 + - Updates the "status_text" field for models which support custom status values + v497 -> 2026-05-27 : https://github.com/inventree/InvenTree/pull/12019 - Adds "location" field to StockCount API endpoint diff --git a/src/backend/InvenTree/InvenTree/serializers.py b/src/backend/InvenTree/InvenTree/serializers.py index 3df76e9e3c..fc9073dd0b 100644 --- a/src/backend/InvenTree/InvenTree/serializers.py +++ b/src/backend/InvenTree/InvenTree/serializers.py @@ -764,6 +764,56 @@ class InvenTreeDecimalField(serializers.FloatField): raise serializers.ValidationError(_('Invalid value')) +class CustomStatusSerializerMixin(serializers.Serializer): + """Serializer mixin for models that support custom status values. + + Provides a `status_text` SerializerMethodField that resolves custom + status labels with a single database query per model per serializer + context (i.e. one query for a whole list page) rather than one query per + object (N+1). + """ + + status_text = serializers.SerializerMethodField() + + @extend_schema_field(serializers.CharField(allow_null=True)) + def get_status_text(self, instance) -> Optional[str]: + """Return the human-readable status text for the instance. + + Uses a per-context cache keyed by model name so that all objects in a + single serialization pass share one DB hit for custom label lookup. + + During write operations DRF may call to_representation on the raw + validated_data dict rather than a model instance (e.g. when building + response headers). Return None in that case — the response body is + always produced from a real instance via a separate serializer call. + """ + if not hasattr(instance, 'get_custom_status'): + return None + + custom_key = instance.get_custom_status() + + if custom_key is None: + return instance.status_class.label(instance.get_status()) + + model_name = instance._meta.model_name + cache_key = f'_custom_status_labels_{model_name}' + + # Cache a dict of custom status labels for this model, if not already cached + if cache_key not in self.context: + from common.models import InvenTreeCustomUserStateModel + + self.context[cache_key] = { + obj.key: obj.label + for obj in InvenTreeCustomUserStateModel.objects.filter( + model__model=model_name + ) + } + + return self.context[cache_key].get( + custom_key, instance.status_class.label(instance.get_status()) + ) + + class NotesFieldMixin: """Serializer mixin for handling 'notes' fields. diff --git a/src/backend/InvenTree/InvenTree/setting/db_backend.py b/src/backend/InvenTree/InvenTree/setting/db_backend.py index 3cedea07d4..6defd52374 100644 --- a/src/backend/InvenTree/InvenTree/setting/db_backend.py +++ b/src/backend/InvenTree/InvenTree/setting/db_backend.py @@ -5,10 +5,23 @@ from pathlib import Path import structlog from InvenTree.config import get_boolean_setting, get_config_value, get_setting +from InvenTree.ready import isInWorkerThread logger = structlog.get_logger('inventree') +def _get_conn_max_age() -> int | None: + """Return the configured CONN_MAX_AGE value. + + Accepts an integer number of seconds, or 'None' for unlimited persistence. + Defaults to 0 (close the connection after each request). + """ + value = get_setting('INVENTREE_DB_CONN_MAX_AGE', 'database.conn_max_age', 0) + if value is None or str(value).strip().lower() == 'none': + return None + return int(value) + + def get_db_backend(): """Return the database backend configuration.""" # For the core database configuration values, we test for UPPERCASE configuration values as a backup, @@ -43,6 +56,13 @@ def get_db_backend(): ) or get_config_value('database.OPTIONS') or {}, + # Seconds to keep idle connections open across requests (0 = close after each request). + # Set to None for unlimited. Enable CONN_HEALTH_CHECKS alongside any non-zero value + # so stale connections are detected before use rather than causing request failures. + 'CONN_MAX_AGE': _get_conn_max_age(), + 'CONN_HEALTH_CHECKS': get_boolean_setting( + 'INVENTREE_DB_CONN_HEALTH_CHECKS', 'database.conn_health_checks', False + ), } # Check for required keys @@ -113,9 +133,9 @@ def set_postgres_options(db_options: dict): if 'connect_timeout' not in db_options: # The DB server is in the same data center, it should not take very # long to connect to the database server - # # seconds, 2 is minimum allowed by libpq - db_options['connect_timeout'] = int( - get_setting('INVENTREE_DB_TIMEOUT', 'database.timeout', 2) + # Note: 2 seconds is minimum allowed by libpq + db_options['connect_timeout'] = max( + 2, int(get_setting('INVENTREE_DB_TIMEOUT', 'database.timeout', 10)) ) # Setup TCP keepalive @@ -133,7 +153,7 @@ def set_postgres_options(db_options: dict): if 'keepalives_idle' not in db_options: db_options['keepalives_idle'] = int( get_setting( - 'INVENTREE_DB_TCP_KEEPALIVES_IDLE', 'database.tcp_keepalives_idle', 1 + 'INVENTREE_DB_TCP_KEEPALIVES_IDLE', 'database.tcp_keepalives_idle', 5 ) ) @@ -143,7 +163,7 @@ def set_postgres_options(db_options: dict): get_setting( 'INVENTREE_DB_TCP_KEEPALIVES_INTERVAL', 'database.tcp_keepalives_interval', - '1', + '5', ) ) @@ -157,10 +177,13 @@ def set_postgres_options(db_options: dict): ) ) - # # Milliseconds for how long pending data should remain unacked - # by the remote server - # TODO: Supported starting in PSQL 11 - # "tcp_user_timeout": int(os.getenv("PGTCP_USER_TIMEOUT", "1000"), + # # Milliseconds for how long pending data should remain unacked by the remote server + if 'tcp_user_timeout' not in db_options: + db_options['tcp_user_timeout'] = int( + get_setting( + 'INVENTREE_DB_TCP_USER_TIMEOUT', 'database.tcp_user_timeout', '2000' + ) + ) # Postgres's default isolation level is Read Committed which is # normally fine, but most developers think the database server is @@ -178,10 +201,21 @@ def set_postgres_options(db_options: dict): else IsolationLevel.READ_COMMITTED ) + # Specify the application name for the database connection + # This can be useful for debugging and monitoring purposes + if 'application_name' not in db_options: + db_options['application_name'] = ( + 'inventree-worker' if isInWorkerThread() else 'inventree-server' + ) + def set_mysql_options(db_options: dict): """Set database options specific to mysql backend.""" - # TODO TCP time outs and keepalives + # Timeout values + if 'connect_timeout' not in db_options: + db_options['connect_timeout'] = int( + get_setting('INVENTREE_DB_TIMEOUT', 'database.timeout', 10) + ) # MariaDB's default isolation level is Repeatable Read which is # normally fine, but most developers think the database server is diff --git a/src/backend/InvenTree/InvenTree/settings.py b/src/backend/InvenTree/InvenTree/settings.py index 3aa32669be..3b06820c98 100644 --- a/src/backend/InvenTree/InvenTree/settings.py +++ b/src/backend/InvenTree/InvenTree/settings.py @@ -56,7 +56,11 @@ INVENTREE_BASE_URL = 'https://inventree.org' INVENTREE_NEWS_URL = f'{INVENTREE_BASE_URL}/news/feed.atom' # Determine if we are running in "test" mode e.g. "manage.py test" -TESTING = 'test' in sys.argv or 'TESTING' in os.environ or 'pytest' in sys.argv +TESTING = ( + 'test' in sys.argv + or 'TESTING' in os.environ + or any('pytest' in arg for arg in sys.argv) +) if TESTING: # Use a weaker password hasher for testing (improves testing speed) diff --git a/src/backend/InvenTree/InvenTree/tasks.py b/src/backend/InvenTree/InvenTree/tasks.py index c231bf2197..729dd007b7 100644 --- a/src/backend/InvenTree/InvenTree/tasks.py +++ b/src/backend/InvenTree/InvenTree/tasks.py @@ -282,40 +282,31 @@ def offload_task( # function was passed - use that _func = taskname else: - # Split path + # Split on the last dot: everything before is the module path, + # everything after is the function name. rsplit handles any depth + # (e.g. 'app.module.func' or 'app.sub.module.func'). try: - app, mod, func = taskname.split('.') - app_mod = app + '.' + mod + module_path, func_name = taskname.rsplit('.', 1) except ValueError: raise_warning( f"WARNING: '{taskname}' not started - Malformed function path" ) return False - # Import module from app try: - _mod = importlib.import_module(app_mod) + _mod = importlib.import_module(module_path) except ModuleNotFoundError: log_error('offload_task', scope='worker') raise_warning( - f"WARNING: '{taskname}' not started - No module named '{app_mod}'" + f"WARNING: '{taskname}' not started - No module named '{module_path}'" ) return False - # Retrieve function - try: - _func = getattr(_mod, func) - except AttributeError: # pragma: no cover - # getattr does not work for local import - _func = None - - try: - if not _func: - _func = eval(func) # pragma: no cover - except NameError: + _func = getattr(_mod, func_name, None) + if _func is None: log_error('offload_task', scope='worker') raise_warning( - f"WARNING: '{taskname}' not started - No function named '{func}'" + f"WARNING: '{taskname}' not started - No function named '{func_name}'" ) return False diff --git a/src/backend/InvenTree/InvenTree/test_helpers/__init__.py b/src/backend/InvenTree/InvenTree/test_helpers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/backend/InvenTree/InvenTree/test_helpers/sample_tasks.py b/src/backend/InvenTree/InvenTree/test_helpers/sample_tasks.py new file mode 100644 index 0000000000..39f16907f8 --- /dev/null +++ b/src/backend/InvenTree/InvenTree/test_helpers/sample_tasks.py @@ -0,0 +1,6 @@ +"""Sample task functions used by offload_task unit tests.""" + + +def get_result(): + """Return a fixed value; used to verify 4-part module paths resolve correctly.""" + return 'abc' diff --git a/src/backend/InvenTree/InvenTree/test_tasks.py b/src/backend/InvenTree/InvenTree/test_tasks.py index 04df009198..0638c51d9b 100644 --- a/src/backend/InvenTree/InvenTree/test_tasks.py +++ b/src/backend/InvenTree/InvenTree/test_tasks.py @@ -67,26 +67,72 @@ class InvenTreeTaskTests(PluginRegistryMixin, TestCase): # Run with string ref InvenTree.tasks.offload_task('InvenTree.test_tasks.get_result') - # Error runs + # The error-path tests below all use force_sync=True so that the sync + # resolution code is exercised regardless of whether background workers + # happen to be running in this environment. + # Malformed taskname with self.assertWarnsMessage( UserWarning, "WARNING: 'InvenTree' not started - Malformed function path" ): - InvenTree.tasks.offload_task('InvenTree') + InvenTree.tasks.offload_task('InvenTree', force_sync=True) # Non existent app with self.assertWarnsMessage( UserWarning, "WARNING: 'InvenTreeABC.test_tasks.doesnotmatter' not started - No module named 'InvenTreeABC.test_tasks'", ): - InvenTree.tasks.offload_task('InvenTreeABC.test_tasks.doesnotmatter') + InvenTree.tasks.offload_task( + 'InvenTreeABC.test_tasks.doesnotmatter', force_sync=True + ) # Non existent function with self.assertWarnsMessage( UserWarning, "WARNING: 'InvenTree.test_tasks.doesnotexist' not started - No function named 'doesnotexist'", ): - InvenTree.tasks.offload_task('InvenTree.test_tasks.doesnotexist') + InvenTree.tasks.offload_task( + 'InvenTree.test_tasks.doesnotexist', force_sync=True + ) + + def test_offloading_deep_path(self): + """Test that task paths with more than 3 components are resolved correctly. + + Previously the sync fallback split on '.' requiring exactly 3 parts, + so a 4-part path raised ValueError and returned False with no warning. + Now rsplit('.', 1) is used, so any depth works. + """ + # 4-part valid path: module = 'InvenTree.test_helpers.sample_tasks', func = 'get_result' + InvenTree.tasks.offload_task( + 'InvenTree.test_helpers.sample_tasks.get_result', force_sync=True + ) + + # 4-part path with a non-existent module — should warn cleanly rather than crash + with self.assertWarnsMessage( + UserWarning, + "WARNING: 'InvenTreeABC.sub.tasks.doesnotmatter' not started - No module named 'InvenTreeABC.sub.tasks'", + ): + InvenTree.tasks.offload_task( + 'InvenTreeABC.sub.tasks.doesnotmatter', force_sync=True + ) + + def test_offloading_no_eval_bypass(self): + """Verify that names in tasks.py scope cannot be resolved via an eval bypass. + + The previous implementation fell back to eval(func) when getattr returned + None, evaluating the function name in the local scope of offload_task rather + than in the target module. This meant a Python builtin like 'eval', or any + name imported into tasks.py, could be invoked via a crafted taskname. The + replacement uses only getattr on the imported module and rejects anything + not found there. + """ + # 'eval' is a Python builtin; it is not an attribute of InvenTree.test_tasks. + # Previously eval('eval') in tasks.py's scope would resolve it; now it fails. + with self.assertWarnsMessage( + UserWarning, + "WARNING: 'InvenTree.test_tasks.eval' not started - No function named 'eval'", + ): + InvenTree.tasks.offload_task('InvenTree.test_tasks.eval', force_sync=True) def test_task_heartbeat(self): """Test the task heartbeat.""" diff --git a/src/backend/InvenTree/InvenTree/tests.py b/src/backend/InvenTree/InvenTree/tests.py index 977834b450..e0977075d5 100644 --- a/src/backend/InvenTree/InvenTree/tests.py +++ b/src/backend/InvenTree/InvenTree/tests.py @@ -1488,7 +1488,7 @@ class TestOffloadTask(InvenTreeTestCase): offload_task('dummy_task.numbers', 1, 1, 1, force_sync=True) ) - self.assertIn('Malformed function path', str(log.output)) + self.assertIn("No module named \\'dummy_task\\'", str(log.output)) # Offload dummy task with a Part instance # This should succeed, ensuring that the Part instance is correctly pickled diff --git a/src/backend/InvenTree/InvenTree/tracing.py b/src/backend/InvenTree/InvenTree/tracing.py index f97da6c0eb..77122903e7 100644 --- a/src/backend/InvenTree/InvenTree/tracing.py +++ b/src/backend/InvenTree/InvenTree/tracing.py @@ -50,12 +50,17 @@ def setup_tracing( """ if InvenTree.ready.isImportingData() or InvenTree.ready.isRunningMigrations(): return + if endpoint is None or headers is None: print( 'Tracing endpoint or headers not specified - skipping tracing setup' ) # pragma: no cover return # pragma: no cover + # check if trace is already set up - if so, skip + if trace.get_tracer_provider() is not None: + return + # Logger configuration logger = logging.getLogger('inventree') @@ -163,29 +168,34 @@ def setup_tracing( def setup_instruments(db_engine: str): # pragma: no cover """Run auto-instrumentation for OpenTelemetry tracing.""" - DjangoInstrumentor().instrument() - RedisInstrumentor().instrument() - RequestsInstrumentor().instrument() - SystemMetricsInstrumentor().instrument() + if not DjangoInstrumentor()._is_instrumented_by_opentelemetry: + DjangoInstrumentor().instrument() + RedisInstrumentor().instrument() + RequestsInstrumentor().instrument() + SystemMetricsInstrumentor().instrument() db_engine = str(db_engine).lower().strip() # DBs if 'sqlite' in db_engine: - SQLite3Instrumentor().instrument() + inst = SQLite3Instrumentor() + if not inst._is_instrumented_by_opentelemetry: + inst.instrument() elif 'postgresql' in db_engine: try: from opentelemetry.instrumentation.psycopg import PsycopgInstrumentor - PsycopgInstrumentor().instrument( - enable_commenter=False, commenter_options={} - ) + inst = PsycopgInstrumentor() + if not inst._is_instrumented_by_opentelemetry: + inst.instrument(enable_commenter=False, commenter_options={}) except ModuleNotFoundError: pass elif 'mysql' in db_engine: try: from opentelemetry.instrumentation.pymysql import PyMySQLInstrumentor - PyMySQLInstrumentor().instrument() + inst = PyMySQLInstrumentor() + if not inst._is_instrumented_by_opentelemetry: + inst.instrument() except ModuleNotFoundError: pass diff --git a/src/backend/InvenTree/build/serializers.py b/src/backend/InvenTree/build/serializers.py index 888ad883bd..0db1cc0b1b 100644 --- a/src/backend/InvenTree/build/serializers.py +++ b/src/backend/InvenTree/build/serializers.py @@ -32,6 +32,7 @@ from common.settings import get_global_setting from generic.states.fields import InvenTreeCustomStatusSerializerMixin from InvenTree.mixins import DataImportExportSerializerMixin from InvenTree.serializers import ( + CustomStatusSerializerMixin, FilterableSerializerMixin, InvenTreeDecimalField, InvenTreeModelSerializer, @@ -53,6 +54,7 @@ from .status_codes import BuildStatus class BuildSerializer( + CustomStatusSerializerMixin, FilterableSerializerMixin, NotesFieldMixin, DataImportExportSerializerMixin, @@ -116,8 +118,6 @@ class BuildSerializer( level = serializers.IntegerField(label=_('Build Level'), read_only=True) - status_text = serializers.CharField(source='get_status_display', read_only=True) - part_detail = OptionalField( serializer_class=part_serializers.PartBriefSerializer, serializer_kwargs={'source': 'part', 'many': False, 'read_only': True}, diff --git a/src/backend/InvenTree/build/test_api.py b/src/backend/InvenTree/build/test_api.py index 5ba8824b3a..6050774e80 100644 --- a/src/backend/InvenTree/build/test_api.py +++ b/src/backend/InvenTree/build/test_api.py @@ -1817,6 +1817,97 @@ class BuildConsumeTest(BuildAPITest): self.assertEqual(line.consumed, 100) +class BuildCustomStatusTest(BuildAPITest): + """Tests for custom status values on Build orders.""" + + url = reverse('api-build-list') + + def test_custom_status_query_count(self): + """Test that listing Build orders with custom statuses does not cause N+1 queries. + + Ensures that resolving 'status_text' for custom status values is O(1) + in database queries, not O(N) relative to the number of results. + """ + from django.contrib.contenttypes.models import ContentType + + from common.models import InvenTreeCustomUserStateModel + + build_content_type = ContentType.objects.get_for_model(Build) + + # 10 custom status values - different keys, labels, and logical_keys + logical_keys = [ + BuildStatus.PENDING.value, + BuildStatus.PRODUCTION.value, + BuildStatus.ON_HOLD.value, + BuildStatus.CANCELLED.value, + BuildStatus.COMPLETE.value, + BuildStatus.PENDING.value, + BuildStatus.PRODUCTION.value, + BuildStatus.ON_HOLD.value, + BuildStatus.CANCELLED.value, + BuildStatus.COMPLETE.value, + ] + + custom_statuses = [ + InvenTreeCustomUserStateModel.objects.create( + key=3000 + i, + name=f'BuildCustomStatus{i}', + label=f'Build Custom Status Label {i}', + color='secondary', + logical_key=logical_keys[i], + model=build_content_type, + reference_status='BuildStatus', + ) + for i in range(10) + ] + + part = Part.objects.filter(assembly=True).first() + + # Build is an MPTT tree model; bulk_create requires tree fields to be + # populated manually. All new orders are root nodes (no parent) so each + # gets its own unique tree_id. + from django.db.models import Max + + next_tree_id = (Build.objects.aggregate(m=Max('tree_id'))['m'] or 0) + 1 + + # Create 100 build orders, cycling through the 10 custom statuses + Build.objects.bulk_create([ + Build( + part=part, + reference=f'BO-QTEST-{i}', + quantity=1, + status=custom_statuses[i % 10].logical_key, + status_custom_key=custom_statuses[i % 10].key, + lft=1, + rght=2, + level=0, + tree_id=next_tree_id + i, + ) + for i in range(100) + ]) + + # Lookup: custom_key -> custom_status_object, for quick per-row assertions + custom_lookup = {cs.key: cs for cs in custom_statuses} + + # Query count must stay below the fixed threshold regardless of limit. + # An N+1 bug would push limit=50 or limit=100 well over the threshold. + for limit in [1, 10, 50, 100]: + response = self.get( + self.url, data={'limit': limit}, expected_code=200, max_query_count=50 + ) + + for result in response.data['results']: + cs = custom_lookup.get(result['status_custom_key']) + + if cs is None: + # Build from fixtures - no custom status assigned + continue + + self.assertEqual(result['status'], cs.logical_key) + self.assertEqual(result['status_custom_key'], cs.key) + self.assertEqual(result['status_text'], cs.label) + + class BuildAutoAllocateAPITest(InvenTreeAPITestCase): """API integration tests for BuildAutoAllocate endpoint back-ports (stock_sort_by, build_lines).""" diff --git a/src/backend/InvenTree/common/admin.py b/src/backend/InvenTree/common/admin.py index 5703423afc..e35d269f0b 100644 --- a/src/backend/InvenTree/common/admin.py +++ b/src/backend/InvenTree/common/admin.py @@ -79,21 +79,37 @@ class AttachmentAdmin(admin.ModelAdmin): @admin.register(common.models.DataOutput) class DataOutputAdmin(admin.ModelAdmin): - """Admin interface for DataOutput objects.""" + """Admin interface for DataOutput objects - view and delete only.""" list_display = ('user', 'created', 'output_type', 'output') list_filter = ('user', 'output_type') + def has_add_permission(self, request): + """Prevent addition of new DataOutput objects via the admin interface.""" + return False + + def has_change_permission(self, request, obj=None): + """Prevent modification of DataOutput objects via the admin interface.""" + return False + @admin.register(common.models.BarcodeScanResult) class BarcodeScanResultAdmin(admin.ModelAdmin): - """Admin interface for BarcodeScanResult objects.""" + """Admin interface for BarcodeScanResult objects - read-only audit log.""" list_display = ('data', 'timestamp', 'user', 'endpoint', 'result') list_filter = ('user', 'endpoint', 'result') + def has_add_permission(self, request): + """Prevent addition of new BarcodeScanResult objects via the admin interface.""" + return False + + def has_change_permission(self, request, obj=None): + """Prevent modification of BarcodeScanResult objects via the admin interface.""" + return False + @admin.register(common.models.ProjectCode) class ProjectCodeAdmin(admin.ModelAdmin): @@ -139,14 +155,22 @@ class WebhookAdmin(admin.ModelAdmin): @admin.register(common.models.NotificationEntry) class NotificationEntryAdmin(admin.ModelAdmin): - """Admin settings for NotificationEntry.""" + """Admin settings for NotificationEntry - view and delete only.""" list_display = ('key', 'uid', 'updated') + def has_add_permission(self, request): + """Prevent addition of new NotificationEntry objects via the admin interface.""" + return False + + def has_change_permission(self, request, obj=None): + """Prevent modification of NotificationEntry objects via the admin interface.""" + return False + @admin.register(common.models.NotificationMessage) class NotificationMessageAdmin(admin.ModelAdmin): - """Admin settings for NotificationMessage.""" + """Admin settings for NotificationMessage - view and delete only.""" list_display = ( 'age_human', @@ -162,14 +186,46 @@ class NotificationMessageAdmin(admin.ModelAdmin): search_fields = ('name', 'category', 'message') + def has_add_permission(self, request): + """Prevent addition of new NotificationMessage objects via the admin interface.""" + return False + + def has_change_permission(self, request, obj=None): + """Prevent modification of NotificationMessage objects via the admin interface.""" + return False + @admin.register(common.models.NewsFeedEntry) class NewsFeedEntryAdmin(admin.ModelAdmin): - """Admin settings for NewsFeedEntry.""" + """Admin settings for NewsFeedEntry - view and delete only.""" list_display = ('title', 'author', 'published', 'summary') + def has_add_permission(self, request): + """Prevent addition of new NewsFeedEntry objects via the admin interface.""" + return False -admin.site.register(common.models.WebhookMessage, admin.ModelAdmin) -admin.site.register(common.models.EmailMessage, admin.ModelAdmin) -admin.site.register(common.models.EmailThread, admin.ModelAdmin) + def has_change_permission(self, request, obj=None): + """Prevent modification of NewsFeedEntry objects via the admin interface.""" + return False + + +class ReadOnlyAdmin(admin.ModelAdmin): + """Base admin class that prevents all modifications.""" + + def has_add_permission(self, request): + """Prevent addition of new objects via the admin interface.""" + return False + + def has_change_permission(self, request, obj=None): + """Prevent modification of objects via the admin interface.""" + return False + + def has_delete_permission(self, request, obj=None): + """Prevent deletion of objects via the admin interface.""" + return False + + +admin.site.register(common.models.WebhookMessage, ReadOnlyAdmin) +admin.site.register(common.models.EmailMessage, ReadOnlyAdmin) +admin.site.register(common.models.EmailThread, ReadOnlyAdmin) diff --git a/src/backend/InvenTree/common/models.py b/src/backend/InvenTree/common/models.py index 022efae6a4..23d5352e9b 100644 --- a/src/backend/InvenTree/common/models.py +++ b/src/backend/InvenTree/common/models.py @@ -3355,6 +3355,11 @@ class EmailThread(InvenTree.models.InvenTreeMetadataModel): unique_together = [['key', 'global_id']] ordering = ['-updated'] + @staticmethod + def get_api_url(): + """Return the API URL associated with the EmailThread model.""" + return reverse('api-email-list') + key = models.CharField( max_length=250, verbose_name=_('Key'), diff --git a/src/backend/InvenTree/common/setting/user.py b/src/backend/InvenTree/common/setting/user.py index c9cae610ba..f5a2cd3f38 100644 --- a/src/backend/InvenTree/common/setting/user.py +++ b/src/backend/InvenTree/common/setting/user.py @@ -235,6 +235,12 @@ USER_SETTINGS: dict[str, InvenTreeSettingsKeyType] = { 'default': False, 'validator': bool, }, + 'SHOW_EXTRA_MODEL_INFO': { + 'name': _('Show Extra Model Information'), + 'description': _('Display extra information in model selection dropdowns'), + 'default': False, + 'validator': bool, + }, 'SHOW_FULL_LOCATION_IN_TABLES': { 'name': _('Show full stock location in tables'), 'description': _( diff --git a/src/backend/InvenTree/generic/states/test_transition.py b/src/backend/InvenTree/generic/states/test_transition.py index b8cb3c371d..37561ceb56 100644 --- a/src/backend/InvenTree/generic/states/test_transition.py +++ b/src/backend/InvenTree/generic/states/test_transition.py @@ -121,3 +121,7 @@ class TransitionTests(InvenTreeTestCase): self.assertIn( "ValueError('This is a broken transition plugin!')", str(cm.output[0]) ) + + # Ensure the plugin is now disabled + registry.set_plugin_state('sample-transition', False) + registry.set_plugin_state('sample-broken-transition', False) diff --git a/src/backend/InvenTree/generic/states/transition.py b/src/backend/InvenTree/generic/states/transition.py index 80ae2f7f3c..785ac16600 100644 --- a/src/backend/InvenTree/generic/states/transition.py +++ b/src/backend/InvenTree/generic/states/transition.py @@ -60,7 +60,7 @@ class TransitionMethod: **kwargs: Additional keyword arguments for custom logic. Returns: - result: bool - True if the transition method was successful, False otherwise. + result: bool - True if the transition method was successful (and no further transitions are attempted), False otherwise. Raises: ValidationError: Alert the user that the transition failed diff --git a/src/backend/InvenTree/order/serializers.py b/src/backend/InvenTree/order/serializers.py index 81c2a95fec..91b0c9d2d2 100644 --- a/src/backend/InvenTree/order/serializers.py +++ b/src/backend/InvenTree/order/serializers.py @@ -31,6 +31,7 @@ from importer.registry import register_importer from InvenTree.helpers import extract_serial_numbers, hash_barcode, normalize, str2bool from InvenTree.mixins import DataImportExportSerializerMixin from InvenTree.serializers import ( + CustomStatusSerializerMixin, FilterableSerializerMixin, InvenTreeCurrencySerializer, InvenTreeDecimalField, @@ -100,7 +101,10 @@ class DuplicateOrderSerializer(serializers.Serializer): class AbstractOrderSerializer( - DataImportExportSerializerMixin, FilterableSerializerMixin, serializers.Serializer + CustomStatusSerializerMixin, + DataImportExportSerializerMixin, + FilterableSerializerMixin, + serializers.Serializer, ): """Abstract serializer class which provides fields common to all order types.""" @@ -118,9 +122,6 @@ class AbstractOrderSerializer( read_only=True, allow_null=True, label=_('Completed Lines') ) - # Human-readable status text (read-only) - status_text = serializers.CharField(source='get_status_display', read_only=True) - # status field cannot be set directly status = serializers.IntegerField(read_only=True, label=_('Order Status')) diff --git a/src/backend/InvenTree/order/test_api.py b/src/backend/InvenTree/order/test_api.py index 5f686d6b5f..508d1849ee 100644 --- a/src/backend/InvenTree/order/test_api.py +++ b/src/backend/InvenTree/order/test_api.py @@ -16,7 +16,7 @@ from icalendar import Calendar from rest_framework import status from common.currency import currency_codes -from common.models import InvenTreeSetting +from common.models import InvenTreeCustomUserStateModel, InvenTreeSetting from common.settings import set_global_setting from company.models import Company, SupplierPart, SupplierPriceBreak from InvenTree.unit_test import InvenTreeAPITestCase @@ -737,6 +737,69 @@ class PurchaseOrderTest(OrderTest): ) self.assertEqual(response.status_code, 200) + def test_po_custom_status_query_count(self): + """Test that listing PurchaseOrders with custom statuses does not cause N+1 queries. + + Ensures that resolving the 'status_text' field for custom status values + is O(1) in database queries, not O(N) relative to the number of results. + """ + from django.contrib.contenttypes.models import ContentType + + po_content_type = ContentType.objects.get_for_model(models.PurchaseOrder) + + # 10 custom status values - different keys, labels, and logical_keys + logical_keys = [ + PurchaseOrderStatus.PENDING.value, + PurchaseOrderStatus.PLACED.value, + PurchaseOrderStatus.ON_HOLD.value, + PurchaseOrderStatus.COMPLETE.value, + PurchaseOrderStatus.CANCELLED.value, + PurchaseOrderStatus.LOST.value, + PurchaseOrderStatus.RETURNED.value, + PurchaseOrderStatus.PENDING.value, + PurchaseOrderStatus.PLACED.value, + PurchaseOrderStatus.ON_HOLD.value, + ] + + custom_statuses = [ + InvenTreeCustomUserStateModel.objects.create( + key=1000 + i, + name=f'PoCustomStatus{i}', + label=f'PO Custom Status Label {i}', + color='secondary', + logical_key=logical_keys[i], + model=po_content_type, + reference_status='PurchaseOrderStatus', + ) + for i in range(10) + ] + + # Create 100 purchase orders, cycling through the custom statuses + supplier = Company.objects.filter(is_supplier=True).first() + models.PurchaseOrder.objects.bulk_create([ + models.PurchaseOrder( + supplier=supplier, + reference=f'PO-QTEST-{i}', + status=custom_statuses[i % 10].logical_key, + status_custom_key=custom_statuses[i % 10].key, + ) + for i in range(100) + ]) + + # Query count must stay below the fixed threshold for all limit values. + # An N+1 bug would push limit=50 or limit=100 well over the threshold. + for limit in [1, 5, 10, 25, 50, 100]: + response = self.get( + self.LIST_URL, + data={'limit': limit}, + expected_code=200, + max_query_count=50, + ) + + for result in response.data['results']: + self.assertIn('status_text', result) + self.assertIsNotNone(result['status_text']) + class PurchaseOrderLineItemTest(OrderTest): """Unit tests for PurchaseOrderLineItems.""" @@ -771,7 +834,7 @@ class PurchaseOrderLineItemTest(OrderTest): # Try to delete a set of line items via their IDs self.delete(url, {'items': [1, 2]}, expected_code=200) - # We should have 2 less PurchaseOrderLineItems after deletign them + # We should have 2 less PurchaseOrderLineItems after deleting them self.assertEqual(models.PurchaseOrderLineItem.objects.count(), n - 2) def test_po_line_merge_pricing(self): diff --git a/src/backend/InvenTree/plugin/base/barcodes/api.py b/src/backend/InvenTree/plugin/base/barcodes/api.py index c67a9bed2b..cf9b0d8dd6 100644 --- a/src/backend/InvenTree/plugin/base/barcodes/api.py +++ b/src/backend/InvenTree/plugin/base/barcodes/api.py @@ -533,10 +533,23 @@ class BarcodePOReceive(BarcodeView): # Now, look just for "supplier-barcode" plugins plugins = registry.with_mixin(PluginMixinEnum.SUPPLIER_BARCODE) + plugin_slug = None + plugin_response = None + plugin_error = None + + no_supplier_plugin_error = [] + + supplier_purchase_order = None + + plugin_supplier = None + + supplier_part = None + for current_plugin in plugins: try: + # Will either Output Debugresponse if No_Match is True or return the regular response if No_Match is False result = current_plugin.scan_receive_item( barcode, request.user, @@ -546,12 +559,58 @@ class BarcodePOReceive(BarcodeView): line_item=line_item, auto_allocate=auto_allocate, ) + except Exception: log_error('BarcodePOReceive.handle_barcode', plugin=current_plugin.slug) continue - if result is None: - continue + no_match = result.get('no_match', True) + + # No_Match Determines if it found a exact match for all the required fields from scan_recieve_item + if no_match is True: + supplier_found = False + + try: + plugin_slug = current_plugin.slug + supplier_purchase_order = result.get('PO') + plugin_supplier = result.get('supplier') + supplier_part = result.get('supplier_part') + except KeyError as e: + log_error( + f'BarcodePOReceive.handle_barcode debugresponse: KeyError {e}' + ) + continue + + # Supplier does not have associated Supplier ID + if plugin_supplier is None: + no_supplier_plugin_error.append(plugin_slug) + continue + + # No Purchase Order or Supplier Part Found + if supplier_purchase_order is None and supplier_part is None: + continue + + # Purchase Order exists and is found but Supplier part does not exist + if supplier_purchase_order != None and supplier_part is None: + # Supplier was Found + supplier_found = True + plugin_error = _('Purchase order Found\rNo supplier Part Match') + + # Supplier Part is Found but Purchase Order does not exist + elif supplier_purchase_order is None and supplier_part != None: + # Supplier was Found + supplier_found = True + plugin_error = _('Supplier Part Found\rNo Purchase Order Match') + + # Supplier for PO or Supplier part in barcode was found + if supplier_found is True: + # Adds info on for what was found in the barcode + response['supplier_matches'] = { + 'purchase_order': supplier_purchase_order, + 'no_match': no_match, + 'supplier': plugin_supplier, + 'supplier_part': supplier_part, + } if 'error' in result: logger.info( @@ -569,13 +628,20 @@ class BarcodePOReceive(BarcodeView): response['plugin'] = plugin.name if plugin else None - if plugin_response: + # If there is a plugin response, and there is a match (no_match = false), combine the dictionaries + if plugin_response and plugin_response.get('no_match') is False: response = {**response, **plugin_response} + elif no_supplier_plugin_error: + response['no_supplier_plugin_error'] = no_supplier_plugin_error # A plugin has not been found! if plugin is None: response['error'] = _('No plugin match for supplier barcode') + # A plugin was found, with a Error + elif plugin_error: + response['error'] = plugin_error + self.log_scan(request, response, 'success' in response) if 'error' in response: @@ -787,11 +853,11 @@ class BarcodeScanResultList(BarcodeScanResultMixin, BulkDeleteMixin, ListAPI): filterset_class = BarcodeScanResultFilter filter_backends = SEARCH_ORDER_FILTER - ordering_fields = ['user', 'plugin', 'timestamp', 'endpoint', 'result'] + ordering_fields = ['user', 'timestamp', 'endpoint', 'result'] ordering = '-timestamp' - search_fields = ['plugin'] + search_fields = ['data'] class BarcodeScanResultDetail(BarcodeScanResultMixin, RetrieveDestroyAPI): diff --git a/src/backend/InvenTree/plugin/base/barcodes/mixins.py b/src/backend/InvenTree/plugin/base/barcodes/mixins.py index c744cc04f2..fe9d2e684b 100644 --- a/src/backend/InvenTree/plugin/base/barcodes/mixins.py +++ b/src/backend/InvenTree/plugin/base/barcodes/mixins.py @@ -320,7 +320,7 @@ class SupplierBarcodeMixin(BarcodeMixin): location=None, auto_allocate: bool = True, **kwargs, - ) -> dict | None: + ) -> dict: """Attempt to receive an item against a PurchaseOrder via barcode scanning. Arguments: @@ -344,32 +344,50 @@ class SupplierBarcodeMixin(BarcodeMixin): # Extract supplier information supplier = supplier or self.get_supplier(cache=True) - if not supplier: + """Construct Debug Response + This is returned if a perfect match is not found with the info provided from the barcode + + Response Info: + 'supplier': get supplier ID + 'PO': Represented for "Purchase Order", find PO number to supplier + 'supplier_part': find supplier part info to supplier + 'no_match': Boolean, did we find a perfect match with info given? False is Yes, True is No + """ + debug_response = {} + + if supplier is None: # No supplier information available - return None + debug_response['supplier'] = None + else: + debug_response['supplier'] = supplier.name # Extract purchase order information purchase_order = purchase_order or self.get_purchase_order() - if not purchase_order or purchase_order.supplier != supplier: + if purchase_order is None or purchase_order.supplier != supplier: # Purchase order does not match supplier - return None + debug_response['PO'] = None + else: + debug_response['PO'] = purchase_order.reference supplier_part = self.get_supplier_part() - if not supplier_part: + if supplier_part is None: # No supplier part information available - return None + debug_response['supplier_part'] = None + else: + debug_response['supplier_part'] = str(supplier_part.part) # Attempt to find matching line item - if not line_item: + if not line_item and purchase_order != None: line_items = purchase_order.lines.filter(part=supplier_part) if line_items.count() == 1: line_item = line_items.first() - if not line_item: - # No line item information available - return None + # If Purchase Order or Supplier Part does not exist, throw debug response + if debug_response['PO'] is None or debug_response['supplier_part'] is None: + debug_response['no_match'] = True + return debug_response if line_item.part != supplier_part: return {'error': _('Supplier part does not match line item')} @@ -406,7 +424,8 @@ class SupplierBarcodeMixin(BarcodeMixin): 'supplier_part': supplier_part.pk, 'purchase_order': purchase_order.pk, 'location': location.pk if location else None, - } + }, + 'no_match': False, } if action_required: diff --git a/src/backend/InvenTree/plugin/base/integration/AppMixin.py b/src/backend/InvenTree/plugin/base/integration/AppMixin.py index b3690afee1..507d188ccc 100644 --- a/src/backend/InvenTree/plugin/base/integration/AppMixin.py +++ b/src/backend/InvenTree/plugin/base/integration/AppMixin.py @@ -51,7 +51,9 @@ class AppMixin: """ from common.settings import get_global_setting - if settings.PLUGIN_TESTING or get_global_setting('ENABLE_PLUGINS_APP'): + if settings.PLUGIN_TESTING or get_global_setting( + 'ENABLE_PLUGINS_APP', create=False + ): logger.info('Registering IntegrationPlugin apps') apps_changed = False diff --git a/src/backend/InvenTree/plugin/builtin/suppliers/test_supplier_barcodes.py b/src/backend/InvenTree/plugin/builtin/suppliers/test_supplier_barcodes.py index 5ae4a6dce7..d194102581 100644 --- a/src/backend/InvenTree/plugin/builtin/suppliers/test_supplier_barcodes.py +++ b/src/backend/InvenTree/plugin/builtin/suppliers/test_supplier_barcodes.py @@ -428,6 +428,95 @@ class SupplierBarcodePOReceiveTests(InvenTreeAPITestCase): # Quantity should be pre-filled with the remaining quantity self.assertEqual(5, response.data['lineitem']['quantity']) + def test_receive_includes_no_match_false(self): + """Successful receive merges no_match=False from the plugin response.""" + self.purchase_order1.place_order() + url = reverse('api-barcode-po-receive') + + result = self.post(url, data={'barcode': DIGIKEY_BARCODE}, expected_code=200) + + self.assertIn('success', result.data) + self.assertIn('no_match', result.data) + self.assertFalse(result.data['no_match']) + + def test_partial_match_po_found_no_supplier_part(self): + """Barcode references a known PO but the SKU has no matching supplier part.""" + url = reverse('api-barcode-po-receive') + + # '1K72991337' matches purchase_order1 via supplier_reference; SKU is unknown + result = self.post( + url, data={'barcode': DIGIKEY_BARCODE_PO_NO_PART}, expected_code=400 + ) + + self.assertIn('error', result.data) + self.assertIn('Purchase order Found', result.data['error']) + self.assertIn('No supplier Part Match', result.data['error']) + + self.assertIn('supplier_matches', result.data) + matches = result.data['supplier_matches'] + self.assertIsNotNone(matches['purchase_order']) + # DRF's ValidationError converts None leaves to ErrorDetail('None') + self.assertEqual(str(matches['supplier_part']), 'None') + self.assertIsNotNone(matches['supplier']) + self.assertTrue(matches['no_match']) + + def test_partial_match_supplier_part_found_no_po(self): + """Barcode references a known supplier part but the PO reference has no match.""" + url = reverse('api-barcode-po-receive') + + # 'P296-LM358BIDDFRCT-ND' matches the existing supplier part; PO ref is unknown + result = self.post( + url, data={'barcode': DIGIKEY_BARCODE_PART_NO_PO}, expected_code=400 + ) + + self.assertIn('error', result.data) + self.assertIn('Supplier Part Found', result.data['error']) + self.assertIn('No Purchase Order Match', result.data['error']) + + self.assertIn('supplier_matches', result.data) + matches = result.data['supplier_matches'] + # DRF's ValidationError converts None leaves to ErrorDetail('None') + self.assertEqual(str(matches['purchase_order']), 'None') + self.assertIsNotNone(matches['supplier_part']) + self.assertIsNotNone(matches['supplier']) + self.assertTrue(matches['no_match']) + + def test_no_supplier_plugin_error(self): + """Plugins that cannot resolve a supplier are listed in no_supplier_plugin_error.""" + url = reverse('api-barcode-po-receive') + + digikey_plugin = registry.get_plugin('digikeyplugin') + original_supplier_id = digikey_plugin.get_setting('SUPPLIER_ID') + + # Use a non-existent PK so get_supplier() returns None + digikey_plugin.set_setting('SUPPLIER_ID', 99999) + if hasattr(digikey_plugin, '_supplier'): + del digikey_plugin._supplier + + try: + result = self.post( + url, data={'barcode': DIGIKEY_BARCODE}, expected_code=400 + ) + + self.assertIn('no_supplier_plugin_error', result.data) + self.assertIn('digikeyplugin', result.data['no_supplier_plugin_error']) + finally: + digikey_plugin.set_setting('SUPPLIER_ID', original_supplier_id) + if hasattr(digikey_plugin, '_supplier'): + del digikey_plugin._supplier + + def test_no_supplier_matches_when_both_missing(self): + """When neither PO nor supplier part can be resolved, no supplier_matches is set.""" + url = reverse('api-barcode-po-receive') + + # Completely unknown barcode — no PO, no part, no supplier + result = self.post( + url, data={'barcode': 'COMPLETELY-UNKNOWN-BARCODE-XYZ'}, expected_code=400 + ) + + self.assertIn('error', result.data) + self.assertNotIn('supplier_matches', result.data) + DIGIKEY_BARCODE = ( '[)>\x1e06\x1dP296-LM358BIDDFRCT-ND\x1d1PLM358BIDDFR\x1dK\x1d1K72991337\x1d' @@ -476,3 +565,17 @@ TME_QRCODE = ( ) TME_DATAMATRIX_CODE = 'PWBP-302 1PMPNWBP-302 Q1 K19361337/1' + +# DigiKey barcode: '1K72991337' matches purchase_order1 via supplier_reference, +# but 'PNONEXISTENT-SKU' has no matching supplier part in the test database. +DIGIKEY_BARCODE_PO_NO_PART = ( + '[)>\x1e06\x1dPNONEXISTENT-SKU-XXXX\x1d1PNONEXISTENT-MPN\x1dK\x1d1K72991337\x1d' + '10K85781337\x1d11K1\x1d4LPH\x1dQ10\x1d11ZPICK' +) + +# DigiKey barcode: 'P296-LM358BIDDFRCT-ND' matches the existing supplier part, +# but '1KBADORDER-XXXXX' matches no purchase order in the test database. +DIGIKEY_BARCODE_PART_NO_PO = ( + '[)>\x1e06\x1dP296-LM358BIDDFRCT-ND\x1d1PLM358BIDDFR\x1dK\x1d1KBADORDER-XXXXX\x1d' + '10K85781337\x1d11K1\x1d4LPH\x1dQ10\x1d11ZPICK' +) diff --git a/src/backend/InvenTree/stock/admin.py b/src/backend/InvenTree/stock/admin.py index 77f5c49fbd..d72e844cb7 100644 --- a/src/backend/InvenTree/stock/admin.py +++ b/src/backend/InvenTree/stock/admin.py @@ -77,12 +77,24 @@ class StockItemAdmin(admin.ModelAdmin): @admin.register(StockItemTracking) class StockTrackingAdmin(admin.ModelAdmin): - """Admin class for StockTracking.""" + """Admin class for StockTracking - read-only to preserve audit trail integrity.""" list_display = ('item', 'date', 'label') autocomplete_fields = ['item'] + def has_add_permission(self, request): + """Prevent addition of new tracking entries via the admin interface.""" + return False + + def has_change_permission(self, request, obj=None): + """Prevent modification of tracking entries via the admin interface.""" + return False + + def has_delete_permission(self, request, obj=None): + """Prevent deletion of tracking entries via the admin interface.""" + return False + @admin.register(StockItemTestResult) class StockItemTestResultAdmin(admin.ModelAdmin): diff --git a/src/backend/InvenTree/stock/serializers.py b/src/backend/InvenTree/stock/serializers.py index ea558c7a4c..a1f8fb2063 100644 --- a/src/backend/InvenTree/stock/serializers.py +++ b/src/backend/InvenTree/stock/serializers.py @@ -33,6 +33,7 @@ from generic.states.fields import InvenTreeCustomStatusSerializerMixin from importer.registry import register_importer from InvenTree.mixins import DataImportExportSerializerMixin from InvenTree.serializers import ( + CustomStatusSerializerMixin, InvenTreeCurrencySerializer, InvenTreeDecimalField, OptionalField, @@ -308,6 +309,7 @@ class StockItemTestResultSerializer( @register_importer() class StockItemSerializer( + CustomStatusSerializerMixin, InvenTree.serializers.FilterableSerializerMixin, DataImportExportSerializerMixin, InvenTreeCustomStatusSerializerMixin, @@ -565,10 +567,6 @@ class StockItemSerializer( return queryset - status_text = serializers.CharField( - source='get_status_display', read_only=True, label=_('Status') - ) - SKU = serializers.CharField( source='supplier_part.SKU', read_only=True, diff --git a/src/backend/InvenTree/stock/test_api.py b/src/backend/InvenTree/stock/test_api.py index ebe1a0e4bd..fdc0ebce54 100644 --- a/src/backend/InvenTree/stock/test_api.py +++ b/src/backend/InvenTree/stock/test_api.py @@ -1411,6 +1411,82 @@ class CustomStockItemStatusTest(StockAPITestCase): self.assertEqual(status['value'], self.status.key) self.assertEqual(status['display_name'], self.status.label) + def test_custom_status_query_count(self): + """Test that listing StockItems with custom statuses does not cause N+1 queries. + + Ensures that resolving 'status_text' for custom status values is O(1) + in database queries, not O(N) relative to the number of results. + """ + stock_content_type = ContentType.objects.get(model='stockitem') + + # 10 custom status values - different keys, labels, and logical_keys + logical_keys = [ + StockStatus.OK.value, + StockStatus.ATTENTION.value, + StockStatus.DAMAGED.value, + StockStatus.DESTROYED.value, + StockStatus.REJECTED.value, + StockStatus.LOST.value, + StockStatus.QUARANTINED.value, + StockStatus.RETURNED.value, + StockStatus.OK.value, + StockStatus.ATTENTION.value, + ] + + custom_statuses = [ + InvenTreeCustomUserStateModel.objects.create( + key=2000 + i, + name=f'StockCustomStatus{i}', + label=f'Stock Custom Status Label {i}', + color='secondary', + logical_key=logical_keys[i], + model=stock_content_type, + reference_status='StockStatus', + ) + for i in range(10) + ] + + part = Part.objects.filter(active=True, virtual=False).first() + + # Create 500 stock items, cycling through the 10 custom statuses + StockItem.objects.bulk_create([ + StockItem( + part=part, + quantity=1, + level=0, + tree_id=0, + lft=0, + rght=0, + status=custom_statuses[i % 10].logical_key, + status_custom_key=custom_statuses[i % 10].key, + ) + for i in range(500) + ]) + + # Lookup: custom_key -> custom_status_object, for quick per-row assertions + custom_lookup = {cs.key: cs for cs in custom_statuses} + + # Query count must stay below the fixed threshold regardless of limit. + # An N+1 bug would push limit=100 or limit=500 well over the threshold. + for limit in [50, 100, 500]: + response = self.get( + self.list_url, + data={'limit': limit}, + expected_code=200, + max_query_count=50, + ) + + for result in response.data['results']: + cs = custom_lookup.get(result['status_custom_key']) + + if cs is None: + # Item from fixtures - no custom status assigned + continue + + self.assertEqual(result['status'], cs.logical_key) + self.assertEqual(result['status_custom_key'], cs.key) + self.assertEqual(result['status_text'], cs.label) + class StockItemTest(StockAPITestCase): """Series of API tests for the StockItem API.""" diff --git a/src/backend/InvenTree/users/admin.py b/src/backend/InvenTree/users/admin.py index cea0f9305e..ff6a84ef9c 100644 --- a/src/backend/InvenTree/users/admin.py +++ b/src/backend/InvenTree/users/admin.py @@ -118,10 +118,8 @@ class InvenTreeGroupAdminForm(forms.ModelForm): class InvenTreeUserAdmin(UserAdmin): """Custom admin page for the User model. - Hides the "permissions" view as this is now handled - entirely by groups and RuleSets. - - (And it's confusing!) + - Restrict user creation and editing to superuser accounts + - Hides the "permissions" view as this is handled by RuleSets """ list_display = ( @@ -132,6 +130,7 @@ class InvenTreeUserAdmin(UserAdmin): 'is_staff', 'last_login', ) # display last connection for each user in user admin panel. + fieldsets = ( (None, {'fields': ('username', 'password')}), (_('Personal info'), {'fields': ('first_name', 'last_name', 'email')}), @@ -142,6 +141,15 @@ class InvenTreeUserAdmin(UserAdmin): (_('Important dates'), {'fields': ('last_login', 'date_joined')}), ) + def get_readonly_fields(self, request, obj=None): + """Make all fields read-only for non-superusers.""" + fields = super().get_readonly_fields(request, obj) + + if not request.user.is_superuser: + fields += ('is_staff', 'is_superuser') + + return fields + @admin.register(Owner) class OwnerAdmin(admin.ModelAdmin): diff --git a/src/backend/requirements-3.14.txt b/src/backend/requirements-3.14.txt index 5b881a24da..28255c9611 100644 --- a/src/backend/requirements-3.14.txt +++ b/src/backend/requirements-3.14.txt @@ -95,22 +95,22 @@ bleach==4.1.0 \ # via # -c src/backend/requirements.txt # django-markdownify -blessed==1.39.0 \ - --hash=sha256:666e7e3fd0a4e38c3a262eaaf1e22a4ce2c81337aa17593c3f60ea136ec24fe1 \ - --hash=sha256:b04fc7141a20a3b2ade6cad741051f1e3ac59cc1e7e90915ed1f9e521332bea4 +blessed==1.42.0 \ + --hash=sha256:34b460b77562ed21f807cfd7c527b983b0cc300c98810c8076f283b7bcd45ba7 \ + --hash=sha256:f96c4a6dc664b48e0b832fa732acc16df67abd30f0ec35babf99025982f21852 # via # -c src/backend/requirements.txt # -r src/backend/requirements.in -boto3==1.43.8 \ - --hash=sha256:1894497c383e3cdf50e210f1f57a43e9f4047a5d3accc73ffdb7eacc3b0f011b \ - --hash=sha256:d1235602d715c727c1923ef4bcdb5612a20575a9a5e4f2db00d571e0ea1f85fc +boto3==1.43.14 \ + --hash=sha256:574335744656cfed0b362a0a0467aaf2eb2bf15526edcd02d31d3c661f4b09e4 \ + --hash=sha256:5c0a994b3182061ee101812e721100717a4d664f9f4ceaf4a86b6d032ce9fc2d # via # -c src/backend/requirements.txt # django-anymail # django-storages -botocore==1.43.8 \ - --hash=sha256:611ad8b1f60661373cd39d9391ff16f1eaf8f5cb1d0a691563a4201d1a2603ce \ - --hash=sha256:6257d2655c3abe75eaa49e218b7d883cdc7cea64652b451e5feb08a6c169da3c +botocore==1.43.14 \ + --hash=sha256:1f4a2a95ea78c10398e78431e98c1fe47adb54a7b10a32975144c1f541186658 \ + --hash=sha256:b9e500737e43d2f147c9d4e23b54360335e77d4c0ba90a318f51b65e06cb8516 # via # -c src/backend/requirements.txt # boto3 @@ -219,9 +219,9 @@ brotli==1.2.0 \ # via # -c src/backend/requirements.txt # fonttools -certifi==2026.4.22 \ - --hash=sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a \ - --hash=sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580 +certifi==2026.5.20 \ + --hash=sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897 \ + --hash=sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d # via # -c src/backend/requirements.txt # requests @@ -971,25 +971,19 @@ gunicorn==26.0.0 \ # via # -c src/backend/requirements.txt # -r src/backend/requirements.in -icalendar==7.1.0 \ - --hash=sha256:10cd223c792fcc43bee4c3ebe3149d4cf32406c85cfef146624df5a0d414260f \ - --hash=sha256:6de875370d22fc4aff172ad7c439b39fb109dc2eab9ce358fcb95e8689ad7b56 +icalendar==7.1.2 \ + --hash=sha256:01c76243c76c549f58bb51510a8f0a4edb7c539726adda1356dfd0dc04fb7a53 \ + --hash=sha256:ebc43ebeb357be98984b573d975118008dee3410d8df28b054ef2943cf3e367e # via # -c src/backend/requirements.txt # django-ical -idna==3.15 \ - --hash=sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8 \ - --hash=sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc +idna==3.16 \ + --hash=sha256:cc246e3a3f89580c3a951b5ad298ca4638078b2cdd4f115654332b5c26daded5 \ + --hash=sha256:d7a6da03db833450fca25d2358ac9ff06cd624577a4aea3a596d5c0f77b8e03d # via # -c src/backend/requirements.txt # django-anymail # requests -importlib-metadata==8.7.1 \ - --hash=sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb \ - --hash=sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151 - # via - # -c src/backend/requirements.txt - # opentelemetry-api inflection==0.5.1 \ --hash=sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417 \ --hash=sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2 @@ -1014,6 +1008,12 @@ jinja2==3.1.6 \ # via # -c src/backend/requirements.txt # -r src/backend/requirements.in +jinxed==2.0.4 \ + --hash=sha256:58ae0a4a2930b51e9de162208c356b382f5e25b3bc0ad73e2013b02a62d84e8b \ + --hash=sha256:a92ac48923433c0a88577bb0479191788813fd5055ef17ff2b04a701a1b83f19 + # via + # -c src/backend/requirements.txt + # blessed jmespath==1.1.0 \ --hash=sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d \ --hash=sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64 @@ -1039,141 +1039,141 @@ jwcrypto==1.5.7 \ # via # -c src/backend/requirements.txt # django-oauth-toolkit -lxml==6.1.0 \ - --hash=sha256:00750d63ef0031a05331b9223463b1c7c02b9004cef2346a5b2877f0f9494dd2 \ - --hash=sha256:022981127642fe19866d2907d76241bb07ed21749601f727d5d5dd1ce5d1b773 \ - --hash=sha256:045e387d1f4f42a418380930fa3f45c73c9b392faf67e495e58902e68e8f44a7 \ - --hash=sha256:05b9b8787e35bec69e68daf4952b2e6dfcfb0db7ecf1a06f8cdfbbac4eb71aad \ - --hash=sha256:07f98f5496f96bf724b1e3c933c107f0cbf2745db18c03d2e13a291c3afd2635 \ - --hash=sha256:08950a23f296b3f83521577274e3d3b0f3d739bf2e68d01a752e4288bc50d286 \ - --hash=sha256:0d082495c5fcf426e425a6e28daaba1fcb6d8f854a4ff01effb1f1f381203eb9 \ - --hash=sha256:0f0f08beb0182e3e9a86fae124b3c47a7b41b7b69b225e1377db983802404e54 \ - --hash=sha256:1081dd10bc6fa437db2500e13993abf7cc30716d0a2f40e65abb935f02ec559c \ - --hash=sha256:11a873c77a181b4fef9c2e357d08ed399542c2af1390101da66720a19c7c9618 \ - --hash=sha256:183bfb45a493081943be7ea2b5adfc2b611e1cf377cefa8b8a8be404f45ef9a7 \ - --hash=sha256:19f4164243fc206d12ed3d866e80e74f5bc3627966520da1a5f97e42c32a3f39 \ - --hash=sha256:1ae225f66e5938f4fa29d37e009a3bb3b13032ac57eb4eb42afa44f6e4054e69 \ - --hash=sha256:1bc4cc83fb7f66ffb16f74d6dd0162e144333fc36ebcce32246f80c8735b2551 \ - --hash=sha256:1dd6a1c3ad4cb674f44525d9957f3e9c209bb6dd9213245195167a281fcc2bdc \ - --hash=sha256:20cf4d0651987c906a2f5cba4e3a8d6ba4bfdf973cfe2a96c0d6053888ea2ecd \ - --hash=sha256:2173a7bffe97667bbf0767f8a99e587740a8c56fdf3befac4b09cb29a80276fd \ - --hash=sha256:21c3302068f50d1e8728c67c87ba92aa87043abee517aa2576cca1855326b405 \ - --hash=sha256:23a5dc68e08ed13331d61815c08f260f46b4a60fdd1640bbeb82cf89a9d90289 \ - --hash=sha256:23cad0cc86046d4222f7f418910e46b89971c5a45d3c8abfad0f64b7b05e4a9b \ - --hash=sha256:2593a0a6621545b9095b71ad74ed4226eba438a7d9fc3712a99bdb15508cf93a \ - --hash=sha256:264c605ab9c0e4aa1a679636f4582c4d3313700009fac3ec9c3412ed0d8f3e1d \ - --hash=sha256:26c5272c6a4bf4cf32d3f5a7890c942b0e04438691157d341616d02cca74d4bd \ - --hash=sha256:26dd9f57ee3bd41e7d35b4c98a2ffd89ed11591649f421f0ec19f67d50ec67ac \ - --hash=sha256:28902146ffbe5222df411c5d19e5352490122e14447e98cd118907ee3fd6ee62 \ - --hash=sha256:29f5c00cb7d752bce2c70ebd2d31b0a42f9499ffdd3ecb2f31a5b73ee43031ad \ - --hash=sha256:30e7b2ed63b6c8e97cca8af048589a788ab5c9c905f36d9cf1c2bb549f450d2f \ - --hash=sha256:32662519149fd7a9db354175aa5e417d83485a8039b8aaa62f873ceee7ea4cad \ - --hash=sha256:363e47283bde87051b821826e71dde47f107e08614e1aa312ba0c5711e77738c \ - --hash=sha256:3648f20d25102a22b6061c688beb3a805099ea4beb0a01ce62975d926944d292 \ - --hash=sha256:37448bf9c7d7adfc5254763901e2bbd6bb876228dfc1fc7f66e58c06368a7544 \ - --hash=sha256:37fabd1452852636cf38ecdcc9dd5ca4bba7a35d6c53fa09725deeb894a87491 \ - --hash=sha256:398443df51c538bd578529aa7e5f7afc6c292644174b47961f3bf87fe5741120 \ - --hash=sha256:3ae5d8d5427f3cc317e7950f2da7ad276df0cfa37b8de2f5658959e618ea8512 \ - --hash=sha256:3f00972f84450204cd5d93a5395965e348956aaceaadec693a22ec743f8ae3eb \ - --hash=sha256:40d9189f80075f2e1f88db21ef815a2b17b28adf8e50aaf5c789bfe737027f32 \ - --hash=sha256:419c58fc92cc3a2c3fa5f78c63dbf5da70c1fa9c1b25f25727ecee89a96c7de2 \ - --hash=sha256:41dcc4c7b10484257cbd6c37b83ddb26df2b0e5aff5ac00d095689015af868ec \ - --hash=sha256:43e4d297f11080ec9d64a4b1ad7ac02b4484c9f0e2179d9c4ef78e886e747b88 \ - --hash=sha256:45e9dfbd1b661eb64ba0d4dbe762bd210c42d86dd1e5bd2bdf89d634231beb43 \ - --hash=sha256:4642e04449a1e164b5ff71ffd901ddb772dfabf5c9adf1b7be5dffe1212bc037 \ - --hash=sha256:468479e52ecf3ec23799c863336d02c05fc2f7ffd1a1424eeeb9a28d4eb69d13 \ - --hash=sha256:47024feaae386a92a146af0d2aeed65229bf6fff738e6a11dda6b0015fb8fd03 \ - --hash=sha256:481d6e2104285d9add34f41b42b247b76b61c5b5c26c303c2e9707bbf8bd9a64 \ - --hash=sha256:4937460dc5df0cdd2f06a86c285c28afda06aefa3af949f9477d3e8df430c485 \ - --hash=sha256:4a1503c56e4e2b38dc76f2f2da7bae69670c0f1933e27cfa34b2fa5876410b16 \ - --hash=sha256:4b89b098105b8599dc57adac95d1813409ac476d3c948a498775d3d0c6124bfb \ - --hash=sha256:4bd1bdb8a9e0e2dd229de19b5f8aebac80e916921b4b2c6ef8a52bc131d0c1f9 \ - --hash=sha256:4e2c54d6b47361d0f1d3bc8d4e082ad87201e56ccdcca4d3b9ee3644ff595ec8 \ - --hash=sha256:52b0ac6903cf74ebf997eb8c682d2fbac7d1ab7e4c552413eec55868a9b73f39 \ - --hash=sha256:546b66c0dd1bb8d9fa89d7123e5fa19a8aff3a1f2141eb22df96112afb17b842 \ - --hash=sha256:56971379bc5ee8037c5a0f09fa88f66cdb7d37c3e38af3e45cf539f41131ac1f \ - --hash=sha256:5715e0e28736a070f3f34a7ccc09e2fdcba0e3060abbcf61a1a5718ff6d6b105 \ - --hash=sha256:5cfa1a34df366d9dc0d5eaf420f4cf2bb1e1bebe1066d1c2fc28c179f8a4004c \ - --hash=sha256:5d27bbe326c6b539c64b42638b18bc6003a8d88f76213a97ac9ed4f885efeab7 \ - --hash=sha256:6262b87f9e5c1e5fe501d6c153247289af42eb44ad7660b9b3de17baaf92d6f6 \ - --hash=sha256:63aeafc26aac0be8aff14af7871249e87ea1319be92090bfd632ec68e03b16a5 \ - --hash=sha256:690022c7fae793b0489aa68a658822cea83e0d5933781811cabbf5ea3bcfe73d \ - --hash=sha256:6fd8b1df8254ff4fd93fd31da1fc15770bde23ac045be9bb1f87425702f61cc9 \ - --hash=sha256:73becf6d8c81d4c76b1014dbd3584cb26d904492dcf73ca85dc8bff08dcd6d2d \ - --hash=sha256:73d658216fc173cf2c939e90e07b941c5e12736b0bf6a99e7af95459cfe8eabb \ - --hash=sha256:75c4c7c619a744f972f4451bf5adf6d0fb00992a1ffc9fd78e13b0bc817cc99f \ - --hash=sha256:76b958b4ea3104483c20f74866d55aa056546e15ebe83dd7aecd63698f43b755 \ - --hash=sha256:77b9f99b17cbf14026d1e618035077060fc7195dd940d025149f3e2e830fbfcb \ - --hash=sha256:7ba11752e346bd804ea312ec2eea2532dfa8b8d3261d81a32ef9e6ab16256280 \ - --hash=sha256:7da13bb6fbadfafb474e0226a30570a3445cfd47c86296f2446dafbd77079ace \ - --hash=sha256:7e39ab3a28af7784e206d8606ec0e4bcad0190f63a492bca95e94e5a4aef7f6e \ - --hash=sha256:7f4a77d6f7edf9230cee3e1f7f6764722a41604ee5681844f18db9a81ea0ec33 \ - --hash=sha256:80410c3a7e3c617af04de17caa9f9f20adaa817093293d69eae7d7d0522836f5 \ - --hash=sha256:81ff55c70b67d19d52b6fd118a114c0a4c97d799cd3089ff9bd9e2ff4b414ee2 \ - --hash=sha256:857efde87d365706590847b916baff69c0bc9252dc5af030e378c9800c0b10e3 \ - --hash=sha256:89e8d73d09ac696a5ba42ec69787913d53284f12092f651506779314f10ba585 \ - --hash=sha256:8c11b984b5ce6add4dccc7144c7be5d364d298f15b0c6a57da1991baedc750ce \ - --hash=sha256:8c8984e1d8c4b3949e419158fda14d921ff703a9ed8a47236c6eb7a2b6cb4946 \ - --hash=sha256:8e369cbd690e788c8d15e56222d91a09c6a417f49cbc543040cba0fe2e25a79e \ - --hash=sha256:9147d8e386ec3b82c3b15d88927f734f565b0aaadef7def562b853adca45784a \ - --hash=sha256:920354904d1cb86577d4b3cfe2830c2dbe81d6f4449e57ada428f1609b5985f7 \ - --hash=sha256:942454ff253da14218f972b23dc72fa4edf6c943f37edd19cd697618b626fac5 \ - --hash=sha256:972a6451204798675407beaad97b868d0c733d9a74dafefc63120b81b8c2de28 \ - --hash=sha256:976a6b39b1b13e8c354ad8d3f261f3a4ac6609518af91bdb5094760a08f132c4 \ - --hash=sha256:97faa0860e13b05b15a51fb4986421ef7a30f0b3334061c416e0981e9450ca4c \ - --hash=sha256:9c03e048b6ce8e77b09c734e931584894ecd58d08296804ca2d0b184c933ce50 \ - --hash=sha256:9e7b0a4ca6dcc007a4cef00a761bba2dea959de4bd2df98f926b33c92ca5dfb9 \ - --hash=sha256:9eb667bf50856c4a58145f8ca2d5e5be160191e79eb9e30855a476191b3c3495 \ - --hash=sha256:9f93d5b8b07f73e8c77e3c6556a3db269918390c804b5e5fcdd4858232cc8f16 \ - --hash=sha256:a0092f2b107b69601adf562a57c956fbb596e05e3e6651cabd3054113b007e45 \ - --hash=sha256:a02ca8fe48815bddcfca3248efe54451abb9dbf2f7d1c5744c8aa4142d476919 \ - --hash=sha256:a1d9b99e5b2597e4f5aed2484fef835256fa1b68a19e4265c97628ef4bf8bcf4 \ - --hash=sha256:a2853c8b2170cc6cd54a6b4d50d2c1a8a7aeca201f23804b4898525c7a152cfc \ - --hash=sha256:a31286dbb5e74c8e9a5344465b77ab4c5bd511a253b355b5ca2fae7e579fafec \ - --hash=sha256:a86f06f059e22a0d574990ee2df24ede03f7f3c68c1336293eee9536c4c776cd \ - --hash=sha256:ab863fd37458fed6456525f297d21239d987800c46e67da5ef04fc6b3dd93ac8 \ - --hash=sha256:ac4db068889f8772a4a698c5980ec302771bb545e10c4b095d4c8be26749616f \ - --hash=sha256:b6c2f225662bc5ad416bdd06f72ca301b31b39ce4261f0e0097017fc2891b940 \ - --hash=sha256:bb40648d96157f9081886defe13eac99253e663be969ff938a9289eff6e47b72 \ - --hash=sha256:bba078de0031c219e5dd06cf3e6bf8fb8e6e64a77819b358f53bb132e3e03366 \ - --hash=sha256:bc783ee3147e60a25aa0445ea82b3e8aabb83b240f2b95d32cb75587ff781814 \ - --hash=sha256:be10838781cb3be19251e276910cd508fe127e27c3242e50521521a0f3781690 \ - --hash=sha256:bfd57d8008c4965709a919c3e9a98f76c2c7cb319086b3d26858250620023b13 \ - --hash=sha256:c08da09dc003c9e8c70e06b53a11db6fb3b250c21c4236b03c7d7b443c318e7a \ - --hash=sha256:c3592631e652afa34999a088f98ba7dfc7d6aff0d535c410bea77a71743f3819 \ - --hash=sha256:c4a699432846df86cc3de502ee85f445ebad748a1c6021d445f3e514d2cd4b1c \ - --hash=sha256:c4e425db0c5445ef0ad56b0eec54f89b88b2d884656e536a90b2f52aecb4ca86 \ - --hash=sha256:c53fa3a5a52122d590e847a57ccf955557b9634a7f99ff5a35131321b0a85317 \ - --hash=sha256:c6854e9cf99c84beb004eecd7d3a3868ef1109bf2b1df92d7bc11e96a36c2180 \ - --hash=sha256:c748ebcb6877de89f48ab90ca96642ac458fff5dec291a2b9337cd4d0934e383 \ - --hash=sha256:c871299c595ee004d186f61840f0bfc4941aa3f17c8ba4a565ead7e4f4f820ee \ - --hash=sha256:cbd7b79cdcb4986ad78a2662625882747f09db5e4cd7b2ae178a88c9c51b3dfe \ - --hash=sha256:cc16682cc987a3da00aa56a3aa3075b08edb10d9b1e476938cfdbee8f3b67181 \ - --hash=sha256:cec05be8c876f92a5aa07b01d60bbb4d11cfbdd654cad0561c0d7b5c043a61b9 \ - --hash=sha256:d036ee7b99d5148072ac7c9b847193decdfeac633db350363f7bce4fff108f0e \ - --hash=sha256:d0d799ff958655781296ec870d5e2448e75150da2b3d07f13ff5b0c2c35beefd \ - --hash=sha256:d1392c569c032f78a11a25d1de1c43fff13294c793b39e19d84fade3045cbbc3 \ - --hash=sha256:d2f17a16cd8751e8eb233a7e41aecdf8e511712e00088bf9be455f604cd0d28d \ - --hash=sha256:d3829a6e6fd550a219564912d4002c537f65da4c6ae4e093cc34462f4fa027ad \ - --hash=sha256:d43aa26dcda363f21e79afa0668f5029ed7394b3bb8c92a6927a3d34e8b610ea \ - --hash=sha256:d6d8efe71429635f0559579092bb5e60560d7b9115ee38c4adbea35632e7fa24 \ - --hash=sha256:dabecc48db5f42ba348d1f5d5afdc54c6c4cc758e676926c7cd327045749517d \ - --hash=sha256:db88156fcf544cdbf0d95588051515cfdfd4c876fc66444eb98bceb5d6db76de \ - --hash=sha256:de550d129f18d8ab819651ffe4f38b1b713c7e116707de3c0c6400d0ef34fbc1 \ - --hash=sha256:e0af85773850417d994d019741239b901b22c6680206f46a34766926e466141d \ - --hash=sha256:e3c4f84b24a1fcba435157d111c4b755099c6ff00a3daee1ad281817de75ed11 \ - --hash=sha256:e3dd5fe19c9e0ac818a9c7f132a5e43c1339ec1cbbfecb1a938bd3a47875b7c9 \ - --hash=sha256:e69aa6805905807186eb00e66c6d97a935c928275182eb02ee40ba00da9623b2 \ - --hash=sha256:e80807d72f96b96ad5588cb85c75616e4f2795a7737d4630784c51497beb7776 \ - --hash=sha256:ebe33f4ec1b2de38ceb225a1749a2965855bffeef435ba93cd2d5d540783bf2f \ - --hash=sha256:f0cea5b1d3e6e77d71bd2b9972eb2446221a69dc52bb0b9c3c6f6e5700592d93 \ - --hash=sha256:f15401d8d3dbf239e23c818afc10c7207f7b95f9a307e092122b6f86dd43209a \ - --hash=sha256:f504d861d9f2a8f94020130adac88d66de93841707a23a86244263d1e54682f5 \ - --hash=sha256:fc46da94826188ed45cb53bd8e3fc076ae22675aea2087843d4735627f867c6d \ - --hash=sha256:fc7140d7a7386e6b545d41b7358f4d02b656d4053f5fa6859f92f4b9c2572c4d \ - --hash=sha256:fcf3da95e93349e0647d48d4b36a12783105bcc74cb0c416952f9988410846a3 \ - --hash=sha256:fe022f20bc4569ec66b63b3fb275a3d628d9d32da6326b2982584104db6d3086 \ - --hash=sha256:ffb34ea45a82dd637c2c97ae1bbb920850c1e59bcae79ce1c15af531d83e7215 +lxml==6.1.1 \ + --hash=sha256:05a82eb6e1530a64f26225b55cbd178113bd0b5af1c2b625f25e5296742c26d2 \ + --hash=sha256:07a4a68e286ee7a1ed7dfb8af83e615757c0ccfe9f18c6b4ea6771388d9ba8c9 \ + --hash=sha256:09dd5b7075dc2f7709654a46543ba1ea3c2e217b2ed8fbd413a8a945a0f40f60 \ + --hash=sha256:0b7e8a14c8634bf6f7a568634cb395305a6d964aeb5b7ee32248094bed3a7e2c \ + --hash=sha256:104c09bda8d2a562824c0e319d0768ce26a779b7601e0931d33b09b53c392ef7 \ + --hash=sha256:126c93f7f56f0eda92f6d8c619edc463a4f23d9252f1c9d0405a76f25fa9f11a \ + --hash=sha256:162af1091cd785f2f27e62d3547ae9bc58ec5c86dd314d67021fd02463708d83 \ + --hash=sha256:17e0e18d4ad8adbd0399291bc44845b69d9dd68439a3cdebdf35ff902ec05072 \ + --hash=sha256:18b73c339ae29b90fd2d06e58ebd555a751bde9cd6bbd36cc0281b9a2c94e9d8 \ + --hash=sha256:19607c6bbff2a44cf3fe8250abccd20942d3462473e0a721d01d379ed017e462 \ + --hash=sha256:19b7ab10b210b0b3ad7985d9ac4eb66ab09a90b20fe6e2f7ba55d01a234345d0 \ + --hash=sha256:1d4962d4c66bf830a7e59ed6cfc17d148149898a3aefa8ec6e59763e6e3ed085 \ + --hash=sha256:1db753c9115ec7100d073b744d17e25e88a8f90f5c39b2f5dd878149af59671f \ + --hash=sha256:1dde6131244bba38a17c745836ba190bc753fd73c9291666287fd0a3fa3dcf30 \ + --hash=sha256:25c6997a9a534e016695a0ba06b2f07945de682731ff01065b6d5a4474179da1 \ + --hash=sha256:26e6eda8d38c1fcab1090dd196ee87cbd13788e531937610e2589085de074e77 \ + --hash=sha256:27acc820660aaffa4f7c087f29120e12980f7779d56d8492d263170111284740 \ + --hash=sha256:2a0217714657e023ef4293500f65aa20fce6164c8fd6b08fa5bd4a859fb14b9b \ + --hash=sha256:2c8daa471358dc2d6fcf02165e80ec68f77871a286df95bc5cc3816153b0fd2c \ + --hash=sha256:30a89d3ac8faec007453fb541f3f46807eeec88edd5826f6e3fe001752a2c621 \ + --hash=sha256:31033dc34636ea6b7d5cc11b1ddbda78a14de858ba9d3e1ed4b69a3085bc521e \ + --hash=sha256:32ab449a5486f6c758e849bb86710d0e45edc24a04e250c01555f8f5653958f8 \ + --hash=sha256:3483644525531e1d5762b0c44a8e18b6efba321b6dcf8a8952de10b037618bca \ + --hash=sha256:34c2d737beabfe35baada43941ed519251e9a12e779031496bcd5d539fcfd730 \ + --hash=sha256:3779def59032b81e44a5f70096ef6bf2082f8d901937dca354474ba09782e245 \ + --hash=sha256:37a58976370f36d9329d118ad0b953c5aeb9119ac9c6a4e258942a225d0573a1 \ + --hash=sha256:3893c14c4b6ac5b2d54ba8cf03e99fe5104e592de491f19bd6b82756c09f8004 \ + --hash=sha256:3a12689be69a28ddaa0ab99a5a1137da2afd5f8f16df7b5680b66f616d3eda1d \ + --hash=sha256:3ab541146f1f6968c462d6c2ac495148e8cdba2f8347700b2141b6ec5a75bf52 \ + --hash=sha256:3abf332af33a74288675d936fe861fd4344da0dd6622193fbc4f2bfbb35536b5 \ + --hash=sha256:3fd9728a2735fda14f4e8235830c86b539e9661e849665bf926d3f867943b4bf \ + --hash=sha256:424aa57aca0897eb922aef34395bd1289b3b6f04e6bae20ea123c0c7e333cffc \ + --hash=sha256:441dd227fa0690eb9fc81edabc63cdcefc212bba99b906dcf6e32cc1a9d3e533 \ + --hash=sha256:469e3618338bd7ab5beb412d2439825479fcf0dab99e394ca563dbc4eaf6c834 \ + --hash=sha256:47402e62c52ff5988c1e8c6c63177f5708bccf48e366dea4e3dcf1e645e04947 \ + --hash=sha256:4f0dd2f01f9f8a89f565d000e03abcf0a13d692a346c8d22f628d49af098777a \ + --hash=sha256:53b7d2b7a10b1c35c0a5e21e9224accf60c1bbfba523990732e521b2b73adef2 \ + --hash=sha256:53c909b62a0532183542fed00c5a7218258c56292d409bc789886fe1cb04c438 \ + --hash=sha256:54a7f95e4de5fb94e2f9f4b9055c6ba33bf3d628fd77a1d647c5923caa2cdcdc \ + --hash=sha256:556e94a63c9b04716f8e4de2abb65775061f846e89331b6c5be79183a24f98ea \ + --hash=sha256:55b03549819867ea141c0202242c4816c82e52ec36e7e648db9d8da5a3dc3ed6 \ + --hash=sha256:581d4c8ae690a6609e64862dd6b7c2489635c2d13907fc2b20f2bc200ff1d21e \ + --hash=sha256:58bb955caba94e467d2a96da17660d2d704e0675894cba21ab8a775b8621fd1c \ + --hash=sha256:5b7328b46d49fc9477d91ae8f6d55340347d827b7734ba3ea33faae0efef1383 \ + --hash=sha256:5ba186ad207446c65d3bb3d3e0412b032b1d9f595e59861e2354798c5703d955 \ + --hash=sha256:5bec7d03d78d853597d6107854c2310ce3f761fd218fe9fe91d5101fcf6c2efe \ + --hash=sha256:5c6bf403fbb3b3e348a561a5f4f0b9961835657981c802a1df03653eef8a9074 \ + --hash=sha256:5f6994074ebae6ffb04447268e37dc16edc304f9859cf91acb86e0af6c1b395c \ + --hash=sha256:62aeb7e85b5d60320b9d77eef2e773994e2c0ce10121b277e0a19804e1654a5a \ + --hash=sha256:63876be28efefa04a1df615b46770e82042cce445cfdce55160522f57b231ccb \ + --hash=sha256:639f6c857d91d9be29bd7502348d6736dab168b54b5158cd899abf11684dc186 \ + --hash=sha256:640f97d43d867bcb9c75b3af013b64850756b746cb6bce8ace83b70da3abba9d \ + --hash=sha256:649dda677cf3bd6ac9ae14007ba0c824ded8ce5808b53fc7431d9140399118c1 \ + --hash=sha256:6540377fbd53fe1b629172288c464fb18db11ce1fa7dc15891da10aa9dcc3e7f \ + --hash=sha256:6689e828a94eee4f139408c337bb198e014724bb8a8c26d3cfac49d119ed69a6 \ + --hash=sha256:68a9198d0fc122d14bb76837de9aa80cf84caed990b5b237f532ed87d3706736 \ + --hash=sha256:6b1761fbf9ec984e2e9d9c589ef5f5fd684b7c19f92aadd567a26c5224958db6 \ + --hash=sha256:70cdfd80589d59e43e18005dd7244e8895e93db8ab6a620b7e23df5445a4e3d2 \ + --hash=sha256:70ef8a7e102a1508f8121aae5b0867abd663f72c14f0a9c937e6554cb4587b7b \ + --hash=sha256:73bc2086f141224ebddb7fc5c6a36ca58b31b94b561e1dfe8e073e3270fad1e7 \ + --hash=sha256:74a9717fd0d82effef5c2854f0d917231d5324b5a3eb7275c43ac9fa32f97a14 \ + --hash=sha256:752d3bbfe874715ccd0aec7f88d7fc623c0f1fd7aa7b3238a084e017bad2a009 \ + --hash=sha256:762ff394d5bd56da0cf034a23dcce4e13923f15321a2adfa2ac00201dc6d3fca \ + --hash=sha256:76447f65250ed2501ead1a1552f5ce8edff159a86f308348e6a9c4acb5e1f1b4 \ + --hash=sha256:766b010012d59470072c1816b5b6c69f1d243e5db36ea5968e94accf430a4635 \ + --hash=sha256:787b2496d0dbe8cd180984e8d29e3a6f76e7ea34db781cb3bd55e4ba1ef8b4ee \ + --hash=sha256:793033d6c5cdf33a573f910d9bea14ef8f5771820411d118da8e1182edb53d5e \ + --hash=sha256:7d47866cb32fb503450b6edc9df355d10dc49836af2e89901bd6ac6b0896d9d9 \ + --hash=sha256:7f7a92e8583f06b1fd49d01158143b8461cfcd135dcb10ec807270a3051bd603 \ + --hash=sha256:80c2dfadb855da477cf73373ad29a333535dedb9b12bad02c9814c8e2b43bf08 \ + --hash=sha256:83b6b30eb131da7a75b601f28c5d6971e6ed3e887919bf6b6a1ad3c2df289080 \ + --hash=sha256:86281fbdd6a8162756f8d603f37e3435bfa38043adb79c6dc6a2dfee065e7525 \ + --hash=sha256:86c89b9d55ebf820ad7c90bc533410f0d098054f293351f10603c0c46ff598f5 \ + --hash=sha256:876e1ff5930ed8bf295ec5ef9a8155e9b6b1876bbf1deed8b3a8069311875a8f \ + --hash=sha256:88136950da4d13c318bde414ce10219931937851327f44328f2df4d2c4614067 \ + --hash=sha256:88d8cb75b9d82858497a5393e3c63cfbf03035225e4b35a49ed7ccb151e4dc0e \ + --hash=sha256:8be8ad51249698103d24b0571df35a10990fbe93dd043b6c024172189485f5e3 \ + --hash=sha256:8d43ca737b20e106e4aebc42b2f3ae19f00ba63d7eb731698ee083d72d15646f \ + --hash=sha256:8dadbe5b217ff35b6a8d16610dd710219b59b76d13f0e3f0d9f36786206e4485 \ + --hash=sha256:9395002973c827b3ed67db77e6ec09f092919a587022174554096a269378fb13 \ + --hash=sha256:96f2ec43df44b1f76249ee0a615334f9b5b060e1c8bd90e706dad2d14d02f383 \ + --hash=sha256:98fc784c2c1440667aeedf8465bdfe10208acf0ead656a2c68627299f546b315 \ + --hash=sha256:9e36f163528fc50cbef305f02a5fd66d404edf7049cdaff211dbc2cba5a7013e \ + --hash=sha256:9eb9b5a968f6e0f6d640092a567e14529ff8cea2e29d00da6f78a79fa49f013c \ + --hash=sha256:9f76acfb5f68ba982635a53fd985a8044be98a35b43232c2a1ee235ffab3e1dd \ + --hash=sha256:a088f287f7d8275a33c07f2cac6c50b9319309a0200a39e7e75d80c707723099 \ + --hash=sha256:a10bd2fd62e8ce916ececb342f348f190724a098c1faa056fdfb2a22ad5e8660 \ + --hash=sha256:a4bbea04c97f6d78a48e3fbc1cb9116d2780b1b39e03a23f6eb9b603fd61f510 \ + --hash=sha256:aa366a1e55b8ebfe8ca8ddc3cfe75c8ebade181aeb0f661d0cb05986b647f72a \ + --hash=sha256:aa49e06d94aba782c6a02eecb7e507969e7e7a41b267f1b359bb35585f295d5b \ + --hash=sha256:aad9aa39483ed8ec44d6d2e59e5b98a0d80676ef0d92f44bfc374836111f62f5 \ + --hash=sha256:aae97dfdb60715c164419ac2532a76d013c3918a665eb6cb7288098b5f349aaf \ + --hash=sha256:abbefa31eee84842140f67acef1c828e28bba8bbf0c3bc6e5492a9af88152c28 \ + --hash=sha256:ac931cdc9442c1763b8a8f6cd62c0c938737eafc5be75eff88df55fc73bc0d00 \ + --hash=sha256:acd7d70b64c0aae0c7922cca83d288a16f5f6da523637697872253415269baef \ + --hash=sha256:add8cf6ddf9a65116119a28ece0f7886e30af27ba724a7594305f1d1b58a92a1 \ + --hash=sha256:aee395f5d0927f947758b4ec119fd5fc8ec71f07a1c5c52077b30b04c0fa6955 \ + --hash=sha256:b1b963fd8f5caa68e99dfae060d54de1fe9cba899b8718b44a00cdca53c3e590 \ + --hash=sha256:b2d444f2e66624d68e9c6b211e28a76e22fff5fcabcfff4deac18b529b7d4137 \ + --hash=sha256:b8d812c6011c08b8111a15e54dd990b8923692d80adf35488bee34026c35accf \ + --hash=sha256:ba96ae44888e0185281e937633a743ea90d5a196c6000f82565ebb0580012d40 \ + --hash=sha256:bdebcc8a75d38c7598dfb2c9ed852d7a9eb4a10d6e2d0764b919b802bf32ac88 \ + --hash=sha256:c07da4cebf6889f03ebac8d238f62318e29f495de0aa18a51ea14e61ae907e2e \ + --hash=sha256:c08e5c694306507275f2290073350c4f32e383db15213b2c69e7ff39c1193840 \ + --hash=sha256:c4f469aebd783bb741c2ecb2a681008fd26bfe5c16a9a72ed5467f834e810df2 \ + --hash=sha256:c5d7152ec39ca7c402d8fb9bad86140a15b9503bd0c54484e3f1bbe3dd37ceca \ + --hash=sha256:c674693f055fa2495de12292cb45e9944199d8eaef5a2dec45175c7c61cb73e3 \ + --hash=sha256:c6ed5141a5c7507cf3ee76bd363b0d6f801e3321adc35b5d825a23115faa5465 \ + --hash=sha256:c921ba5c51e4e9f63b8b00267d06566e1f63407408a0496da2d1d0bfc819c7fc \ + --hash=sha256:c9a4b821dc7055bf9e05ff5719e18ec501f75c0f0bbfabd573b277559780833d \ + --hash=sha256:c9f79d5325907f13e1be0b3e4dacc1049d1dffc4aeee3c995284bea5fe0fab7d \ + --hash=sha256:cd312b9692e831d2ffcad61eab31d91d4b4655a962e61de8fb410472cbcd37aa \ + --hash=sha256:cea3f4c1af79af13cdb2da0c028111d8f8522d4f22a000c82385535f24e5cf3a \ + --hash=sha256:cecdd5dfdc87b1fd87dbf81d4b037a544f47f4c744200a67013771682d67686a \ + --hash=sha256:cf9d57306d848218f3601fee7601fab1a327c942d56e2e97610583cb4dd74206 \ + --hash=sha256:d34bbf07dbc7ca5970671b1512e928991fb5e9d95365636c9b2d8b4f53af405e \ + --hash=sha256:d49514be2f28d895c38cf9d2b72d7b9a07d00314519f456c0b50b53cfcf4c785 \ + --hash=sha256:d680fbcb768404c601ecb43519ecd8461f6954cb11c06a78962f666832ccfca8 \ + --hash=sha256:db1d75f6617a49c1c01bc7023713e0ff59ab32c9579ae62a7674c0e34f3b0b0a \ + --hash=sha256:dcb292aa7fe485ceff7af4f92e46c5af397daec5dff64871a528f0fc47a3cc5b \ + --hash=sha256:e07c65f443c887bbcf31cc1771d932ecc192a5273943589b3c7572b749f1ffb2 \ + --hash=sha256:e902da4b04e6b52e5893900d4b8ab46068f75f3561f01bf1080957f9fd932ed6 \ + --hash=sha256:e9308ff8241c532df3f3e570f9a5aeed6c853f888512ba4b75638d7c11c95ef6 \ + --hash=sha256:eb7c9811bfaa8b1ed5ed319f5d370dfbcaa59d52ea64be2a5a85e18195930354 \ + --hash=sha256:ebe6af670449830d6d9b752c256a983291c766a1365ba5d5460048f9e33a7818 \ + --hash=sha256:ed21202aec73cda4d55d1ce57b389aadb90ffb044e6cd1080b8347efe1b1ec84 \ + --hash=sha256:efe0374196335f93b53269acd811b944f2e6bdc88e8894f214bd636455484909 \ + --hash=sha256:f64ec5397ea6a41fc1b4af0380d79b44a755b5531dcaccd9940fb260dca93038 \ + --hash=sha256:f6ac4ef4d82dff54670227a69c67782ae0b811b5cf6b17954f1e8f7502fc0d1d \ + --hash=sha256:f6f0ce10945fab9c4c06ce14e22af9059d1a87493a9af4501a5b0b9187e21cf2 \ + --hash=sha256:f8844cd288697c6425c9beba919302241e3278871dc6519515e72b04e987abcf \ + --hash=sha256:fe0306bd29505a9177aac19f1877174b0e7422c222a59f70b2cd41633448c3dc \ + --hash=sha256:ff3f333630ab480244a1bff72043e511a91eb22e7595dead8653ee5612dd8f3d \ + --hash=sha256:ffecec8eb889b58ba9be5b95fb1cc78e22ea8eedea38e8736a1568fe1979250e # via # -c src/backend/requirements.txt # python3-saml @@ -1321,9 +1321,9 @@ openpyxl==3.1.5 \ # via # -c src/backend/requirements.txt # tablib -opentelemetry-api==1.41.1 \ - --hash=sha256:0ad1814d73b875f84494387dae86ce0b12c68556331ce6ce8fe789197c949621 \ - --hash=sha256:a22df900e75c76dc08440710e51f52f1aa6b451b429298896023e60db5b3139f +opentelemetry-api==1.42.1 \ + --hash=sha256:51a69edacadbc03a8950ace1c4c21099cacc538820ac2c9e36277e78cebba714 \ + --hash=sha256:56c63bea9f77b62856be8c47600474acad853b2924b99b1687c4cb6297166716 # via # -c src/backend/requirements.txt # -r src/backend/requirements.in @@ -1341,34 +1341,34 @@ opentelemetry-api==1.41.1 \ # opentelemetry-instrumentation-wsgi # opentelemetry-sdk # opentelemetry-semantic-conventions -opentelemetry-exporter-otlp==1.41.1 \ - --hash=sha256:299a2f0541ca175df186f5ac58fd5db177ba1e9b72b0826049062f750d55b47f \ - --hash=sha256:db276c5a80c02b063994e80950d00ca1bfddcf6520f608335b7dc2db0c0eb9c6 +opentelemetry-exporter-otlp==1.42.1 \ + --hash=sha256:2d9ebaed714377a67d224d46795ddcc11d2c877fa5de35fda70b6f3b010729a9 \ + --hash=sha256:aedd54545bb0587cd45210abdc8be545af9c01413f3307786e276df1e3c83bee # via # -c src/backend/requirements.txt # -r src/backend/requirements.in -opentelemetry-exporter-otlp-proto-common==1.41.1 \ - --hash=sha256:0e253156ea9c36b0bd3d2440c5c9ba7dd1f3fb64ba7a08fc85fbac536b56e1fb \ - --hash=sha256:10da74dad6a49344b9b7b21b6182e3060373a235fde1528616d5f01f92e66aa9 +opentelemetry-exporter-otlp-proto-common==1.42.1 \ + --hash=sha256:04f1f01fb597c4249dfcd7f8b861c902c2102369d376d9d346ff38de4469a2ee \ + --hash=sha256:f48d395ab815b444da118868977e9798ea354c25737d5cf39578ae894011c140 # via # -c src/backend/requirements.txt # opentelemetry-exporter-otlp-proto-grpc # opentelemetry-exporter-otlp-proto-http -opentelemetry-exporter-otlp-proto-grpc==1.41.1 \ - --hash=sha256:537926dcef951136992479af1d9cd88f25e33d56c530e9f020ed57774dca2f94 \ - --hash=sha256:b05df8fa1333dc9a3fda36b676b96b5095ab6016d3f0c3296d430d629ba1443b +opentelemetry-exporter-otlp-proto-grpc==1.42.1 \ + --hash=sha256:0ae1177e2038b18a929b3098215243631ef91136cba26b7e2b12790ceb7e87cc \ + --hash=sha256:975c4461f167dd8ed8857d68d3b6b25f3d272eab896f6a9470d0f5b90e2faf15 # via # -c src/backend/requirements.txt # opentelemetry-exporter-otlp -opentelemetry-exporter-otlp-proto-http==1.41.1 \ - --hash=sha256:1a21e8f49c7a946d935551e90947d6c3eb39236723c6624401da0f33d68edcb4 \ - --hash=sha256:4747a9604c8550ab38c6fd6180e2fcb80de3267060bef2c306bad3cb443302bc +opentelemetry-exporter-otlp-proto-http==1.42.1 \ + --hash=sha256:00a16da1b312a1d6c7233d600d557c91df71125af73020f3b9a7765bd699d59d \ + --hash=sha256:bf142a21035d7571ac3a09cb2e5639f49886f243972883cfe777ed3bf02b734d # via # -c src/backend/requirements.txt # opentelemetry-exporter-otlp -opentelemetry-instrumentation==0.62b1 \ - --hash=sha256:90e92a905ba4f84db06ac3aec96701df6c079b2d66e9379f8739f0a1bdcc7f45 \ - --hash=sha256:976fc6e640f2006599e97429c949e622c108d0c17c2059347d1e6c93c707f257 +opentelemetry-instrumentation==0.63b1 \ + --hash=sha256:32368d6ae52c8de20aa790a6ad86b10a76f09956092337ae37d675773990e541 \ + --hash=sha256:f1986716d52cc316ea5f60189098726a9071d8ecc0eee96c9ed110be08bade9c # via # -c src/backend/requirements.txt # opentelemetry-instrumentation-dbapi @@ -1380,82 +1380,82 @@ opentelemetry-instrumentation==0.62b1 \ # opentelemetry-instrumentation-sqlite3 # opentelemetry-instrumentation-system-metrics # opentelemetry-instrumentation-wsgi -opentelemetry-instrumentation-dbapi==0.62b1 \ - --hash=sha256:82755a0f013980b2c000a10452fb91ab5135b937a8afe37c45f34e62ecc7a2b8 \ - --hash=sha256:dd2cc4085e832a708835218833b839a88ef0ace66851d14debefe6b2ef363a3c +opentelemetry-instrumentation-dbapi==0.63b1 \ + --hash=sha256:1bc842ddcacb7ec3622ef306ffdb3c584bf97df79d381e03604ea6eb23c63781 \ + --hash=sha256:406978ed56bcfc5fd246fd918e6b36d0f5de26fa396c78cf63326a7b530597c8 # via # -c src/backend/requirements.txt # opentelemetry-instrumentation-psycopg # opentelemetry-instrumentation-pymysql # opentelemetry-instrumentation-sqlite3 -opentelemetry-instrumentation-django==0.62b1 \ - --hash=sha256:268c9b3d234510ab835acea0524ba65361389f6e2c291b495ba8072c7bb59003 \ - --hash=sha256:5ac96ef25019fbea5e2739c9b4b7ce17e4379e2b1ec9fdce6b696a5c09ceabbe +opentelemetry-instrumentation-django==0.63b1 \ + --hash=sha256:909ea4afbb18f16eb811d7ac330ed0f6fda7765191d49bccb8cca38c4fbc005f \ + --hash=sha256:f2071d2f92e4779c5a14dd452b0dfe426343599e6efa9d888304fb639a9f3101 # via # -c src/backend/requirements.txt # -r src/backend/requirements.in -opentelemetry-instrumentation-psycopg==0.62b1 \ - --hash=sha256:61c8a1794d6e11c7bca1735c2b87ef8c04b5d7202e962409d66d204f84c01be1 \ - --hash=sha256:f08e843e81656271ddc8c8194a54a5f3c8ebe0cb137e3166af9a2ff014978b5e +opentelemetry-instrumentation-psycopg==0.63b1 \ + --hash=sha256:4d74ec2dda8e54ead9bd71f134af984792f77e1d8809591ae53edadddd4c8fc0 \ + --hash=sha256:86819f4cccea21a47920807826495797b7261cc1893c8e855415e76fb7b8e2e8 # via # -c src/backend/requirements.txt # -r src/backend/requirements.in -opentelemetry-instrumentation-pymysql==0.62b1 \ - --hash=sha256:12e42f891978a3a41bc564bb81e2299d3654659589f705064a4bb10e93e493f8 \ - --hash=sha256:50d2efdde1bd8d826cbceffe5b3ce0ffd421349d609200eab0f3e632945cd304 +opentelemetry-instrumentation-pymysql==0.63b1 \ + --hash=sha256:2cf6a24e136b5b0c3f6e59675fc44b42ad2b97474b075a82859f9868a9252648 \ + --hash=sha256:6b54b83c86ac6c2e8dae642bbee4894e2283ceb5da23d571f1abb7060f7b62f4 # via # -c src/backend/requirements.txt # -r src/backend/requirements.in -opentelemetry-instrumentation-redis==0.62b1 \ - --hash=sha256:2d3c421d95e05ade075bee5becbe34e743b1cdf5bdee2085cb524f88c4f13dcb \ - --hash=sha256:9aedd02c1acf631251d1d676634db47da9da04e0a626cd0c7d83fe0eb791d165 +opentelemetry-instrumentation-redis==0.63b1 \ + --hash=sha256:28d235159df43cc2bc8779af5c602afad1e08603fff75ac8ca34dd1bf30a9cb9 \ + --hash=sha256:f0e51c4006f68e340abbf28a7995feff004de78649697cbdf3bac0072cacd082 # via # -c src/backend/requirements.txt # -r src/backend/requirements.in -opentelemetry-instrumentation-requests==0.62b1 \ - --hash=sha256:67a79c4b67e2192445c1cf03d62126fa623065688d8bd1a9f87f858b0e5f0286 \ - --hash=sha256:ca348f2f51b715c21e86d82106d784f7069ae849c3e636ab37e34dc0ba510b8c +opentelemetry-instrumentation-requests==0.63b1 \ + --hash=sha256:513fcaa3d93debbdb359c00ce1a137a34a89ee908c51ac43beb7e8c18ac2b3cd \ + --hash=sha256:935c980a11e33bfd7ed969c741e4bd7c84077045651469f10e163534368d87f7 # via # -c src/backend/requirements.txt # -r src/backend/requirements.in -opentelemetry-instrumentation-sqlite3==0.62b1 \ - --hash=sha256:14ba18aed35a01e1d89bb814e49f7d6dd90bb68b2034ccf047801ca4659dba27 \ - --hash=sha256:3ab0d614bab9956126ce81a22315bf58d56d653a65675a66e8b525f880ec3016 +opentelemetry-instrumentation-sqlite3==0.63b1 \ + --hash=sha256:3d61afda8358dc32135fabd27e5934bd25ea0eed68d64c34508f24ba8d723efc \ + --hash=sha256:cc11c68cccde061dc8bcc5f80fe00bb0105a0fbb11b390859a23ca91ccd4e3b6 # via # -c src/backend/requirements.txt # -r src/backend/requirements.in -opentelemetry-instrumentation-system-metrics==0.62b1 \ - --hash=sha256:260130cb63e3028290305d8f25b744fb3a279677769dd65287ce60086cc11d32 \ - --hash=sha256:b8097e0d3747102fa545723bb67f50f572a7618b6479359fa6533007d2f621aa +opentelemetry-instrumentation-system-metrics==0.63b1 \ + --hash=sha256:995051f47876d79461aed8b7aa205d4584d90794ef864342cc748929c389bb42 \ + --hash=sha256:d6d4d7a1a854be4165143cf6420ee5894188762eb367d7bf9da5be4a83a4b632 # via # -c src/backend/requirements.txt # -r src/backend/requirements.in -opentelemetry-instrumentation-wsgi==0.62b1 \ - --hash=sha256:02a364fd9c940a46b19c825c5bfe386b007d5292ef91573894164836953fe831 \ - --hash=sha256:a2df11de0113f504043e2b0fa0288238a93ee49ff607bd5100cb2d3a75bc771f +opentelemetry-instrumentation-wsgi==0.63b1 \ + --hash=sha256:03d61c4678ce82402e7f37b6a3dbd84cb97b85b3cb416a78c2e74c7c6d9451fa \ + --hash=sha256:86779715262227d3436bdfb16aabd1c524b0f236725a69e8754ceda76c6d79dc # via # -c src/backend/requirements.txt # -r src/backend/requirements.in # opentelemetry-instrumentation-django -opentelemetry-proto==1.41.1 \ - --hash=sha256:0496713b804d127a4147e32849fbaf5683fac8ee98550e8e7679cd706c289720 \ - --hash=sha256:4b9d2eb631237ea43b80e16c073af438554e32bc7e9e3f8ca4a9582f900020e5 +opentelemetry-proto==1.42.1 \ + --hash=sha256:c6a51e6b4f05ae63565f3a113217f3d2bfaec68f78c02d7a6c85f9010d1cfca6 \ + --hash=sha256:dedb74cba2886c59c7789b227a7a670613025a07489040050aedff6e5c0fb43c # via # -c src/backend/requirements.txt # opentelemetry-exporter-otlp-proto-common # opentelemetry-exporter-otlp-proto-grpc # opentelemetry-exporter-otlp-proto-http -opentelemetry-sdk==1.41.1 \ - --hash=sha256:724b615e1215b5aeacda0abb8a6a8922c9a1853068948bd0bd225a56d0c792e6 \ - --hash=sha256:edee379c126c1bce952b0c812b48fe8ff35b30df0eecf17e98afa4d598b7d85d +opentelemetry-sdk==1.42.1 \ + --hash=sha256:083cd4bbfaa5aa7b5a9e552430d9951219967cfb27aa61feb13a77aba1fc839d \ + --hash=sha256:8c834e8f8c9ba4171d4ec843d0cb8a67e4c7394d3f9e9297e582cbd9456ddbf7 # via # -c src/backend/requirements.txt # -r src/backend/requirements.in # opentelemetry-exporter-otlp-proto-grpc # opentelemetry-exporter-otlp-proto-http -opentelemetry-semantic-conventions==0.62b1 \ - --hash=sha256:c5cc6e04a7f8c7cdd30be2ed81499fa4e75bfbd52c9cb70d40af1f9cd3619802 \ - --hash=sha256:cf506938103d331fbb78eded0d9788095f7fd59016f2bda813c3324e5a74a93c +opentelemetry-semantic-conventions==0.63b1 \ + --hash=sha256:3daf963611334b365e98a57438183eb012d3bfb40b2d931a9af613476b8701a9 \ + --hash=sha256:dfe5ef4dee82586b746f522b818ceb298d00b3d59f660042bd79404bff8d0682 # via # -c src/backend/requirements.txt # opentelemetry-instrumentation @@ -1463,11 +1463,12 @@ opentelemetry-semantic-conventions==0.62b1 \ # opentelemetry-instrumentation-django # opentelemetry-instrumentation-redis # opentelemetry-instrumentation-requests + # opentelemetry-instrumentation-system-metrics # opentelemetry-instrumentation-wsgi # opentelemetry-sdk -opentelemetry-util-http==0.62b1 \ - --hash=sha256:adf6facbb89aef8f8bc566e2f04624942ba08a7b678b3479a91051a8f4dc70a3 \ - --hash=sha256:c57e8a6c19fc422c288e6074e882f506f85030b69b7376182f74f9257b9261f0 +opentelemetry-util-http==0.63b1 \ + --hash=sha256:6284194028c59cd439f8acfe388145069a6127f11dc077e1344a2094adacc3f8 \ + --hash=sha256:ba1268f00922ee522dba2ae38458060f99486e7385a8056985901ca9685adfff # via # -c src/backend/requirements.txt # opentelemetry-instrumentation-django @@ -1681,9 +1682,9 @@ pydyf==0.12.1 \ # via # -c src/backend/requirements.txt # weasyprint -pyjwt[crypto]==2.12.1 \ - --hash=sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c \ - --hash=sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b +pyjwt[crypto]==2.13.0 \ + --hash=sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423 \ + --hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728 # via # -c src/backend/requirements.txt # django-allauth @@ -1717,9 +1718,9 @@ pynacl==1.6.2 \ # via # -c src/backend/requirements.txt # paramiko -pypdf==6.11.0 \ - --hash=sha256:062b51c81b0910e6d2755e99e1c5547a0a23b7d0a32322af66240d8edcfabe87 \ - --hash=sha256:769394d5756d5b304c9b6bef88b54b1816b328e7e6fc9254e625529a15ed4ab8 +pypdf==6.12.1 \ + --hash=sha256:3953a097b9f26d4e0ead5ff95943d9971377557662a91d8872186053cd71d30a \ + --hash=sha256:8fa2a2321cf16247ed848bd7c97f193a60c08670d04abed5b0138327e51c43b0 # via # -c src/backend/requirements.txt # -r src/backend/requirements.in @@ -2383,12 +2384,6 @@ xmlsec==1.3.17 \ # -c src/backend/requirements.txt # -r src/backend/requirements.in # python3-saml -zipp==3.23.1 \ - --hash=sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc \ - --hash=sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110 - # via - # -c src/backend/requirements.txt - # importlib-metadata zopfli==0.4.1 \ --hash=sha256:02086247dd12fda929f9bfe8b3962b6bcdbfc8c82e99255aebcf367867cf0760 \ --hash=sha256:07a5cdc5d1aaa6c288c5d9f5a5383042ba743641abf8e2fd898dcad622d8a38e \ diff --git a/src/backend/requirements-dev-3.14.txt b/src/backend/requirements-dev-3.14.txt index d2664d0399..0e39fa8047 100644 --- a/src/backend/requirements-dev-3.14.txt +++ b/src/backend/requirements-dev-3.14.txt @@ -11,9 +11,9 @@ build==1.5.0 \ --hash=sha256:13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f \ --hash=sha256:302c22c3ba2a0fd5f3911918651341ebb3896176cbdec15bd421f80b1afc7647 # via pip-tools -certifi==2026.4.22 \ - --hash=sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a \ - --hash=sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580 +certifi==2026.5.20 \ + --hash=sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897 \ + --hash=sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d # via # -c src/backend/requirements-3.14.txt # -c src/backend/requirements.txt @@ -242,9 +242,9 @@ charset-normalizer==3.4.7 \ # -c src/backend/requirements.txt # pdfminer-six # requests -click==8.3.3 \ - --hash=sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2 \ - --hash=sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613 +click==8.4.1 \ + --hash=sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2 \ + --hash=sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96 # via pip-tools coverage[toml]==7.14.0 \ --hash=sha256:057a6af2f160a85384cde4ab36f0d2777bae1057bae255f95413cdd382aa5c74 \ @@ -448,9 +448,9 @@ gprof2dot==2025.4.14 \ --hash=sha256:0742e4c0b4409a5e8777e739388a11e1ed3750be86895655312ea7c20bd0090e \ --hash=sha256:35743e2d2ca027bf48fa7cba37021aaf4a27beeae1ae8e05a50b55f1f921a6ce # via django-silk -idna==3.15 \ - --hash=sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8 \ - --hash=sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc +idna==3.16 \ + --hash=sha256:cc246e3a3f89580c3a951b5ad298ca4638078b2cdd4f115654332b5c26daded5 \ + --hash=sha256:d7a6da03db833450fca25d2358ac9ff06cd624577a4aea3a596d5c0f77b8e03d # via # -c src/backend/requirements-3.14.txt # -c src/backend/requirements.txt @@ -496,24 +496,24 @@ pluggy==1.6.0 \ --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 # via pytest -prek==0.4.0 \ - --hash=sha256:01d69684306c67917ed69497d0f523216f13f2306723b62e1ace454209477b45 \ - --hash=sha256:0d19efff4537e4a68b5bd61dcbf5ccb2dd8343df3ea9482aabe37d241ff18ddf \ - --hash=sha256:0d9483404e5b8bf65111ee6b821e6cf7a5ba9c3a2065e7a0b7a39a17daaffcc9 \ - --hash=sha256:44e3716a3e2983add2c094775d564536342011e84f770132389819c965513d6d \ - --hash=sha256:473ce0262e36b8e77b327941ad6d5747ac451b4c441cdc86dab95640b68fff72 \ - --hash=sha256:47f42477c8453c7440e4e656e5ab0c2a1e4c25daa5ed441a9ac1a2b7634abc12 \ - --hash=sha256:6e3a0e43d7f345a5e32962736c335b5fc466d499f2556fb6f24028dd08108618 \ - --hash=sha256:7203bff21e622a5482e956384c43990b0092faab02f72118275839a77a45e939 \ - --hash=sha256:944670565dfb3800465355f299effa31572822566d10c6210e6f76bf399ddf53 \ - --hash=sha256:a8272f32e698eae514556086ad73d02026ec00bbd4d26c420f761ea857cfd795 \ - --hash=sha256:a860fda0f27f872622689358a583e5f2a5771241331848233274f4cfeb8ae9bb \ - --hash=sha256:b218d92ad5d2ff0b59240d7d837fa9edc61849894958acb3a225efff7a2faa65 \ - --hash=sha256:b60bd4fdc323896a5bada2708734d16ff59ee5e32b8c390ae2ebe328426964a3 \ - --hash=sha256:ce2a8feb5dc1f1e748879d0e14c2145353059b830ac5e3f64d92127ab16efc78 \ - --hash=sha256:e46277b577991ccdc7b13e2cfa7d260ae13e77f5af543e50e188799f649f0ad8 \ - --hash=sha256:f4cba2132e038349b4b0a00a73b300e4192bae9f78fff8df0365c00bd19140e7 \ - --hash=sha256:ff1f1fa311023418d40a443bca6928a9f5ce073d0b9130b641954183aa31736d +prek==0.4.1 \ + --hash=sha256:051c442b570b53756225410240577bee1aeace6be52955dfacf45a9783223b36 \ + --hash=sha256:10e7e78ffe65dfba7d687a8c71b2f473554d1ba60f43c742105da4c0030feed9 \ + --hash=sha256:17ffa9d8dd40791b9b99cafe558c5cc28e78e5be57607b280b15f0dab90264e9 \ + --hash=sha256:2ab3460641762edf128b1ec8e833ce7e9ae015d1268a894560cb90d3393a7527 \ + --hash=sha256:2d1805123ab5d730629de588bf319ea39e7078b589b3288c95740f1b4780a1d4 \ + --hash=sha256:5b4a348537924b20e208cbd87ef58e96ec37d691c5bec2969209c40de0ecf72e \ + --hash=sha256:60928d1dad45ff3e491d3083a50643cc213aa2d54f1dbd8d702d7193773c020e \ + --hash=sha256:622a8812bda87cf4ddcae2dab5ccecc55b88d70c677129dbe25e975d923179f0 \ + --hash=sha256:6e69a9c02ead38706a5d2a4ae209dccba08ccb5d0026e1d08e723c66ab964750 \ + --hash=sha256:76663998827a2cbc94f5e209319809655489b5bd1f8e70568a623372e80253f0 \ + --hash=sha256:7c515492ef3585e6bcd7b83f1bb1cb131038abc88ed2c843de1e4c3ceb865b19 \ + --hash=sha256:8fa707971465d8ad021c907e43691aad7bb98942943e61e294ece5f95d9fbc78 \ + --hash=sha256:b25807e0aa57d2118747e127b58e7a1bf41d5d7b3323f5f3f1f3cb10031245cc \ + --hash=sha256:c0877e82c52359d655fe1072b3a5228639184d1d5f03c6803b6530cd6da1ef20 \ + --hash=sha256:cdf4503a240369f66321213d9c4bc6f925014b64ff7121de9e9920c9b9838ce2 \ + --hash=sha256:dc744fedf98df8a00a9e3bcd629b163fee5e9f9e22bce66029d9945241586165 \ + --hash=sha256:efa95331c4c171a867c0064c19d8a4abc94a1c1c920c8b8092f2d7d87f4b90a8 # via -r src/backend/requirements-dev.in pycparser==3.0 \ --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ @@ -540,36 +540,36 @@ pytest==9.0.3 \ # via # pytest-codspeed # pytest-django -pytest-codspeed==5.0.2 \ - --hash=sha256:0621a458c52e77aa113c8d6e14037b90ce3cb5a8dd10a7656b71641999baef8c \ - --hash=sha256:0fd7db3e6fb6bd28abbf0059dd54ee6233f5faf5c08597b1e9624821417e8d99 \ - --hash=sha256:1b87b6a5e3c0e05ea043790aae08791dd6b3e7f487b18ec1bce145a60c78a130 \ - --hash=sha256:1ee33ac4c3bd7317b6956c0b6cb250f759e02072bb14fd0324de0df71d5d488f \ - --hash=sha256:22332fefae895fc80a36ac8a6d5b314663efcad9e833aed8452388441b95c50f \ - --hash=sha256:33245c1fd96b1a4299604f6791e7fded376605c140ad778db7032dcd46a74d1c \ - --hash=sha256:3658d3b42a15c6f40fa385629a8a8655dbedadd5d7bb5a01bc342b47f73da252 \ - --hash=sha256:3799ca9e54d6958d1b388371d00f928fcc4e1e68427d312348dd413a1bba5e0b \ - --hash=sha256:3a5ce30d2bcfbeb329b61f3435369720ed122caa1dd898464acbcd7edc63cf04 \ - --hash=sha256:3d15eaa6ca380d0d7cb5b7b8692f362a8aac3832dff6867a0c7068fb8c7a4ef1 \ - --hash=sha256:40b12cbf88eb69583d7063a4f5c986a7eed14f750a49764ef39a565ffa33d540 \ - --hash=sha256:439cd9d87ad449b7db327724b8fdc4a1ae79090b166b77c4e5e15102a371f6c7 \ - --hash=sha256:53473907ee2a7569b5ce6ffbfd2ba1793d284a37ff5c8670ed3149133c3ed37b \ - --hash=sha256:687e5aa0fd101adbfe98f36dc253cd4e3b77d90ad96260e6e7e78bde4319c357 \ - --hash=sha256:793d423dc76fd52b67495318681be18c541a7cfe30432ab2f272cd393422c56b \ - --hash=sha256:7b033d25f40c47733234f29c10629f14d004540c743a5c30718e2aa768d7cbb3 \ - --hash=sha256:82d3c9db57ccaef5177e1096b4dbbf8f3fde8d25c568e38d31a259474c94e5b4 \ - --hash=sha256:93fea30b2d7266343dd505a182bdf1eb47f96f5fa2929f1d9aff01d3b60e1589 \ - --hash=sha256:a14a6515cd315745b4b5b4739a72b287782c00a35f2927e55c310499b79d6bc2 \ - --hash=sha256:a88fcddd08bdb1afe043ac4f992e032baee92c88990a611111e0c00d77927cfe \ - --hash=sha256:b42d2aae3ac94192b8843fa7578eae584223bcb6334c50ca9f0e9ebafd40053b \ - --hash=sha256:c20756925af58ad9d5b584d66a9b8dc709f9b243e6d8fd377e2a1b5a99bf9229 \ - --hash=sha256:c2c09ec82a2def144816c6ffb311252c6ff0624189b3b5e674d889920b6d926c \ - --hash=sha256:cd07e12c38f6974c969e76d070aba448c92fad66601cda4fd289afa52c81ef13 \ - --hash=sha256:d223b0fe74625e633c86934a1da3ed1607f694fb3981a598bcfc02811e54808e \ - --hash=sha256:d394d0d27ead72d0b00906e3832f4dcb9aadb81887a4f379c534c32c0ab965b7 \ - --hash=sha256:d6d24532a8fee7018b9a33df51e1a14e27ae6b2b0772e6ad477ce5c561ab06a5 \ - --hash=sha256:dedc9e4542832a3487aedf0b448217f34fdc794676b9e0daeaf408a343322c2b \ - --hash=sha256:fbd1e86900e7ebbbf3cdf5a48124412d2b75283ab1378994ac27ba3308e262fc +pytest-codspeed==5.0.3 \ + --hash=sha256:005348ea52ace3ede2e2f595913912ad2564cca7b124211a88dc78a9cb1fca63 \ + --hash=sha256:0c383c9121deb58a69f174188e9e4488ffc0daced0ed276abf87747182511901 \ + --hash=sha256:20eba63765be9d1b6cacbbfad84b87d49eb04b357a7045a0899880da181f81e3 \ + --hash=sha256:25464363c7f9b9bd5022e969c0addba616fa40ac9b8f0fc9e030c4538863b32d \ + --hash=sha256:2eeb25fb1ac3f73c4de50e739e78fea396b89782bdb740bf2a7cd2df21f8d4ee \ + --hash=sha256:3a2097247985f5d915a94b80c5552d10979ca858c859fc3edef1bf2baa5c9b7a \ + --hash=sha256:4c682f6645d4eb472f3bd95dbda1805e3af4243610572cb7d6bf94a88e8a0b6c \ + --hash=sha256:51e192905a2230f9956e6160732f76577836953a4a1fb2b1e7be74e51ac7b2a0 \ + --hash=sha256:6524c57fec279a22ffef6112af404036afc71b4704758ae9f0abda429b8478d4 \ + --hash=sha256:73c5c9d98a3372a42611989ccfa437cce3842431ac6d6b9ab42c4f0e59c070f7 \ + --hash=sha256:782f9985b6f6b45b8bc20152d206d3a52b56dd088ba81cb70a71f0b39841be9e \ + --hash=sha256:7ac4344f34bbcdd17f6f8c30dbac3da2f80d223dd112e568fd7f7c2cd4cbc693 \ + --hash=sha256:85505c96a3477c346ec2d2b7dced8478f4c651e2b1666ee102d53a832b511853 \ + --hash=sha256:8df77b3409f54f4a268f77f3ff74992fe1d995cdbaf2cecf8ad74d32db217ce7 \ + --hash=sha256:91afef90e6a96b013495e4702ef5d6358614a449e71008cdc194ef668778b92f \ + --hash=sha256:9aa0815b90196f3c20d736ea8691381e97f12bbe8c7d87af10a351e434b452cb \ + --hash=sha256:a2e0ab65df73e837666d12357280ca50ff6d6ac03ea5266703be518b68170edf \ + --hash=sha256:a4bcdb4b6522738152885ef067e0c8524d5699828d780fb6f464cdb3db44369c \ + --hash=sha256:a5d8695a227ea1c3a41d25db5b3fe720bf1b4808bd38862be811a4efd902c792 \ + --hash=sha256:abe793da40f87295d33988673d34f06ea569848b44490b847552cd416816258a \ + --hash=sha256:bce0a6ea93a5b19658f713312bb67554c19283ab15b454a1e3e55a13e78130f8 \ + --hash=sha256:bf4cc4178cbace8f4d2bd240408276bc4da3850ac5fcb5fb5f8a74ab417615bb \ + --hash=sha256:c3a9ed38dfa776443b86f4b49a982e8443d0953db4974bd2673d63cc904ae1ad \ + --hash=sha256:dbe6a4a00b449b6ba2771f644cbc38bdf55acf5c812e60e5659110e19dd9f510 \ + --hash=sha256:ec9fa6f0af0a9feb0e0bd517fb59ef28f806fbd50c0c6900ac26cbb4d080eba5 \ + --hash=sha256:efd43f82ea03ced8488a767ded9473f050791ab7783ea8654107e1e0ac66af40 \ + --hash=sha256:f56d0339cd98d26f6e561987be25bdd2761a5d53d8f73493b1ebe02d0d451093 \ + --hash=sha256:f852bee785a7a124cb1720b1915670c6742af87747dc4d838f3ffdbd365ce9d9 \ + --hash=sha256:fe2ea83c924c2250675b75686c3ee456b8cf0208d83d552e182a195fdf467378 # via -r src/backend/requirements-dev.in pytest-django==4.12.0 \ --hash=sha256:3ff300c49f8350ba2953b90297d23bf5f589db69545f56f1ec5f8cff5da83e85 \ @@ -626,13 +626,13 @@ ty==0.0.1a21 \ --hash=sha256:e941e9a9d1e54b03eeaf9c3197c26a19cf76009fd5e41e16e5657c1c827bd6d3 \ --hash=sha256:ecf41706b803827b0de8717f32a434dad1e67be9f4b8caf403e12013179ea06a # via -r src/backend/requirements-dev.in -types-psycopg2==2.9.21.20260509 \ - --hash=sha256:0422105f691a409e9d8048c2205aca9d694b70823248c6614393444017e9f088 \ - --hash=sha256:69f6dae384bbea830dff23621936423035db152af901331e6f9c46f7c4f4b24f +types-psycopg2==2.9.21.20260518 \ + --hash=sha256:2fd728a4fa3860db0a4a9813e5f49c30ed329b3d2e60e73530d9db01b2b98420 \ + --hash=sha256:8b1f80d90d6799a4fcdac12198b382a8feee9ed4340d5f69b56fc5ffa0644143 # via django-types -types-pyyaml==6.0.12.20260510 \ - --hash=sha256:09c1f1cb65a6eebea1e2e51ccf4918b8288e152909609a35cdb0d805efd125ad \ - --hash=sha256:3492eb9ba4d9d833473214c4d5736cccf5f37d93f5854059721e1c84f785309d +types-pyyaml==6.0.12.20260518 \ + --hash=sha256:d2150f75a231c9fe9c7463bd29487d93e60bac90400287351384bc2284eba7cd \ + --hash=sha256:d917f83fb38462550338c1297faedd860b3ec83912b96b1e3d73255f7473e466 # via django-stubs typing-extensions==4.15.0 \ --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ diff --git a/src/backend/requirements-dev.txt b/src/backend/requirements-dev.txt index e1213ddf4d..a7099a7df0 100644 --- a/src/backend/requirements-dev.txt +++ b/src/backend/requirements-dev.txt @@ -10,9 +10,9 @@ build==1.5.0 \ --hash=sha256:13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f \ --hash=sha256:302c22c3ba2a0fd5f3911918651341ebb3896176cbdec15bd421f80b1afc7647 # via pip-tools -certifi==2026.4.22 \ - --hash=sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a \ - --hash=sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580 +certifi==2026.5.20 \ + --hash=sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897 \ + --hash=sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d # via # -c src/backend/requirements.txt # requests @@ -104,7 +104,6 @@ cffi==2.0.0 \ # via # -c src/backend/requirements.txt # cryptography - # pytest-codspeed charset-normalizer==3.4.7 \ --hash=sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc \ --hash=sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c \ @@ -239,9 +238,9 @@ charset-normalizer==3.4.7 \ # -c src/backend/requirements.txt # pdfminer-six # requests -click==8.3.3 \ - --hash=sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2 \ - --hash=sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613 +click==8.4.1 \ + --hash=sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2 \ + --hash=sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96 # via pip-tools coverage[toml]==7.13.5 \ --hash=sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256 \ @@ -443,9 +442,9 @@ gprof2dot==2025.4.14 \ --hash=sha256:0742e4c0b4409a5e8777e739388a11e1ed3750be86895655312ea7c20bd0090e \ --hash=sha256:35743e2d2ca027bf48fa7cba37021aaf4a27beeae1ae8e05a50b55f1f921a6ce # via django-silk -idna==3.15 \ - --hash=sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8 \ - --hash=sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc +idna==3.16 \ + --hash=sha256:cc246e3a3f89580c3a951b5ad298ca4638078b2cdd4f115654332b5c26daded5 \ + --hash=sha256:d7a6da03db833450fca25d2358ac9ff06cd624577a4aea3a596d5c0f77b8e03d # via # -c src/backend/requirements.txt # requests @@ -489,24 +488,24 @@ pluggy==1.6.0 \ --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 # via pytest -prek==0.3.13 \ - --hash=sha256:0d89ac712c60e34d1550a606ad5fdfb8ad71d44ced8afa2fa5cbc106be4abd9e \ - --hash=sha256:326fac2bdce00074ce6c5046b861d310638aee2b9de1ed241ba7eb32bdc83898 \ - --hash=sha256:64b59a1550780af2bba37297c704b17f81d8e9df6288af1fab4017938e33b1db \ - --hash=sha256:7ca8c526a23873177fb3b92013500b08ef5f8bedc7263f9f3a44dd2f49645a26 \ - --hash=sha256:841049f89f5ec9f4035299283d11e566ac5a068e3742ead1055ea04f886831fc \ - --hash=sha256:9ce6cd8f114ba9bbdbe97422103fd886101949b1c42e588a7543c4436ead2020 \ - --hash=sha256:a5bbb175478438a871e3281d2c3c3f067288af73ad81707a9bdebfd769766c7d \ - --hash=sha256:a65327a014d838341af757dfc05a706d10e8e33f039bc32bb3dbe2fa21c440c0 \ - --hash=sha256:a9fd74e0aec550c6b8d41076fdcdd6ff121cd7d94d743c1338bd794784e3c775 \ - --hash=sha256:b00d38f01235073c35aa5f48df57fefef45a6cec2ae0884d750345a2c7220370 \ - --hash=sha256:b6a200843a36a5b0c41764ce7639ccb3471d48b097f1c5e3fc8f034219b42626 \ - --hash=sha256:bdacaad8f35f343e063d251211fe34db1de9e5cc591795361ad69a6485202258 \ - --hash=sha256:c48586ee3708bfbf3df80121f55583e9a7d0fa166b08172c091fe5971e92a0ac \ - --hash=sha256:cc03e924a24d8d961f56195853c8b206cb196be6db4ad8312125dae847d718ac \ - --hash=sha256:e5530a867bcf5b172b7513a64e71b06a337d1d184696227ae953845867376b8d \ - --hash=sha256:f00328f1c520d8fefb910ab0d3c6764ee330d227952baa19b7e3de7242bd8b3b \ - --hash=sha256:f9b5265863d18b5be4ea094fdce4fd6ca61a8c89a70ee3d8ee153b3e0ed6b272 +prek==0.4.1 \ + --hash=sha256:051c442b570b53756225410240577bee1aeace6be52955dfacf45a9783223b36 \ + --hash=sha256:10e7e78ffe65dfba7d687a8c71b2f473554d1ba60f43c742105da4c0030feed9 \ + --hash=sha256:17ffa9d8dd40791b9b99cafe558c5cc28e78e5be57607b280b15f0dab90264e9 \ + --hash=sha256:2ab3460641762edf128b1ec8e833ce7e9ae015d1268a894560cb90d3393a7527 \ + --hash=sha256:2d1805123ab5d730629de588bf319ea39e7078b589b3288c95740f1b4780a1d4 \ + --hash=sha256:5b4a348537924b20e208cbd87ef58e96ec37d691c5bec2969209c40de0ecf72e \ + --hash=sha256:60928d1dad45ff3e491d3083a50643cc213aa2d54f1dbd8d702d7193773c020e \ + --hash=sha256:622a8812bda87cf4ddcae2dab5ccecc55b88d70c677129dbe25e975d923179f0 \ + --hash=sha256:6e69a9c02ead38706a5d2a4ae209dccba08ccb5d0026e1d08e723c66ab964750 \ + --hash=sha256:76663998827a2cbc94f5e209319809655489b5bd1f8e70568a623372e80253f0 \ + --hash=sha256:7c515492ef3585e6bcd7b83f1bb1cb131038abc88ed2c843de1e4c3ceb865b19 \ + --hash=sha256:8fa707971465d8ad021c907e43691aad7bb98942943e61e294ece5f95d9fbc78 \ + --hash=sha256:b25807e0aa57d2118747e127b58e7a1bf41d5d7b3323f5f3f1f3cb10031245cc \ + --hash=sha256:c0877e82c52359d655fe1072b3a5228639184d1d5f03c6803b6530cd6da1ef20 \ + --hash=sha256:cdf4503a240369f66321213d9c4bc6f925014b64ff7121de9e9920c9b9838ce2 \ + --hash=sha256:dc744fedf98df8a00a9e3bcd629b163fee5e9f9e22bce66029d9945241586165 \ + --hash=sha256:efa95331c4c171a867c0064c19d8a4abc94a1c1c920c8b8092f2d7d87f4b90a8 # via -r src/backend/requirements-dev.in pycparser==3.0 \ --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ @@ -532,23 +531,36 @@ pytest==9.0.3 \ # via # pytest-codspeed # pytest-django -pytest-codspeed==4.4.0 \ - --hash=sha256:06943110e7a8a4b54f4b13aaa3ff8db39caa02b2f61705916887649e36b9713a \ - --hash=sha256:215170441e57bfcbefd179dfd86ccd54ed0ee235e0602a068ce4448b35f13cb2 \ - --hash=sha256:29b1bf8a36e18d11641a5e610e23a94036b04185e3099978d81a873a5bd3635c \ - --hash=sha256:33c38e0e797c74506004f231fc53eab0e412987de281755f714018334381aa3a \ - --hash=sha256:340dbb1cc5a21434e0e29bd68ab03c7dc7ad9bfde09d1980b7161352c4c2f048 \ - --hash=sha256:413666266762f9cef1321ba971a9e127b97a1f1dad40ddfd2184c2bc5ac157f9 \ - --hash=sha256:4afa9455a9b198c5e898224c751182fcf53f67f11fb27c2c3346284da1baa018 \ - --hash=sha256:56d5dd94dcb69460f916acb9c69865d0171b98acec3ce256645d0c0275b553d7 \ - --hash=sha256:6a5c1d51e7ca72ffe247c99b9a97a54191185e8f7a27528e2200d7416da2a68b \ - --hash=sha256:83479a6719598d2910969a60cc410c7283c262c876422a9157dca2f2ab42fa1d \ - --hash=sha256:98524af4ddd6006ea064791bb15a43957d78fab040cb6f499ca73a369da373e6 \ - --hash=sha256:a6aab2fa73523f538e7729c20ccf4a1e8e921324c9877a816b05334135950fd9 \ - --hash=sha256:e258e6c3d5a8a02ae02a64831be3acd44c19210ffbf13321bdbb8c111c5c6fe4 \ - --hash=sha256:edb7c101d9c50439a42cf02cfa9c0ac92da618841636bbebf87c3fa54669442a \ - --hash=sha256:ee3e1964446011ca192eebf0350227df231a5b88af57e518f2a4328fc8ca5131 \ - --hash=sha256:f3ae6f4053042c3a9ae3b05416fb42253c5e514e89391eb25e9c9e3ac8de8677 +pytest-codspeed==5.0.3 \ + --hash=sha256:005348ea52ace3ede2e2f595913912ad2564cca7b124211a88dc78a9cb1fca63 \ + --hash=sha256:0c383c9121deb58a69f174188e9e4488ffc0daced0ed276abf87747182511901 \ + --hash=sha256:20eba63765be9d1b6cacbbfad84b87d49eb04b357a7045a0899880da181f81e3 \ + --hash=sha256:25464363c7f9b9bd5022e969c0addba616fa40ac9b8f0fc9e030c4538863b32d \ + --hash=sha256:2eeb25fb1ac3f73c4de50e739e78fea396b89782bdb740bf2a7cd2df21f8d4ee \ + --hash=sha256:3a2097247985f5d915a94b80c5552d10979ca858c859fc3edef1bf2baa5c9b7a \ + --hash=sha256:4c682f6645d4eb472f3bd95dbda1805e3af4243610572cb7d6bf94a88e8a0b6c \ + --hash=sha256:51e192905a2230f9956e6160732f76577836953a4a1fb2b1e7be74e51ac7b2a0 \ + --hash=sha256:6524c57fec279a22ffef6112af404036afc71b4704758ae9f0abda429b8478d4 \ + --hash=sha256:73c5c9d98a3372a42611989ccfa437cce3842431ac6d6b9ab42c4f0e59c070f7 \ + --hash=sha256:782f9985b6f6b45b8bc20152d206d3a52b56dd088ba81cb70a71f0b39841be9e \ + --hash=sha256:7ac4344f34bbcdd17f6f8c30dbac3da2f80d223dd112e568fd7f7c2cd4cbc693 \ + --hash=sha256:85505c96a3477c346ec2d2b7dced8478f4c651e2b1666ee102d53a832b511853 \ + --hash=sha256:8df77b3409f54f4a268f77f3ff74992fe1d995cdbaf2cecf8ad74d32db217ce7 \ + --hash=sha256:91afef90e6a96b013495e4702ef5d6358614a449e71008cdc194ef668778b92f \ + --hash=sha256:9aa0815b90196f3c20d736ea8691381e97f12bbe8c7d87af10a351e434b452cb \ + --hash=sha256:a2e0ab65df73e837666d12357280ca50ff6d6ac03ea5266703be518b68170edf \ + --hash=sha256:a4bcdb4b6522738152885ef067e0c8524d5699828d780fb6f464cdb3db44369c \ + --hash=sha256:a5d8695a227ea1c3a41d25db5b3fe720bf1b4808bd38862be811a4efd902c792 \ + --hash=sha256:abe793da40f87295d33988673d34f06ea569848b44490b847552cd416816258a \ + --hash=sha256:bce0a6ea93a5b19658f713312bb67554c19283ab15b454a1e3e55a13e78130f8 \ + --hash=sha256:bf4cc4178cbace8f4d2bd240408276bc4da3850ac5fcb5fb5f8a74ab417615bb \ + --hash=sha256:c3a9ed38dfa776443b86f4b49a982e8443d0953db4974bd2673d63cc904ae1ad \ + --hash=sha256:dbe6a4a00b449b6ba2771f644cbc38bdf55acf5c812e60e5659110e19dd9f510 \ + --hash=sha256:ec9fa6f0af0a9feb0e0bd517fb59ef28f806fbd50c0c6900ac26cbb4d080eba5 \ + --hash=sha256:efd43f82ea03ced8488a767ded9473f050791ab7783ea8654107e1e0ac66af40 \ + --hash=sha256:f56d0339cd98d26f6e561987be25bdd2761a5d53d8f73493b1ebe02d0d451093 \ + --hash=sha256:f852bee785a7a124cb1720b1915670c6742af87747dc4d838f3ffdbd365ce9d9 \ + --hash=sha256:fe2ea83c924c2250675b75686c3ee456b8cf0208d83d552e182a195fdf467378 # via -r src/backend/requirements-dev.in pytest-django==4.12.0 \ --hash=sha256:3ff300c49f8350ba2953b90297d23bf5f589db69545f56f1ec5f8cff5da83e85 \ @@ -651,13 +663,13 @@ ty==0.0.1a21 \ --hash=sha256:e941e9a9d1e54b03eeaf9c3197c26a19cf76009fd5e41e16e5657c1c827bd6d3 \ --hash=sha256:ecf41706b803827b0de8717f32a434dad1e67be9f4b8caf403e12013179ea06a # via -r src/backend/requirements-dev.in -types-psycopg2==2.9.21.20260422 \ - --hash=sha256:ad7574fa8e25d9aa96ab96cd280c4dee20872725cd1fe6a6d3facc354f2644d4 \ - --hash=sha256:e240684ac37946c5a2a058b04ea1f2fd0e4ee2655719b8c3ec9abf37f96da5ba +types-psycopg2==2.9.21.20260518 \ + --hash=sha256:2fd728a4fa3860db0a4a9813e5f49c30ed329b3d2e60e73530d9db01b2b98420 \ + --hash=sha256:8b1f80d90d6799a4fcdac12198b382a8feee9ed4340d5f69b56fc5ffa0644143 # via django-types -types-pyyaml==6.0.12.20260408 \ - --hash=sha256:92a73f2b8d7f39ef392a38131f76b970f8c66e4c42b3125ae872b7c93b556307 \ - --hash=sha256:fbc42037d12159d9c801ebfcc79ebd28335a7c13b08a4cfbc6916df78fee9384 +types-pyyaml==6.0.12.20260518 \ + --hash=sha256:d2150f75a231c9fe9c7463bd29487d93e60bac90400287351384bc2284eba7cd \ + --hash=sha256:d917f83fb38462550338c1297faedd860b3ec83912b96b1e3d73255f7473e466 # via django-stubs typing-extensions==4.15.0 \ --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ diff --git a/src/backend/requirements.txt b/src/backend/requirements.txt index d021fb3e61..6fc804d069 100644 --- a/src/backend/requirements.txt +++ b/src/backend/requirements.txt @@ -91,19 +91,19 @@ bleach==4.1.0 \ --hash=sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da \ --hash=sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994 # via django-markdownify -blessed==1.39.0 \ - --hash=sha256:666e7e3fd0a4e38c3a262eaaf1e22a4ce2c81337aa17593c3f60ea136ec24fe1 \ - --hash=sha256:b04fc7141a20a3b2ade6cad741051f1e3ac59cc1e7e90915ed1f9e521332bea4 +blessed==1.42.0 \ + --hash=sha256:34b460b77562ed21f807cfd7c527b983b0cc300c98810c8076f283b7bcd45ba7 \ + --hash=sha256:f96c4a6dc664b48e0b832fa732acc16df67abd30f0ec35babf99025982f21852 # via -r src/backend/requirements.in -boto3==1.43.8 \ - --hash=sha256:1894497c383e3cdf50e210f1f57a43e9f4047a5d3accc73ffdb7eacc3b0f011b \ - --hash=sha256:d1235602d715c727c1923ef4bcdb5612a20575a9a5e4f2db00d571e0ea1f85fc +boto3==1.43.14 \ + --hash=sha256:574335744656cfed0b362a0a0467aaf2eb2bf15526edcd02d31d3c661f4b09e4 \ + --hash=sha256:5c0a994b3182061ee101812e721100717a4d664f9f4ceaf4a86b6d032ce9fc2d # via # django-anymail # django-storages -botocore==1.43.8 \ - --hash=sha256:611ad8b1f60661373cd39d9391ff16f1eaf8f5cb1d0a691563a4201d1a2603ce \ - --hash=sha256:6257d2655c3abe75eaa49e218b7d883cdc7cea64652b451e5feb08a6c169da3c +botocore==1.43.14 \ + --hash=sha256:1f4a2a95ea78c10398e78431e98c1fe47adb54a7b10a32975144c1f541186658 \ + --hash=sha256:b9e500737e43d2f147c9d4e23b54360335e77d4c0ba90a318f51b65e06cb8516 # via # boto3 # s3transfer @@ -209,9 +209,9 @@ brotli==1.2.0 \ --hash=sha256:fc1530af5c3c275b8524f2e24841cbe2599d74462455e9bae5109e9ff42e9361 \ --hash=sha256:ff09cd8c5eec3b9d02d2408db41be150d8891c5566addce57513bf546e3d6c6d # via fonttools -certifi==2026.4.22 \ - --hash=sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a \ - --hash=sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580 +certifi==2026.5.20 \ + --hash=sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897 \ + --hash=sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d # via # requests # sentry-sdk @@ -864,20 +864,16 @@ gunicorn==26.0.0 \ --hash=sha256:40233d26a5f0d1872916188c276e21641155111c2853f0c2cd55260aec0d24fc \ --hash=sha256:ca9346f85e3a4aeeb64d491045c16b9a35647abd37ea15efe53080eb8b090baf # via -r src/backend/requirements.in -icalendar==7.1.0 \ - --hash=sha256:10cd223c792fcc43bee4c3ebe3149d4cf32406c85cfef146624df5a0d414260f \ - --hash=sha256:6de875370d22fc4aff172ad7c439b39fb109dc2eab9ce358fcb95e8689ad7b56 +icalendar==7.1.2 \ + --hash=sha256:01c76243c76c549f58bb51510a8f0a4edb7c539726adda1356dfd0dc04fb7a53 \ + --hash=sha256:ebc43ebeb357be98984b573d975118008dee3410d8df28b054ef2943cf3e367e # via django-ical -idna==3.15 \ - --hash=sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8 \ - --hash=sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc +idna==3.16 \ + --hash=sha256:cc246e3a3f89580c3a951b5ad298ca4638078b2cdd4f115654332b5c26daded5 \ + --hash=sha256:d7a6da03db833450fca25d2358ac9ff06cd624577a4aea3a596d5c0f77b8e03d # via # django-anymail # requests -importlib-metadata==8.7.1 \ - --hash=sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb \ - --hash=sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151 - # via opentelemetry-api inflection==0.5.1 \ --hash=sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417 \ --hash=sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2 @@ -894,6 +890,10 @@ jinja2==3.1.6 \ --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 # via -r src/backend/requirements.in +jinxed==2.0.4 \ + --hash=sha256:58ae0a4a2930b51e9de162208c356b382f5e25b3bc0ad73e2013b02a62d84e8b \ + --hash=sha256:a92ac48923433c0a88577bb0479191788813fd5055ef17ff2b04a701a1b83f19 + # via blessed jmespath==1.1.0 \ --hash=sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d \ --hash=sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64 @@ -912,141 +912,141 @@ jwcrypto==1.5.7 \ --hash=sha256:70204d7cca406eda8c82352e3c41ba2d946610dafd19e54403f0a1f4f18633c6 \ --hash=sha256:729463fefe28b6de5cf1ebfda3e94f1a1b41d2799148ef98a01cb9678ebe2bb0 # via django-oauth-toolkit -lxml==6.1.0 \ - --hash=sha256:00750d63ef0031a05331b9223463b1c7c02b9004cef2346a5b2877f0f9494dd2 \ - --hash=sha256:022981127642fe19866d2907d76241bb07ed21749601f727d5d5dd1ce5d1b773 \ - --hash=sha256:045e387d1f4f42a418380930fa3f45c73c9b392faf67e495e58902e68e8f44a7 \ - --hash=sha256:05b9b8787e35bec69e68daf4952b2e6dfcfb0db7ecf1a06f8cdfbbac4eb71aad \ - --hash=sha256:07f98f5496f96bf724b1e3c933c107f0cbf2745db18c03d2e13a291c3afd2635 \ - --hash=sha256:08950a23f296b3f83521577274e3d3b0f3d739bf2e68d01a752e4288bc50d286 \ - --hash=sha256:0d082495c5fcf426e425a6e28daaba1fcb6d8f854a4ff01effb1f1f381203eb9 \ - --hash=sha256:0f0f08beb0182e3e9a86fae124b3c47a7b41b7b69b225e1377db983802404e54 \ - --hash=sha256:1081dd10bc6fa437db2500e13993abf7cc30716d0a2f40e65abb935f02ec559c \ - --hash=sha256:11a873c77a181b4fef9c2e357d08ed399542c2af1390101da66720a19c7c9618 \ - --hash=sha256:183bfb45a493081943be7ea2b5adfc2b611e1cf377cefa8b8a8be404f45ef9a7 \ - --hash=sha256:19f4164243fc206d12ed3d866e80e74f5bc3627966520da1a5f97e42c32a3f39 \ - --hash=sha256:1ae225f66e5938f4fa29d37e009a3bb3b13032ac57eb4eb42afa44f6e4054e69 \ - --hash=sha256:1bc4cc83fb7f66ffb16f74d6dd0162e144333fc36ebcce32246f80c8735b2551 \ - --hash=sha256:1dd6a1c3ad4cb674f44525d9957f3e9c209bb6dd9213245195167a281fcc2bdc \ - --hash=sha256:20cf4d0651987c906a2f5cba4e3a8d6ba4bfdf973cfe2a96c0d6053888ea2ecd \ - --hash=sha256:2173a7bffe97667bbf0767f8a99e587740a8c56fdf3befac4b09cb29a80276fd \ - --hash=sha256:21c3302068f50d1e8728c67c87ba92aa87043abee517aa2576cca1855326b405 \ - --hash=sha256:23a5dc68e08ed13331d61815c08f260f46b4a60fdd1640bbeb82cf89a9d90289 \ - --hash=sha256:23cad0cc86046d4222f7f418910e46b89971c5a45d3c8abfad0f64b7b05e4a9b \ - --hash=sha256:2593a0a6621545b9095b71ad74ed4226eba438a7d9fc3712a99bdb15508cf93a \ - --hash=sha256:264c605ab9c0e4aa1a679636f4582c4d3313700009fac3ec9c3412ed0d8f3e1d \ - --hash=sha256:26c5272c6a4bf4cf32d3f5a7890c942b0e04438691157d341616d02cca74d4bd \ - --hash=sha256:26dd9f57ee3bd41e7d35b4c98a2ffd89ed11591649f421f0ec19f67d50ec67ac \ - --hash=sha256:28902146ffbe5222df411c5d19e5352490122e14447e98cd118907ee3fd6ee62 \ - --hash=sha256:29f5c00cb7d752bce2c70ebd2d31b0a42f9499ffdd3ecb2f31a5b73ee43031ad \ - --hash=sha256:30e7b2ed63b6c8e97cca8af048589a788ab5c9c905f36d9cf1c2bb549f450d2f \ - --hash=sha256:32662519149fd7a9db354175aa5e417d83485a8039b8aaa62f873ceee7ea4cad \ - --hash=sha256:363e47283bde87051b821826e71dde47f107e08614e1aa312ba0c5711e77738c \ - --hash=sha256:3648f20d25102a22b6061c688beb3a805099ea4beb0a01ce62975d926944d292 \ - --hash=sha256:37448bf9c7d7adfc5254763901e2bbd6bb876228dfc1fc7f66e58c06368a7544 \ - --hash=sha256:37fabd1452852636cf38ecdcc9dd5ca4bba7a35d6c53fa09725deeb894a87491 \ - --hash=sha256:398443df51c538bd578529aa7e5f7afc6c292644174b47961f3bf87fe5741120 \ - --hash=sha256:3ae5d8d5427f3cc317e7950f2da7ad276df0cfa37b8de2f5658959e618ea8512 \ - --hash=sha256:3f00972f84450204cd5d93a5395965e348956aaceaadec693a22ec743f8ae3eb \ - --hash=sha256:40d9189f80075f2e1f88db21ef815a2b17b28adf8e50aaf5c789bfe737027f32 \ - --hash=sha256:419c58fc92cc3a2c3fa5f78c63dbf5da70c1fa9c1b25f25727ecee89a96c7de2 \ - --hash=sha256:41dcc4c7b10484257cbd6c37b83ddb26df2b0e5aff5ac00d095689015af868ec \ - --hash=sha256:43e4d297f11080ec9d64a4b1ad7ac02b4484c9f0e2179d9c4ef78e886e747b88 \ - --hash=sha256:45e9dfbd1b661eb64ba0d4dbe762bd210c42d86dd1e5bd2bdf89d634231beb43 \ - --hash=sha256:4642e04449a1e164b5ff71ffd901ddb772dfabf5c9adf1b7be5dffe1212bc037 \ - --hash=sha256:468479e52ecf3ec23799c863336d02c05fc2f7ffd1a1424eeeb9a28d4eb69d13 \ - --hash=sha256:47024feaae386a92a146af0d2aeed65229bf6fff738e6a11dda6b0015fb8fd03 \ - --hash=sha256:481d6e2104285d9add34f41b42b247b76b61c5b5c26c303c2e9707bbf8bd9a64 \ - --hash=sha256:4937460dc5df0cdd2f06a86c285c28afda06aefa3af949f9477d3e8df430c485 \ - --hash=sha256:4a1503c56e4e2b38dc76f2f2da7bae69670c0f1933e27cfa34b2fa5876410b16 \ - --hash=sha256:4b89b098105b8599dc57adac95d1813409ac476d3c948a498775d3d0c6124bfb \ - --hash=sha256:4bd1bdb8a9e0e2dd229de19b5f8aebac80e916921b4b2c6ef8a52bc131d0c1f9 \ - --hash=sha256:4e2c54d6b47361d0f1d3bc8d4e082ad87201e56ccdcca4d3b9ee3644ff595ec8 \ - --hash=sha256:52b0ac6903cf74ebf997eb8c682d2fbac7d1ab7e4c552413eec55868a9b73f39 \ - --hash=sha256:546b66c0dd1bb8d9fa89d7123e5fa19a8aff3a1f2141eb22df96112afb17b842 \ - --hash=sha256:56971379bc5ee8037c5a0f09fa88f66cdb7d37c3e38af3e45cf539f41131ac1f \ - --hash=sha256:5715e0e28736a070f3f34a7ccc09e2fdcba0e3060abbcf61a1a5718ff6d6b105 \ - --hash=sha256:5cfa1a34df366d9dc0d5eaf420f4cf2bb1e1bebe1066d1c2fc28c179f8a4004c \ - --hash=sha256:5d27bbe326c6b539c64b42638b18bc6003a8d88f76213a97ac9ed4f885efeab7 \ - --hash=sha256:6262b87f9e5c1e5fe501d6c153247289af42eb44ad7660b9b3de17baaf92d6f6 \ - --hash=sha256:63aeafc26aac0be8aff14af7871249e87ea1319be92090bfd632ec68e03b16a5 \ - --hash=sha256:690022c7fae793b0489aa68a658822cea83e0d5933781811cabbf5ea3bcfe73d \ - --hash=sha256:6fd8b1df8254ff4fd93fd31da1fc15770bde23ac045be9bb1f87425702f61cc9 \ - --hash=sha256:73becf6d8c81d4c76b1014dbd3584cb26d904492dcf73ca85dc8bff08dcd6d2d \ - --hash=sha256:73d658216fc173cf2c939e90e07b941c5e12736b0bf6a99e7af95459cfe8eabb \ - --hash=sha256:75c4c7c619a744f972f4451bf5adf6d0fb00992a1ffc9fd78e13b0bc817cc99f \ - --hash=sha256:76b958b4ea3104483c20f74866d55aa056546e15ebe83dd7aecd63698f43b755 \ - --hash=sha256:77b9f99b17cbf14026d1e618035077060fc7195dd940d025149f3e2e830fbfcb \ - --hash=sha256:7ba11752e346bd804ea312ec2eea2532dfa8b8d3261d81a32ef9e6ab16256280 \ - --hash=sha256:7da13bb6fbadfafb474e0226a30570a3445cfd47c86296f2446dafbd77079ace \ - --hash=sha256:7e39ab3a28af7784e206d8606ec0e4bcad0190f63a492bca95e94e5a4aef7f6e \ - --hash=sha256:7f4a77d6f7edf9230cee3e1f7f6764722a41604ee5681844f18db9a81ea0ec33 \ - --hash=sha256:80410c3a7e3c617af04de17caa9f9f20adaa817093293d69eae7d7d0522836f5 \ - --hash=sha256:81ff55c70b67d19d52b6fd118a114c0a4c97d799cd3089ff9bd9e2ff4b414ee2 \ - --hash=sha256:857efde87d365706590847b916baff69c0bc9252dc5af030e378c9800c0b10e3 \ - --hash=sha256:89e8d73d09ac696a5ba42ec69787913d53284f12092f651506779314f10ba585 \ - --hash=sha256:8c11b984b5ce6add4dccc7144c7be5d364d298f15b0c6a57da1991baedc750ce \ - --hash=sha256:8c8984e1d8c4b3949e419158fda14d921ff703a9ed8a47236c6eb7a2b6cb4946 \ - --hash=sha256:8e369cbd690e788c8d15e56222d91a09c6a417f49cbc543040cba0fe2e25a79e \ - --hash=sha256:9147d8e386ec3b82c3b15d88927f734f565b0aaadef7def562b853adca45784a \ - --hash=sha256:920354904d1cb86577d4b3cfe2830c2dbe81d6f4449e57ada428f1609b5985f7 \ - --hash=sha256:942454ff253da14218f972b23dc72fa4edf6c943f37edd19cd697618b626fac5 \ - --hash=sha256:972a6451204798675407beaad97b868d0c733d9a74dafefc63120b81b8c2de28 \ - --hash=sha256:976a6b39b1b13e8c354ad8d3f261f3a4ac6609518af91bdb5094760a08f132c4 \ - --hash=sha256:97faa0860e13b05b15a51fb4986421ef7a30f0b3334061c416e0981e9450ca4c \ - --hash=sha256:9c03e048b6ce8e77b09c734e931584894ecd58d08296804ca2d0b184c933ce50 \ - --hash=sha256:9e7b0a4ca6dcc007a4cef00a761bba2dea959de4bd2df98f926b33c92ca5dfb9 \ - --hash=sha256:9eb667bf50856c4a58145f8ca2d5e5be160191e79eb9e30855a476191b3c3495 \ - --hash=sha256:9f93d5b8b07f73e8c77e3c6556a3db269918390c804b5e5fcdd4858232cc8f16 \ - --hash=sha256:a0092f2b107b69601adf562a57c956fbb596e05e3e6651cabd3054113b007e45 \ - --hash=sha256:a02ca8fe48815bddcfca3248efe54451abb9dbf2f7d1c5744c8aa4142d476919 \ - --hash=sha256:a1d9b99e5b2597e4f5aed2484fef835256fa1b68a19e4265c97628ef4bf8bcf4 \ - --hash=sha256:a2853c8b2170cc6cd54a6b4d50d2c1a8a7aeca201f23804b4898525c7a152cfc \ - --hash=sha256:a31286dbb5e74c8e9a5344465b77ab4c5bd511a253b355b5ca2fae7e579fafec \ - --hash=sha256:a86f06f059e22a0d574990ee2df24ede03f7f3c68c1336293eee9536c4c776cd \ - --hash=sha256:ab863fd37458fed6456525f297d21239d987800c46e67da5ef04fc6b3dd93ac8 \ - --hash=sha256:ac4db068889f8772a4a698c5980ec302771bb545e10c4b095d4c8be26749616f \ - --hash=sha256:b6c2f225662bc5ad416bdd06f72ca301b31b39ce4261f0e0097017fc2891b940 \ - --hash=sha256:bb40648d96157f9081886defe13eac99253e663be969ff938a9289eff6e47b72 \ - --hash=sha256:bba078de0031c219e5dd06cf3e6bf8fb8e6e64a77819b358f53bb132e3e03366 \ - --hash=sha256:bc783ee3147e60a25aa0445ea82b3e8aabb83b240f2b95d32cb75587ff781814 \ - --hash=sha256:be10838781cb3be19251e276910cd508fe127e27c3242e50521521a0f3781690 \ - --hash=sha256:bfd57d8008c4965709a919c3e9a98f76c2c7cb319086b3d26858250620023b13 \ - --hash=sha256:c08da09dc003c9e8c70e06b53a11db6fb3b250c21c4236b03c7d7b443c318e7a \ - --hash=sha256:c3592631e652afa34999a088f98ba7dfc7d6aff0d535c410bea77a71743f3819 \ - --hash=sha256:c4a699432846df86cc3de502ee85f445ebad748a1c6021d445f3e514d2cd4b1c \ - --hash=sha256:c4e425db0c5445ef0ad56b0eec54f89b88b2d884656e536a90b2f52aecb4ca86 \ - --hash=sha256:c53fa3a5a52122d590e847a57ccf955557b9634a7f99ff5a35131321b0a85317 \ - --hash=sha256:c6854e9cf99c84beb004eecd7d3a3868ef1109bf2b1df92d7bc11e96a36c2180 \ - --hash=sha256:c748ebcb6877de89f48ab90ca96642ac458fff5dec291a2b9337cd4d0934e383 \ - --hash=sha256:c871299c595ee004d186f61840f0bfc4941aa3f17c8ba4a565ead7e4f4f820ee \ - --hash=sha256:cbd7b79cdcb4986ad78a2662625882747f09db5e4cd7b2ae178a88c9c51b3dfe \ - --hash=sha256:cc16682cc987a3da00aa56a3aa3075b08edb10d9b1e476938cfdbee8f3b67181 \ - --hash=sha256:cec05be8c876f92a5aa07b01d60bbb4d11cfbdd654cad0561c0d7b5c043a61b9 \ - --hash=sha256:d036ee7b99d5148072ac7c9b847193decdfeac633db350363f7bce4fff108f0e \ - --hash=sha256:d0d799ff958655781296ec870d5e2448e75150da2b3d07f13ff5b0c2c35beefd \ - --hash=sha256:d1392c569c032f78a11a25d1de1c43fff13294c793b39e19d84fade3045cbbc3 \ - --hash=sha256:d2f17a16cd8751e8eb233a7e41aecdf8e511712e00088bf9be455f604cd0d28d \ - --hash=sha256:d3829a6e6fd550a219564912d4002c537f65da4c6ae4e093cc34462f4fa027ad \ - --hash=sha256:d43aa26dcda363f21e79afa0668f5029ed7394b3bb8c92a6927a3d34e8b610ea \ - --hash=sha256:d6d8efe71429635f0559579092bb5e60560d7b9115ee38c4adbea35632e7fa24 \ - --hash=sha256:dabecc48db5f42ba348d1f5d5afdc54c6c4cc758e676926c7cd327045749517d \ - --hash=sha256:db88156fcf544cdbf0d95588051515cfdfd4c876fc66444eb98bceb5d6db76de \ - --hash=sha256:de550d129f18d8ab819651ffe4f38b1b713c7e116707de3c0c6400d0ef34fbc1 \ - --hash=sha256:e0af85773850417d994d019741239b901b22c6680206f46a34766926e466141d \ - --hash=sha256:e3c4f84b24a1fcba435157d111c4b755099c6ff00a3daee1ad281817de75ed11 \ - --hash=sha256:e3dd5fe19c9e0ac818a9c7f132a5e43c1339ec1cbbfecb1a938bd3a47875b7c9 \ - --hash=sha256:e69aa6805905807186eb00e66c6d97a935c928275182eb02ee40ba00da9623b2 \ - --hash=sha256:e80807d72f96b96ad5588cb85c75616e4f2795a7737d4630784c51497beb7776 \ - --hash=sha256:ebe33f4ec1b2de38ceb225a1749a2965855bffeef435ba93cd2d5d540783bf2f \ - --hash=sha256:f0cea5b1d3e6e77d71bd2b9972eb2446221a69dc52bb0b9c3c6f6e5700592d93 \ - --hash=sha256:f15401d8d3dbf239e23c818afc10c7207f7b95f9a307e092122b6f86dd43209a \ - --hash=sha256:f504d861d9f2a8f94020130adac88d66de93841707a23a86244263d1e54682f5 \ - --hash=sha256:fc46da94826188ed45cb53bd8e3fc076ae22675aea2087843d4735627f867c6d \ - --hash=sha256:fc7140d7a7386e6b545d41b7358f4d02b656d4053f5fa6859f92f4b9c2572c4d \ - --hash=sha256:fcf3da95e93349e0647d48d4b36a12783105bcc74cb0c416952f9988410846a3 \ - --hash=sha256:fe022f20bc4569ec66b63b3fb275a3d628d9d32da6326b2982584104db6d3086 \ - --hash=sha256:ffb34ea45a82dd637c2c97ae1bbb920850c1e59bcae79ce1c15af531d83e7215 +lxml==6.1.1 \ + --hash=sha256:05a82eb6e1530a64f26225b55cbd178113bd0b5af1c2b625f25e5296742c26d2 \ + --hash=sha256:07a4a68e286ee7a1ed7dfb8af83e615757c0ccfe9f18c6b4ea6771388d9ba8c9 \ + --hash=sha256:09dd5b7075dc2f7709654a46543ba1ea3c2e217b2ed8fbd413a8a945a0f40f60 \ + --hash=sha256:0b7e8a14c8634bf6f7a568634cb395305a6d964aeb5b7ee32248094bed3a7e2c \ + --hash=sha256:104c09bda8d2a562824c0e319d0768ce26a779b7601e0931d33b09b53c392ef7 \ + --hash=sha256:126c93f7f56f0eda92f6d8c619edc463a4f23d9252f1c9d0405a76f25fa9f11a \ + --hash=sha256:162af1091cd785f2f27e62d3547ae9bc58ec5c86dd314d67021fd02463708d83 \ + --hash=sha256:17e0e18d4ad8adbd0399291bc44845b69d9dd68439a3cdebdf35ff902ec05072 \ + --hash=sha256:18b73c339ae29b90fd2d06e58ebd555a751bde9cd6bbd36cc0281b9a2c94e9d8 \ + --hash=sha256:19607c6bbff2a44cf3fe8250abccd20942d3462473e0a721d01d379ed017e462 \ + --hash=sha256:19b7ab10b210b0b3ad7985d9ac4eb66ab09a90b20fe6e2f7ba55d01a234345d0 \ + --hash=sha256:1d4962d4c66bf830a7e59ed6cfc17d148149898a3aefa8ec6e59763e6e3ed085 \ + --hash=sha256:1db753c9115ec7100d073b744d17e25e88a8f90f5c39b2f5dd878149af59671f \ + --hash=sha256:1dde6131244bba38a17c745836ba190bc753fd73c9291666287fd0a3fa3dcf30 \ + --hash=sha256:25c6997a9a534e016695a0ba06b2f07945de682731ff01065b6d5a4474179da1 \ + --hash=sha256:26e6eda8d38c1fcab1090dd196ee87cbd13788e531937610e2589085de074e77 \ + --hash=sha256:27acc820660aaffa4f7c087f29120e12980f7779d56d8492d263170111284740 \ + --hash=sha256:2a0217714657e023ef4293500f65aa20fce6164c8fd6b08fa5bd4a859fb14b9b \ + --hash=sha256:2c8daa471358dc2d6fcf02165e80ec68f77871a286df95bc5cc3816153b0fd2c \ + --hash=sha256:30a89d3ac8faec007453fb541f3f46807eeec88edd5826f6e3fe001752a2c621 \ + --hash=sha256:31033dc34636ea6b7d5cc11b1ddbda78a14de858ba9d3e1ed4b69a3085bc521e \ + --hash=sha256:32ab449a5486f6c758e849bb86710d0e45edc24a04e250c01555f8f5653958f8 \ + --hash=sha256:3483644525531e1d5762b0c44a8e18b6efba321b6dcf8a8952de10b037618bca \ + --hash=sha256:34c2d737beabfe35baada43941ed519251e9a12e779031496bcd5d539fcfd730 \ + --hash=sha256:3779def59032b81e44a5f70096ef6bf2082f8d901937dca354474ba09782e245 \ + --hash=sha256:37a58976370f36d9329d118ad0b953c5aeb9119ac9c6a4e258942a225d0573a1 \ + --hash=sha256:3893c14c4b6ac5b2d54ba8cf03e99fe5104e592de491f19bd6b82756c09f8004 \ + --hash=sha256:3a12689be69a28ddaa0ab99a5a1137da2afd5f8f16df7b5680b66f616d3eda1d \ + --hash=sha256:3ab541146f1f6968c462d6c2ac495148e8cdba2f8347700b2141b6ec5a75bf52 \ + --hash=sha256:3abf332af33a74288675d936fe861fd4344da0dd6622193fbc4f2bfbb35536b5 \ + --hash=sha256:3fd9728a2735fda14f4e8235830c86b539e9661e849665bf926d3f867943b4bf \ + --hash=sha256:424aa57aca0897eb922aef34395bd1289b3b6f04e6bae20ea123c0c7e333cffc \ + --hash=sha256:441dd227fa0690eb9fc81edabc63cdcefc212bba99b906dcf6e32cc1a9d3e533 \ + --hash=sha256:469e3618338bd7ab5beb412d2439825479fcf0dab99e394ca563dbc4eaf6c834 \ + --hash=sha256:47402e62c52ff5988c1e8c6c63177f5708bccf48e366dea4e3dcf1e645e04947 \ + --hash=sha256:4f0dd2f01f9f8a89f565d000e03abcf0a13d692a346c8d22f628d49af098777a \ + --hash=sha256:53b7d2b7a10b1c35c0a5e21e9224accf60c1bbfba523990732e521b2b73adef2 \ + --hash=sha256:53c909b62a0532183542fed00c5a7218258c56292d409bc789886fe1cb04c438 \ + --hash=sha256:54a7f95e4de5fb94e2f9f4b9055c6ba33bf3d628fd77a1d647c5923caa2cdcdc \ + --hash=sha256:556e94a63c9b04716f8e4de2abb65775061f846e89331b6c5be79183a24f98ea \ + --hash=sha256:55b03549819867ea141c0202242c4816c82e52ec36e7e648db9d8da5a3dc3ed6 \ + --hash=sha256:581d4c8ae690a6609e64862dd6b7c2489635c2d13907fc2b20f2bc200ff1d21e \ + --hash=sha256:58bb955caba94e467d2a96da17660d2d704e0675894cba21ab8a775b8621fd1c \ + --hash=sha256:5b7328b46d49fc9477d91ae8f6d55340347d827b7734ba3ea33faae0efef1383 \ + --hash=sha256:5ba186ad207446c65d3bb3d3e0412b032b1d9f595e59861e2354798c5703d955 \ + --hash=sha256:5bec7d03d78d853597d6107854c2310ce3f761fd218fe9fe91d5101fcf6c2efe \ + --hash=sha256:5c6bf403fbb3b3e348a561a5f4f0b9961835657981c802a1df03653eef8a9074 \ + --hash=sha256:5f6994074ebae6ffb04447268e37dc16edc304f9859cf91acb86e0af6c1b395c \ + --hash=sha256:62aeb7e85b5d60320b9d77eef2e773994e2c0ce10121b277e0a19804e1654a5a \ + --hash=sha256:63876be28efefa04a1df615b46770e82042cce445cfdce55160522f57b231ccb \ + --hash=sha256:639f6c857d91d9be29bd7502348d6736dab168b54b5158cd899abf11684dc186 \ + --hash=sha256:640f97d43d867bcb9c75b3af013b64850756b746cb6bce8ace83b70da3abba9d \ + --hash=sha256:649dda677cf3bd6ac9ae14007ba0c824ded8ce5808b53fc7431d9140399118c1 \ + --hash=sha256:6540377fbd53fe1b629172288c464fb18db11ce1fa7dc15891da10aa9dcc3e7f \ + --hash=sha256:6689e828a94eee4f139408c337bb198e014724bb8a8c26d3cfac49d119ed69a6 \ + --hash=sha256:68a9198d0fc122d14bb76837de9aa80cf84caed990b5b237f532ed87d3706736 \ + --hash=sha256:6b1761fbf9ec984e2e9d9c589ef5f5fd684b7c19f92aadd567a26c5224958db6 \ + --hash=sha256:70cdfd80589d59e43e18005dd7244e8895e93db8ab6a620b7e23df5445a4e3d2 \ + --hash=sha256:70ef8a7e102a1508f8121aae5b0867abd663f72c14f0a9c937e6554cb4587b7b \ + --hash=sha256:73bc2086f141224ebddb7fc5c6a36ca58b31b94b561e1dfe8e073e3270fad1e7 \ + --hash=sha256:74a9717fd0d82effef5c2854f0d917231d5324b5a3eb7275c43ac9fa32f97a14 \ + --hash=sha256:752d3bbfe874715ccd0aec7f88d7fc623c0f1fd7aa7b3238a084e017bad2a009 \ + --hash=sha256:762ff394d5bd56da0cf034a23dcce4e13923f15321a2adfa2ac00201dc6d3fca \ + --hash=sha256:76447f65250ed2501ead1a1552f5ce8edff159a86f308348e6a9c4acb5e1f1b4 \ + --hash=sha256:766b010012d59470072c1816b5b6c69f1d243e5db36ea5968e94accf430a4635 \ + --hash=sha256:787b2496d0dbe8cd180984e8d29e3a6f76e7ea34db781cb3bd55e4ba1ef8b4ee \ + --hash=sha256:793033d6c5cdf33a573f910d9bea14ef8f5771820411d118da8e1182edb53d5e \ + --hash=sha256:7d47866cb32fb503450b6edc9df355d10dc49836af2e89901bd6ac6b0896d9d9 \ + --hash=sha256:7f7a92e8583f06b1fd49d01158143b8461cfcd135dcb10ec807270a3051bd603 \ + --hash=sha256:80c2dfadb855da477cf73373ad29a333535dedb9b12bad02c9814c8e2b43bf08 \ + --hash=sha256:83b6b30eb131da7a75b601f28c5d6971e6ed3e887919bf6b6a1ad3c2df289080 \ + --hash=sha256:86281fbdd6a8162756f8d603f37e3435bfa38043adb79c6dc6a2dfee065e7525 \ + --hash=sha256:86c89b9d55ebf820ad7c90bc533410f0d098054f293351f10603c0c46ff598f5 \ + --hash=sha256:876e1ff5930ed8bf295ec5ef9a8155e9b6b1876bbf1deed8b3a8069311875a8f \ + --hash=sha256:88136950da4d13c318bde414ce10219931937851327f44328f2df4d2c4614067 \ + --hash=sha256:88d8cb75b9d82858497a5393e3c63cfbf03035225e4b35a49ed7ccb151e4dc0e \ + --hash=sha256:8be8ad51249698103d24b0571df35a10990fbe93dd043b6c024172189485f5e3 \ + --hash=sha256:8d43ca737b20e106e4aebc42b2f3ae19f00ba63d7eb731698ee083d72d15646f \ + --hash=sha256:8dadbe5b217ff35b6a8d16610dd710219b59b76d13f0e3f0d9f36786206e4485 \ + --hash=sha256:9395002973c827b3ed67db77e6ec09f092919a587022174554096a269378fb13 \ + --hash=sha256:96f2ec43df44b1f76249ee0a615334f9b5b060e1c8bd90e706dad2d14d02f383 \ + --hash=sha256:98fc784c2c1440667aeedf8465bdfe10208acf0ead656a2c68627299f546b315 \ + --hash=sha256:9e36f163528fc50cbef305f02a5fd66d404edf7049cdaff211dbc2cba5a7013e \ + --hash=sha256:9eb9b5a968f6e0f6d640092a567e14529ff8cea2e29d00da6f78a79fa49f013c \ + --hash=sha256:9f76acfb5f68ba982635a53fd985a8044be98a35b43232c2a1ee235ffab3e1dd \ + --hash=sha256:a088f287f7d8275a33c07f2cac6c50b9319309a0200a39e7e75d80c707723099 \ + --hash=sha256:a10bd2fd62e8ce916ececb342f348f190724a098c1faa056fdfb2a22ad5e8660 \ + --hash=sha256:a4bbea04c97f6d78a48e3fbc1cb9116d2780b1b39e03a23f6eb9b603fd61f510 \ + --hash=sha256:aa366a1e55b8ebfe8ca8ddc3cfe75c8ebade181aeb0f661d0cb05986b647f72a \ + --hash=sha256:aa49e06d94aba782c6a02eecb7e507969e7e7a41b267f1b359bb35585f295d5b \ + --hash=sha256:aad9aa39483ed8ec44d6d2e59e5b98a0d80676ef0d92f44bfc374836111f62f5 \ + --hash=sha256:aae97dfdb60715c164419ac2532a76d013c3918a665eb6cb7288098b5f349aaf \ + --hash=sha256:abbefa31eee84842140f67acef1c828e28bba8bbf0c3bc6e5492a9af88152c28 \ + --hash=sha256:ac931cdc9442c1763b8a8f6cd62c0c938737eafc5be75eff88df55fc73bc0d00 \ + --hash=sha256:acd7d70b64c0aae0c7922cca83d288a16f5f6da523637697872253415269baef \ + --hash=sha256:add8cf6ddf9a65116119a28ece0f7886e30af27ba724a7594305f1d1b58a92a1 \ + --hash=sha256:aee395f5d0927f947758b4ec119fd5fc8ec71f07a1c5c52077b30b04c0fa6955 \ + --hash=sha256:b1b963fd8f5caa68e99dfae060d54de1fe9cba899b8718b44a00cdca53c3e590 \ + --hash=sha256:b2d444f2e66624d68e9c6b211e28a76e22fff5fcabcfff4deac18b529b7d4137 \ + --hash=sha256:b8d812c6011c08b8111a15e54dd990b8923692d80adf35488bee34026c35accf \ + --hash=sha256:ba96ae44888e0185281e937633a743ea90d5a196c6000f82565ebb0580012d40 \ + --hash=sha256:bdebcc8a75d38c7598dfb2c9ed852d7a9eb4a10d6e2d0764b919b802bf32ac88 \ + --hash=sha256:c07da4cebf6889f03ebac8d238f62318e29f495de0aa18a51ea14e61ae907e2e \ + --hash=sha256:c08e5c694306507275f2290073350c4f32e383db15213b2c69e7ff39c1193840 \ + --hash=sha256:c4f469aebd783bb741c2ecb2a681008fd26bfe5c16a9a72ed5467f834e810df2 \ + --hash=sha256:c5d7152ec39ca7c402d8fb9bad86140a15b9503bd0c54484e3f1bbe3dd37ceca \ + --hash=sha256:c674693f055fa2495de12292cb45e9944199d8eaef5a2dec45175c7c61cb73e3 \ + --hash=sha256:c6ed5141a5c7507cf3ee76bd363b0d6f801e3321adc35b5d825a23115faa5465 \ + --hash=sha256:c921ba5c51e4e9f63b8b00267d06566e1f63407408a0496da2d1d0bfc819c7fc \ + --hash=sha256:c9a4b821dc7055bf9e05ff5719e18ec501f75c0f0bbfabd573b277559780833d \ + --hash=sha256:c9f79d5325907f13e1be0b3e4dacc1049d1dffc4aeee3c995284bea5fe0fab7d \ + --hash=sha256:cd312b9692e831d2ffcad61eab31d91d4b4655a962e61de8fb410472cbcd37aa \ + --hash=sha256:cea3f4c1af79af13cdb2da0c028111d8f8522d4f22a000c82385535f24e5cf3a \ + --hash=sha256:cecdd5dfdc87b1fd87dbf81d4b037a544f47f4c744200a67013771682d67686a \ + --hash=sha256:cf9d57306d848218f3601fee7601fab1a327c942d56e2e97610583cb4dd74206 \ + --hash=sha256:d34bbf07dbc7ca5970671b1512e928991fb5e9d95365636c9b2d8b4f53af405e \ + --hash=sha256:d49514be2f28d895c38cf9d2b72d7b9a07d00314519f456c0b50b53cfcf4c785 \ + --hash=sha256:d680fbcb768404c601ecb43519ecd8461f6954cb11c06a78962f666832ccfca8 \ + --hash=sha256:db1d75f6617a49c1c01bc7023713e0ff59ab32c9579ae62a7674c0e34f3b0b0a \ + --hash=sha256:dcb292aa7fe485ceff7af4f92e46c5af397daec5dff64871a528f0fc47a3cc5b \ + --hash=sha256:e07c65f443c887bbcf31cc1771d932ecc192a5273943589b3c7572b749f1ffb2 \ + --hash=sha256:e902da4b04e6b52e5893900d4b8ab46068f75f3561f01bf1080957f9fd932ed6 \ + --hash=sha256:e9308ff8241c532df3f3e570f9a5aeed6c853f888512ba4b75638d7c11c95ef6 \ + --hash=sha256:eb7c9811bfaa8b1ed5ed319f5d370dfbcaa59d52ea64be2a5a85e18195930354 \ + --hash=sha256:ebe6af670449830d6d9b752c256a983291c766a1365ba5d5460048f9e33a7818 \ + --hash=sha256:ed21202aec73cda4d55d1ce57b389aadb90ffb044e6cd1080b8347efe1b1ec84 \ + --hash=sha256:efe0374196335f93b53269acd811b944f2e6bdc88e8894f214bd636455484909 \ + --hash=sha256:f64ec5397ea6a41fc1b4af0380d79b44a755b5531dcaccd9940fb260dca93038 \ + --hash=sha256:f6ac4ef4d82dff54670227a69c67782ae0b811b5cf6b17954f1e8f7502fc0d1d \ + --hash=sha256:f6f0ce10945fab9c4c06ce14e22af9059d1a87493a9af4501a5b0b9187e21cf2 \ + --hash=sha256:f8844cd288697c6425c9beba919302241e3278871dc6519515e72b04e987abcf \ + --hash=sha256:fe0306bd29505a9177aac19f1877174b0e7422c222a59f70b2cd41633448c3dc \ + --hash=sha256:ff3f333630ab480244a1bff72043e511a91eb22e7595dead8653ee5612dd8f3d \ + --hash=sha256:ffecec8eb889b58ba9be5b95fb1cc78e22ea8eedea38e8736a1568fe1979250e # via # python3-saml # xmlsec @@ -1184,9 +1184,9 @@ openpyxl==3.1.5 \ --hash=sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2 \ --hash=sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050 # via tablib -opentelemetry-api==1.41.1 \ - --hash=sha256:0ad1814d73b875f84494387dae86ce0b12c68556331ce6ce8fe789197c949621 \ - --hash=sha256:a22df900e75c76dc08440710e51f52f1aa6b451b429298896023e60db5b3139f +opentelemetry-api==1.42.1 \ + --hash=sha256:51a69edacadbc03a8950ace1c4c21099cacc538820ac2c9e36277e78cebba714 \ + --hash=sha256:56c63bea9f77b62856be8c47600474acad853b2924b99b1687c4cb6297166716 # via # -r src/backend/requirements.in # opentelemetry-exporter-otlp-proto-grpc @@ -1203,27 +1203,27 @@ opentelemetry-api==1.41.1 \ # opentelemetry-instrumentation-wsgi # opentelemetry-sdk # opentelemetry-semantic-conventions -opentelemetry-exporter-otlp==1.41.1 \ - --hash=sha256:299a2f0541ca175df186f5ac58fd5db177ba1e9b72b0826049062f750d55b47f \ - --hash=sha256:db276c5a80c02b063994e80950d00ca1bfddcf6520f608335b7dc2db0c0eb9c6 +opentelemetry-exporter-otlp==1.42.1 \ + --hash=sha256:2d9ebaed714377a67d224d46795ddcc11d2c877fa5de35fda70b6f3b010729a9 \ + --hash=sha256:aedd54545bb0587cd45210abdc8be545af9c01413f3307786e276df1e3c83bee # via -r src/backend/requirements.in -opentelemetry-exporter-otlp-proto-common==1.41.1 \ - --hash=sha256:0e253156ea9c36b0bd3d2440c5c9ba7dd1f3fb64ba7a08fc85fbac536b56e1fb \ - --hash=sha256:10da74dad6a49344b9b7b21b6182e3060373a235fde1528616d5f01f92e66aa9 +opentelemetry-exporter-otlp-proto-common==1.42.1 \ + --hash=sha256:04f1f01fb597c4249dfcd7f8b861c902c2102369d376d9d346ff38de4469a2ee \ + --hash=sha256:f48d395ab815b444da118868977e9798ea354c25737d5cf39578ae894011c140 # via # opentelemetry-exporter-otlp-proto-grpc # opentelemetry-exporter-otlp-proto-http -opentelemetry-exporter-otlp-proto-grpc==1.41.1 \ - --hash=sha256:537926dcef951136992479af1d9cd88f25e33d56c530e9f020ed57774dca2f94 \ - --hash=sha256:b05df8fa1333dc9a3fda36b676b96b5095ab6016d3f0c3296d430d629ba1443b +opentelemetry-exporter-otlp-proto-grpc==1.42.1 \ + --hash=sha256:0ae1177e2038b18a929b3098215243631ef91136cba26b7e2b12790ceb7e87cc \ + --hash=sha256:975c4461f167dd8ed8857d68d3b6b25f3d272eab896f6a9470d0f5b90e2faf15 # via opentelemetry-exporter-otlp -opentelemetry-exporter-otlp-proto-http==1.41.1 \ - --hash=sha256:1a21e8f49c7a946d935551e90947d6c3eb39236723c6624401da0f33d68edcb4 \ - --hash=sha256:4747a9604c8550ab38c6fd6180e2fcb80de3267060bef2c306bad3cb443302bc +opentelemetry-exporter-otlp-proto-http==1.42.1 \ + --hash=sha256:00a16da1b312a1d6c7233d600d557c91df71125af73020f3b9a7765bd699d59d \ + --hash=sha256:bf142a21035d7571ac3a09cb2e5639f49886f243972883cfe777ed3bf02b734d # via opentelemetry-exporter-otlp -opentelemetry-instrumentation==0.62b1 \ - --hash=sha256:90e92a905ba4f84db06ac3aec96701df6c079b2d66e9379f8739f0a1bdcc7f45 \ - --hash=sha256:976fc6e640f2006599e97429c949e622c108d0c17c2059347d1e6c93c707f257 +opentelemetry-instrumentation==0.63b1 \ + --hash=sha256:32368d6ae52c8de20aa790a6ad86b10a76f09956092337ae37d675773990e541 \ + --hash=sha256:f1986716d52cc316ea5f60189098726a9071d8ecc0eee96c9ed110be08bade9c # via # opentelemetry-instrumentation-dbapi # opentelemetry-instrumentation-django @@ -1234,75 +1234,76 @@ opentelemetry-instrumentation==0.62b1 \ # opentelemetry-instrumentation-sqlite3 # opentelemetry-instrumentation-system-metrics # opentelemetry-instrumentation-wsgi -opentelemetry-instrumentation-dbapi==0.62b1 \ - --hash=sha256:82755a0f013980b2c000a10452fb91ab5135b937a8afe37c45f34e62ecc7a2b8 \ - --hash=sha256:dd2cc4085e832a708835218833b839a88ef0ace66851d14debefe6b2ef363a3c +opentelemetry-instrumentation-dbapi==0.63b1 \ + --hash=sha256:1bc842ddcacb7ec3622ef306ffdb3c584bf97df79d381e03604ea6eb23c63781 \ + --hash=sha256:406978ed56bcfc5fd246fd918e6b36d0f5de26fa396c78cf63326a7b530597c8 # via # opentelemetry-instrumentation-psycopg # opentelemetry-instrumentation-pymysql # opentelemetry-instrumentation-sqlite3 -opentelemetry-instrumentation-django==0.62b1 \ - --hash=sha256:268c9b3d234510ab835acea0524ba65361389f6e2c291b495ba8072c7bb59003 \ - --hash=sha256:5ac96ef25019fbea5e2739c9b4b7ce17e4379e2b1ec9fdce6b696a5c09ceabbe +opentelemetry-instrumentation-django==0.63b1 \ + --hash=sha256:909ea4afbb18f16eb811d7ac330ed0f6fda7765191d49bccb8cca38c4fbc005f \ + --hash=sha256:f2071d2f92e4779c5a14dd452b0dfe426343599e6efa9d888304fb639a9f3101 # via -r src/backend/requirements.in -opentelemetry-instrumentation-psycopg==0.62b1 \ - --hash=sha256:61c8a1794d6e11c7bca1735c2b87ef8c04b5d7202e962409d66d204f84c01be1 \ - --hash=sha256:f08e843e81656271ddc8c8194a54a5f3c8ebe0cb137e3166af9a2ff014978b5e +opentelemetry-instrumentation-psycopg==0.63b1 \ + --hash=sha256:4d74ec2dda8e54ead9bd71f134af984792f77e1d8809591ae53edadddd4c8fc0 \ + --hash=sha256:86819f4cccea21a47920807826495797b7261cc1893c8e855415e76fb7b8e2e8 # via -r src/backend/requirements.in -opentelemetry-instrumentation-pymysql==0.62b1 \ - --hash=sha256:12e42f891978a3a41bc564bb81e2299d3654659589f705064a4bb10e93e493f8 \ - --hash=sha256:50d2efdde1bd8d826cbceffe5b3ce0ffd421349d609200eab0f3e632945cd304 +opentelemetry-instrumentation-pymysql==0.63b1 \ + --hash=sha256:2cf6a24e136b5b0c3f6e59675fc44b42ad2b97474b075a82859f9868a9252648 \ + --hash=sha256:6b54b83c86ac6c2e8dae642bbee4894e2283ceb5da23d571f1abb7060f7b62f4 # via -r src/backend/requirements.in -opentelemetry-instrumentation-redis==0.62b1 \ - --hash=sha256:2d3c421d95e05ade075bee5becbe34e743b1cdf5bdee2085cb524f88c4f13dcb \ - --hash=sha256:9aedd02c1acf631251d1d676634db47da9da04e0a626cd0c7d83fe0eb791d165 +opentelemetry-instrumentation-redis==0.63b1 \ + --hash=sha256:28d235159df43cc2bc8779af5c602afad1e08603fff75ac8ca34dd1bf30a9cb9 \ + --hash=sha256:f0e51c4006f68e340abbf28a7995feff004de78649697cbdf3bac0072cacd082 # via -r src/backend/requirements.in -opentelemetry-instrumentation-requests==0.62b1 \ - --hash=sha256:67a79c4b67e2192445c1cf03d62126fa623065688d8bd1a9f87f858b0e5f0286 \ - --hash=sha256:ca348f2f51b715c21e86d82106d784f7069ae849c3e636ab37e34dc0ba510b8c +opentelemetry-instrumentation-requests==0.63b1 \ + --hash=sha256:513fcaa3d93debbdb359c00ce1a137a34a89ee908c51ac43beb7e8c18ac2b3cd \ + --hash=sha256:935c980a11e33bfd7ed969c741e4bd7c84077045651469f10e163534368d87f7 # via -r src/backend/requirements.in -opentelemetry-instrumentation-sqlite3==0.62b1 \ - --hash=sha256:14ba18aed35a01e1d89bb814e49f7d6dd90bb68b2034ccf047801ca4659dba27 \ - --hash=sha256:3ab0d614bab9956126ce81a22315bf58d56d653a65675a66e8b525f880ec3016 +opentelemetry-instrumentation-sqlite3==0.63b1 \ + --hash=sha256:3d61afda8358dc32135fabd27e5934bd25ea0eed68d64c34508f24ba8d723efc \ + --hash=sha256:cc11c68cccde061dc8bcc5f80fe00bb0105a0fbb11b390859a23ca91ccd4e3b6 # via -r src/backend/requirements.in -opentelemetry-instrumentation-system-metrics==0.62b1 \ - --hash=sha256:260130cb63e3028290305d8f25b744fb3a279677769dd65287ce60086cc11d32 \ - --hash=sha256:b8097e0d3747102fa545723bb67f50f572a7618b6479359fa6533007d2f621aa +opentelemetry-instrumentation-system-metrics==0.63b1 \ + --hash=sha256:995051f47876d79461aed8b7aa205d4584d90794ef864342cc748929c389bb42 \ + --hash=sha256:d6d4d7a1a854be4165143cf6420ee5894188762eb367d7bf9da5be4a83a4b632 # via -r src/backend/requirements.in -opentelemetry-instrumentation-wsgi==0.62b1 \ - --hash=sha256:02a364fd9c940a46b19c825c5bfe386b007d5292ef91573894164836953fe831 \ - --hash=sha256:a2df11de0113f504043e2b0fa0288238a93ee49ff607bd5100cb2d3a75bc771f +opentelemetry-instrumentation-wsgi==0.63b1 \ + --hash=sha256:03d61c4678ce82402e7f37b6a3dbd84cb97b85b3cb416a78c2e74c7c6d9451fa \ + --hash=sha256:86779715262227d3436bdfb16aabd1c524b0f236725a69e8754ceda76c6d79dc # via # -r src/backend/requirements.in # opentelemetry-instrumentation-django -opentelemetry-proto==1.41.1 \ - --hash=sha256:0496713b804d127a4147e32849fbaf5683fac8ee98550e8e7679cd706c289720 \ - --hash=sha256:4b9d2eb631237ea43b80e16c073af438554e32bc7e9e3f8ca4a9582f900020e5 +opentelemetry-proto==1.42.1 \ + --hash=sha256:c6a51e6b4f05ae63565f3a113217f3d2bfaec68f78c02d7a6c85f9010d1cfca6 \ + --hash=sha256:dedb74cba2886c59c7789b227a7a670613025a07489040050aedff6e5c0fb43c # via # opentelemetry-exporter-otlp-proto-common # opentelemetry-exporter-otlp-proto-grpc # opentelemetry-exporter-otlp-proto-http -opentelemetry-sdk==1.41.1 \ - --hash=sha256:724b615e1215b5aeacda0abb8a6a8922c9a1853068948bd0bd225a56d0c792e6 \ - --hash=sha256:edee379c126c1bce952b0c812b48fe8ff35b30df0eecf17e98afa4d598b7d85d +opentelemetry-sdk==1.42.1 \ + --hash=sha256:083cd4bbfaa5aa7b5a9e552430d9951219967cfb27aa61feb13a77aba1fc839d \ + --hash=sha256:8c834e8f8c9ba4171d4ec843d0cb8a67e4c7394d3f9e9297e582cbd9456ddbf7 # via # -r src/backend/requirements.in # opentelemetry-exporter-otlp-proto-grpc # opentelemetry-exporter-otlp-proto-http -opentelemetry-semantic-conventions==0.62b1 \ - --hash=sha256:c5cc6e04a7f8c7cdd30be2ed81499fa4e75bfbd52c9cb70d40af1f9cd3619802 \ - --hash=sha256:cf506938103d331fbb78eded0d9788095f7fd59016f2bda813c3324e5a74a93c +opentelemetry-semantic-conventions==0.63b1 \ + --hash=sha256:3daf963611334b365e98a57438183eb012d3bfb40b2d931a9af613476b8701a9 \ + --hash=sha256:dfe5ef4dee82586b746f522b818ceb298d00b3d59f660042bd79404bff8d0682 # via # opentelemetry-instrumentation # opentelemetry-instrumentation-dbapi # opentelemetry-instrumentation-django # opentelemetry-instrumentation-redis # opentelemetry-instrumentation-requests + # opentelemetry-instrumentation-system-metrics # opentelemetry-instrumentation-wsgi # opentelemetry-sdk -opentelemetry-util-http==0.62b1 \ - --hash=sha256:adf6facbb89aef8f8bc566e2f04624942ba08a7b678b3479a91051a8f4dc70a3 \ - --hash=sha256:c57e8a6c19fc422c288e6074e882f506f85030b69b7376182f74f9257b9261f0 +opentelemetry-util-http==0.63b1 \ + --hash=sha256:6284194028c59cd439f8acfe388145069a6127f11dc077e1344a2094adacc3f8 \ + --hash=sha256:ba1268f00922ee522dba2ae38458060f99486e7385a8056985901ca9685adfff # via # opentelemetry-instrumentation-django # opentelemetry-instrumentation-requests @@ -1490,9 +1491,9 @@ pydyf==0.12.1 \ --hash=sha256:ea25b4e1fe7911195cb57067560daaa266639184e8335365cc3ee5214e7eaadc \ --hash=sha256:fbd7e759541ac725c29c506612003de393249b94310ea78ae44cb1d04b220095 # via weasyprint -pyjwt[crypto]==2.12.1 \ - --hash=sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c \ - --hash=sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b +pyjwt[crypto]==2.13.0 \ + --hash=sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423 \ + --hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728 # via # django-allauth # djangorestframework-simplejwt @@ -1523,9 +1524,9 @@ pynacl==1.6.2 \ --hash=sha256:d29bfe37e20e015a7d8b23cfc8bd6aa7909c92a1b8f41ee416bbb3e79ef182b2 \ --hash=sha256:fe9847ca47d287af41e82be1dd5e23023d3c31a951da134121ab02e42ac218c9 # via paramiko -pypdf==6.11.0 \ - --hash=sha256:062b51c81b0910e6d2755e99e1c5547a0a23b7d0a32322af66240d8edcfabe87 \ - --hash=sha256:769394d5756d5b304c9b6bef88b54b1816b328e7e6fc9254e625529a15ed4ab8 +pypdf==6.12.1 \ + --hash=sha256:3953a097b9f26d4e0ead5ff95943d9971377557662a91d8872186053cd71d30a \ + --hash=sha256:8fa2a2321cf16247ed848bd7c97f193a60c08670d04abed5b0138327e51c43b0 # via -r src/backend/requirements.in pyphen==0.17.2 \ --hash=sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd \ @@ -2130,10 +2131,6 @@ xmlsec==1.3.17 \ # via # -r src/backend/requirements.in # python3-saml -zipp==3.23.1 \ - --hash=sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc \ - --hash=sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110 - # via importlib-metadata zopfli==0.4.1 \ --hash=sha256:02086247dd12fda929f9bfe8b3962b6bcdbfc8c82e99255aebcf367867cf0760 \ --hash=sha256:07a5cdc5d1aaa6c288c5d9f5a5383042ba743641abf8e2fd898dcad622d8a38e \ diff --git a/src/frontend/lib/enums/ModelInformation.tsx b/src/frontend/lib/enums/ModelInformation.tsx index 63aeb6dff5..4eb6b4934d 100644 --- a/src/frontend/lib/enums/ModelInformation.tsx +++ b/src/frontend/lib/enums/ModelInformation.tsx @@ -10,6 +10,7 @@ export interface ModelInformationInterface { url_detail?: string; api_endpoint: ApiEndpoints; admin_url?: string; + pk_field?: string; supports_barcode?: boolean; icon: keyof InvenTreeIconType; } diff --git a/src/frontend/lib/types/Forms.tsx b/src/frontend/lib/types/Forms.tsx index 83e78a8e1a..b918205b97 100644 --- a/src/frontend/lib/types/Forms.tsx +++ b/src/frontend/lib/types/Forms.tsx @@ -2,6 +2,7 @@ import type { DefaultMantineColor, MantineStyleProp } from '@mantine/core'; import type { UseFormReturnType } from '@mantine/form'; import type { JSX, ReactNode } from 'react'; import type { FieldValues, UseFormReturn } from 'react-hook-form'; +import type { NavigateFunction } from 'react-router-dom'; import type { ApiEndpoints } from '../enums/ApiEndpoints'; import type { ModelType } from '../enums/ModelType'; import type { PathParams, UiSizeType } from './Core'; @@ -160,6 +161,7 @@ export type ApiFormFieldSet = Record; * @param modelType : Define a model type for this form * @param follow : Boolean, follow the result of the form (if possible) * @param table : Table to update on success (if provided) + * @param navigate : Optional navigate function to use for following results (if follow is true) */ export interface ApiFormProps { url: ApiEndpoints | string; @@ -189,6 +191,7 @@ export interface ApiFormProps { follow?: boolean; actions?: ApiFormAction[]; timeout?: number; + navigate?: NavigateFunction; keepOpenOption?: boolean; onKeepOpenChange?: (keepOpen: boolean) => void; } diff --git a/src/frontend/lib/types/Rendering.tsx b/src/frontend/lib/types/Rendering.tsx index 2ad427c519..ebd64dd745 100644 --- a/src/frontend/lib/types/Rendering.tsx +++ b/src/frontend/lib/types/Rendering.tsx @@ -15,6 +15,7 @@ export interface InstanceRenderInterface { link?: boolean; navigate?: any; showSecondary?: boolean; + showHover?: boolean; extra?: Record; } diff --git a/src/frontend/src/components/dashboard/DashboardWidget.tsx b/src/frontend/src/components/dashboard/DashboardWidget.tsx index cc806d01b9..b6809bcc08 100644 --- a/src/frontend/src/components/dashboard/DashboardWidget.tsx +++ b/src/frontend/src/components/dashboard/DashboardWidget.tsx @@ -5,6 +5,7 @@ import { IconX } from '@tabler/icons-react'; import { Boundary } from '@lib/components/Boundary'; import type { ModelType } from '@lib/index'; +import type { InvenTreeIconType } from '@lib/types/Icons'; import type { JSX } from 'react'; /** @@ -22,6 +23,7 @@ export interface DashboardWidgetProps { minWidth?: number; minHeight?: number; modelType?: ModelType; + icon?: keyof InvenTreeIconType; render: () => JSX.Element; visible?: () => boolean; } diff --git a/src/frontend/src/components/dashboard/DashboardWidgetDrawer.tsx b/src/frontend/src/components/dashboard/DashboardWidgetDrawer.tsx index 397111102e..b4ad089471 100644 --- a/src/frontend/src/components/dashboard/DashboardWidgetDrawer.tsx +++ b/src/frontend/src/components/dashboard/DashboardWidgetDrawer.tsx @@ -12,10 +12,11 @@ import { Tooltip } from '@mantine/core'; import { useDebouncedValue } from '@mantine/hooks'; -import { IconBackspace, IconLayoutGridAdd } from '@tabler/icons-react'; +import { IconBackspace } from '@tabler/icons-react'; import { useMemo, useState } from 'react'; import { StylishText } from '@lib/components/StylishText'; +import { InvenTreeIcon } from '../../functions/icons'; import { useDashboardItems } from '../../hooks/UseDashboardItems'; /** @@ -105,7 +106,7 @@ export default function DashboardWidgetDrawer({ onAddWidget(widget.label); }} > - + diff --git a/src/frontend/src/components/dashboard/DashboardWidgetLibrary.tsx b/src/frontend/src/components/dashboard/DashboardWidgetLibrary.tsx index 5ef7b5f04f..76ada27f8d 100644 --- a/src/frontend/src/components/dashboard/DashboardWidgetLibrary.tsx +++ b/src/frontend/src/components/dashboard/DashboardWidgetLibrary.tsx @@ -8,6 +8,7 @@ import ColorToggleDashboardWidget from './widgets/ColorToggleWidget'; import GetStartedWidget from './widgets/GetStartedWidget'; import LanguageSelectDashboardWidget from './widgets/LanguageSelectWidget'; import NewsWidget from './widgets/NewsWidget'; +import OrderHistoryWidget from './widgets/OrderHistoryWidget'; import QueryCountDashboardWidget from './widgets/QueryCountDashboardWidget'; import QueryDashboardWidget from './widgets/QueryDashboardWidget'; import StocktakeDashboardWidget from './widgets/StocktakeDashboardWidget'; @@ -43,6 +44,7 @@ function BuiltinQueryCountWidgets(): DashboardWidgetProps[] { title: t`Invalid BOMs`, description: t`Assemblies requiring bill of materials validation`, modelType: ModelType.part, + icon: 'exclamation', params: { active: true, // Only show active parts assembly: true, // Only show parts which are assemblies @@ -94,7 +96,8 @@ function BuiltinQueryCountWidgets(): DashboardWidgetProps[] { description: t`Show the number of stock items which have expired`, modelType: ModelType.stockitem, params: { expired: true }, - enabled: globalSettings.isSet('STOCK_ENABLE_EXPIRY') + enabled: globalSettings.isSet('STOCK_ENABLE_EXPIRY'), + icon: 'overdue' }), QueryCountDashboardWidget({ title: t`Stale Stock Items`, @@ -116,14 +119,16 @@ function BuiltinQueryCountWidgets(): DashboardWidgetProps[] { label: 'ovr-bo', description: t`Show the number of build orders which are overdue`, modelType: ModelType.build, - params: { overdue: true } + params: { overdue: true }, + icon: 'overdue' }), QueryCountDashboardWidget({ title: t`Assigned Build Orders`, label: 'asn-bo', description: t`Show the number of build orders which are assigned to you`, modelType: ModelType.build, - params: { assigned_to_me: true, outstanding: true } + params: { assigned_to_me: true, outstanding: true }, + icon: 'responsible' }), QueryCountDashboardWidget({ title: t`Active Sales Orders`, @@ -137,14 +142,16 @@ function BuiltinQueryCountWidgets(): DashboardWidgetProps[] { label: 'ovr-so', description: t`Show the number of sales orders which are overdue`, modelType: ModelType.salesorder, - params: { overdue: true } + params: { overdue: true }, + icon: 'overdue' }), QueryCountDashboardWidget({ title: t`Assigned Sales Orders`, label: 'asn-so', description: t`Show the number of sales orders which are assigned to you`, modelType: ModelType.salesorder, - params: { assigned_to_me: true, outstanding: true } + params: { assigned_to_me: true, outstanding: true }, + icon: 'responsible' }), QueryCountDashboardWidget({ title: t`Pending Shipments`, @@ -165,14 +172,16 @@ function BuiltinQueryCountWidgets(): DashboardWidgetProps[] { label: 'ovr-po', description: t`Show the number of purchase orders which are overdue`, modelType: ModelType.purchaseorder, - params: { overdue: true } + params: { overdue: true }, + icon: 'overdue' }), QueryCountDashboardWidget({ title: t`Assigned Purchase Orders`, label: 'asn-po', description: t`Show the number of purchase orders which are assigned to you`, modelType: ModelType.purchaseorder, - params: { assigned_to_me: true, outstanding: true } + params: { assigned_to_me: true, outstanding: true }, + icon: 'responsible' }), QueryCountDashboardWidget({ title: t`Active Return Orders`, @@ -186,14 +195,16 @@ function BuiltinQueryCountWidgets(): DashboardWidgetProps[] { label: 'ovr-ro', description: t`Show the number of return orders which are overdue`, modelType: ModelType.returnorder, - params: { overdue: true } + params: { overdue: true }, + icon: 'overdue' }), QueryCountDashboardWidget({ title: t`Assigned Return Orders`, label: 'asn-ro', description: t`Show the number of return orders which are assigned to you`, modelType: ModelType.returnorder, - params: { assigned_to_me: true, outstanding: true } + params: { assigned_to_me: true, outstanding: true }, + icon: 'responsible' }) ]; @@ -207,6 +218,26 @@ function BuiltinQueryCountWidgets(): DashboardWidgetProps[] { }); } +function BuiltinHistoryWidgets(): DashboardWidgetProps[] { + return [ + OrderHistoryWidget({ + modelType: ModelType.build + }), + OrderHistoryWidget({ + modelType: ModelType.salesorder + }), + OrderHistoryWidget({ + modelType: ModelType.purchaseorder + }), + OrderHistoryWidget({ + modelType: ModelType.returnorder + }), + OrderHistoryWidget({ + modelType: ModelType.transferorder + }) + ]; +} + function BuiltinGettingStartedWidgets(): DashboardWidgetProps[] { return [ { @@ -215,6 +246,7 @@ function BuiltinGettingStartedWidgets(): DashboardWidgetProps[] { description: t`Getting started with InvenTree`, minWidth: 5, minHeight: 4, + icon: 'info', render: () => }, { @@ -223,6 +255,7 @@ function BuiltinGettingStartedWidgets(): DashboardWidgetProps[] { description: t`The latest news from InvenTree`, minWidth: 5, minHeight: 4, + icon: 'news', render: () => } ]; @@ -243,6 +276,7 @@ function BuiltinActionWidgets(): DashboardWidgetProps[] { export default function DashboardWidgetLibrary(): DashboardWidgetProps[] { return [ ...BuiltinQueryCountWidgets(), + ...BuiltinHistoryWidgets(), ...BuiltinGettingStartedWidgets(), ...BuiltinSettingsWidgets(), ...BuiltinActionWidgets() diff --git a/src/frontend/src/components/dashboard/widgets/OrderHistoryWidget.tsx b/src/frontend/src/components/dashboard/widgets/OrderHistoryWidget.tsx new file mode 100644 index 0000000000..0823a720c6 --- /dev/null +++ b/src/frontend/src/components/dashboard/widgets/OrderHistoryWidget.tsx @@ -0,0 +1,131 @@ +import { ModelInformationDict } from '@lib/enums/ModelInformation'; +import type { ModelType } from '@lib/enums/ModelType'; +import { apiUrl } from '@lib/functions/Api'; +import { StylishText } from '@lib/index'; +import { t } from '@lingui/core/macro'; +import { BarChart } from '@mantine/charts'; +import { Stack } from '@mantine/core'; +import { useDocumentVisibility } from '@mantine/hooks'; +import { useQuery } from '@tanstack/react-query'; +import dayjs from 'dayjs'; +import { useMemo } from 'react'; +import { useApi } from '../../../contexts/ApiContext'; +import { useUserState } from '../../../states/UserState'; +import type { DashboardWidgetProps } from '../DashboardWidget'; + +function OrderHistoryComponent({ + modelType, + title +}: { + modelType: ModelType; + title: string; +}) { + const modelInfo = useMemo(() => { + return ModelInformationDict[modelType]; + }, [modelType]); + + const url = useMemo(() => { + return apiUrl(modelInfo.api_endpoint); + }, [modelInfo]); + + const api = useApi(); + const visibility = useDocumentVisibility(); + + const endDate = dayjs().add(1, 'day').format('YYYY-MM-DD'); + const startDate = dayjs() + .subtract(12, 'month') + .subtract(1, 'day') + .format('YYYY-MM-DD'); + + const params = { + completed_after: startDate, + completed_before: endDate + }; + + const query = useQuery({ + queryKey: ['dashboard-order-summary', modelType, params, visibility], + enabled: visibility === 'visible', + refetchOnWindowFocus: true, + refetchOnMount: true, + refetchInterval: 10 * 60 * 1000, // 10 minute refetch interval + staleTime: 5 * 60 * 1000, // 5 minute stale time + queryFn: () => { + if (visibility !== 'visible') { + return []; + } + + return api.get(url, { params }).then((res) => { + return res.data ?? []; + }); + } + }); + + const chartData = useMemo(() => { + const months = Array.from({ length: 13 }, (_, i) => ({ + month: dayjs() + .subtract(12 - i, 'month') + .format('MMM YY'), + count: 0 + })); + + for (const order of query.data || []) { + // Build orders use `completion_date`; all other order types use `complete_date` + const dateStr = + order.complete_date ?? order.completion_date ?? order.shipment_date; + if (!dateStr) continue; + const label = dayjs(dateStr).format('MMM YY'); + const entry = months.find((m) => m.month === label); + if (entry) entry.count++; + } + + return months; + }, [query.data]); + + return ( + + {title} + + + ); +} + +/** + * Display a simple chart of the number of completed orders per month, for the last 12 months. + */ +export default function OrderHistoryWidget({ + modelType +}: { + modelType: ModelType; +}): DashboardWidgetProps { + const user = useUserState(); + + const modelInfo = useMemo(() => { + return ModelInformationDict[modelType]; + }, [modelType]); + + // Extract translated model labels + const models = modelInfo.label_multiple(); + + return { + label: `${modelType}-history`, + title: t`Completed ${models}`, + description: t`Display number of completed ${models} per month`, + minHeight: 2, + minWidth: 3, + modelType: modelType, + icon: 'chart_bar', + visible: () => user.hasViewPermission(modelType), + render: () => ( + + ) + }; +} diff --git a/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx b/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx index 5e6b0afc0b..bee2c7b431 100644 --- a/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx +++ b/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx @@ -129,6 +129,7 @@ export default function QueryCountDashboardWidget({ title, description, modelType, + icon, enabled = true, params }: { @@ -136,6 +137,7 @@ export default function QueryCountDashboardWidget({ title: string; description: string; modelType: ModelType; + icon?: keyof InvenTreeIconType; enabled?: boolean; params: any; }): DashboardWidgetProps { @@ -147,6 +149,7 @@ export default function QueryCountDashboardWidget({ modelType: modelType, minWidth: 2, minHeight: 1, + icon: icon, render: () => ( ) diff --git a/src/frontend/src/components/dashboard/widgets/StocktakeDashboardWidget.tsx b/src/frontend/src/components/dashboard/widgets/StocktakeDashboardWidget.tsx index 03e94af071..b4e3ae9c60 100644 --- a/src/frontend/src/components/dashboard/widgets/StocktakeDashboardWidget.tsx +++ b/src/frontend/src/components/dashboard/widgets/StocktakeDashboardWidget.tsx @@ -66,6 +66,7 @@ export default function StocktakeDashboardWidget(): DashboardWidgetProps { minHeight: 1, minWidth: 2, render: () => , + icon: 'stocktake', enabled: user.hasAddRole(UserRoles.part) }; } diff --git a/src/frontend/src/components/forms/ApiForm.tsx b/src/frontend/src/components/forms/ApiForm.tsx index 7531e9b927..df6c56e5ad 100644 --- a/src/frontend/src/components/forms/ApiForm.tsx +++ b/src/frontend/src/components/forms/ApiForm.tsx @@ -20,7 +20,7 @@ import { type SubmitHandler, useForm } from 'react-hook-form'; -import { type NavigateFunction, useNavigate } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import { Boundary } from '@lib/components/Boundary'; import { isTrue } from '@lib/functions/Conversion'; @@ -176,14 +176,15 @@ export function ApiForm({ props.onKeepOpenChange?.(v); }; - // Accessor for the navigation function (which is used to redirect the user) - let navigate: NavigateFunction | null = null; + let navigate = props.navigate || null; - try { - navigate = useNavigate(); - } catch (_error) { - // Note: If we launch a form within a plugin context, useNavigate() may not be available - navigate = null; + if (!navigate) { + try { + navigate = useNavigate(); + } catch (_error) { + // Note: If we launch a form within a plugin context, useNavigate() may not be available + navigate = null; + } } const [fields, setFields] = useState( @@ -658,6 +659,7 @@ export function ApiForm({ definition={field} control={form.control} url={url} + navigate={navigate} setFields={setFields} onKeyDown={(value) => { if ( diff --git a/src/frontend/src/components/forms/fields/ApiFormField.tsx b/src/frontend/src/components/forms/fields/ApiFormField.tsx index cd53a45f1d..32acc3bdad 100644 --- a/src/frontend/src/components/forms/fields/ApiFormField.tsx +++ b/src/frontend/src/components/forms/fields/ApiFormField.tsx @@ -6,6 +6,7 @@ import { type Control, type FieldValues, useController } from 'react-hook-form'; import type { ApiFormFieldSet, ApiFormFieldType } from '@lib/types/Forms'; import { IconFileUpload } from '@tabler/icons-react'; +import type { NavigateFunction } from 'react-router-dom'; import DateTimeField from '../DateTimeField'; import { BooleanField } from './BooleanField'; import { ChoiceField } from './ChoiceField'; @@ -26,6 +27,7 @@ export function ApiFormField({ definition, control, hideLabels, + navigate, url, setFields, onKeyDown @@ -34,6 +36,7 @@ export function ApiFormField({ definition: ApiFormFieldType; control: Control; hideLabels?: boolean; + navigate?: NavigateFunction | null; url?: string; setFields?: React.Dispatch>; onKeyDown?: (value: any) => void; @@ -119,9 +122,10 @@ export function ApiFormField({ case 'related field': return ( ); case 'email': diff --git a/src/frontend/src/components/forms/fields/RelatedModelField.tsx b/src/frontend/src/components/forms/fields/RelatedModelField.tsx index 70e427549e..bb88471c0c 100644 --- a/src/frontend/src/components/forms/fields/RelatedModelField.tsx +++ b/src/frontend/src/components/forms/fields/RelatedModelField.tsx @@ -32,6 +32,7 @@ import { import { apiUrl } from '@lib/functions/Api'; import type { ApiFormFieldType } from '@lib/types/Forms'; import { IconPlus } from '@tabler/icons-react'; +import type { NavigateFunction } from 'react-router-dom'; import { useApi } from '../../../contexts/ApiContext'; import { useCreateApiFormModal } from '../../../hooks/UseForm'; import { @@ -50,11 +51,13 @@ export function RelatedModelField({ controller, fieldName, definition, + navigate, limit = 10 }: Readonly<{ controller: UseControllerReturn; definition: ApiFormFieldType; fieldName: string; + navigate?: NavigateFunction | null; limit?: number; }>) { const api = useApi(); diff --git a/src/frontend/src/components/render/Instance.tsx b/src/frontend/src/components/render/Instance.tsx index 191c56f2a2..258f800c58 100644 --- a/src/frontend/src/components/render/Instance.tsx +++ b/src/frontend/src/components/render/Instance.tsx @@ -1,16 +1,20 @@ import { t } from '@lingui/core/macro'; import { + ActionIcon, Alert, Anchor, + Box, Group, + HoverCard, type MantineSize, Paper, Skeleton, Space, + Stack, Text } from '@mantine/core'; import { useQuery } from '@tanstack/react-query'; -import { type ReactNode, useCallback } from 'react'; +import { type ReactNode, useCallback, useMemo } from 'react'; import { ModelInformationDict } from '@lib/enums/ModelInformation'; import { ModelType } from '@lib/enums/ModelType'; @@ -24,9 +28,16 @@ import type { } from '@lib/types/Rendering'; export type { InstanceRenderInterface } from '@lib/types/Rendering'; -import { getBaseUrl, navigateToLink, shortenString } from '@lib/index'; +import { + getBaseUrl, + getDetailUrl, + navigateToLink, + shortenString +} from '@lib/index'; +import { IconLink } from '@tabler/icons-react'; import { useApi } from '../../contexts/ApiContext'; import { usePluginState } from '../../states/PluginState'; +import { useUserSettingsState } from '../../states/SettingsStates'; import { Thumbnail } from '../images/Thumbnail'; import { RenderBuildItem, RenderBuildLine, RenderBuildOrder } from './Build'; import { @@ -125,11 +136,98 @@ export function RenderInstance(props: RenderInstanceProps): ReactNode { props.custom_model ?? props.model ?? '' ); - // provider component - if (!RenderComponent) { - return ; - } - return ; + const userSettings = useUserSettingsState(); + + // Extract model information from the defined model type + const modelInfo = useMemo(() => { + if (!props.model) { + return undefined; + } + + return ModelInformationDict[ + props.model.toString().toLowerCase() as ModelType + ]; + }, [props.model]); + + const showHover: boolean = useMemo(() => { + if (!modelInfo) { + return false; + } + + // Override with the props.showHover attribute + if (props.showHover !== undefined) { + return props.showHover; + } + + // If not specified, fall back to the user configured setting + return userSettings.isSet('SHOW_EXTRA_MODEL_INFO'); + }, [props.showHover, modelInfo, userSettings]); + + // Extract model ID from the provided instance data, using the defined primary key field (or 'pk' as a fallback) + const modelId = useMemo(() => { + if (!modelInfo || !props.instance) { + return undefined; + } + + return props.instance[modelInfo.pk_field ?? 'pk']; + }, [modelInfo, props.instance]); + + const detailUrl = useMemo(() => { + if (!props.model) { + return undefined; + } + + return getDetailUrl(props.model, modelId, true); + }, [props.model]); + + return ( + + + + {!!RenderComponent ? ( + + ) : ( + + )} + + + + + + + {modelInfo?.label()} + + {modelId && {`[${t`ID`}: ${modelId}]`}} + + {detailUrl && ( + { + if (props.navigate) { + navigateToLink(detailUrl, props.navigate, event); + } + }} + > + + + + + {t`View details`} + + + )} + + + + ); } export function RenderRemoteInstance({ diff --git a/src/frontend/src/defaults/actions.tsx b/src/frontend/src/defaults/actions.tsx index 36e7df4402..fb0af4ef78 100644 --- a/src/frontend/src/defaults/actions.tsx +++ b/src/frontend/src/defaults/actions.tsx @@ -124,6 +124,17 @@ export function getActions(navigate: NavigateFunction) { leftSection: }); + globalSettings.isSet('TRANSFERORDER_ENABLED') && + user?.hasViewRole(UserRoles.transfer_order) && + _actions.push({ + id: 'transfer-orders', + label: t`Transfer Orders`, + description: t`Go to Transfer Orders`, + onClick: () => + navigate(ModelInformationDict['transferorder'].url_overview!), + leftSection: + }); + globalSettings.isSet('RETURNORDER_ENABLED') && user?.hasViewRole(UserRoles.return_order) && _actions.push({ @@ -172,6 +183,17 @@ export function getActions(navigate: NavigateFunction) { }); staff && + user?.hasViewPermission(ModelType.error) && + _actions.push({ + id: 'error-logs', + label: t`Error Logs`, + description: t`View error logs for this instance`, + onClick: () => navigate('/settings/admin/errors'), + leftSection: + }); + + staff && + user?.hasViewPermission(ModelType.pluginconfig) && _actions.push({ id: 'plugin-settings', label: t`Plugins`, diff --git a/src/frontend/src/forms/StockForms.tsx b/src/frontend/src/forms/StockForms.tsx index a5464ea2e0..9f49633047 100644 --- a/src/frontend/src/forms/StockForms.tsx +++ b/src/frontend/src/forms/StockForms.tsx @@ -1,3 +1,16 @@ +import { ActionButton } from '@lib/components/ActionButton'; +import { StylishText } from '@lib/components/StylishText'; +import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; +import { ModelType } from '@lib/enums/ModelType'; +import { apiUrl } from '@lib/functions/Api'; +import { getDetailUrl } from '@lib/functions/Navigation'; +import type { + ApiFormAdjustFilterType, + ApiFormFieldChoice, + ApiFormFieldSet, + ApiFormModalProps, + StockOperationProps +} from '@lib/types/Forms'; import { t } from '@lingui/core/macro'; import { Alert, @@ -20,34 +33,14 @@ import { IconUsersGroup } from '@tabler/icons-react'; import { useQuery, useSuspenseQuery } from '@tanstack/react-query'; -import { - type JSX, - Suspense, - useCallback, - useEffect, - useMemo, - useState -} from 'react'; -import { ActionButton } from '@lib/components/ActionButton'; -import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; -import { ModelType } from '@lib/enums/ModelType'; import dayjs from 'dayjs'; +import { type JSX, Suspense, useCallback, useEffect, useMemo, useState } from 'react'; +import { useFormContext } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; import { api } from '../App'; import RemoveRowButton from '../components/buttons/RemoveRowButton'; import { StandaloneField } from '../components/forms/StandaloneField'; - -import { StylishText } from '@lib/components/StylishText'; -import { apiUrl } from '@lib/functions/Api'; -import { getDetailUrl } from '@lib/functions/Navigation'; -import type { - ApiFormAdjustFilterType, - ApiFormFieldChoice, - ApiFormFieldSet, - ApiFormModalProps, - StockOperationProps -} from '@lib/types/Forms'; import { TableFieldExtraRow, type TableFieldRowProps @@ -497,12 +490,31 @@ function StockItemDefaultMove({ function moveToDefault( stockItem: any, value: StockItemQuantity, - refresh: () => void + refresh: () => void, + options?: { + title?: string; + onConfirm?: (location: number) => void; + } ) { + const location = + stockItem.part_detail?.default_location ?? + stockItem.part_detail?.category_default_location; + modals.openConfirmModal({ - title: {t`Confirm Stock Transfer`}, + title: ( + {options?.title ?? t`Confirm Stock Transfer`} + ), children: , onConfirm: () => { + if (!location) { + return; + } + + if (options?.onConfirm) { + options.onConfirm(location); + return; + } + if ( stockItem.location === stockItem.part_detail?.default_location || stockItem.location === stockItem.part_detail?.category_default_location @@ -519,9 +531,7 @@ function moveToDefault( status: stockItem.status } ], - location: - stockItem.part_detail?.default_location ?? - stockItem.part_detail?.category_default_location + location: location }) .then((response) => { refresh(); @@ -556,6 +566,7 @@ function StockOperationsRow({ setMax = false, merge = false, transferMerge = false, + returnStock = false, record }: { props: TableFieldRowProps; @@ -565,8 +576,11 @@ function StockOperationsRow({ setMax?: boolean; merge?: boolean; transferMerge?: boolean; + returnStock?: boolean; record?: any; }) { + const form = useFormContext(); + const statusOptions: ApiFormFieldChoice[] = useMemo(() => { return ( StatusFilterOptions(ModelType.stockitem)()?.map((choice) => { @@ -685,7 +699,22 @@ function StockOperationsRow({ {transfer && ( - moveToDefault(record, props.item.quantity, removeAndRefresh) + moveToDefault( + record, + props.item.quantity, + removeAndRefresh, + returnStock + ? { + title: t`Confirm Stock Return`, + onConfirm: (location: number) => { + form.setValue('location', location, { + shouldDirty: true, + shouldValidate: true + }); + } + } + : undefined + ) } icon={} tooltip={t`Move to default location`} @@ -866,6 +895,7 @@ function stockReturnFields(items: any[]): ApiFormFieldSet { key={record.pk} record={record} transfer + returnStock changeStatus /> ); @@ -880,12 +910,25 @@ function stockReturnFields(items: any[]): ApiFormFieldSet { ] }, location: { + field_type: 'related field', + api_url: apiUrl(ApiEndpoints.stock_location_list), + model: ModelType.stocklocation, + required: true, filters: { structural: false } }, - merge: {}, - notes: {} + merge: { + field_type: 'boolean', + label: t`Merge into existing stock`, + description: t`Merge returned items into existing stock items if possible`, + value: false + }, + notes: { + field_type: 'string', + label: t`Notes`, + description: t`Stock transaction notes` + } }; return fields; @@ -1630,11 +1673,7 @@ export function useTestResultFields({ /** * Modal form for finding a particular stock item by serial number */ -export function useFindSerialNumberForm({ - partId -}: { - partId: number; -}) { +export function useFindSerialNumberForm({ partId }: { partId: number }) { const navigate = useNavigate(); return useApiFormModal({ diff --git a/src/frontend/src/functions/icons.tsx b/src/frontend/src/functions/icons.tsx index e169f89e79..bdc1826e89 100644 --- a/src/frontend/src/functions/icons.tsx +++ b/src/frontend/src/functions/icons.tsx @@ -15,10 +15,13 @@ import { IconCalendar, IconCalendarCheck, IconCalendarDot, + IconCalendarExclamation, IconCalendarStats, IconCalendarTime, IconCalendarX, IconCancel, + IconChartBar, + IconChartLine, IconCheck, IconCircleCheck, IconCircleDashedCheck, @@ -63,6 +66,7 @@ import { IconMapPin, IconMapPinHeart, IconMinusVertical, + IconNews, IconNotes, IconNumber123, IconNumbers, @@ -158,6 +162,7 @@ const icons: InvenTreeIconType = { transfer_orders: IconTransfer, sales_orders: IconTruckDelivery, scheduling: IconCalendarStats, + overdue: IconCalendarExclamation, scrap: IconCircleX, shipment: IconCubeSend, test_templates: IconTestPipe, @@ -267,7 +272,11 @@ const icons: InvenTreeIconType = { plugin: IconPlug, history: IconHistory, dashboard: IconLayoutDashboard, - search: IconSearch + search: IconSearch, + + chart_bar: IconChartBar, + chart_line: IconChartLine, + news: IconNews }; /** diff --git a/src/frontend/src/pages/Index/Settings/UserSettings.tsx b/src/frontend/src/pages/Index/Settings/UserSettings.tsx index 88dfbcfdb0..ae71430928 100644 --- a/src/frontend/src/pages/Index/Settings/UserSettings.tsx +++ b/src/frontend/src/pages/Index/Settings/UserSettings.tsx @@ -61,6 +61,7 @@ export default function UserSettings() { 'FORMS_CLOSE_USING_ESCAPE', 'DISPLAY_STOCKTAKE_TAB', 'ENABLE_LAST_BREADCRUMB', + 'SHOW_EXTRA_MODEL_INFO', 'SHOW_FULL_LOCATION_IN_TABLES', 'SHOW_FULL_CATEGORY_IN_TABLES', 'SHOW_BOM_SUBASSEMBLY_LEVELS' diff --git a/src/frontend/src/pages/build/BuildDetail.tsx b/src/frontend/src/pages/build/BuildDetail.tsx index 0c762ff920..7460634ccb 100644 --- a/src/frontend/src/pages/build/BuildDetail.tsx +++ b/src/frontend/src/pages/build/BuildDetail.tsx @@ -331,7 +331,7 @@ export default function BuildDetail() { name: 'can_build', unit: build.part_detail?.units, label: t`Can Build`, - hidden: partRequirementsQuery.isFetching + hidden: partRequirements?.can_build === undefined }, { type: 'progressbar', @@ -454,12 +454,7 @@ export default function BuildDetail() { ); - }, [ - build, - instanceQuery, - partRequirements, - partRequirementsQuery.isFetching - ]); + }, [build, instanceQuery, partRequirements, partRequirementsQuery]); const buildPanels: PanelType[] = useMemo(() => { return [ @@ -597,6 +592,7 @@ export default function BuildDetail() { build, id, user, + partRequirements, buildStatus, globalSettings, showChildBuilds, diff --git a/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx b/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx index 26c04bf547..80b06490f6 100644 --- a/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx +++ b/src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx @@ -258,23 +258,28 @@ export default function BarcodeScanHistoryTable() { icon={} title={t`Logging Disabled`} > - {t`Barcode logging is not enabled`} + + {t`Barcode logging is not enabled.`}{' '} + {t`No barcode scan history will be recorded.`} + )} - { - setSelectedResult(row); - open(); - } - }} - /> + {globalSettings.isSet('BARCODE_STORE_RESULTS') && ( + { + setSelectedResult(row); + open(); + } + }} + /> + )} ); diff --git a/src/frontend/tests/pages/pui_build.spec.ts b/src/frontend/tests/pages/pui_build.spec.ts index 362f9b34e2..b54fadc6d7 100644 --- a/src/frontend/tests/pages/pui_build.spec.ts +++ b/src/frontend/tests/pages/pui_build.spec.ts @@ -45,8 +45,12 @@ test('Build Order - Basic Tests', async ({ browser }) => { // Load a particular build order await page.getByRole('cell', { name: 'BO0017' }).click(); + await loadTab(page, 'Build Details'); + // This build order should be "on hold" await page.getByText('On Hold').first().waitFor(); + await page.getByText('Can Build').first().waitFor(); + await page.getByText('Completed Outputs').first().waitFor(); // Edit the build order (via keyboard shortcut) await page.keyboard.press('Control+E'); diff --git a/src/frontend/tests/pages/pui_stock.spec.ts b/src/frontend/tests/pages/pui_stock.spec.ts index 9f9c345eeb..c9d5699d1c 100644 --- a/src/frontend/tests/pages/pui_stock.spec.ts +++ b/src/frontend/tests/pages/pui_stock.spec.ts @@ -168,9 +168,14 @@ test('Stock - Filters', async ({ browser }) => { // Filter by custom status code await clearTableFilters(page); await setTableChoiceFilter(page, 'Status', 'Incoming goods inspection'); - await page.getByText('1 - 8 / 8').waitFor(); await page.getByRole('cell', { name: '1551AGY' }).first().waitFor(); + + await page.getByPlaceholder('Search').clear(); + await page.getByPlaceholder('Search').fill('blue'); await page.getByRole('cell', { name: 'widget.blue' }).first().waitFor(); + + await page.getByPlaceholder('Search').clear(); + await page.getByPlaceholder('Search').fill('002.01'); await page.getByRole('cell', { name: '002.01-PCBA' }).first().waitFor(); await clearTableFilters(page); @@ -324,10 +329,20 @@ test('Stock - Stock Actions', async ({ browser }) => { await page.getByRole('option', { name: status }).click(); }; + // Duplicate the stock item first - prevent impacting other tests + await page + .getByRole('button', { name: 'action-menu-stock-item-actions' }) + .click(); + await page + .getByRole('menuitem', { name: 'action-menu-stock-item-actions-duplicate' }) + .click(); + await page.getByRole('button', { name: 'Submit' }).click(); + await page.waitForLoadState('networkidle'); + // Check for required values await page.getByText('Status', { exact: true }).waitFor(); await page.getByText('Custom Status', { exact: true }).waitFor(); - await page.getByText('Attention needed').waitFor(); + await page.getByText('Attention needed').first().waitFor(); await page .getByLabel('Stock Details') .getByText('Incoming goods inspection') @@ -705,6 +720,7 @@ test('Transfer Order - Allocate and Transfer', async ({ browser }) => { await page.getByRole('button', { name: 'Issue Order' }).click(); await page.getByRole('button', { name: 'Submit' }).click(); await page.getByText('Order issued').waitFor(); + await page.getByText('Issued', { exact: true }).first().waitFor(); await loadTab(page, 'Line Items'); diff --git a/src/frontend/tests/pui_forms.spec.ts b/src/frontend/tests/pui_forms.spec.ts index c86a490720..a812d42f05 100644 --- a/src/frontend/tests/pui_forms.spec.ts +++ b/src/frontend/tests/pui_forms.spec.ts @@ -1,9 +1,44 @@ /** Unit tests for form validation, rendering, etc */ import { expect, test } from 'playwright/test'; +import { createApi } from './api'; import { stevenuser } from './defaults'; import { navigate } from './helpers'; import { doCachedLogin } from './login'; +// Test hover form action in related fields +test('Forms - Hover', async ({ browser }) => { + const page = await doCachedLogin(browser, { + user: stevenuser, + url: 'purchasing/index/purchaseorders' + }); + + // Patch user settings to ensure we can see "extra model info" on hover + const api = await createApi({ + username: stevenuser.username, + password: stevenuser.testcred + }); + + const response = await api.patch('settings/user/SHOW_EXTRA_MODEL_INFO/', { + data: { + value: 'true' + } + }); + + expect(response.status()).toBe(200); + + await page + .getByRole('button', { name: 'action-button-add-purchase-' }) + .click(); + await page.getByLabel('related-field-supplier').fill('mou'); + await page.waitForLoadState('networkidle'); + await page.waitForTimeout(250); + await page.getByRole('option', { name: 'Mouser Electronics' }).hover(); + + // Check for hover info + await page.getByText('Company[ID: 2]').waitFor(); + await page.getByRole('link', { name: 'View details' }).waitFor(); +}); + test('Forms - Stock Item Validation', async ({ browser }) => { const page = await doCachedLogin(browser, { user: stevenuser, diff --git a/src/frontend/tests/pui_plugins.spec.ts b/src/frontend/tests/pui_plugins.spec.ts index d42187ec85..a4bc753303 100644 --- a/src/frontend/tests/pui_plugins.spec.ts +++ b/src/frontend/tests/pui_plugins.spec.ts @@ -253,3 +253,60 @@ test('Plugins - Locate Item', async ({ browser }) => { await page.getByRole('button', { name: 'Submit' }).click(); await page.getByText('Item location requested').waitFor(); }); + +/** + * Perform a full run through of validating a UI plugin: + * + * - Activate the plugin + * - Check that a custom panel is added + * - Check that expected translated text is added + * - Check that expected UI elements can be operated + * + * Note: This tests assumes that: + * + * - The inventree-plugin-creator tool has been installed + * - The default plugin has been created, build and installed + */ +test('Plugins - Creator', async ({ browser }) => { + const page = await doCachedLogin(browser, { + user: adminuser + }); + + // Ensure that the SampleIntegration plugin is enabled + await setPluginState({ + plugin: 'my-custom-plugin', + state: true + }); + + // Allow time for installation of plugin static files, etc + await page.waitForTimeout(2500); + + await navigate(page, 'part/106/details/'); + await loadTab(page, 'My Custom Plugin'); + + // Check for correctly translated code + await page.getByText('Translated text, provided by custom code!').waitFor(); + + // Check for incrementing counter value + for (let i = 0; i < 5; i++) { + await page.getByText(`Counter: ${i}`).waitFor(); + await page.getByRole('button', { name: 'Increment Counter' }).click(); + } + + // Edit part form + await page.getByRole('button', { name: 'Edit Part' }).click(); + await page + .getByText('This is a custom form launched from within a plugin!') + .waitFor(); + await page + .getByRole('textbox', { name: 'text-field-name' }) + .fill('New part name'); + await page.getByRole('button', { name: 'Cancel' }).click(); + + // View a custom table + await page.getByRole('button', { name: 'Custom Table Example' }).click(); + await page.getByRole('textbox', { name: 'table-search-input' }).fill('red'); + await page.waitForLoadState('networkidle'); + await page.getByRole('cell', { name: 'Red Square Table' }).first().waitFor(); +}); +// Ensure that the sample full run plugin is enabled diff --git a/src/frontend/tests/pui_settings.spec.ts b/src/frontend/tests/pui_settings.spec.ts index c594f003f6..4cfa94632c 100644 --- a/src/frontend/tests/pui_settings.spec.ts +++ b/src/frontend/tests/pui_settings.spec.ts @@ -385,10 +385,20 @@ test('Settings - Admin - Barcode History', async ({ browser }) => { await page.waitForTimeout(500); - // Barcode history is displayed in table - barcodes.forEach(async (barcode) => { + const checkBarcode = async (barcode: string) => { + await page.getByRole('textbox', { name: 'table-search-input' }).clear(); + await page + .getByRole('textbox', { name: 'table-search-input' }) + .fill(barcode); + await page.waitForLoadState('networkidle'); await page.getByText(barcode).first().waitFor(); - }); + }; + + for (const barcode of barcodes) { + await checkBarcode(barcode); + } + + await page.waitForTimeout(2500); }); test('Settings - Admin - Parameter', async ({ browser }) => { diff --git a/src/frontend/yarn.lock b/src/frontend/yarn.lock index a29b16cdba..7ed725e86b 100644 --- a/src/frontend/yarn.lock +++ b/src/frontend/yarn.lock @@ -2369,11 +2369,11 @@ attr-accept@^2.2.4: integrity sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ== axios@^1.13.6: - version "1.15.2" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.15.2.tgz#eb8fb6d30349abace6ade5b4cb4d9e8a0dc23e5b" - integrity sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A== + version "1.16.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.16.0.tgz#f8e5dd931cef2a5f8c32216d5784eda2f8750eb7" + integrity sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w== dependencies: - follow-redirects "^1.15.11" + follow-redirects "^1.16.0" form-data "^4.0.5" proxy-from-env "^2.1.0" @@ -3196,7 +3196,7 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" -follow-redirects@^1.15.11: +follow-redirects@^1.16.0: version "1.16.0" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.16.0.tgz#28474a159d3b9d11ef62050a14ed60e4df6d61bc" integrity sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==