Merge branch 'master' into fix-frontend-vulns
This commit is contained in:
commit
c366e4d8a5
|
|
@ -1,9 +1,7 @@
|
|||
# Dockerfile for the InvenTree devcontainer
|
||||
# This container is used for development of the InvenTree project, and includes all necessary dependencies for both backend and frontend development.
|
||||
|
||||
# In contrast with the "production" image (which is based on an Alpine image)
|
||||
# we use a Debian-based image for the devcontainer
|
||||
|
||||
FROM mcr.microsoft.com/devcontainers/python:3.11-bookworm@sha256:e754c29c4e3ffcf6c794c1020e36a0812341d88ec9569a34704b975fa89e8848
|
||||
FROM mcr.microsoft.com/devcontainers/python:3.14-trixie@sha256:66af9ee0a89e76f83b5c54a0cfdc19966ec8e443e294707f8f9b45f465b42cf1
|
||||
|
||||
# InvenTree paths
|
||||
ENV INVENTREE_HOME="/home/inventree"
|
||||
|
|
@ -25,10 +23,10 @@ RUN chmod +x init.sh
|
|||
|
||||
# Install required base packages
|
||||
RUN apt update && apt install -y \
|
||||
python3.11-dev python3.11-venv \
|
||||
python3-dev python3-venv \
|
||||
postgresql-client \
|
||||
libldap2-dev libsasl2-dev \
|
||||
libpango1.0-0 libcairo2 \
|
||||
libpango-1.0-0 libcairo2 \
|
||||
poppler-utils weasyprint
|
||||
|
||||
# Install packages required for frontend development
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ pool:
|
|||
strategy:
|
||||
matrix:
|
||||
Python39:
|
||||
PYTHON_VERSION: '3.11'
|
||||
PYTHON_VERSION: 3.12
|
||||
maxParallel: 3
|
||||
|
||||
steps:
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ on:
|
|||
- l10
|
||||
|
||||
env:
|
||||
python_version: 3.11
|
||||
python_version: 3.12
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ jobs:
|
|||
contents: read
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
python_version: "3.11"
|
||||
python_version: 3.12
|
||||
runs-on: ubuntu-latest # in the future we can try to use alternative runners here
|
||||
|
||||
steps:
|
||||
|
|
@ -134,7 +134,7 @@ jobs:
|
|||
contents: read
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
python_version: "3.11"
|
||||
python_version: 3.12
|
||||
runs-on: ubuntu-latest # in the future we can try to use alternative runners here
|
||||
|
||||
steps:
|
||||
|
|
@ -158,7 +158,7 @@ jobs:
|
|||
id-token: write
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
python_version: "3.11"
|
||||
python_version: 3.12
|
||||
runs-on: ubuntu-latest # in the future we can try to use alternative runners here
|
||||
|
||||
steps:
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ on:
|
|||
branches-ignore: ["l10*"]
|
||||
|
||||
env:
|
||||
python_version: 3.11
|
||||
python_version: 3.12
|
||||
node_version: 24
|
||||
# The OS version must be set per job
|
||||
server_start_sleep: 60
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ permissions:
|
|||
contents: read
|
||||
|
||||
env:
|
||||
python_version: 3.11
|
||||
python_version: 3.12
|
||||
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ on:
|
|||
branches-ignore: ["l10*"]
|
||||
|
||||
env:
|
||||
python_version: 3.11
|
||||
python_version: 3.12
|
||||
node_version: 24
|
||||
# The OS version must be set per job
|
||||
server_start_sleep: 60
|
||||
|
|
@ -222,7 +222,7 @@ jobs:
|
|||
echo "Downloaded api.yaml"
|
||||
- name: Running OpenAPI Spec diff action
|
||||
id: breaking_changes
|
||||
uses: oasdiff/oasdiff-action/diff@3530478ec30f84adedbfeb28f0d9527a290f50a9 # pin@main
|
||||
uses: oasdiff/oasdiff-action/diff@5fbe96ede8d0c53aeadef122d7a0abb79152d493 # pin@main
|
||||
with:
|
||||
base: "api.yaml"
|
||||
revision: "src/backend/InvenTree/schema.yml"
|
||||
|
|
@ -363,7 +363,7 @@ jobs:
|
|||
pip install .
|
||||
if: needs.paths-filter.outputs.submit-performance == 'true'
|
||||
- name: Performance Reporting
|
||||
uses: CodSpeedHQ/action@9d332c4d90b43981c3e55ae8e38e68709996240f # pin@v4.17.0
|
||||
uses: CodSpeedHQ/action@c145068895e045cc725ee76fcd2307624b65c3af # pin@v4.17.5
|
||||
# check if we are in inventree/inventree - reporting only works in that OIDC context
|
||||
if: github.repository == 'inventree/InvenTree' && needs.paths-filter.outputs.submit-performance == 'true'
|
||||
with:
|
||||
|
|
@ -379,7 +379,7 @@ jobs:
|
|||
continue-on-error: true # continue if a step fails so that coverage gets pushed
|
||||
strategy:
|
||||
matrix:
|
||||
python_version: [3.11, 3.14]
|
||||
python_version: [3.12, 3.14]
|
||||
|
||||
env:
|
||||
INVENTREE_DB_NAME: ./inventree.sqlite
|
||||
|
|
@ -454,7 +454,7 @@ jobs:
|
|||
env:
|
||||
node_version: '>=24'
|
||||
- name: Performance Reporting
|
||||
uses: CodSpeedHQ/action@9d332c4d90b43981c3e55ae8e38e68709996240f # pin@v4.17.0
|
||||
uses: CodSpeedHQ/action@c145068895e045cc725ee76fcd2307624b65c3af # pin@v4.17.5
|
||||
with:
|
||||
mode: walltime
|
||||
run: inv dev.test --pytest
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ on:
|
|||
permissions:
|
||||
contents: read
|
||||
env:
|
||||
python_version: 3.11
|
||||
python_version: 3.12
|
||||
|
||||
jobs:
|
||||
stable:
|
||||
|
|
@ -151,9 +151,9 @@ jobs:
|
|||
fail-fast: false
|
||||
matrix:
|
||||
target:
|
||||
- ubuntu:22.04
|
||||
- ubuntu:24.04
|
||||
- debian:12
|
||||
- ubuntu:26.04
|
||||
- debian:13
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ on:
|
|||
- master
|
||||
|
||||
env:
|
||||
python_version: 3.11
|
||||
python_version: 3.12
|
||||
node_version: 24
|
||||
|
||||
permissions:
|
||||
|
|
@ -56,7 +56,7 @@ jobs:
|
|||
echo "Resetting to HEAD~"
|
||||
git reset HEAD~ || true
|
||||
- name: crowdin action
|
||||
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # pin@v2
|
||||
uses: crowdin/github-action@52aa776766211d83d975df51f3b9c53c2f8ba35f # pin@v2
|
||||
with:
|
||||
upload_sources: true
|
||||
upload_translations: false
|
||||
|
|
|
|||
10
.pkgr.yml
10
.pkgr.yml
|
|
@ -21,9 +21,9 @@ before:
|
|||
dependencies:
|
||||
- curl
|
||||
- poppler-utils
|
||||
- "python3.11 | python3.12 | python3.13 | python3.14"
|
||||
- "python3.11-venv | python3.12-venv | python3.13-venv | python3.14-venv"
|
||||
- "python3.11-dev | python3.12-dev | python3.13-dev | python3.14-dev"
|
||||
- "python3.12 | python3.13 | python3.14"
|
||||
- "python3.12-venv | python3.13-venv | python3.14-venv"
|
||||
- "python3.12-dev | python3.13-dev | python3.14-dev"
|
||||
- python3-pip
|
||||
- python3-cffi
|
||||
- python3-brotli
|
||||
|
|
@ -36,6 +36,6 @@ dependencies:
|
|||
- jq
|
||||
- "libffi7 | libffi8"
|
||||
targets:
|
||||
ubuntu-22.04: true
|
||||
ubuntu-24.04: true
|
||||
debian-12: true
|
||||
ubuntu-26.04: true
|
||||
debian-13: true
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ repos:
|
|||
files: src/backend/requirements-dev\.(in|txt)$
|
||||
- id: pip-compile
|
||||
name: pip-compile requirements-dev.in 3.14
|
||||
args: [src/backend/requirements-dev.in, -o, src/backend/requirements-dev-3.14.txt, -c, src/backend/requirements-3.14.txt, --python-version=3.14, -c, src/backend/requirements.txt]
|
||||
args: [src/backend/requirements-dev.in, -o, src/backend/requirements-dev-3.14.txt, -c, src/backend/requirements-3.14.txt, --python-version=3.14, -c, src/backend/requirements.txt, -c, src/backend/requirements-dev.txt]
|
||||
files: src/backend/requirements-dev\.(in|txt)$
|
||||
- id: pip-compile
|
||||
name: pip-compile requirements.txt
|
||||
|
|
@ -97,4 +97,4 @@ repos:
|
|||
rev: 0.4.3
|
||||
hooks:
|
||||
- id: teyit
|
||||
language_version: python3.11
|
||||
language_version: python3.12
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@
|
|||
"name": "InvenTree invoke schema",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/.venv/lib/python3.11/site-packages/invoke/__main__.py",
|
||||
"program": "${workspaceFolder}/.venv/lib/python3.12/site-packages/invoke/__main__.py",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"args": [
|
||||
"dev.schema","--ignore-warnings"
|
||||
|
|
|
|||
14
CHANGELOG.md
14
CHANGELOG.md
|
|
@ -5,7 +5,19 @@ All major notable changes to this project will be documented in this file (start
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## Unreleased - YYYY-MM-DD
|
||||
## Unreleased - xxxx.xx.xx
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- [#12223](https://github.com/inventree/InvenTree/pull/12223) removes support for python 3.11 and stops providing packages for Debian 11 and Ubuntu 20.04.
|
||||
|
||||
### Added
|
||||
|
||||
### Changed
|
||||
|
||||
### Removed
|
||||
|
||||
## 1.4.0 - 2026-06-24
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ flag_management:
|
|||
carryforward: true
|
||||
statuses:
|
||||
- type: project
|
||||
target: 40%
|
||||
target: 38%
|
||||
- name: web
|
||||
carryforward: true
|
||||
statuses:
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ function detect_python() {
|
|||
echo "${On_Red}"
|
||||
echo "# POI07| Python ${SETUP_PYTHON} not found - aborting!"
|
||||
echo "# POI07| Please ensure python can be executed with the command '$SETUP_PYTHON' by the current user '$USER'."
|
||||
echo "# POI07| If you are using a different python version, please set the environment variable SETUP_PYTHON to the correct command - eg. 'python3.11'."
|
||||
echo "# POI07| If you are using a different python version, please set the environment variable SETUP_PYTHON to the correct command - eg. 'python3.12'."
|
||||
echo "${Color_Off}"
|
||||
exit 1
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export DATA_DIR=${APP_HOME}/data
|
|||
export SETUP_NGINX_FILE=${SETUP_NGINX_FILE:-/etc/nginx/sites-enabled/inventree.conf}
|
||||
export SETUP_ADMIN_PASSWORD_FILE=${CONF_DIR}/admin_password.txt
|
||||
export SETUP_NO_CALLS=${SETUP_NO_CALLS:-false}
|
||||
export SETUP_PYTHON=${SETUP_PYTHON:-python3.11}
|
||||
export SETUP_PYTHON=${SETUP_PYTHON:-python3.12}
|
||||
export SETUP_ADMIN_NOCREATION=${SETUP_ADMIN_NOCREATION:-false}
|
||||
# SETUP_DEBUG can be set to get debug info
|
||||
# SETUP_EXTRA_PIP can be set to install extra pip packages
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ import json
|
|||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
from distutils.version import StrictVersion # type: ignore[import]
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
from packaging.version import Version
|
||||
|
||||
here = Path(__file__).parent
|
||||
|
||||
|
|
@ -57,7 +57,7 @@ def fetch_rtd_versions():
|
|||
print('No RTD token found - skipping RTD version fetch')
|
||||
|
||||
# Sort versions by version number
|
||||
versions = sorted(versions, key=lambda x: StrictVersion(x['version']), reverse=True)
|
||||
versions = sorted(versions, key=lambda x: Version(x['version']), reverse=True)
|
||||
|
||||
# Add "latest" version first
|
||||
if not any(x['title'] == 'latest' for x in versions):
|
||||
|
|
|
|||
|
|
@ -54,14 +54,14 @@ class InvenTreeBarcodePlugin(BarcodeMixin, InvenTreePlugin):
|
|||
VERSION = "0.0.1"
|
||||
AUTHOR = "Michael"
|
||||
|
||||
def scan(self, barcode_data):
|
||||
def scan(self, barcode_data, user, **kwargs):
|
||||
if barcode_data.startswith("PART-"):
|
||||
try:
|
||||
pk = int(barcode_data.split("PART-")[1])
|
||||
instance = Part.objects.get(pk=pk)
|
||||
label = Part.barcode_model_type()
|
||||
|
||||
return {label: instance.format_matched_response()}
|
||||
return {label: instance.format_matched_response(user=user)}
|
||||
except Part.DoesNotExist:
|
||||
pass
|
||||
```
|
||||
|
|
|
|||
|
|
@ -226,6 +226,7 @@ Configuration of stock item options
|
|||
{{ globalsetting("STOCK_SHOW_INSTALLED_ITEMS") }}
|
||||
{{ globalsetting("STOCK_ENFORCE_BOM_INSTALLATION") }}
|
||||
{{ globalsetting("STOCK_ALLOW_OUT_OF_STOCK_TRANSFER") }}
|
||||
{{ globalsetting("STOCK_MERGE_ON_TRANSFER") }}
|
||||
{{ globalsetting("TEST_STATION_DATA") }}
|
||||
|
||||
### Build Orders
|
||||
|
|
|
|||
|
|
@ -325,7 +325,7 @@ ARG INVENTREE_TAG
|
|||
|
||||
# prebuild stage - needs a lot of build dependencies
|
||||
# make sure, the alpine and python version matches the version used in the inventree base image
|
||||
FROM python:3.11-alpine3.18 as prebuild
|
||||
FROM python:3.12-alpine3.18 as prebuild
|
||||
|
||||
# Install whatever development dependency is needed (e.g. cups-dev, gcc, the musl-dev build tools and the pip pycups package)
|
||||
RUN apk add --no-cache cups-dev gcc musl-dev && \
|
||||
|
|
|
|||
|
|
@ -366,7 +366,7 @@ extra:
|
|||
# provider: google
|
||||
# property: UA-143467500-1
|
||||
|
||||
min_python_version: 3.11
|
||||
min_python_version: 3.12
|
||||
min_invoke_version: 2.0.0
|
||||
django_version: 5.2
|
||||
docker_postgres_version: 17
|
||||
|
|
|
|||
|
|
@ -97,25 +97,25 @@ skip-magic-trailing-comma = true
|
|||
line-ending = "auto"
|
||||
|
||||
[tool.uv.pip]
|
||||
python-version = "3.11"
|
||||
python-version = "3.12"
|
||||
no-strip-extras=true
|
||||
generate-hashes=true
|
||||
|
||||
[tool.ty.environment]
|
||||
root = ["src/backend/InvenTree"]
|
||||
extra-paths = ["src/backend/InvenTree"]
|
||||
|
||||
[tool.ty.rules]
|
||||
unresolved-reference="ignore" # 21 # see https://github.com/astral-sh/ty/issues/220
|
||||
unresolved-attribute="ignore" # 505 # need Plugin Mixin typing
|
||||
call-non-callable="ignore" # 8 ##
|
||||
invalid-assignment="ignore" # 17 # need to wait for better django field stubs
|
||||
# invalid-method-override="ignore" # 99
|
||||
invalid-method-override="ignore" # 104
|
||||
invalid-return-type="ignore" # 22 ##
|
||||
# possibly-missing-attribute="ignore" # 25 # https://github.com/astral-sh/ty/issues/164
|
||||
unknown-argument="ignore" # 3 # need to wait for better django field stubs
|
||||
invalid-argument-type="ignore" # 49
|
||||
no-matching-overload="ignore" # 3 # need to wait for betterdjango field stubs
|
||||
possibly-unbound-attribute="ignore" # 21
|
||||
# possibly-unbound-attribute="ignore" # 21
|
||||
|
||||
[tool.coverage.run]
|
||||
source = ["src/backend/InvenTree", "InvenTree"]
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@ python:
|
|||
- requirements: src/backend/requirements.txt
|
||||
|
||||
build:
|
||||
os: "ubuntu-22.04"
|
||||
os: "ubuntu-24.04"
|
||||
tools:
|
||||
python: "3.11"
|
||||
python: "3.12"
|
||||
jobs:
|
||||
post_install:
|
||||
- pip install -U invoke
|
||||
|
|
|
|||
|
|
@ -494,7 +494,7 @@ class BulkCreateMixin:
|
|||
if unique_create_fields := getattr(self, 'unique_create_fields', None):
|
||||
existing = collections.defaultdict(list)
|
||||
for idx, item in enumerate(data):
|
||||
key = tuple(item[v] for v in list(unique_create_fields)) # type: ignore[not-subscriptable]
|
||||
key = tuple(item[v] for v in list(unique_create_fields)) # ty: ignore[not-subscriptable]
|
||||
existing[key].append(idx)
|
||||
|
||||
unique_errors = [[] for _ in range(len(data))]
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 511
|
||||
INVENTREE_API_VERSION = 512
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
v512 -> 2026-06-20 : 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.
|
||||
|
||||
v511 -> 2026-06-19 : https://github.com/inventree/InvenTree/pull/12204
|
||||
- Adds new filtering options to PartCategoryTree and StockLocationTree API endpoints
|
||||
|
||||
|
|
|
|||
|
|
@ -19,10 +19,10 @@ logger = structlog.get_logger('inventree')
|
|||
|
||||
|
||||
def log_error(
|
||||
path,
|
||||
error_name=None,
|
||||
error_info=None,
|
||||
error_data=None,
|
||||
path: Optional[str] = None,
|
||||
error_name: Optional[str] = None,
|
||||
error_info: Optional[str] = None,
|
||||
error_data: Optional[str] = None,
|
||||
scope: Optional[str] = None,
|
||||
plugin: Optional[str] = None,
|
||||
):
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ from django_q.models import Task
|
|||
from error_report.models import Error
|
||||
from mptt.exceptions import InvalidMove
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from stdimage.models import StdImageField
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
|
|
@ -1334,8 +1335,16 @@ class InvenTreeBarcodeMixin(models.Model):
|
|||
|
||||
return generate_barcode(self)
|
||||
|
||||
def format_matched_response(self):
|
||||
def format_matched_response(self, user, **kwargs):
|
||||
"""Format a standard response for a matched barcode."""
|
||||
# Check permission for this object
|
||||
from users.permissions import check_user_permission
|
||||
|
||||
if not check_user_permission(user, self, 'view'):
|
||||
raise PermissionDenied(
|
||||
_('User does not have permission to view this model')
|
||||
)
|
||||
|
||||
data = {'pk': self.pk}
|
||||
|
||||
if hasattr(self, 'get_api_url'):
|
||||
|
|
|
|||
|
|
@ -282,7 +282,7 @@ def postprocess_print_stats(result, generator, request, public):
|
|||
scopes = {}
|
||||
for path, details in rlt_dict.items():
|
||||
if details['oauth']:
|
||||
for scope in details['oauth']:
|
||||
for scope in list(details['oauth']):
|
||||
if scope not in scopes:
|
||||
scopes[scope] = []
|
||||
scopes[scope].append(path)
|
||||
|
|
@ -343,7 +343,7 @@ def schema_for_view_output_options(view_class):
|
|||
return extended_view
|
||||
|
||||
|
||||
def exclude_from_schema(klass: type, alternative_path: str) -> type:
|
||||
def exclude_from_schema(klass: type[Any], alternative_path: str) -> type[Any]:
|
||||
"""Decorator to exclude a view from the OpenAPI schema.
|
||||
|
||||
This is used to hide legacy endpoints from the schema, while still retaining them for backwards compatibility.
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ def get_ldap_config(debug: bool = False) -> dict:
|
|||
The returned settings will be updated into the globals() object,
|
||||
and will be used to configure the LDAP authentication backend.
|
||||
"""
|
||||
import django_auth_ldap.config # type: ignore[unresolved-import]
|
||||
import ldap # type: ignore[unresolved-import]
|
||||
import django_auth_ldap.config # ty: ignore[unresolved-import]
|
||||
import ldap # ty: ignore[unresolved-import]
|
||||
|
||||
# get global options from dict and use ldap.OPT_* as keys and values
|
||||
global_options_dict = get_setting(
|
||||
|
|
|
|||
|
|
@ -15,10 +15,10 @@ from datetime import timedelta as td
|
|||
from .api_version import INVENTREE_API_TEXT, INVENTREE_API_VERSION
|
||||
|
||||
# InvenTree software version
|
||||
INVENTREE_SW_VERSION = '1.4.0 dev'
|
||||
INVENTREE_SW_VERSION = '1.5.0 dev'
|
||||
|
||||
# Minimum supported Python version
|
||||
MIN_PYTHON_VERSION = (3, 11)
|
||||
MIN_PYTHON_VERSION = (3, 12)
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models import F, OuterRef, Q, Subquery, Sum
|
||||
from django.db.models import DecimalField, F, OuterRef, Q, Subquery, Sum
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.urls import include, path
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
|
@ -494,9 +494,25 @@ class BuildLineFilter(FilterSet):
|
|||
|
||||
def filter_allocated(self, queryset, name, value):
|
||||
"""Filter by whether each BuildLine is fully allocated."""
|
||||
allocated_subquery = (
|
||||
BuildItem.objects
|
||||
.filter(build_line=OuterRef('pk'))
|
||||
.values('build_line')
|
||||
.annotate(total=Sum('quantity'))
|
||||
.values('total')
|
||||
)
|
||||
|
||||
queryset = queryset.alias(
|
||||
allocated_quantity=Coalesce(
|
||||
Subquery(allocated_subquery), 0, output_field=DecimalField()
|
||||
)
|
||||
)
|
||||
|
||||
if str2bool(value):
|
||||
return queryset.filter(allocated__gte=F('quantity') - F('consumed'))
|
||||
return queryset.filter(allocated__lt=F('quantity') - F('consumed'))
|
||||
return queryset.filter(
|
||||
allocated_quantity__gte=F('quantity') - F('consumed')
|
||||
)
|
||||
return queryset.filter(allocated_quantity__lt=F('quantity') - F('consumed'))
|
||||
|
||||
consumed = rest_filters.BooleanFilter(label=_('Consumed'), method='filter_consumed')
|
||||
|
||||
|
|
@ -528,7 +544,9 @@ class BuildLineFilter(FilterSet):
|
|||
)
|
||||
|
||||
queryset = queryset.alias(
|
||||
allocated_quantity=Coalesce(Subquery(allocated_subquery), 0)
|
||||
allocated_quantity=Coalesce(
|
||||
Subquery(allocated_subquery), 0, output_field=DecimalField()
|
||||
)
|
||||
)
|
||||
|
||||
# A query filter construct to determine the total quantity available for this BuildLine,
|
||||
|
|
|
|||
|
|
@ -577,7 +577,7 @@ class CustomUnitViewset(DataExportViewMixin, viewsets.ModelViewSet):
|
|||
return {
|
||||
'name': k,
|
||||
'is_alias': reg.get_name(k) == k,
|
||||
'compatible_units': [str(a) for a in unit.compatible_units()],
|
||||
'compatible_units': [str(a) for a in unit.compatible_units()], # ty:ignore[missing-argument]
|
||||
'isdimensionless': unit.dimensionless,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -793,6 +793,14 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
|
|||
'default': False,
|
||||
'validator': bool,
|
||||
},
|
||||
'STOCK_MERGE_ON_TRANSFER': {
|
||||
'name': _('Merge stock with existing stock on transfer by default'),
|
||||
'description': _(
|
||||
'Default state for merge stock on transfer behaviour. (Can be changed per transfer if desired)'
|
||||
),
|
||||
'default': False,
|
||||
'validator': bool,
|
||||
},
|
||||
'BUILDORDER_REFERENCE_PATTERN': {
|
||||
'name': _('Build Order Reference Pattern'),
|
||||
'description': _('Required pattern for generating Build Order reference field'),
|
||||
|
|
|
|||
|
|
@ -1,19 +1,7 @@
|
|||
"""Types for settings."""
|
||||
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
from typing import Any, TypedDict
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import NotRequired # pragma: no cover
|
||||
else:
|
||||
|
||||
class NotRequired: # pragma: no cover
|
||||
"""NotRequired type helper is only supported with Python 3.11+."""
|
||||
|
||||
def __class_getitem__(cls, item):
|
||||
"""Return the item."""
|
||||
return item
|
||||
from typing import Any, NotRequired, TypedDict
|
||||
|
||||
|
||||
class SettingsKeyType(TypedDict, total=False):
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ TESTING = 'test' in sys.argv
|
|||
|
||||
def clear():
|
||||
if not TESTING: # pragma: no cover
|
||||
os.system('cls' if os.name == 'nt' else 'clear')
|
||||
os.system('cls' if os.name == 'nt' else 'clear') # ty:ignore[deprecated]
|
||||
|
||||
|
||||
def reverse_association(apps, schema_editor): # pragma: no cover
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -272,7 +272,7 @@ class ReturnOrderReportContext(report.mixins.BaseReportContext, TypedDict):
|
|||
customer: Optional[Company]
|
||||
|
||||
|
||||
class TransferOrderReportContext(report.mixins.BaseReportContext, TypedDict):
|
||||
class TransferOrderReportContext(BaseOrderReportContext, TypedDict):
|
||||
"""Context for the transfer order model.
|
||||
|
||||
Attributes:
|
||||
|
|
@ -639,7 +639,8 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
|||
def report_context(self) -> PurchaseOrderReportContext:
|
||||
"""Return report context data for this PurchaseOrder."""
|
||||
return_ctx = super().report_context()
|
||||
return_ctx.update({'supplier': self.supplier})
|
||||
|
||||
return_ctx.update({'supplier': self.supplier}) # ty:ignore[invalid-key]
|
||||
return return_ctx
|
||||
|
||||
def get_absolute_url(self) -> str:
|
||||
|
|
@ -1372,7 +1373,8 @@ class SalesOrder(TotalPriceMixin, Order):
|
|||
def report_context(self) -> SalesOrderReportContext:
|
||||
"""Generate report context data for this SalesOrder."""
|
||||
return_ctx = super().report_context()
|
||||
return_ctx.update({'customer': self.customer})
|
||||
|
||||
return_ctx.update({'customer': self.customer}) # ty:ignore[invalid-key]
|
||||
return return_ctx
|
||||
|
||||
def get_absolute_url(self) -> str:
|
||||
|
|
@ -2931,7 +2933,8 @@ class ReturnOrder(TotalPriceMixin, Order):
|
|||
def report_context(self) -> ReturnOrderReportContext:
|
||||
"""Generate report context data for this ReturnOrder."""
|
||||
return_ctx = super().report_context()
|
||||
return_ctx.update({'customer': self.customer})
|
||||
|
||||
return_ctx.update({'customer': self.customer}) # ty:ignore[invalid-key]
|
||||
return return_ctx
|
||||
|
||||
def get_absolute_url(self):
|
||||
|
|
|
|||
|
|
@ -153,7 +153,9 @@ class BarcodeView(CreateAPIView):
|
|||
|
||||
for current_plugin in plugins:
|
||||
try:
|
||||
result = current_plugin.scan(barcode)
|
||||
result = current_plugin.scan(barcode, user=request.user, **kwargs)
|
||||
except PermissionDenied as exc:
|
||||
raise exc
|
||||
except Exception:
|
||||
log_error('BarcodeView.scan_barcode', plugin=current_plugin.slug)
|
||||
continue
|
||||
|
|
@ -282,7 +284,7 @@ class BarcodeAssign(BarcodeView):
|
|||
|
||||
# First check if the provided barcode matches an existing database entry
|
||||
if inventree_barcode_plugin:
|
||||
result = inventree_barcode_plugin.scan(barcode)
|
||||
result = inventree_barcode_plugin.scan(barcode, user=request.user, **kwargs)
|
||||
|
||||
if result is not None:
|
||||
result['error'] = _('Barcode matches existing item')
|
||||
|
|
@ -459,7 +461,9 @@ class BarcodePOAllocate(BarcodeView):
|
|||
manufacturer_part=response.get('manufacturerpart', None),
|
||||
)
|
||||
response['success'] = _('Matched supplier part')
|
||||
response['supplierpart'] = supplier_part.format_matched_response()
|
||||
response['supplierpart'] = supplier_part.format_matched_response(
|
||||
user=request.user
|
||||
)
|
||||
except ValidationError as e:
|
||||
response['error'] = str(e)
|
||||
|
||||
|
|
@ -524,7 +528,7 @@ class BarcodePOReceive(BarcodeView):
|
|||
filter(lambda plugin: plugin.name == 'InvenTreeBarcode', plugins)
|
||||
)
|
||||
|
||||
if result := internal_barcode_plugin.scan(barcode):
|
||||
if result := internal_barcode_plugin.scan(barcode, user=request.user, **kwargs):
|
||||
if 'stockitem' in result:
|
||||
response['error'] = _('Item has already been received')
|
||||
self.log_scan(request, response, False)
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ class BarcodeMixin:
|
|||
"""Does this plugin have everything needed to process a barcode."""
|
||||
return True
|
||||
|
||||
def scan(self, barcode_data):
|
||||
def scan(self, barcode_data: str, user, **kwargs) -> dict | None:
|
||||
"""Scan a barcode against this plugin.
|
||||
|
||||
This method is explicitly called from the /scan/ API endpoint,
|
||||
|
|
@ -261,7 +261,7 @@ class SupplierBarcodeMixin(BarcodeMixin):
|
|||
'extract_barcode_fields must be implemented by each plugin'
|
||||
)
|
||||
|
||||
def scan(self, barcode_data: str) -> dict | None:
|
||||
def scan(self, barcode_data: str, user, **kwargs) -> dict | None:
|
||||
"""Perform a generic 'scan' operation on a supplier barcode.
|
||||
|
||||
The supplier barcode may provide sufficient information to match against
|
||||
|
|
@ -297,7 +297,7 @@ class SupplierBarcodeMixin(BarcodeMixin):
|
|||
for k, v in matches.items():
|
||||
if v and hasattr(v, 'pk'):
|
||||
has_match = True
|
||||
data[k] = v.format_matched_response()
|
||||
data[k] = v.format_matched_response(user=user)
|
||||
|
||||
if not has_match:
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ class BarcodeAPITest(InvenTreeAPITestCase):
|
|||
"""Tests for barcode api."""
|
||||
|
||||
fixtures = ['category', 'part', 'location', 'stock']
|
||||
roles = ['stock.view', 'stock_location.view', 'part.view']
|
||||
|
||||
def setUp(self):
|
||||
"""Setup for all tests."""
|
||||
|
|
@ -259,6 +260,7 @@ class SOAllocateTest(InvenTreeAPITestCase):
|
|||
"""Unit tests for the barcode endpoint for allocating items to a sales order."""
|
||||
|
||||
fixtures = ['category', 'company', 'part', 'location', 'stock']
|
||||
roles = ['stock.view']
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
|
@ -343,10 +345,14 @@ class SOAllocateTest(InvenTreeAPITestCase):
|
|||
# Test with barcode which points to a *part* instance
|
||||
item.part.assign_barcode(barcode_data='abcde')
|
||||
|
||||
# missing permission for viewing the part - error
|
||||
self.postBarcode('abcde', sales_order=self.sales_order.pk, expected_code=403)
|
||||
|
||||
# Add part.view role and test again
|
||||
self.assignRole('part.view')
|
||||
result = self.postBarcode(
|
||||
'abcde', sales_order=self.sales_order.pk, expected_code=400
|
||||
)
|
||||
|
||||
self.assertIn('does not match an existing stock item', str(result['error']))
|
||||
|
||||
def test_submit(self):
|
||||
|
|
|
|||
|
|
@ -48,11 +48,11 @@ class InvenTreeInternalBarcodePlugin(SettingsMixin, BarcodeMixin, InvenTreePlugi
|
|||
},
|
||||
}
|
||||
|
||||
def format_matched_response(self, label, model, instance):
|
||||
def format_matched_response(self, label, model, instance, user, **kwargs):
|
||||
"""Format a response for the scanned data."""
|
||||
return {label: instance.format_matched_response()}
|
||||
return {label: instance.format_matched_response(user=user, **kwargs)}
|
||||
|
||||
def scan(self, barcode_data):
|
||||
def scan(self, barcode_data, user, **kwargs):
|
||||
"""Scan a barcode against this plugin.
|
||||
|
||||
Here we are looking for a dict object which contains a reference to a particular InvenTree database object
|
||||
|
|
@ -79,7 +79,7 @@ class InvenTreeInternalBarcodePlugin(SettingsMixin, BarcodeMixin, InvenTreePlugi
|
|||
|
||||
try:
|
||||
instance = model.objects.get(pk=int(pk))
|
||||
return self.format_matched_response(label, model, instance)
|
||||
return self.format_matched_response(label, model, instance, user=user)
|
||||
except (ValueError, model.DoesNotExist):
|
||||
pass
|
||||
|
||||
|
|
@ -111,7 +111,9 @@ class InvenTreeInternalBarcodePlugin(SettingsMixin, BarcodeMixin, InvenTreePlugi
|
|||
instance = model.objects.get(pk=pk)
|
||||
|
||||
return {
|
||||
**self.format_matched_response(label, model, instance),
|
||||
**self.format_matched_response(
|
||||
label, model, instance, user=user
|
||||
),
|
||||
'success': succcess_message,
|
||||
}
|
||||
except (ValueError, model.DoesNotExist):
|
||||
|
|
@ -129,7 +131,7 @@ class InvenTreeInternalBarcodePlugin(SettingsMixin, BarcodeMixin, InvenTreePlugi
|
|||
|
||||
if instance is not None:
|
||||
return {
|
||||
**self.format_matched_response(label, model, instance),
|
||||
**self.format_matched_response(label, model, instance, user=user),
|
||||
'success': succcess_message,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ class TestInvenTreeBarcode(InvenTreeAPITestCase):
|
|||
"""Tests for the integrated InvenTreeBarcode barcode plugin."""
|
||||
|
||||
fixtures = ['category', 'part', 'location', 'stock', 'company', 'supplier_part']
|
||||
roles = ['stock.view', 'stock_location.view', 'part.view']
|
||||
|
||||
def setUp(self):
|
||||
"""Set up the test case."""
|
||||
|
|
|
|||
|
|
@ -14,6 +14,13 @@ class SupplierBarcodeTests(InvenTreeAPITestCase):
|
|||
"""Tests barcode parsing for all suppliers."""
|
||||
|
||||
SCAN_URL = reverse('api-barcode-scan')
|
||||
roles = [
|
||||
'stock.view',
|
||||
'stock_location.view',
|
||||
'part.view',
|
||||
'company.view',
|
||||
'order.view',
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
|
@ -176,6 +183,8 @@ class SupplierBarcodeTests(InvenTreeAPITestCase):
|
|||
class SupplierBarcodePOReceiveTests(InvenTreeAPITestCase):
|
||||
"""Tests barcode scanning to receive a purchase order item."""
|
||||
|
||||
roles = ['stock.view', 'stock_location.view']
|
||||
|
||||
def setUp(self):
|
||||
"""Create supplier part and purchase_order."""
|
||||
super().setUp()
|
||||
|
|
|
|||
|
|
@ -108,9 +108,6 @@ def handle_error(error, do_raise: bool = True, do_log: bool = True, log_name: st
|
|||
|
||||
def get_entrypoints():
|
||||
"""Returns list for entrypoints for InvenTree plugins."""
|
||||
# on python before 3.12, we need to use importlib_metadata
|
||||
if sys.version_info < (3, 12):
|
||||
return entry_points().get('inventree_plugins', [])
|
||||
return entry_points(group='inventree_plugins')
|
||||
|
||||
|
||||
|
|
@ -190,13 +187,11 @@ def get_modules(pkg, path=None):
|
|||
continue
|
||||
|
||||
try:
|
||||
if sys.version_info < (3, 12):
|
||||
module = finder.find_module(name).load_module(name)
|
||||
else:
|
||||
spec = finder.find_spec(name)
|
||||
module = module_from_spec(spec)
|
||||
sys.modules[name] = module
|
||||
spec.loader.exec_module(module)
|
||||
spec = finder.find_spec(name, path)
|
||||
module = module_from_spec(spec)
|
||||
sys.modules[name] = module
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
pkg_names = getattr(module, '__all__', None)
|
||||
for k, v in vars(module).items():
|
||||
if not k.startswith('_') and (pkg_names is None or k in pkg_names):
|
||||
|
|
|
|||
|
|
@ -117,8 +117,7 @@ def plugins_file_hash():
|
|||
|
||||
try:
|
||||
with pf.open('rb') as f:
|
||||
# Note: Once we support 3.11 as a minimum, we can use hashlib.file_digest
|
||||
return hashlib.sha256(f.read()).hexdigest()
|
||||
return hashlib.file_digest(f, 'sha256').hexdigest()
|
||||
except Exception:
|
||||
log_error('plugins_file_hash', scope='plugins')
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import warnings
|
|||
from datetime import datetime
|
||||
from importlib.metadata import PackageNotFoundError, metadata
|
||||
from pathlib import Path
|
||||
from sysconfig import get_path
|
||||
from typing import Optional
|
||||
|
||||
from django.conf import settings
|
||||
|
|
@ -517,7 +518,6 @@ class InvenTreePlugin(VersionMixin, MixinBase, MetaBase):
|
|||
return InvenTree.helpers.pui_url(f'/settings/admin/plugin/{config.pk}/')
|
||||
return InvenTree.helpers.pui_url('/settings/admin/plugin/')
|
||||
|
||||
# region package info
|
||||
@mark_final
|
||||
def _get_package_commit(self):
|
||||
"""Get last git commit for the plugin."""
|
||||
|
|
@ -527,10 +527,9 @@ class InvenTreePlugin(VersionMixin, MixinBase, MetaBase):
|
|||
@mark_final
|
||||
def is_editable(cls):
|
||||
"""Returns if the current part is editable."""
|
||||
from distutils.sysconfig import get_python_lib
|
||||
|
||||
pkg_name = cls.__name__.split('.')[0]
|
||||
dist_info = list(Path(get_python_lib()).glob(f'{pkg_name}-*.dist-info'))
|
||||
py_lib = get_path('platlib')
|
||||
dist_info = list(Path(py_lib).glob(f'{pkg_name}-*.dist-info'))
|
||||
return bool(len(dist_info) == 1)
|
||||
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -585,15 +585,9 @@ class PluginsRegistry:
|
|||
|
||||
# Gather Modules
|
||||
if parent_path:
|
||||
# On python 3.12 use new loader method
|
||||
if sys.version_info < (3, 12):
|
||||
raw_module = _load_source(
|
||||
plugin_dir, str(parent_obj.joinpath('__init__.py'))
|
||||
)
|
||||
else:
|
||||
raw_module = SourceFileLoader(
|
||||
plugin_dir, str(parent_obj.joinpath('__init__.py'))
|
||||
).load_module()
|
||||
raw_module = SourceFileLoader(
|
||||
plugin_dir, str(parent_obj.joinpath('__init__.py'))
|
||||
).load_module()
|
||||
else:
|
||||
raw_module = importlib.import_module(plugin_dir)
|
||||
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ class SampleSupplierPlugin(SupplierMixin, InvenTreePlugin):
|
|||
name=p['name'],
|
||||
description=p['description'],
|
||||
exact=p['sku'] == term,
|
||||
price=f'{p["price"][1][0]:.2f}€',
|
||||
price=f'{p["price"][1][0]:.2f}€', # ty:ignore[not-subscriptable]
|
||||
link=p['link'],
|
||||
image_url=p['image_url'],
|
||||
existing_part=getattr(
|
||||
|
|
|
|||
|
|
@ -73,7 +73,9 @@ class ReportConfig(AppConfig):
|
|||
def cleanup(self):
|
||||
"""Cleanup old label and report outputs."""
|
||||
try:
|
||||
from report.tasks import cleanup_old_report_outputs # type: ignore[import]
|
||||
from report.tasks import (
|
||||
cleanup_old_report_outputs, # type: ignore[import] # ty: ignore[unresolved-import]
|
||||
)
|
||||
|
||||
cleanup_old_report_outputs()
|
||||
except Exception:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"""Template tags for rendering various barcodes."""
|
||||
|
||||
from django import template
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
import barcode as python_barcode
|
||||
|
|
@ -70,7 +71,7 @@ def qrcode(data: str, **kwargs) -> str:
|
|||
data = str(data).strip()
|
||||
|
||||
if not data:
|
||||
raise ValueError("No data provided to 'qrcode' template tag")
|
||||
raise ValidationError("qrcode: No data provided to 'qrcode' template tag")
|
||||
|
||||
# Extract other arguments from kwargs
|
||||
fill_color = kwargs.pop('fill_color', 'black')
|
||||
|
|
@ -115,7 +116,7 @@ def barcode(data: str, barcode_class='code128', **kwargs) -> str:
|
|||
data = str(data).strip()
|
||||
|
||||
if not data:
|
||||
raise ValueError("No data provided to 'barcode' template tag")
|
||||
raise ValidationError("barcode: No data provided to 'barcode' template tag")
|
||||
|
||||
constructor = python_barcode.get_barcode_class(barcode_class)
|
||||
|
||||
|
|
@ -134,17 +135,26 @@ def barcode(data: str, barcode_class='code128', **kwargs) -> str:
|
|||
|
||||
|
||||
@register.simple_tag()
|
||||
def datamatrix(data: str, **kwargs) -> str:
|
||||
def datamatrix(
|
||||
data: str,
|
||||
rectangular: bool = False,
|
||||
fill_color: str = 'black',
|
||||
back_color: str = 'white',
|
||||
scale: float = 1.0,
|
||||
border: int = 1,
|
||||
fmt: str = 'PNG',
|
||||
**kwargs,
|
||||
) -> str:
|
||||
"""Render a DataMatrix barcode.
|
||||
|
||||
Arguments:
|
||||
data: Data to encode
|
||||
|
||||
Keyword Arguments:
|
||||
fill_color (str): Foreground color (default = 'black')
|
||||
back_color (str): Background color (default = 'white')
|
||||
scale (float): Matrix scaling factor (default = 1)
|
||||
border (int): Border width (default = 1)
|
||||
rectangular: Whether to generate a rectangular DataMatrix (default = False)
|
||||
fill_color: Foreground color (default = 'black')
|
||||
back_color: Background color (default = 'white')
|
||||
scale: Scaling factor (default = 1)
|
||||
border: Border width (default = 1)
|
||||
fmt: Generated image format (default = 'PNG')
|
||||
|
||||
Returns:
|
||||
image (str): base64 encoded image data
|
||||
|
|
@ -154,14 +164,11 @@ def datamatrix(data: str, **kwargs) -> str:
|
|||
data = str(data).strip()
|
||||
|
||||
if not data:
|
||||
raise ValueError("No data provided to 'datamatrix' template tag")
|
||||
raise ValidationError(
|
||||
"datamatrix: No data provided to 'datamatrix' template tag"
|
||||
)
|
||||
|
||||
dm = DataMatrix(data)
|
||||
|
||||
fill_color = kwargs.pop('fill_color', 'black')
|
||||
back_color = kwargs.pop('back_color', 'white')
|
||||
|
||||
border = kwargs.pop('border', 1)
|
||||
dm = DataMatrix(data, rect=rectangular)
|
||||
|
||||
try:
|
||||
border = int(border)
|
||||
|
|
@ -180,8 +187,6 @@ def datamatrix(data: str, **kwargs) -> str:
|
|||
except Exception:
|
||||
bg = ImageColor.getcolor('white', 'RGB')
|
||||
|
||||
scale = kwargs.pop('scale', 1)
|
||||
|
||||
height = len(dm.matrix) + 2 * border
|
||||
width = len(dm.matrix[0]) + 2 * border
|
||||
|
||||
|
|
@ -193,7 +198,8 @@ def datamatrix(data: str, **kwargs) -> str:
|
|||
if value:
|
||||
img.putpixel((x + border, y + border), fg)
|
||||
|
||||
if scale != 1:
|
||||
img = img.resize((int(width * scale), int(height * scale)))
|
||||
img = img.resize(
|
||||
(int(width * scale), int(height * scale)), Image.Resampling.NEAREST
|
||||
)
|
||||
|
||||
return image_data(img, fmt='PNG')
|
||||
return image_data(img, fmt=fmt)
|
||||
|
|
|
|||
|
|
@ -1021,7 +1021,7 @@ def format_number(
|
|||
number = float(number)
|
||||
|
||||
if multiplier is not None:
|
||||
number *= multiplier
|
||||
number *= float(multiplier)
|
||||
|
||||
if integer:
|
||||
number = int(number)
|
||||
|
|
|
|||
|
|
@ -440,7 +440,6 @@ class ReportTagTest(PartImageTestMixin, InvenTreeTestCase):
|
|||
]
|
||||
|
||||
for tz, fmt, locale, expected in tests:
|
||||
print(tz, fmt, locale, expected)
|
||||
result = report_tags.format_datetime(
|
||||
time, timezone=tz, fmt=fmt, locale=locale
|
||||
)
|
||||
|
|
@ -857,7 +856,7 @@ class BarcodeTagTest(TestCase):
|
|||
self.assertTrue(barcode.startswith('data:image/bmp;'))
|
||||
|
||||
# Test empty tag
|
||||
with self.assertRaises(ValueError):
|
||||
with self.assertRaises(ValidationError):
|
||||
barcode_tags.barcode('')
|
||||
|
||||
def test_qrcode(self):
|
||||
|
|
@ -877,7 +876,7 @@ class BarcodeTagTest(TestCase):
|
|||
self.assertEqual(len(qrcode), 309720)
|
||||
|
||||
# Test empty tag
|
||||
with self.assertRaises(ValueError):
|
||||
with self.assertRaises(ValidationError):
|
||||
barcode_tags.qrcode('')
|
||||
|
||||
def test_clean_barcode(self):
|
||||
|
|
@ -908,7 +907,7 @@ class BarcodeTagTest(TestCase):
|
|||
)
|
||||
|
||||
# Test empty tag
|
||||
with self.assertRaises(ValueError):
|
||||
with self.assertRaises(ValidationError):
|
||||
barcode_tags.datamatrix('')
|
||||
|
||||
# Failure cases with wrong args
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ if __name__ == '__main__':
|
|||
os.mkdir(STATIC_FOLDER)
|
||||
|
||||
print('Downloading tabler icons...')
|
||||
os.system(f'npm install --prefix {TMP_FOLDER} @tabler/icons @tabler/icons-webfont')
|
||||
os.system(f'npm install --prefix {TMP_FOLDER} @tabler/icons @tabler/icons-webfont') # ty:ignore[deprecated]
|
||||
|
||||
print(f'Copying tabler icons to {STATIC_FOLDER}...')
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ from rest_framework.generics import GenericAPIView
|
|||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ValidationError
|
||||
|
||||
import common.filters
|
||||
import common.models
|
||||
import common.settings
|
||||
import InvenTree.helpers
|
||||
|
|
|
|||
|
|
@ -2206,6 +2206,35 @@ class StockItem(
|
|||
|
||||
return True
|
||||
|
||||
def find_merge_target(self, location):
|
||||
"""Find an existing stock item at location that can absorb this item."""
|
||||
if location is None:
|
||||
return None
|
||||
|
||||
candidates = list(
|
||||
StockItem.objects
|
||||
.filter(part=self.part, location=location)
|
||||
.exclude(pk=self.pk)
|
||||
.order_by('pk')
|
||||
)
|
||||
|
||||
if not candidates:
|
||||
return None
|
||||
|
||||
if self.batch:
|
||||
batch_matches = [c for c in candidates if c.batch == self.batch]
|
||||
search_order = batch_matches + [
|
||||
c for c in candidates if c not in batch_matches
|
||||
]
|
||||
else:
|
||||
search_order = candidates
|
||||
|
||||
for target in search_order:
|
||||
if target.can_merge(other=self, raise_error=False):
|
||||
return target
|
||||
|
||||
return None
|
||||
|
||||
@transaction.atomic
|
||||
def merge_stock_items(self, other_items, raise_error=False, **kwargs):
|
||||
"""Merge another stock item into this one; the two become one!
|
||||
|
|
@ -2227,7 +2256,7 @@ class StockItem(
|
|||
|
||||
user = kwargs.get('user')
|
||||
location = kwargs.get('location', self.location)
|
||||
notes = kwargs.get('notes')
|
||||
notes = kwargs.get('notes') or ''
|
||||
|
||||
parent_id = self.parent.pk if self.parent else None
|
||||
|
||||
|
|
@ -2245,9 +2274,12 @@ class StockItem(
|
|||
)
|
||||
return
|
||||
|
||||
merged_quantity = Decimal(0)
|
||||
|
||||
for other in other_items:
|
||||
tree_ids.add(other.tree_id)
|
||||
|
||||
merged_quantity += other.quantity
|
||||
self.quantity += other.quantity
|
||||
|
||||
if other.purchase_price:
|
||||
|
|
@ -2271,15 +2303,25 @@ class StockItem(
|
|||
|
||||
other.delete()
|
||||
|
||||
transfer_deltas = kwargs.pop('transfer_deltas', None)
|
||||
|
||||
tracking_deltas = {
|
||||
'quantity': float(self.quantity),
|
||||
'added': float(merged_quantity),
|
||||
}
|
||||
|
||||
if location:
|
||||
tracking_deltas['location'] = location.pk
|
||||
|
||||
if transfer_deltas:
|
||||
tracking_deltas = {**transfer_deltas, **tracking_deltas}
|
||||
|
||||
self.add_tracking_entry(
|
||||
StockHistoryCode.MERGED_STOCK_ITEMS,
|
||||
user,
|
||||
quantity=self.quantity,
|
||||
notes=notes,
|
||||
deltas={
|
||||
'location': location.pk if location else None,
|
||||
'quantity': self.quantity,
|
||||
},
|
||||
deltas=tracking_deltas,
|
||||
)
|
||||
|
||||
# Update the location of the item
|
||||
|
|
@ -2340,6 +2382,8 @@ class StockItem(
|
|||
status: If provided, override the status (default = existing status)
|
||||
packaging: If provided, override the packaging (default = existing packaging)
|
||||
allow_production: If True, allow splitting of stock which is in production (default = False)
|
||||
record_tracking: If False, skip tracking entries (for merge-on-transfer)
|
||||
split_transfer_deltas: Optional dict to receive split tracking deltas
|
||||
|
||||
Returns:
|
||||
The new StockItem object
|
||||
|
|
@ -2352,6 +2396,8 @@ class StockItem(
|
|||
"""
|
||||
# Run initial checks to test if the stock item can actually be "split"
|
||||
allow_production = kwargs.get('allow_production', False)
|
||||
record_tracking = kwargs.pop('record_tracking', True)
|
||||
split_transfer_deltas = kwargs.pop('split_transfer_deltas', None)
|
||||
|
||||
# Cannot split a stock item which is in production
|
||||
if self.is_building and not allow_production:
|
||||
|
|
@ -2424,15 +2470,23 @@ class StockItem(
|
|||
|
||||
new_stock.save(add_note=False)
|
||||
|
||||
# Add a stock tracking entry for the newly created item
|
||||
new_stock.add_tracking_entry(
|
||||
StockHistoryCode.SPLIT_FROM_PARENT,
|
||||
user,
|
||||
quantity=quantity,
|
||||
notes=notes,
|
||||
location=location,
|
||||
deltas=deltas,
|
||||
)
|
||||
if split_transfer_deltas is not None:
|
||||
split_transfer_deltas.clear()
|
||||
split_transfer_deltas.update(deltas)
|
||||
|
||||
if location:
|
||||
split_transfer_deltas['location'] = location.pk
|
||||
|
||||
if record_tracking:
|
||||
# Add a stock tracking entry for the newly created item
|
||||
new_stock.add_tracking_entry(
|
||||
StockHistoryCode.SPLIT_FROM_PARENT,
|
||||
user,
|
||||
quantity=quantity,
|
||||
notes=notes,
|
||||
location=location,
|
||||
deltas=deltas,
|
||||
)
|
||||
|
||||
# Copy the test results of this part to the new one
|
||||
new_stock.copyTestResultsFrom(self)
|
||||
|
|
@ -2445,6 +2499,7 @@ class StockItem(
|
|||
notes=notes,
|
||||
location=location,
|
||||
stockitem=new_stock,
|
||||
record_tracking=record_tracking,
|
||||
)
|
||||
|
||||
# Rebuild the tree for this parent item
|
||||
|
|
@ -2754,7 +2809,10 @@ class StockItem(
|
|||
code: The stock history code to use
|
||||
notes: Optional notes for the stock removal
|
||||
status: Optionally adjust the stock status
|
||||
record_tracking: If False, skip creating a tracking entry
|
||||
"""
|
||||
record_tracking = kwargs.pop('record_tracking', True)
|
||||
|
||||
# Cannot remove items from a serialized part
|
||||
if self.serialized:
|
||||
return False
|
||||
|
|
@ -2804,9 +2862,10 @@ class StockItem(
|
|||
|
||||
self.save(add_note=False)
|
||||
|
||||
self.add_tracking_entry(
|
||||
code, user, notes=kwargs.get('notes', ''), deltas=deltas
|
||||
)
|
||||
if record_tracking:
|
||||
self.add_tracking_entry(
|
||||
code, user, notes=kwargs.get('notes', ''), deltas=deltas
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
|
|
|||
|
|
@ -1648,7 +1648,7 @@ class StockAdjustmentItemSerializer(serializers.Serializer):
|
|||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
fields = ['pk', 'quantity', 'batch', 'status', 'packaging']
|
||||
fields = ['pk', 'quantity', 'batch', 'status', 'packaging', 'merge']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the serializer."""
|
||||
|
|
@ -1722,6 +1722,15 @@ class StockAdjustmentItemSerializer(serializers.Serializer):
|
|||
help_text=_('Packaging this stock item is stored in'),
|
||||
)
|
||||
|
||||
merge = serializers.BooleanField(
|
||||
default=False,
|
||||
required=False,
|
||||
label=_('Merge into existing stock'),
|
||||
help_text=_(
|
||||
'Merge this item into existing stock at the destination if possible'
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class StockAdjustmentSerializer(serializers.Serializer):
|
||||
"""Base class for managing stock adjustment actions via the API."""
|
||||
|
|
@ -1888,6 +1897,7 @@ class StockTransferSerializer(StockAdjustmentSerializer):
|
|||
# Required fields
|
||||
stock_item = item['pk']
|
||||
quantity = item['quantity']
|
||||
merge = item.get('merge', False)
|
||||
|
||||
# Optional fields
|
||||
kwargs = {}
|
||||
|
|
@ -1896,6 +1906,47 @@ class StockTransferSerializer(StockAdjustmentSerializer):
|
|||
if field_value := item.get(field_name, None):
|
||||
kwargs[field_name] = field_value
|
||||
|
||||
if merge:
|
||||
target = stock_item.find_merge_target(location)
|
||||
|
||||
if target:
|
||||
merge_kwargs = {
|
||||
'location': location,
|
||||
'notes': notes,
|
||||
'user': request.user,
|
||||
**kwargs,
|
||||
}
|
||||
|
||||
if quantity < stock_item.quantity:
|
||||
transfer_deltas = {}
|
||||
|
||||
piece = stock_item.splitStock(
|
||||
quantity,
|
||||
location,
|
||||
request.user,
|
||||
notes=notes,
|
||||
allow_production=True,
|
||||
record_tracking=False,
|
||||
split_transfer_deltas=transfer_deltas,
|
||||
**kwargs,
|
||||
)
|
||||
merge_kwargs['transfer_deltas'] = transfer_deltas
|
||||
target.merge_stock_items([piece], **merge_kwargs)
|
||||
else:
|
||||
transfer_deltas = {'stockitem': stock_item.pk}
|
||||
|
||||
if location:
|
||||
transfer_deltas['location'] = location.pk
|
||||
|
||||
for field_name in StockItem.optional_transfer_fields():
|
||||
if field_name in kwargs:
|
||||
transfer_deltas[field_name] = kwargs[field_name]
|
||||
|
||||
merge_kwargs['transfer_deltas'] = transfer_deltas
|
||||
target.merge_stock_items([stock_item], **merge_kwargs)
|
||||
|
||||
continue
|
||||
|
||||
stock_item.move(
|
||||
location, notes, request.user, quantity=quantity, **kwargs
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2412,6 +2412,178 @@ class StocktakeTest(StockAPITestCase):
|
|||
self.assertIn('does not exist', str(response.data['location']))
|
||||
|
||||
|
||||
class StockTransferMergeTest(StockAPITestCase):
|
||||
"""Tests for optional merge-on-transfer behavior."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up stock items for merge transfer tests."""
|
||||
super().setUp()
|
||||
|
||||
self.part = Part.objects.get(pk=1)
|
||||
self.dest = StockLocation.objects.get(pk=2)
|
||||
self.source_loc = StockLocation.objects.get(pk=5)
|
||||
self.url = reverse('api-stock-transfer')
|
||||
|
||||
# Remove fixture stock at the destination so merge targets are deterministic
|
||||
StockItem.objects.filter(part=self.part, location=self.dest).delete()
|
||||
|
||||
def test_transfer_without_merge_creates_separate_lot(self):
|
||||
"""Transfer without merge leaves multiple stock rows at destination."""
|
||||
existing = StockItem.objects.create(
|
||||
part=self.part, location=self.dest, quantity=100
|
||||
)
|
||||
incoming = StockItem.objects.create(
|
||||
part=self.part, location=self.source_loc, quantity=50
|
||||
)
|
||||
|
||||
self.post(
|
||||
self.url,
|
||||
{
|
||||
'items': [{'pk': incoming.pk, 'quantity': 50, 'merge': False}],
|
||||
'location': self.dest.pk,
|
||||
},
|
||||
expected_code=201,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
StockItem.objects.filter(part=self.part, location=self.dest).count(), 2
|
||||
)
|
||||
|
||||
existing.refresh_from_db()
|
||||
self.assertEqual(existing.quantity, 100)
|
||||
|
||||
def test_transfer_with_merge_combines_lots(self):
|
||||
"""Transfer with merge combines into an existing compatible lot."""
|
||||
existing = StockItem.objects.create(
|
||||
part=self.part, location=self.dest, quantity=100
|
||||
)
|
||||
incoming = StockItem.objects.create(
|
||||
part=self.part, location=self.source_loc, quantity=50
|
||||
)
|
||||
|
||||
self.post(
|
||||
self.url,
|
||||
{
|
||||
'items': [{'pk': incoming.pk, 'quantity': 50, 'merge': True}],
|
||||
'location': self.dest.pk,
|
||||
},
|
||||
expected_code=201,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
StockItem.objects.filter(part=self.part, location=self.dest).count(), 1
|
||||
)
|
||||
|
||||
existing.refresh_from_db()
|
||||
self.assertEqual(existing.quantity, 150)
|
||||
self.assertFalse(StockItem.objects.filter(pk=incoming.pk).exists())
|
||||
|
||||
def test_transfer_mixed_merge_per_item(self):
|
||||
"""Each transfer line can merge or move independently."""
|
||||
existing = StockItem.objects.create(
|
||||
part=self.part, location=self.dest, quantity=100
|
||||
)
|
||||
merge_incoming = StockItem.objects.create(
|
||||
part=self.part, location=self.source_loc, quantity=30
|
||||
)
|
||||
separate_incoming = StockItem.objects.create(
|
||||
part=self.part, location=self.source_loc, quantity=20
|
||||
)
|
||||
|
||||
self.post(
|
||||
self.url,
|
||||
{
|
||||
'items': [
|
||||
{'pk': merge_incoming.pk, 'quantity': 30, 'merge': True},
|
||||
{'pk': separate_incoming.pk, 'quantity': 20, 'merge': False},
|
||||
],
|
||||
'location': self.dest.pk,
|
||||
},
|
||||
expected_code=201,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
StockItem.objects.filter(part=self.part, location=self.dest).count(), 2
|
||||
)
|
||||
|
||||
existing.refresh_from_db()
|
||||
self.assertEqual(existing.quantity, 130)
|
||||
self.assertFalse(StockItem.objects.filter(pk=merge_incoming.pk).exists())
|
||||
self.assertTrue(StockItem.objects.filter(pk=separate_incoming.pk).exists())
|
||||
|
||||
def test_transfer_merge_does_not_copy_source_tracking(self):
|
||||
"""Transfer merge keeps destination history and adds a single merge entry."""
|
||||
existing = StockItem.objects.create(
|
||||
part=self.part, location=self.dest, quantity=100
|
||||
)
|
||||
incoming = StockItem.objects.create(
|
||||
part=self.part, location=self.source_loc, quantity=50
|
||||
)
|
||||
|
||||
incoming.add_tracking_entry(
|
||||
StockHistoryCode.STOCK_UPDATE, self.user, notes='Source tracking entry'
|
||||
)
|
||||
|
||||
incoming_pk = incoming.pk
|
||||
tracking_count = existing.tracking_info.count()
|
||||
|
||||
self.post(
|
||||
self.url,
|
||||
{
|
||||
'items': [{'pk': incoming.pk, 'quantity': 50, 'merge': True}],
|
||||
'location': self.dest.pk,
|
||||
},
|
||||
expected_code=201,
|
||||
)
|
||||
|
||||
existing.refresh_from_db()
|
||||
|
||||
self.assertFalse(
|
||||
existing.tracking_info.filter(notes='Source tracking entry').exists()
|
||||
)
|
||||
self.assertEqual(existing.tracking_info.count(), tracking_count + 1)
|
||||
merge_entry = existing.tracking_info.filter(
|
||||
tracking_type=StockHistoryCode.MERGED_STOCK_ITEMS
|
||||
).first()
|
||||
self.assertIsNotNone(merge_entry)
|
||||
self.assertEqual(merge_entry.deltas['added'], 50.0)
|
||||
self.assertEqual(merge_entry.deltas['quantity'], 150.0)
|
||||
self.assertEqual(merge_entry.deltas['stockitem'], incoming_pk)
|
||||
self.assertEqual(merge_entry.deltas['location'], self.dest.pk)
|
||||
|
||||
def test_transfer_merge_partial_reuses_split_transfer_deltas(self):
|
||||
"""Partial merge reuses split transfer deltas on the merge tracking entry."""
|
||||
existing = StockItem.objects.create(
|
||||
part=self.part, location=self.dest, quantity=100
|
||||
)
|
||||
incoming = StockItem.objects.create(
|
||||
part=self.part, location=self.source_loc, quantity=100
|
||||
)
|
||||
|
||||
self.post(
|
||||
self.url,
|
||||
{
|
||||
'items': [{'pk': incoming.pk, 'quantity': 30, 'merge': True}],
|
||||
'location': self.dest.pk,
|
||||
},
|
||||
expected_code=201,
|
||||
)
|
||||
|
||||
incoming.refresh_from_db()
|
||||
self.assertEqual(incoming.quantity, 70)
|
||||
|
||||
merge_entry = existing.tracking_info.filter(
|
||||
tracking_type=StockHistoryCode.MERGED_STOCK_ITEMS
|
||||
).first()
|
||||
self.assertEqual(merge_entry.deltas['stockitem'], incoming.pk)
|
||||
self.assertEqual(merge_entry.deltas['location'], self.dest.pk)
|
||||
self.assertFalse(
|
||||
incoming.tracking_info.filter(
|
||||
tracking_type=StockHistoryCode.SPLIT_CHILD_ITEM
|
||||
).exists()
|
||||
)
|
||||
|
||||
|
||||
class StockItemDeletionTest(StockAPITestCase):
|
||||
"""Tests for stock item deletion via the API."""
|
||||
|
||||
|
|
|
|||
|
|
@ -729,6 +729,13 @@ class StockTest(StockTestBase):
|
|||
self.assertEqual(s1.quantity, 60)
|
||||
self.assertIsNone(s1.purchase_price)
|
||||
|
||||
merge_entry = s1.tracking_info.filter(
|
||||
tracking_type=StockHistoryCode.MERGED_STOCK_ITEMS
|
||||
).first()
|
||||
self.assertIsNotNone(merge_entry)
|
||||
self.assertEqual(merge_entry.deltas['added'], 50.0)
|
||||
self.assertEqual(merge_entry.deltas['quantity'], 60.0)
|
||||
|
||||
part.stock_items.all().delete()
|
||||
|
||||
# Create some stock items with pricing information
|
||||
|
|
|
|||
|
|
@ -47,9 +47,7 @@ User.add_to_class('__str__', user_model_str) # Overriding User.__str__
|
|||
|
||||
|
||||
if settings.LDAP_AUTH:
|
||||
from django_auth_ldap.backend import ( # type: ignore[unresolved-import]
|
||||
populate_user,
|
||||
)
|
||||
from django_auth_ldap.backend import populate_user # ty: ignore[unresolved-import]
|
||||
|
||||
@receiver(populate_user)
|
||||
def create_email_address(user, **kwargs):
|
||||
|
|
|
|||
|
|
@ -1,21 +1,25 @@
|
|||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile src/backend/requirements-dev.in -o src/backend/requirements-dev-3.14.txt -c src/backend/requirements-3.14.txt --python-version=3.14 -c src/backend/requirements.txt
|
||||
# uv pip compile src/backend/requirements-dev.in -o src/backend/requirements-dev-3.14.txt -c src/backend/requirements-3.14.txt --python-version=3.14 -c src/backend/requirements.txt -c src/backend/requirements-dev.txt
|
||||
asgiref==3.11.1 \
|
||||
--hash=sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce \
|
||||
--hash=sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133
|
||||
# via
|
||||
# -c src/backend/requirements-3.14.txt
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# -c src/backend/requirements.txt
|
||||
# django
|
||||
build==1.5.0 \
|
||||
--hash=sha256:13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f \
|
||||
--hash=sha256:302c22c3ba2a0fd5f3911918651341ebb3896176cbdec15bd421f80b1afc7647
|
||||
# via pip-tools
|
||||
# via
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# pip-tools
|
||||
certifi==2026.6.17 \
|
||||
--hash=sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432 \
|
||||
--hash=sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db
|
||||
# via
|
||||
# -c src/backend/requirements-3.14.txt
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# -c src/backend/requirements.txt
|
||||
# requests
|
||||
cffi==2.0.0 \
|
||||
|
|
@ -105,6 +109,7 @@ cffi==2.0.0 \
|
|||
--hash=sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf
|
||||
# via
|
||||
# -c src/backend/requirements-3.14.txt
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# -c src/backend/requirements.txt
|
||||
# cryptography
|
||||
charset-normalizer==3.4.7 \
|
||||
|
|
@ -239,13 +244,16 @@ charset-normalizer==3.4.7 \
|
|||
--hash=sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464
|
||||
# via
|
||||
# -c src/backend/requirements-3.14.txt
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# -c src/backend/requirements.txt
|
||||
# pdfminer-six
|
||||
# requests
|
||||
click==8.4.1 \
|
||||
--hash=sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2 \
|
||||
--hash=sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96
|
||||
# via pip-tools
|
||||
# via
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# pip-tools
|
||||
coverage[toml]==7.14.1 \
|
||||
--hash=sha256:0177614a0370f227888b4e436a7c55686d6a9f90eb1ade2b624ba685a1686e86 \
|
||||
--hash=sha256:01b7733daad0237daa01ef80fe2dfceffc911e6a17fa7b55d14aa8214eaaaecd \
|
||||
|
|
@ -353,7 +361,9 @@ coverage[toml]==7.14.1 \
|
|||
--hash=sha256:fad54e871165f6ec2f536063ac74c3104508a12963e64072ba44bd822de52b0c \
|
||||
--hash=sha256:fc459e5d73be2d6332fcfe8dbf3d8994671fe33c700f4565988ecfa511547253 \
|
||||
--hash=sha256:fd86572566fb40189a8260446158235159bc7a82dfbc87a3b39cf4fb57fcec1c
|
||||
# via -r src/backend/requirements-dev.in
|
||||
# via
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# -r src/backend/requirements-dev.in
|
||||
cryptography==48.0.1 \
|
||||
--hash=sha256:08a597acce1ff37f347400087776599e2348a3a8bc53b44120e463cd274efe4a \
|
||||
--hash=sha256:09f73a725d582cef64b91281a322cd798d14a33b2b6f2b7ad9531dc336d84c02 \
|
||||
|
|
@ -406,6 +416,7 @@ cryptography==48.0.1 \
|
|||
--hash=sha256:fe0180af5bf9236518a087e35bf2d9a347d5f5f51e63c579d683ddff424e3d46
|
||||
# via
|
||||
# -c src/backend/requirements-3.14.txt
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# -c src/backend/requirements.txt
|
||||
# pdfminer-six
|
||||
django==5.2.15 \
|
||||
|
|
@ -413,6 +424,7 @@ django==5.2.15 \
|
|||
--hash=sha256:5154a9bf84ac01dde011e367f355c07dbb329532e06810dcf3ef2af269e236e7
|
||||
# via
|
||||
# -c src/backend/requirements-3.14.txt
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# -c src/backend/requirements.txt
|
||||
# django-silk
|
||||
# django-slowtests
|
||||
|
|
@ -420,62 +432,88 @@ django==5.2.15 \
|
|||
# django-stubs-ext
|
||||
django-querycount==0.8.3 \
|
||||
--hash=sha256:0782484e8a1bd29498fa0195a67106e47cdcc98fafe80cebb1991964077cb694
|
||||
# via -r src/backend/requirements-dev.in
|
||||
# via
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# -r src/backend/requirements-dev.in
|
||||
django-silk==5.5.0 \
|
||||
--hash=sha256:41fcabe65d59d31ccdb69daeb3c3e6d30879ab2c00b0875b111fda0f69aec065 \
|
||||
--hash=sha256:82b5a690d623935be916dd145e2f605a4ac9454d54e03df6a7ffcf38333c8444
|
||||
# via -r src/backend/requirements-dev.in
|
||||
# via
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# -r src/backend/requirements-dev.in
|
||||
django-slowtests==1.1.1 \
|
||||
--hash=sha256:3c6936d420c9df444ac03625b41d97de043c662bbde61fbcd33e4cd407d0c247
|
||||
# via -r src/backend/requirements-dev.in
|
||||
# via
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# -r src/backend/requirements-dev.in
|
||||
django-stubs==6.0.5 \
|
||||
--hash=sha256:7742b8e60afc68a8545158e2bdb103376d5a092f7902c17f370920a9c08eb957 \
|
||||
--hash=sha256:9fb9eede9b2fc47b36c3dc4a93652eb959dff45466ec4a580e4a22782192d746
|
||||
# via -r src/backend/requirements-dev.in
|
||||
# via
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# -r src/backend/requirements-dev.in
|
||||
django-stubs-ext==6.0.5 \
|
||||
--hash=sha256:1cc325e991303849bce70e19748981b225ef08b37256f263e113caf97cd3bcc0 \
|
||||
--hash=sha256:a9932c8233d6dd4e34ae0b192026379c1a9be8f0b7c27aa1fa09ae215169773e
|
||||
# via django-stubs
|
||||
# via
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# django-stubs
|
||||
django-test-migrations==1.5.0 \
|
||||
--hash=sha256:1cbff04b1e82c5564a6f635284907b381cc11a2ff883adff46776d9126824f07 \
|
||||
--hash=sha256:96a08f085fc8bfaa53d44618341d82a2d22fd194c821cd81b147b66f0bec0da8
|
||||
# via -r src/backend/requirements-dev.in
|
||||
# via
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# -r src/backend/requirements-dev.in
|
||||
django-types==0.24.0 \
|
||||
--hash=sha256:af903de8b9ee963b7594459a7a20cb8eaaab176ae2b3244ecaa089e0c570b0d1 \
|
||||
--hash=sha256:ddb478ca733e0dde5475118dd59ab340156980f9659fd92de2083326ae96100a
|
||||
# via -r src/backend/requirements-dev.in
|
||||
# via
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# -r src/backend/requirements-dev.in
|
||||
gprof2dot==2025.4.14 \
|
||||
--hash=sha256:0742e4c0b4409a5e8777e739388a11e1ed3750be86895655312ea7c20bd0090e \
|
||||
--hash=sha256:35743e2d2ca027bf48fa7cba37021aaf4a27beeae1ae8e05a50b55f1f921a6ce
|
||||
# via django-silk
|
||||
# via
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# django-silk
|
||||
idna==3.18 \
|
||||
--hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \
|
||||
--hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848
|
||||
# via
|
||||
# -c src/backend/requirements-3.14.txt
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# -c src/backend/requirements.txt
|
||||
# requests
|
||||
iniconfig==2.3.0 \
|
||||
--hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \
|
||||
--hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12
|
||||
# via pytest
|
||||
# via
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# pytest
|
||||
isort==8.0.1 \
|
||||
--hash=sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d \
|
||||
--hash=sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75
|
||||
# via -r src/backend/requirements-dev.in
|
||||
# via
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# -r src/backend/requirements-dev.in
|
||||
markdown-it-py==4.2.0 \
|
||||
--hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \
|
||||
--hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a
|
||||
# via rich
|
||||
# via
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# rich
|
||||
mdurl==0.1.2 \
|
||||
--hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
|
||||
--hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
|
||||
# via markdown-it-py
|
||||
# via
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# markdown-it-py
|
||||
packaging==26.2 \
|
||||
--hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \
|
||||
--hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661
|
||||
# via
|
||||
# -c src/backend/requirements-3.14.txt
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# -c src/backend/requirements.txt
|
||||
# build
|
||||
# pytest
|
||||
|
|
@ -483,19 +521,27 @@ packaging==26.2 \
|
|||
pdfminer-six==20260107 \
|
||||
--hash=sha256:366585ba97e80dffa8f00cebe303d2f381884d8637af4ce422f1df3ef38111a9 \
|
||||
--hash=sha256:96bfd431e3577a55a0efd25676968ca4ce8fd5b53f14565f85716ff363889602
|
||||
# via -r src/backend/requirements-dev.in
|
||||
# via
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# -r src/backend/requirements-dev.in
|
||||
pip==26.1.2 \
|
||||
--hash=sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab \
|
||||
--hash=sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605
|
||||
# via pip-tools
|
||||
# via
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# pip-tools
|
||||
pip-tools==7.5.3 \
|
||||
--hash=sha256:3aac0c473240ae90db7213c033401f345b05197293ccbdd2704e52e7a783785e \
|
||||
--hash=sha256:8fa364779ebc010cbfe17cb9de404457ac733e100840423f28f6955de7742d41
|
||||
# via -r src/backend/requirements-dev.in
|
||||
# via
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# -r src/backend/requirements-dev.in
|
||||
pluggy==1.6.0 \
|
||||
--hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \
|
||||
--hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746
|
||||
# via pytest
|
||||
# via
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# pytest
|
||||
prek==0.4.5 \
|
||||
--hash=sha256:14109d37b33e5529db41a3539d4f8f72d295f6eeddede3964994d898b8cec05c \
|
||||
--hash=sha256:1fd98b986767dafdb6b4305b563ee5a3a8f13bd3c78b98d708626815ea9f147f \
|
||||
|
|
@ -514,30 +560,36 @@ prek==0.4.5 \
|
|||
--hash=sha256:e491a1a4641d91d8b03dcce5588397e76d2a5b432c9b0a6c70475972b4512ab4 \
|
||||
--hash=sha256:f7517774c72b001573520dc7111156779fd3e5b4452c11f09ff53c71a067e835 \
|
||||
--hash=sha256:fccd11613ae92619d1ecda0ab3359ceebeb38898909ec84a8d383733d12158cc
|
||||
# via -r src/backend/requirements-dev.in
|
||||
# via
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# -r src/backend/requirements-dev.in
|
||||
pycparser==3.0 \
|
||||
--hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \
|
||||
--hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992
|
||||
# via
|
||||
# -c src/backend/requirements-3.14.txt
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# -c src/backend/requirements.txt
|
||||
# cffi
|
||||
pygments==2.20.0 \
|
||||
--hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \
|
||||
--hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176
|
||||
# via
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# pytest
|
||||
# rich
|
||||
pyproject-hooks==1.2.0 \
|
||||
--hash=sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8 \
|
||||
--hash=sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913
|
||||
# via
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# build
|
||||
# pip-tools
|
||||
pytest==9.1.1 \
|
||||
--hash=sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313 \
|
||||
--hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c
|
||||
# via
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# pytest-codspeed
|
||||
# pytest-django
|
||||
pytest-codspeed==5.0.3 \
|
||||
|
|
@ -570,31 +622,41 @@ pytest-codspeed==5.0.3 \
|
|||
--hash=sha256:f56d0339cd98d26f6e561987be25bdd2761a5d53d8f73493b1ebe02d0d451093 \
|
||||
--hash=sha256:f852bee785a7a124cb1720b1915670c6742af87747dc4d838f3ffdbd365ce9d9 \
|
||||
--hash=sha256:fe2ea83c924c2250675b75686c3ee456b8cf0208d83d552e182a195fdf467378
|
||||
# via -r src/backend/requirements-dev.in
|
||||
# via
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# -r src/backend/requirements-dev.in
|
||||
pytest-django==4.12.0 \
|
||||
--hash=sha256:3ff300c49f8350ba2953b90297d23bf5f589db69545f56f1ec5f8cff5da83e85 \
|
||||
--hash=sha256:df94ec819a83c8979c8f6de13d9cdfbe76e8c21d39473cfe2b40c9fc9be3c758
|
||||
# via -r src/backend/requirements-dev.in
|
||||
# via
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# -r src/backend/requirements-dev.in
|
||||
requests==2.34.2 \
|
||||
--hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \
|
||||
--hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed
|
||||
# via
|
||||
# -c src/backend/requirements-3.14.txt
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# -c src/backend/requirements.txt
|
||||
# requests-mock
|
||||
requests-mock==1.12.1 \
|
||||
--hash=sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563 \
|
||||
--hash=sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401
|
||||
# via -r src/backend/requirements-dev.in
|
||||
# via
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# -r src/backend/requirements-dev.in
|
||||
rich==15.0.0 \
|
||||
--hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \
|
||||
--hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36
|
||||
# via pytest-codspeed
|
||||
# via
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# pytest-codspeed
|
||||
setuptools==82.0.1 \
|
||||
--hash=sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9 \
|
||||
--hash=sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb
|
||||
# via
|
||||
# -c src/backend/requirements-3.14.txt
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# -c src/backend/requirements.txt
|
||||
# -r src/backend/requirements-dev.in
|
||||
# pip-tools
|
||||
|
|
@ -603,42 +665,50 @@ sqlparse==0.5.5 \
|
|||
--hash=sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e
|
||||
# via
|
||||
# -c src/backend/requirements-3.14.txt
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# -c src/backend/requirements.txt
|
||||
# django
|
||||
# django-silk
|
||||
ty==0.0.1a21 \
|
||||
--hash=sha256:0efba2e52b58f536f4198ba5c4a36cac2ba67d83ec6f429ebc7704233bcda4c3 \
|
||||
--hash=sha256:1474d883129bb63da3b2380fc7ead824cd3baf6a9551e6aa476ffefc58057af3 \
|
||||
--hash=sha256:1f276ceab23a1410aec09508248c76ae0989c67fb7a0c287e0d4564994295531 \
|
||||
--hash=sha256:218d53e7919e885bd98e9196d9cb952d82178b299aa36da6f7f39333eb7400ed \
|
||||
--hash=sha256:21f708d02b6588323ffdbfdba38830dd0ecfd626db50aa6006b296b5470e52f9 \
|
||||
--hash=sha256:334d2a212ebf42a0e55d57561926af7679fe1e878175e11dcb81ad8df892844e \
|
||||
--hash=sha256:3c3bc66fcae41eff133cfe326dd65d82567a2fb5d4efe2128773b10ec2766819 \
|
||||
--hash=sha256:5dfc73299d441cc6454e36ed0a976877415024143dfca6592dc36f7701424383 \
|
||||
--hash=sha256:7505aeb8bf2a62f00f12cfa496f6c965074d75c8126268776565284c8a12d5dd \
|
||||
--hash=sha256:84243455f295ed850bd53f7089819321807d4e6ee3b1cbff6086137ae0259466 \
|
||||
--hash=sha256:87a200c21e02962e8a27374d9d152582331d57d709672431be58f4f898bf6cad \
|
||||
--hash=sha256:9463cac96b8f1bb5ba740fe1d42cd6bd152b43c5b159b2f07f8fd629bcdded34 \
|
||||
--hash=sha256:a8c769987d00fbc33054ff7e342633f475ea10dc43bc60fb9fb056159d48cb90 \
|
||||
--hash=sha256:ba13d03b9e095216ceb4e4d554a308517f28ab0a6e4dcd07cfe94563e4c2c489 \
|
||||
--hash=sha256:be8f457d7841b7ead2a3f6b65ba668abc172a1150a0f1f6c0958af3725dbb61a \
|
||||
--hash=sha256:cc0880ec344fbdf736b05d8d0da01f0caaaa02409bd9a24b68d18d0127a79b0e \
|
||||
--hash=sha256:e941e9a9d1e54b03eeaf9c3197c26a19cf76009fd5e41e16e5657c1c827bd6d3 \
|
||||
--hash=sha256:ecf41706b803827b0de8717f32a434dad1e67be9f4b8caf403e12013179ea06a
|
||||
# via -r src/backend/requirements-dev.in
|
||||
ty==0.0.51 \
|
||||
--hash=sha256:08adbe53fb8bc9e7f00e89bf1d3c875a02cda76d83f109d2e6ab1ff35a7bfa8c \
|
||||
--hash=sha256:25a5b31e6f23fd5dc63ad29087ded09932409e4154e2fe07bbaed015035990bb \
|
||||
--hash=sha256:2faed19a8f1505370de071c008df52a994fc03a204f3267c3a33a32ca26f854f \
|
||||
--hash=sha256:429a997394dac73870d71b87cc90efc54da3efaf319e72ca18aeef35a78aef90 \
|
||||
--hash=sha256:49a21237f6fd1de56beaff0a3e85fe022a09a3401e67e3abec41ce838a5d4d2e \
|
||||
--hash=sha256:608d417cd1eaf79bcbd713d9830d5e3db9d57ec225c3af3e4ac9a9ff66b45d70 \
|
||||
--hash=sha256:61b4b6a003c3ebe53a63a1125c9b6542aa01bc1b6c9a235d01ee328d000d61a9 \
|
||||
--hash=sha256:62ced5e380284f12b2dc4802a3e4ed3dac39913fc6719afde7978814a4c7f169 \
|
||||
--hash=sha256:62d94f06e8c317e89b6884f2bde443040e596b88c7c79bd944c84c105b06257a \
|
||||
--hash=sha256:79d1877e93460f936bc10ed1a31525702b7ce51075763ccba993be17f0b9e905 \
|
||||
--hash=sha256:947986bd82d324b3a5c58ce03f1dad160cdf36443d3e8f64b3484b861ba9bc64 \
|
||||
--hash=sha256:abd92913bc90d1705ef9391ff8c6822b61e2e827fa295eb30bf0dfabcf815645 \
|
||||
--hash=sha256:b90172d46365bb9d51a7011cbb5c60cc4f514f42c86635df6c092b717f85e1ac \
|
||||
--hash=sha256:bc7459348a253247bbfb2669a021e614281b86bbea24c36112b8a6e1a2499a16 \
|
||||
--hash=sha256:c1bd1355aee86af01e4e21b0bc16fc460fb05905761f0d8b8d70841de0feade8 \
|
||||
--hash=sha256:cc233a6235fb23e2a44b14731a10043e37ba2f30f2c361cf49ad3633c5b9da9c \
|
||||
--hash=sha256:dc5e93695ab5dcbf1eef663aee60ec23a413547cc9cb06adcb0d842e9166bd0f \
|
||||
--hash=sha256:f8f52952cff665bc52a36147e610c10f5699d30007d7a14ab7f345cff93476ff
|
||||
# via
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# -r src/backend/requirements-dev.in
|
||||
types-psycopg2==2.9.21.20260518 \
|
||||
--hash=sha256:2fd728a4fa3860db0a4a9813e5f49c30ed329b3d2e60e73530d9db01b2b98420 \
|
||||
--hash=sha256:8b1f80d90d6799a4fcdac12198b382a8feee9ed4340d5f69b56fc5ffa0644143
|
||||
# via django-types
|
||||
# via
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# django-types
|
||||
types-pyyaml==6.0.12.20260518 \
|
||||
--hash=sha256:d2150f75a231c9fe9c7463bd29487d93e60bac90400287351384bc2284eba7cd \
|
||||
--hash=sha256:d917f83fb38462550338c1297faedd860b3ec83912b96b1e3d73255f7473e466
|
||||
# via django-stubs
|
||||
# via
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# django-stubs
|
||||
typing-extensions==4.15.0 \
|
||||
--hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \
|
||||
--hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548
|
||||
# via
|
||||
# -c src/backend/requirements-3.14.txt
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# -c src/backend/requirements.txt
|
||||
# django-stubs
|
||||
# django-stubs-ext
|
||||
|
|
@ -648,9 +718,12 @@ urllib3==2.7.0 \
|
|||
--hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897
|
||||
# via
|
||||
# -c src/backend/requirements-3.14.txt
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# -c src/backend/requirements.txt
|
||||
# requests
|
||||
wheel==0.47.0 \
|
||||
--hash=sha256:212281cab4dff978f6cedd499cd893e1f620791ca6ff7107cf270781e587eced \
|
||||
--hash=sha256:cc72bd1009ba0cf63922e28f94d9d83b920aa2bb28f798a31d0691b02fa3c9b3
|
||||
# via pip-tools
|
||||
# via
|
||||
# -c src/backend/requirements-dev.txt
|
||||
# pip-tools
|
||||
|
|
|
|||
|
|
@ -594,74 +594,25 @@ sqlparse==0.5.5 \
|
|||
# -c src/backend/requirements.txt
|
||||
# django
|
||||
# django-silk
|
||||
tomli==2.4.1 \
|
||||
--hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \
|
||||
--hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \
|
||||
--hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \
|
||||
--hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \
|
||||
--hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \
|
||||
--hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \
|
||||
--hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \
|
||||
--hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \
|
||||
--hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \
|
||||
--hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \
|
||||
--hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \
|
||||
--hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \
|
||||
--hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \
|
||||
--hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \
|
||||
--hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \
|
||||
--hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \
|
||||
--hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \
|
||||
--hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \
|
||||
--hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \
|
||||
--hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \
|
||||
--hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \
|
||||
--hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \
|
||||
--hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \
|
||||
--hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \
|
||||
--hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \
|
||||
--hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \
|
||||
--hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \
|
||||
--hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \
|
||||
--hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \
|
||||
--hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \
|
||||
--hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \
|
||||
--hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \
|
||||
--hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \
|
||||
--hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \
|
||||
--hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \
|
||||
--hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \
|
||||
--hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \
|
||||
--hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \
|
||||
--hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \
|
||||
--hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \
|
||||
--hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \
|
||||
--hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \
|
||||
--hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \
|
||||
--hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \
|
||||
--hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \
|
||||
--hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \
|
||||
--hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049
|
||||
# via coverage
|
||||
ty==0.0.1a21 \
|
||||
--hash=sha256:0efba2e52b58f536f4198ba5c4a36cac2ba67d83ec6f429ebc7704233bcda4c3 \
|
||||
--hash=sha256:1474d883129bb63da3b2380fc7ead824cd3baf6a9551e6aa476ffefc58057af3 \
|
||||
--hash=sha256:1f276ceab23a1410aec09508248c76ae0989c67fb7a0c287e0d4564994295531 \
|
||||
--hash=sha256:218d53e7919e885bd98e9196d9cb952d82178b299aa36da6f7f39333eb7400ed \
|
||||
--hash=sha256:21f708d02b6588323ffdbfdba38830dd0ecfd626db50aa6006b296b5470e52f9 \
|
||||
--hash=sha256:334d2a212ebf42a0e55d57561926af7679fe1e878175e11dcb81ad8df892844e \
|
||||
--hash=sha256:3c3bc66fcae41eff133cfe326dd65d82567a2fb5d4efe2128773b10ec2766819 \
|
||||
--hash=sha256:5dfc73299d441cc6454e36ed0a976877415024143dfca6592dc36f7701424383 \
|
||||
--hash=sha256:7505aeb8bf2a62f00f12cfa496f6c965074d75c8126268776565284c8a12d5dd \
|
||||
--hash=sha256:84243455f295ed850bd53f7089819321807d4e6ee3b1cbff6086137ae0259466 \
|
||||
--hash=sha256:87a200c21e02962e8a27374d9d152582331d57d709672431be58f4f898bf6cad \
|
||||
--hash=sha256:9463cac96b8f1bb5ba740fe1d42cd6bd152b43c5b159b2f07f8fd629bcdded34 \
|
||||
--hash=sha256:a8c769987d00fbc33054ff7e342633f475ea10dc43bc60fb9fb056159d48cb90 \
|
||||
--hash=sha256:ba13d03b9e095216ceb4e4d554a308517f28ab0a6e4dcd07cfe94563e4c2c489 \
|
||||
--hash=sha256:be8f457d7841b7ead2a3f6b65ba668abc172a1150a0f1f6c0958af3725dbb61a \
|
||||
--hash=sha256:cc0880ec344fbdf736b05d8d0da01f0caaaa02409bd9a24b68d18d0127a79b0e \
|
||||
--hash=sha256:e941e9a9d1e54b03eeaf9c3197c26a19cf76009fd5e41e16e5657c1c827bd6d3 \
|
||||
--hash=sha256:ecf41706b803827b0de8717f32a434dad1e67be9f4b8caf403e12013179ea06a
|
||||
ty==0.0.51 \
|
||||
--hash=sha256:08adbe53fb8bc9e7f00e89bf1d3c875a02cda76d83f109d2e6ab1ff35a7bfa8c \
|
||||
--hash=sha256:25a5b31e6f23fd5dc63ad29087ded09932409e4154e2fe07bbaed015035990bb \
|
||||
--hash=sha256:2faed19a8f1505370de071c008df52a994fc03a204f3267c3a33a32ca26f854f \
|
||||
--hash=sha256:429a997394dac73870d71b87cc90efc54da3efaf319e72ca18aeef35a78aef90 \
|
||||
--hash=sha256:49a21237f6fd1de56beaff0a3e85fe022a09a3401e67e3abec41ce838a5d4d2e \
|
||||
--hash=sha256:608d417cd1eaf79bcbd713d9830d5e3db9d57ec225c3af3e4ac9a9ff66b45d70 \
|
||||
--hash=sha256:61b4b6a003c3ebe53a63a1125c9b6542aa01bc1b6c9a235d01ee328d000d61a9 \
|
||||
--hash=sha256:62ced5e380284f12b2dc4802a3e4ed3dac39913fc6719afde7978814a4c7f169 \
|
||||
--hash=sha256:62d94f06e8c317e89b6884f2bde443040e596b88c7c79bd944c84c105b06257a \
|
||||
--hash=sha256:79d1877e93460f936bc10ed1a31525702b7ce51075763ccba993be17f0b9e905 \
|
||||
--hash=sha256:947986bd82d324b3a5c58ce03f1dad160cdf36443d3e8f64b3484b861ba9bc64 \
|
||||
--hash=sha256:abd92913bc90d1705ef9391ff8c6822b61e2e827fa295eb30bf0dfabcf815645 \
|
||||
--hash=sha256:b90172d46365bb9d51a7011cbb5c60cc4f514f42c86635df6c092b717f85e1ac \
|
||||
--hash=sha256:bc7459348a253247bbfb2669a021e614281b86bbea24c36112b8a6e1a2499a16 \
|
||||
--hash=sha256:c1bd1355aee86af01e4e21b0bc16fc460fb05905761f0d8b8d70841de0feade8 \
|
||||
--hash=sha256:cc233a6235fb23e2a44b14731a10043e37ba2f30f2c361cf49ad3633c5b9da9c \
|
||||
--hash=sha256:dc5e93695ab5dcbf1eef663aee60ec23a413547cc9cb06adcb0d842e9166bd0f \
|
||||
--hash=sha256:f8f52952cff665bc52a36147e610c10f5699d30007d7a14ab7f345cff93476ff
|
||||
# via -r src/backend/requirements-dev.in
|
||||
types-psycopg2==2.9.21.20260518 \
|
||||
--hash=sha256:2fd728a4fa3860db0a4a9813e5f49c30ed329b3d2e60e73530d9db01b2b98420 \
|
||||
|
|
|
|||
|
|
@ -8,10 +8,6 @@ asgiref==3.11.1 \
|
|||
# django-allauth
|
||||
# django-cors-headers
|
||||
# django-structlog
|
||||
async-timeout==5.0.1 \
|
||||
--hash=sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c \
|
||||
--hash=sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3
|
||||
# via redis
|
||||
attrs==26.1.0 \
|
||||
--hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \
|
||||
--hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32
|
||||
|
|
@ -1929,7 +1925,6 @@ typing-extensions==4.15.0 \
|
|||
--hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548
|
||||
# via
|
||||
# django-redis
|
||||
# dulwich
|
||||
# flexcache
|
||||
# flexparser
|
||||
# grpcio
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue