2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-04-02 09:31:02 +00:00

chore(backend): Bump ty (#11537)

* bump ty - there is better django support now

* more fixes

* fix usage of types

* add missing type

* fix access

* ensure itteration is safe

* fix uncombat decimal usage

* ?potential breaking: change access key

* remove now obsolete igtnore

* ignore errors on StdImageField

* remove usage of unkonw parameter

* fix diff error

* fix schema creation

* fix coverage quirk

* those are unneeded now?

* this seem to have been an issue with 3.12; not occuring on 3.14

* ignore pydantiics

* ignore edge cases for now

* include isGenerating fix

* make typing python 3.11 compatible

* fix more errors
This commit is contained in:
Matthias Mair
2026-03-18 08:25:50 +01:00
committed by GitHub
parent 865ec47a3b
commit 16103379c9
40 changed files with 114 additions and 96 deletions

View File

@@ -108,13 +108,13 @@ root = ["src/backend/InvenTree"]
unresolved-reference="ignore" # 21 # see https://github.com/astral-sh/ty/issues/220 unresolved-reference="ignore" # 21 # see https://github.com/astral-sh/ty/issues/220
unresolved-attribute="ignore" # 505 # need Plugin Mixin typing unresolved-attribute="ignore" # 505 # need Plugin Mixin typing
call-non-callable="ignore" # 8 ## call-non-callable="ignore" # 8 ##
invalid-argument-type="ignore" # 49
invalid-assignment="ignore" # 17 # need to wait for better django field stubs invalid-assignment="ignore" # 17 # need to wait for better django field stubs
invalid-method-override="ignore" invalid-method-override="ignore" # 99
invalid-return-type="ignore" # 22 ## invalid-return-type="ignore" # 22 ##
possibly-missing-attribute="ignore" # 25 # https://github.com/astral-sh/ty/issues/164 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 unknown-argument="ignore" # 3 # need to wait for better django field stubs
no-matching-overload="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
[tool.coverage.run] [tool.coverage.run]
source = ["src/backend/InvenTree", "InvenTree"] source = ["src/backend/InvenTree", "InvenTree"]

View File

@@ -199,7 +199,7 @@ def regenerate_imagefile(_file, _name: str):
_name: Name of the variation (e.g. 'thumbnail', 'preview') _name: Name of the variation (e.g. 'thumbnail', 'preview')
""" """
name = _file.field.attr_class.get_variation_name(_file.name, _name) name = _file.field.attr_class.get_variation_name(_file.name, _name)
return ImageFieldFile(_file.instance, _file, name) # type: ignore return ImageFieldFile(_file.instance, _file, name) # ty:ignore[too-many-positional-arguments]
def image2name(img_obj: StdImageField, do_preview: bool, do_thumbnail: bool): def image2name(img_obj: StdImageField, do_preview: bool, do_thumbnail: bool):
@@ -311,7 +311,7 @@ def TestIfImageURL(url):
] ]
def str2bool(text, test=True): def str2bool(text, test=True) -> bool:
"""Test if a string 'looks' like a boolean value. """Test if a string 'looks' like a boolean value.
Args: Args:

View File

@@ -8,6 +8,7 @@ import structlog
from allauth.account.models import EmailAddress from allauth.account.models import EmailAddress
import InvenTree.ready import InvenTree.ready
import InvenTree.tasks as tasks
from common.models import Priority, issue_mail from common.models import Priority, issue_mail
logger = structlog.get_logger('inventree') logger = structlog.get_logger('inventree')
@@ -98,7 +99,7 @@ def send_email(
) )
return False, 'INVE-W7: no from_email or DEFAULT_FROM_EMAIL specified' return False, 'INVE-W7: no from_email or DEFAULT_FROM_EMAIL specified'
InvenTree.tasks.offload_task( tasks.offload_task(
issue_mail, issue_mail,
subject=subject, subject=subject,
body=body, body=body,

View File

@@ -99,7 +99,7 @@ class Command(BaseCommand):
if kwargs['include_items']: if kwargs['include_items']:
icons[icon]['items'].append({ icons[icon]['items'].append({
'model': model.__name__.lower(), 'model': model.__name__.lower(),
'id': item.id, # type: ignore 'id': item.id,
}) })
self.stdout.write(f'Writing icon map for {len(icons.keys())} icons') self.stdout.write(f'Writing icon map for {len(icons.keys())} icons')

View File

@@ -1329,7 +1329,7 @@ class InvenTreeBarcodeMixin(models.Model):
@classmethod @classmethod
def lookup_barcode(cls, barcode_hash: str) -> models.Model: def lookup_barcode(cls, barcode_hash: str) -> models.Model:
"""Check if a model instance exists with the specified third-party barcode hash.""" """Check if a model instance exists with the specified third-party barcode hash."""
return cls.objects.filter(barcode_hash=barcode_hash).first() return cls.objects.filter(barcode_hash=barcode_hash).first() # ty:ignore[invalid-return-type]
def assign_barcode( def assign_barcode(
self, self,
@@ -1485,7 +1485,7 @@ class InvenTreeImageMixin(models.Model):
def rename_image(self, filename): def rename_image(self, filename):
"""Rename the uploaded image file using the IMAGE_RENAME function.""" """Rename the uploaded image file using the IMAGE_RENAME function."""
return self.IMAGE_RENAME(filename) # type: ignore return self.IMAGE_RENAME(filename)
image = StdImageField( image = StdImageField(
upload_to=rename_image, upload_to=rename_image,

View File

@@ -376,7 +376,7 @@ def auth_exempt(view_func):
def wrapped_view(*args, **kwargs): def wrapped_view(*args, **kwargs):
return view_func(*args, **kwargs) return view_func(*args, **kwargs)
wrapped_view.auth_exempt = True # type:ignore[unresolved-attribute] wrapped_view.auth_exempt = True
return wraps(view_func)(wrapped_view) return wraps(view_func)(wrapped_view)

View File

@@ -483,7 +483,7 @@ if LDAP_AUTH: # pragma: no cover
) )
AUTH_LDAP_USER_SEARCH = django_auth_ldap.config.LDAPSearch( AUTH_LDAP_USER_SEARCH = django_auth_ldap.config.LDAPSearch(
get_setting('INVENTREE_LDAP_SEARCH_BASE_DN', 'ldap.search_base_dn'), get_setting('INVENTREE_LDAP_SEARCH_BASE_DN', 'ldap.search_base_dn'),
ldap.SCOPE_SUBTREE, # type: ignore[unresolved-attribute] ldap.SCOPE_SUBTREE,
str( str(
get_setting( get_setting(
'INVENTREE_LDAP_SEARCH_FILTER_STR', 'INVENTREE_LDAP_SEARCH_FILTER_STR',
@@ -519,7 +519,7 @@ if LDAP_AUTH: # pragma: no cover
) )
AUTH_LDAP_GROUP_SEARCH = django_auth_ldap.config.LDAPSearch( AUTH_LDAP_GROUP_SEARCH = django_auth_ldap.config.LDAPSearch(
get_setting('INVENTREE_LDAP_GROUP_SEARCH', 'ldap.group_search'), get_setting('INVENTREE_LDAP_GROUP_SEARCH', 'ldap.group_search'),
ldap.SCOPE_SUBTREE, # type: ignore[unresolved-attribute] ldap.SCOPE_SUBTREE,
f'(objectClass={AUTH_LDAP_GROUP_OBJECT_CLASS})', f'(objectClass={AUTH_LDAP_GROUP_OBJECT_CLASS})',
) )
AUTH_LDAP_GROUP_TYPE_CLASS = get_setting( AUTH_LDAP_GROUP_TYPE_CLASS = get_setting(

View File

@@ -49,7 +49,7 @@ def check_provider(provider):
if not app: if not app:
return False return False
if allauth.app_settings.SITES_ENABLED: # type: ignore[unresolved-attribute] if allauth.app_settings.SITES_ENABLED:
# At least one matching site must be specified # At least one matching site must be specified
if not app.sites.exists(): if not app.sites.exists():
logger.error('SocialApp %s has no sites configured', app) logger.error('SocialApp %s has no sites configured', app)

View File

@@ -330,7 +330,7 @@ class ScheduledTask:
QUARTERLY: str = 'Q' QUARTERLY: str = 'Q'
YEARLY: str = 'Y' YEARLY: str = 'Y'
TYPE: tuple[str] = (MINUTES, HOURLY, DAILY, WEEKLY, MONTHLY, QUARTERLY, YEARLY) # type: ignore[invalid-assignment] TYPE: tuple[str] = (MINUTES, HOURLY, DAILY, WEEKLY, MONTHLY, QUARTERLY, YEARLY)
class TaskRegister: class TaskRegister:

View File

@@ -574,7 +574,7 @@ class GeneralApiTests(InvenTreeAPITestCase):
self.assertIn('License file not found at', str(log.output)) self.assertIn('License file not found at', str(log.output))
with TemporaryDirectory() as tmp: # type: ignore[no-matching-overload] with TemporaryDirectory() as tmp:
sample_file = Path(tmp, 'temp.txt') sample_file = Path(tmp, 'temp.txt')
sample_file.write_text('abc', 'utf-8') sample_file.write_text('abc', 'utf-8')

View File

@@ -703,12 +703,12 @@ class TestHelpers(TestCase):
"""Test getMediaUrl.""" """Test getMediaUrl."""
# Str should not work # Str should not work
with self.assertRaises(TypeError): with self.assertRaises(TypeError):
helpers.getMediaUrl('xx/yy.png') # type: ignore helpers.getMediaUrl('xx/yy.png')
# Correct usage # Correct usage
part = Part().image part = Part().image
self.assertEqual( self.assertEqual(
helpers.getMediaUrl(StdImageFieldFile(part, part, 'xx/yy.png')), # type: ignore helpers.getMediaUrl(StdImageFieldFile(part, part, 'xx/yy.png')), # ty:ignore[too-many-positional-arguments]
'/media/xx/yy.png', '/media/xx/yy.png',
) )

View File

@@ -4,7 +4,7 @@ import base64
import logging import logging
from typing import Optional from typing import Optional
from opentelemetry import metrics, trace # type: ignore[import] from opentelemetry import metrics, trace
from opentelemetry.instrumentation.django import DjangoInstrumentor from opentelemetry.instrumentation.django import DjangoInstrumentor
from opentelemetry.instrumentation.redis import RedisInstrumentor from opentelemetry.instrumentation.redis import RedisInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor from opentelemetry.instrumentation.requests import RequestsInstrumentor

View File

@@ -289,8 +289,10 @@ def inventreeBranch():
return ' '.join(branch.splitlines()) return ' '.join(branch.splitlines())
if main_branch is None: if main_branch is None:
return None return None # pragma: no cover - branch information may not be available in all environments
return main_branch.decode('utf-8') return main_branch.decode(
'utf-8'
) # pragma: no cover - branch information may not be available in all environments
def inventreeTarget(): def inventreeTarget():

View File

@@ -17,6 +17,7 @@ from rest_framework.response import Response
import build.models as build_models import build.models as build_models
import build.serializers import build.serializers
import common.models import common.models
import common.serializers
import part.models as part_models import part.models as part_models
import stock.models as stock_models import stock.models as stock_models
import stock.serializers import stock.serializers

View File

@@ -1,7 +1,7 @@
"""Build database model definitions.""" """Build database model definitions."""
import decimal import decimal
from typing import Optional from typing import Optional, TypedDict
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@@ -50,7 +50,7 @@ from stock.status_codes import StockHistoryCode, StockStatus
logger = structlog.get_logger('inventree') logger = structlog.get_logger('inventree')
class BuildReportContext(report.mixins.BaseReportContext): class BuildReportContext(report.mixins.BaseReportContext, TypedDict):
"""Context for the Build model. """Context for the Build model.
Attributes: Attributes:
@@ -1039,7 +1039,7 @@ class Build(
lines = lines.exclude(bom_item__consumable=True) lines = lines.exclude(bom_item__consumable=True)
lines = lines.annotate(allocated=annotate_allocated_quantity()) lines = lines.annotate(allocated=annotate_allocated_quantity())
for build_line in lines: # type: ignore[non-iterable] for build_line in lines:
reduce_by = build_line.allocated - build_line.quantity reduce_by = build_line.allocated - build_line.quantity
if reduce_by <= 0: if reduce_by <= 0:
@@ -1512,10 +1512,10 @@ class Build(
unallocated_quantity -= quantity unallocated_quantity -= quantity
except (ValidationError, serializers.ValidationError) as exc: except (ValidationError, serializers.ValidationError) as exc:
# Catch model errors and re-throw as DRF errors # Re-raise with a Django-compatible validation payload
raise ValidationError( raise ValidationError(
exc.message, detail=serializers.as_serializer_error(exc) serializers.as_serializer_error(exc)
) ) from exc
if unallocated_quantity <= 0: if unallocated_quantity <= 0:
# We have now fully-allocated this BomItem - no need to continue! # We have now fully-allocated this BomItem - no need to continue!
@@ -1695,7 +1695,7 @@ def after_save_build(sender, instance: Build, created: bool, **kwargs):
instance.update_build_line_items() instance.update_build_line_items()
class BuildLineReportContext(report.mixins.BaseReportContext): class BuildLineReportContext(report.mixins.BaseReportContext, TypedDict):
"""Context for the BuildLine model. """Context for the BuildLine model.
Attributes: Attributes:
@@ -1745,6 +1745,7 @@ class BuildLine(report.mixins.InvenTreeReportMixin, InvenTree.models.InvenTreeMo
"""Return the API URL used to access this model.""" """Return the API URL used to access this model."""
return reverse('api-build-line-list') return reverse('api-build-line-list')
# type
def report_context(self) -> BuildLineReportContext: def report_context(self) -> BuildLineReportContext:
"""Generate custom report context for this BuildLine object.""" """Generate custom report context for this BuildLine object."""
return { return {

View File

@@ -230,6 +230,6 @@ def get_price(
quantity = decimal.Decimal(f'{quantity}') quantity = decimal.Decimal(f'{quantity}')
if pb_found: if pb_found:
cost = pb_cost * quantity cost = decimal.Decimal(pb_cost) * quantity
return InvenTree.helpers.normalize(cost + instance.base_cost) return InvenTree.helpers.normalize(cost + instance.base_cost)
return None return None

View File

@@ -85,8 +85,8 @@ class RenderMeta(enums.ChoicesType):
return [] return []
class RenderChoices(models.TextChoices, metaclass=RenderMeta): # type: ignore class RenderChoices(models.TextChoices, metaclass=RenderMeta):
"""Class for creating enumerated string choices for schema rendering.""" """Class for creating enumerated string choices for schema rendering.""" # ty:ignore[conflicting-metaclass]
class MetaMixin(models.Model): class MetaMixin(models.Model):
@@ -1084,7 +1084,7 @@ class BaseInvenTreeSetting(models.Model):
return self.__class__.validator_is_bool(validator) return self.__class__.validator_is_bool(validator)
def as_bool(self): def as_bool(self) -> bool:
"""Return the value of this setting converted to a boolean value. """Return the value of this setting converted to a boolean value.
Warning: Only use on values where is_bool evaluates to true! Warning: Only use on values where is_bool evaluates to true!

View File

@@ -42,7 +42,7 @@ class SettingsValueField(serializers.Field):
"""Return the object instance, not the attribute value.""" """Return the object instance, not the attribute value."""
return instance return instance
def to_representation(self, instance: common_models.InvenTreeSetting) -> str: def to_representation(self, instance: common_models.InvenTreeSetting):
"""Return the value of the setting. """Return the value of the setting.
Protected settings are returned as '***' Protected settings are returned as '***'
@@ -381,7 +381,7 @@ class ConfigSerializer(serializers.Serializer):
"""Return the configuration data as a dictionary.""" """Return the configuration data as a dictionary."""
if not isinstance(instance, str): if not isinstance(instance, str):
instance = list(instance.keys())[0] instance = list(instance.keys())[0]
return {'key': instance, **self.instance[instance]} return {'key': instance, **self.instance.get(instance)}
class NotesImageSerializer(InvenTreeModelSerializer): class NotesImageSerializer(InvenTreeModelSerializer):
@@ -457,7 +457,7 @@ class FlagSerializer(serializers.Serializer):
data = {'key': instance, 'state': flag_state(instance, request=request)} data = {'key': instance, 'state': flag_state(instance, request=request)}
if request and request.user.is_superuser: if request and request.user.is_superuser:
data['conditions'] = self.instance[instance] data['conditions'] = self.instance.get(instance)
return data return data

View File

@@ -26,6 +26,7 @@ class SettingsKeyType(TypedDict, total=False):
validator: Validation function/list of functions for the setting (optional, default: None, e.g: bool, int, str, MinValueValidator, ...) validator: Validation function/list of functions for the setting (optional, default: None, e.g: bool, int, str, MinValueValidator, ...)
default: Default value or function that returns default value (optional) default: Default value or function that returns default value (optional)
choices: Function that returns or value of list[tuple[str: key, str: display value]] (optional) choices: Function that returns or value of list[tuple[str: key, str: display value]] (optional)
model_filters: Filters to apply when querying the associated model (optional)
hidden: Hide this setting from settings page (optional) hidden: Hide this setting from settings page (optional)
before_save: Function that gets called after save with *args, **kwargs (optional) before_save: Function that gets called after save with *args, **kwargs (optional)
after_save: Function that gets called after save with *args, **kwargs (optional) after_save: Function that gets called after save with *args, **kwargs (optional)
@@ -42,6 +43,7 @@ class SettingsKeyType(TypedDict, total=False):
validator: Callable | list[Callable] | tuple[Callable] validator: Callable | list[Callable] | tuple[Callable]
default: Callable | Any default: Callable | Any
choices: list[tuple[str, str]] | Callable[[], list[tuple[str, str]]] choices: list[tuple[str, str]] | Callable[[], list[tuple[str, str]]]
model_filters: dict[str, Any]
hidden: bool hidden: bool
before_save: Callable[..., None] before_save: Callable[..., None]
after_save: Callable[..., None] after_save: Callable[..., None]

View File

@@ -2,6 +2,7 @@
import os import os
from decimal import Decimal from decimal import Decimal
from typing import TypedDict
from django.apps import apps from django.apps import apps
from django.conf import settings from django.conf import settings
@@ -53,7 +54,7 @@ def rename_company_image(instance, filename):
return os.path.join(base, fn) return os.path.join(base, fn)
class CompanyReportContext(report.mixins.BaseReportContext): class CompanyReportContext(report.mixins.BaseReportContext, TypedDict):
"""Report context for the Company model. """Report context for the Company model.
Attributes: Attributes:
@@ -243,7 +244,11 @@ class Company(
# We may have a pre-fetched primary address list # We may have a pre-fetched primary address list
if hasattr(self, 'primary_address_list'): if hasattr(self, 'primary_address_list'):
addresses = self.primary_address_list addresses = self.primary_address_list
return addresses[0] if len(addresses) > 0 else None return (
addresses[0]
if len(addresses) > 0 and isinstance(addresses, list)
else None
)
# Otherwise, query the database # Otherwise, query the database
return self.addresses.filter(primary=True).first() return self.addresses.filter(primary=True).first()

View File

@@ -194,7 +194,7 @@ class InvenTreeCustomStatusSerializerMixin:
"""Ensure the custom field is updated if the leader was changed.""" """Ensure the custom field is updated if the leader was changed."""
self.gather_custom_fields() self.gather_custom_fields()
# Mirror values from leader to follower # Mirror values from leader to follower
for field in self._custom_fields_leader: for field in self._custom_fields_leader or []:
follower_field_name = f'{field}_custom_key' follower_field_name = f'{field}_custom_key'
if ( if (
field in self.initial_data field in self.initial_data
@@ -205,7 +205,7 @@ class InvenTreeCustomStatusSerializerMixin:
setattr(self.instance, follower_field_name, self.initial_data[field]) setattr(self.instance, follower_field_name, self.initial_data[field])
# Mirror values from follower to leader # Mirror values from follower to leader
for field in self._custom_fields_follower: for field in self._custom_fields_follower or []:
leader_field_name = field.replace('_custom_key', '') leader_field_name = field.replace('_custom_key', '')
if field in validated_data and leader_field_name not in self.initial_data: if field in validated_data and leader_field_name not in self.initial_data:
try: try:
@@ -276,7 +276,7 @@ class InvenTreeCustomStatusSerializerMixin:
# Inherit choices from leader # Inherit choices from leader
self.gather_custom_fields() self.gather_custom_fields()
if field_name in self._custom_fields: if self._custom_fields and field_name in self._custom_fields:
leader_field_name = field_name.replace('_custom_key', '') leader_field_name = field_name.replace('_custom_key', '')
leader_field = self.fields[leader_field_name] leader_field = self.fields[leader_field_name]
if hasattr(leader_field, 'choices'): if hasattr(leader_field, 'choices'):

View File

@@ -152,7 +152,7 @@ class DataImportSession(models.Model):
return supported_models().get(self.model_type, None) return supported_models().get(self.model_type, None)
def get_related_model(self, field_name: str) -> models.Model: def get_related_model(self, field_name: str) -> Optional[models.Model]:
"""Return the related model for a given field name. """Return the related model for a given field name.
Arguments: Arguments:
@@ -699,7 +699,7 @@ class DataImportRow(models.Model):
if commit: if commit:
self.save() self.save()
def convert_date_field(self, value: str) -> str: def convert_date_field(self, value: str) -> Optional[str]:
"""Convert an incoming date field to the correct format for the database.""" """Convert an incoming date field to the correct format for the database."""
if value in [None, '']: if value in [None, '']:
return None return None

View File

@@ -111,7 +111,7 @@ class LabelPrinterBaseDriver(BaseDriver):
Returns: Returns:
A class instance of a DRF serializer class, by default this an instance of self.PrintingOptionsSerializer using the *args, **kwargs if existing for this driver A class instance of a DRF serializer class, by default this an instance of self.PrintingOptionsSerializer using the *args, **kwargs if existing for this driver
""" """
return self.PrintingOptionsSerializer(*args, **kwargs) # type: ignore return self.PrintingOptionsSerializer(*args, **kwargs)
# --- helper functions # --- helper functions
@property @property

View File

@@ -138,7 +138,7 @@ class MachineSettingSerializer(GenericReferencedSettingSerializer):
"""Custom init method to make the config_type field read only.""" """Custom init method to make the config_type field read only."""
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.Meta.read_only_fields = ['config_type'] # type: ignore self.Meta.read_only_fields = ['config_type']
class BaseMachineClassSerializer(serializers.Serializer): class BaseMachineClassSerializer(serializers.Serializer):

View File

@@ -232,10 +232,10 @@ class TestLabelPrinterMachineType(InvenTreeAPITestCase):
machine = self.create_machine() machine = self.create_machine()
# setup the label app # setup the label app
apps.get_app_config('report').create_default_labels() # type: ignore apps.get_app_config('report').create_default_labels()
plg_registry.reload_plugins() plg_registry.reload_plugins()
config = cast(PluginConfig, plg_registry.get_plugin(plugin_ref).plugin_config()) # type: ignore config = cast(PluginConfig, plg_registry.get_plugin(plugin_ref).plugin_config())
config.active = True config.active = True
config.save() config.save()

View File

@@ -1,7 +1,7 @@
"""Order model definitions.""" """Order model definitions."""
from decimal import Decimal from decimal import Decimal
from typing import Any, Optional from typing import Any, Optional, TypedDict
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@@ -179,7 +179,7 @@ class TotalPriceMixin(models.Model):
return total return total
class BaseOrderReportContext(report.mixins.BaseReportContext): class BaseOrderReportContext(report.mixins.BaseReportContext, TypedDict):
"""Base context for all order models. """Base context for all order models.
Attributes: Attributes:
@@ -199,7 +199,7 @@ class BaseOrderReportContext(report.mixins.BaseReportContext):
title: str title: str
class PurchaseOrderReportContext(report.mixins.BaseReportContext): class PurchaseOrderReportContext(report.mixins.BaseReportContext, TypedDict):
"""Context for the purchase order model. """Context for the purchase order model.
Attributes: Attributes:
@@ -221,7 +221,7 @@ class PurchaseOrderReportContext(report.mixins.BaseReportContext):
supplier: Optional[Company] supplier: Optional[Company]
class SalesOrderReportContext(report.mixins.BaseReportContext): class SalesOrderReportContext(report.mixins.BaseReportContext, TypedDict):
"""Context for the sales order model. """Context for the sales order model.
Attributes: Attributes:
@@ -243,7 +243,7 @@ class SalesOrderReportContext(report.mixins.BaseReportContext):
customer: Optional[Company] customer: Optional[Company]
class ReturnOrderReportContext(report.mixins.BaseReportContext): class ReturnOrderReportContext(report.mixins.BaseReportContext, TypedDict):
"""Context for the return order model. """Context for the return order model.
Attributes: Attributes:
@@ -570,7 +570,9 @@ class PurchaseOrder(TotalPriceMixin, Order):
def report_context(self) -> PurchaseOrderReportContext: def report_context(self) -> PurchaseOrderReportContext:
"""Return report context data for this PurchaseOrder.""" """Return report context data for this PurchaseOrder."""
return {**super().report_context(), 'supplier': self.supplier} return_ctx = super().report_context()
return_ctx.update({'supplier': self.supplier})
return return_ctx
def get_absolute_url(self) -> str: def get_absolute_url(self) -> str:
"""Get the 'web' URL for this order.""" """Get the 'web' URL for this order."""
@@ -1269,7 +1271,9 @@ class SalesOrder(TotalPriceMixin, Order):
def report_context(self) -> SalesOrderReportContext: def report_context(self) -> SalesOrderReportContext:
"""Generate report context data for this SalesOrder.""" """Generate report context data for this SalesOrder."""
return {**super().report_context(), 'customer': self.customer} return_ctx = super().report_context()
return_ctx.update({'customer': self.customer})
return return_ctx
def get_absolute_url(self) -> str: def get_absolute_url(self) -> str:
"""Get the 'web' URL for this order.""" """Get the 'web' URL for this order."""
@@ -2197,7 +2201,7 @@ class SalesOrderLineItem(OrderLineItem):
return self.shipped >= self.quantity return self.shipped >= self.quantity
class SalesOrderShipmentReportContext(report.mixins.BaseReportContext): class SalesOrderShipmentReportContext(report.mixins.BaseReportContext, TypedDict):
"""Context for the SalesOrderShipment model. """Context for the SalesOrderShipment model.
Attributes: Attributes:
@@ -2662,7 +2666,9 @@ class ReturnOrder(TotalPriceMixin, Order):
def report_context(self) -> ReturnOrderReportContext: def report_context(self) -> ReturnOrderReportContext:
"""Generate report context data for this ReturnOrder.""" """Generate report context data for this ReturnOrder."""
return {**super().report_context(), 'customer': self.customer} return_ctx = super().report_context()
return_ctx.update({'customer': self.customer})
return return_ctx
def get_absolute_url(self): def get_absolute_url(self):
"""Get the 'web' URL for this order.""" """Get the 'web' URL for this order."""

View File

@@ -10,7 +10,7 @@ import os
import re import re
from datetime import timedelta from datetime import timedelta
from decimal import Decimal, InvalidOperation from decimal import Decimal, InvalidOperation
from typing import cast from typing import TypedDict, cast
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
@@ -427,7 +427,7 @@ class PartCategoryParameterTemplate(InvenTree.models.InvenTreeMetadataModel):
) )
class PartReportContext(report.mixins.BaseReportContext): class PartReportContext(report.mixins.BaseReportContext, TypedDict):
"""Report context for the Part model. """Report context for the Part model.
Attributes: Attributes:
@@ -4216,7 +4216,7 @@ class BomItem(InvenTree.models.MetadataMixin, InvenTree.models.InvenTreeModel):
if n <= 0: if n <= 0:
return 0.0 return 0.0
return int(available_stock / n) return int(Decimal(available_stock) / n)
def get_required_quantity(self, build_quantity: float) -> float: def get_required_quantity(self, build_quantity: float) -> float:
"""Calculate the required part quantity, based on the supplied build_quantity. """Calculate the required part quantity, based on the supplied build_quantity.

View File

@@ -12,6 +12,7 @@ from opentelemetry import trace
import common.currency import common.currency
import common.notifications import common.notifications
import InvenTree.helpers
import InvenTree.helpers_model import InvenTree.helpers_model
from common.settings import get_global_setting from common.settings import get_global_setting
from InvenTree.tasks import ( from InvenTree.tasks import (

View File

@@ -50,8 +50,8 @@ class StaleStockNotificationTests(InvenTreeTestCase):
set_global_setting('STOCK_STALE_DAYS', 7, self.user) set_global_setting('STOCK_STALE_DAYS', 7, self.user)
# Clear notifications # Clear notifications
NotificationEntry.objects.all().delete() # type: ignore[attr-defined] NotificationEntry.objects.all().delete()
NotificationMessage.objects.all().delete() # type: ignore[attr-defined] NotificationMessage.objects.all().delete()
def create_stock_items_with_expiry(self): def create_stock_items_with_expiry(self):
"""Create stock items with various expiry dates for testing.""" """Create stock items with various expiry dates for testing."""
@@ -101,7 +101,7 @@ class StaleStockNotificationTests(InvenTreeTestCase):
part.tasks.notify_stale_stock(self.user, []) part.tasks.notify_stale_stock(self.user, [])
# No notifications should be created # No notifications should be created
self.assertEqual(NotificationMessage.objects.count(), 0) # type: ignore[attr-defined] self.assertEqual(NotificationMessage.objects.count(), 0)
def test_notify_stale_stock_single_item(self): def test_notify_stale_stock_single_item(self):
"""Test notify_stale_stock with a single stale item.""" """Test notify_stale_stock with a single stale item."""

View File

@@ -60,7 +60,7 @@ class BarcodeMixin:
"""Does this plugin support barcode generation.""" """Does this plugin support barcode generation."""
try: try:
# Attempt to call the generate method # Attempt to call the generate method
self.generate(None) # type: ignore self.generate(None)
except NotImplementedError: except NotImplementedError:
# If a NotImplementedError is raised, then barcode generation is not supported # If a NotImplementedError is raised, then barcode generation is not supported
return False return False

View File

@@ -161,7 +161,7 @@ class SupplierMixin(SettingsMixin, Generic[PartData]):
# assign parent_part to root_part if root_part has no variant of already # assign parent_part to root_part if root_part has no variant of already
if root_part and not root_part.is_template and not root_part.variant_of: if root_part and not root_part.is_template and not root_part.variant_of:
root_part.variant_of = parent_part # type: ignore root_part.variant_of = parent_part
root_part.save() root_part.save()
return parent_part return parent_part

View File

@@ -83,7 +83,7 @@ class UserInterfaceMixin:
def __init__(self): def __init__(self):
"""Register mixin.""" """Register mixin."""
super().__init__() super().__init__()
self.add_mixin(PluginMixinEnum.USER_INTERFACE, True, __class__) # type: ignore self.add_mixin(PluginMixinEnum.USER_INTERFACE, True, __class__)
def get_ui_features( def get_ui_features(
self, feature_type: FeatureType, context: dict, request: Request, **kwargs self, feature_type: FeatureType, context: dict, request: Request, **kwargs

View File

@@ -7,4 +7,4 @@ class BrokenFileIntegrationPlugin(InvenTreePlugin):
"""An very broken plugin.""" """An very broken plugin."""
aaa = bb # noqa: F821 # type: ignore[unresolved-reference] aaa = bb # noqa: F821

View File

@@ -192,7 +192,7 @@ def get_modules(pkg, path=None):
if sys.version_info < (3, 12): if sys.version_info < (3, 12):
module = finder.find_module(name).load_module(name) module = finder.find_module(name).load_module(name)
else: else:
spec = finder.find_spec(name) spec = finder.find_spec(name) # type: ignore[missing-argument]
module = module_from_spec(spec) module = module_from_spec(spec)
sys.modules[name] = module sys.modules[name] = module
spec.loader.exec_module(module) spec.loader.exec_module(module)

View File

@@ -3,7 +3,7 @@
import inspect import inspect
import warnings import warnings
from datetime import datetime from datetime import datetime
from distutils.sysconfig import get_python_lib # type: ignore[import] from distutils.sysconfig import get_python_lib # type: ignore[unresolved-import]
from importlib.metadata import PackageNotFoundError, metadata from importlib.metadata import PackageNotFoundError, metadata
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
@@ -92,16 +92,16 @@ class MetaBase:
TITLE = None TITLE = None
@mark_final @mark_final
def get_meta_value(self, key: str, old_key: Optional[str] = None, __default=None): def get_meta_value(self, key: str, old_key: Optional[str] = None, default=None):
"""Reference a meta item with a key. """Reference a meta item with a key.
Args: Args:
key (str): key for the value key (str): key for the value
old_key (str, optional): deprecated key - will throw warning old_key (str, optional): deprecated key - will throw warning
__default (optional): Value if nothing with key can be found. Defaults to None. default (optional): Value if nothing with key can be found. Defaults to None.
Returns: Returns:
Value referenced with key, old_key or __default if set and not value found Value referenced with key, old_key or default if set and not value found
""" """
value = getattr(self, key, None) value = getattr(self, key, None)
@@ -117,9 +117,9 @@ class MetaBase:
stacklevel=2, stacklevel=2,
) )
# Use __default if still nothing set # Use default if still nothing set
if (value is None) and __default: if value is None and default is not None:
return __default return default
return value return value
@mark_final @mark_final

View File

@@ -781,9 +781,9 @@ class PluginsRegistry:
f"Plugin '{p}' is not compatible with the current InvenTree version {v}" f"Plugin '{p}' is not compatible with the current InvenTree version {v}"
) )
if v := plg_i.MIN_VERSION: if v := plg_i.MIN_VERSION:
_msg += _(f'Plugin requires at least version {v}') # type: ignore[unsupported-operator] _msg += _(f'Plugin requires at least version {v}') # ty:ignore[unsupported-operator]
if v := plg_i.MAX_VERSION: if v := plg_i.MAX_VERSION:
_msg += _(f'Plugin requires at most version {v}') # type: ignore[unsupported-operator] _msg += _(f'Plugin requires at most version {v}') # ty:ignore[unsupported-operator]
# Log to error stack # Log to error stack
log_registry_error(_msg, reference=f'{p}:init_plugin') log_registry_error(_msg, reference=f'{p}:init_plugin')
else: else:

View File

@@ -152,7 +152,7 @@ class SampleSupplierPlugin(SupplierMixin, InvenTreePlugin):
# after the template part was created, we need to refresh the part from the db because its tree id may have changed # after the template part was created, we need to refresh the part from the db because its tree id may have changed
# which results in an error if saved directly # which results in an error if saved directly
part.refresh_from_db() part.refresh_from_db()
part.variant_of = parent_part # type: ignore part.variant_of = parent_part
part.save() part.save()
return part return part

View File

@@ -254,7 +254,7 @@ class RegistryTests(TestQueryMixin, PluginRegistryMixin, TestCase):
def test_folder_loading(self): def test_folder_loading(self):
"""Test that plugins in folders outside of BASE_DIR get loaded.""" """Test that plugins in folders outside of BASE_DIR get loaded."""
# Run in temporary directory -> always a new random name # Run in temporary directory -> always a new random name
with tempfile.TemporaryDirectory() as tmp: # type: ignore[no-matching-overload] with tempfile.TemporaryDirectory() as tmp:
# Fill directory with sample data # Fill directory with sample data
new_dir = Path(tmp).joinpath('mock') new_dir = Path(tmp).joinpath('mock')
shutil.copytree(self.mockDir(), new_dir) shutil.copytree(self.mockDir(), new_dir)

View File

@@ -704,7 +704,7 @@ class LabelTemplate(TemplateUploadMixin, ReportTemplateBase):
def get_context(self, instance, request=None, **kwargs): def get_context(self, instance, request=None, **kwargs):
"""Supply context data to the label template for rendering.""" """Supply context data to the label template for rendering."""
base_context = super().get_context(instance, request, **kwargs) base_context = super().get_context(instance, request, **kwargs)
label_context: LabelContextExtension = { # type: ignore[invalid-assignment] label_context: LabelContextExtension = {
'width': self.width, 'width': self.width,
'height': self.height, 'height': self.height,
'page_style': None, 'page_style': None,

View File

@@ -710,25 +710,24 @@ tomli==2.4.0 \
--hash=sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa \ --hash=sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa \
--hash=sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087 --hash=sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087
# via coverage # via coverage
ty==0.0.1a21 \ ty==0.0.21 \
--hash=sha256:0efba2e52b58f536f4198ba5c4a36cac2ba67d83ec6f429ebc7704233bcda4c3 \ --hash=sha256:210e7568c9f886c4d01308d751949ee714ad7ad9d7d928d2ba90d329dd880367 \
--hash=sha256:1474d883129bb63da3b2380fc7ead824cd3baf6a9551e6aa476ffefc58057af3 \ --hash=sha256:53508e345b11569f78b21ba8e2b4e61df38a9754947fb3cd9f2ef574367338fb \
--hash=sha256:1f276ceab23a1410aec09508248c76ae0989c67fb7a0c287e0d4564994295531 \ --hash=sha256:553e43571f4a35604c36cfd07d8b61a5eb7a714e3c67f8c4ff2cf674fefbaef9 \
--hash=sha256:218d53e7919e885bd98e9196d9cb952d82178b299aa36da6f7f39333eb7400ed \ --hash=sha256:56b01fd2519637a4ca88344f61c96225f540c98ff18bca321d4eaa7bb0f7aa2f \
--hash=sha256:21f708d02b6588323ffdbfdba38830dd0ecfd626db50aa6006b296b5470e52f9 \ --hash=sha256:56d3b198b64dd0a19b2b66e257deaed2ecea568e722ae5352f3c6fb62027f89d \
--hash=sha256:334d2a212ebf42a0e55d57561926af7679fe1e878175e11dcb81ad8df892844e \ --hash=sha256:62f7f5b235c4f7876db305c36997aea07b7af29b1a068f373d0e2547e25f32ff \
--hash=sha256:3c3bc66fcae41eff133cfe326dd65d82567a2fb5d4efe2128773b10ec2766819 \ --hash=sha256:666f6822e3b9200abfa7e95eb0ddd576460adb8d66b550c0ad2c70abc84a2048 \
--hash=sha256:5dfc73299d441cc6454e36ed0a976877415024143dfca6592dc36f7701424383 \ --hash=sha256:7bdf2f572378de78e1f388d24691c89db51b7caf07cf90f2bfcc1d6b18b70a76 \
--hash=sha256:7505aeb8bf2a62f00f12cfa496f6c965074d75c8126268776565284c8a12d5dd \ --hash=sha256:7e9613994610431ab8625025bd2880dbcb77c5c9fabdd21134cda12d840a529d \
--hash=sha256:84243455f295ed850bd53f7089819321807d4e6ee3b1cbff6086137ae0259466 \ --hash=sha256:a0854d008347ce4a5fb351af132f660a390ab2a1163444d075251d43e6f74b9b \
--hash=sha256:87a200c21e02962e8a27374d9d152582331d57d709672431be58f4f898bf6cad \ --hash=sha256:a4c2ba5d67d64df8fcdefd8b280ac1149d24a73dbda82fa953a0dff9d21400ed \
--hash=sha256:9463cac96b8f1bb5ba740fe1d42cd6bd152b43c5b159b2f07f8fd629bcdded34 \ --hash=sha256:a709d576e5bea84b745d43058d8b9cd4f27f74a0b24acb4b0cbb7d3d41e0d050 \
--hash=sha256:a8c769987d00fbc33054ff7e342633f475ea10dc43bc60fb9fb056159d48cb90 \ --hash=sha256:bef3ab4c7b966bcc276a8ac6c11b63ba222d21355b48d471ea782c4104eee4e0 \
--hash=sha256:ba13d03b9e095216ceb4e4d554a308517f28ab0a6e4dcd07cfe94563e4c2c489 \ --hash=sha256:d23d2c34f7a77d974bb08f0860ef700addc8a683d81a0319f71c08f87506cfd0 \
--hash=sha256:be8f457d7841b7ead2a3f6b65ba668abc172a1150a0f1f6c0958af3725dbb61a \ --hash=sha256:e9de7e11c63c6afc40f3e9ba716374add171aee7fabc70b5146a510705c6d41b \
--hash=sha256:cc0880ec344fbdf736b05d8d0da01f0caaaa02409bd9a24b68d18d0127a79b0e \ --hash=sha256:ee8399f7c453a425291e6688efe430cfae7ab0ac4ffd50eba9f872bf878b54f6 \
--hash=sha256:e941e9a9d1e54b03eeaf9c3197c26a19cf76009fd5e41e16e5657c1c827bd6d3 \ --hash=sha256:f72047996598ac20553fb7e21ba5741e3c82dee4e9eadf10d954551a5fe09391
--hash=sha256:ecf41706b803827b0de8717f32a434dad1e67be9f4b8caf403e12013179ea06a
# via -r src/backend/requirements-dev.in # via -r src/backend/requirements-dev.in
types-psycopg2==2.9.21.20260223 \ types-psycopg2==2.9.21.20260223 \
--hash=sha256:78ed70de2e56bc6b5c26c8c1da8e9af54e49fdc3c94d1504609f3519e2b84f02 \ --hash=sha256:78ed70de2e56bc6b5c26c8c1da8e9af54e49fdc3c94d1504609f3519e2b84f02 \