2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-06-16 05:14:21 +00:00

Merge branch 'master' into generic-parameters

This commit is contained in:
Oliver
2025-11-20 22:32:29 +11:00
committed by GitHub
120 changed files with 81562 additions and 73215 deletions
+3 -2
View File
@@ -11,7 +11,7 @@
"program": "${workspaceFolder}/src/backend/InvenTree/manage.py", "program": "${workspaceFolder}/src/backend/InvenTree/manage.py",
"args": [ "args": [
"runserver", "runserver",
// "0.0.0.0:8000", // expose server in network (useful for testing with mobile app) "0.0.0.0:8000", // expose server in network (useful for testing with mobile app)
// "--noreload" // disable auto-reload // "--noreload" // disable auto-reload
], ],
"django": true, "django": true,
@@ -35,7 +35,8 @@
"request": "launch", "request": "launch",
"program": "${workspaceFolder}/src/backend/InvenTree/manage.py", "program": "${workspaceFolder}/src/backend/InvenTree/manage.py",
"args": [ "args": [
"runserver" "runserver",
"0.0.0.0:8000"
], ],
"django": true, "django": true,
"justMyCode": false "justMyCode": false
+6 -6
View File
@@ -330,13 +330,13 @@ mkdocs-include-markdown-plugin==7.2.0 \
--hash=sha256:4a67a91ade680dc0e15f608e5b6343bec03372ffa112c40a4254c1bfb10f42f3 \ --hash=sha256:4a67a91ade680dc0e15f608e5b6343bec03372ffa112c40a4254c1bfb10f42f3 \
--hash=sha256:d56cdaeb2d113fb66ed0fe4fb7af1da889926b0b9872032be24e19bbb09c9f5b --hash=sha256:d56cdaeb2d113fb66ed0fe4fb7af1da889926b0b9872032be24e19bbb09c9f5b
# via -r docs/requirements.in # via -r docs/requirements.in
mkdocs-macros-plugin==1.4.1 \ mkdocs-macros-plugin==1.5.0 \
--hash=sha256:55a9c93871e3744cdeb0736316783d60830a6d5d97b1132364e6b491607f2332 \ --hash=sha256:12aa45ce7ecb7a445c66b9f649f3dd05e9b92e8af6bc65e4acd91d26f878c01f \
--hash=sha256:5a9e483f6056fe7ad0923802affe699233ca468672e20a9640dba237165b3240 --hash=sha256:c10fabd812bf50f9170609d0ed518e54f1f0e12c334ac29141723a83c881dd6f
# via -r docs/requirements.in # via -r docs/requirements.in
mkdocs-material==9.6.22 \ mkdocs-material==9.7.0 \
--hash=sha256:14ac5f72d38898b2f98ac75a5531aaca9366eaa427b0f49fc2ecf04d99b7ad84 \ --hash=sha256:602b359844e906ee402b7ed9640340cf8a474420d02d8891451733b6b02314ec \
--hash=sha256:87c158b0642e1ada6da0cbd798a3389b0bc5516b90e5ece4a0fb939f00bacd1c --hash=sha256:da2866ea53601125ff5baa8aa06404c6e07af3c5ce3d5de95e3b52b80b442887
# via -r docs/requirements.in # via -r docs/requirements.in
mkdocs-material-extensions==1.3.1 \ mkdocs-material-extensions==1.3.1 \
--hash=sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443 \ --hash=sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443 \
+13 -1
View File
@@ -7,6 +7,7 @@ import os
import random import random
import shutil import shutil
import string import string
import sys
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
@@ -195,7 +196,13 @@ def load_config_data(set_cache: bool = False) -> map | None:
cfg_file = get_config_file() cfg_file = get_config_file()
with open(cfg_file, encoding='utf-8') as cfg: with open(cfg_file, encoding='utf-8') as cfg:
data = yaml.safe_load(cfg) try:
data = yaml.safe_load(cfg)
except yaml.parser.ParserError as error:
logger.error(
"Error reading InvenTree configuration file '%s': %s", cfg_file, error
)
sys.exit(1)
# Set the cache if requested # Set the cache if requested
if set_cache: if set_cache:
@@ -223,6 +230,11 @@ def do_typecast(value, type, var_name=None):
elif type is dict: elif type is dict:
value = to_dict(value) value = to_dict(value)
# Special handling for boolean typecasting
elif type is bool:
val = is_true(value)
return val
elif type is not None: elif type is not None:
# Try to typecast the value # Try to typecast the value
try: try:
+60 -4
View File
@@ -2,30 +2,76 @@
import logging import logging
import re import re
from hashlib import md5
from typing import Optional from typing import Optional
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
import pint import pint
_unit_registry = None
import structlog import structlog
from common.settings import get_global_setting, set_global_setting
from InvenTree.cache import get_session_cache, set_session_cache
_UNIT_REG_CACHE_KEY = 'unit_registry_hash'
_unit_registry = None
_unit_registry_hash: str = ''
logger = structlog.get_logger('inventree') logger = structlog.get_logger('inventree')
# Disable log output for Pint library # Disable log output for Pint library
logging.getLogger('pint').setLevel(logging.ERROR) logging.getLogger('pint').setLevel(logging.ERROR)
def get_unit_registry_hash():
"""Return a hash representing the current state of the unit registry.
We use this to determine if we need to reload the unit registry,
due to changes in the database.
"""
# Look in the session cache first (faster, and potentially newer)
registry_hash = get_session_cache(_UNIT_REG_CACHE_KEY)
if registry_hash is None:
registry_hash = get_global_setting(
'_UNIT_REGISTRY_HASH', create=False, backup_value=''
)
if registry_hash:
set_session_cache(_UNIT_REG_CACHE_KEY, registry_hash)
return registry_hash
def set_unit_registry_hash(registry_hash: str):
"""Save the hash representing the current state of the unit registry.
Because most of the registry is static, we only need to consider the
CustomUnit entries in the database.
"""
global _unit_registry_hash
_unit_registry_hash = registry_hash
# Save to both the global settings and the session cache
set_global_setting('_UNIT_REGISTRY_HASH', registry_hash)
set_session_cache(_UNIT_REG_CACHE_KEY, registry_hash)
def get_unit_registry(): def get_unit_registry():
"""Return a custom instance of the Pint UnitRegistry.""" """Return a custom instance of the Pint UnitRegistry."""
global _unit_registry global _unit_registry
global _unit_registry_hash
# Cache the unit registry for speedier access # Cache the unit registry for speedier access
if _unit_registry is None: if _unit_registry is None:
return reload_unit_registry() return reload_unit_registry()
# Check if the unit registry has changed
if _unit_registry_hash != get_unit_registry_hash():
logger.info('Unit registry hash has changed, reloading unit registry')
return reload_unit_registry()
return _unit_registry return _unit_registry
@@ -60,9 +106,16 @@ def reload_unit_registry():
try: try:
from common.models import CustomUnit from common.models import CustomUnit
# Calculate a hash of all custom units
hash_md5 = md5()
for cu in CustomUnit.objects.all(): for cu in CustomUnit.objects.all():
try: try:
reg.define(cu.fmt_string()) fmt = cu.fmt_string()
reg.define(fmt)
hash_md5.update(fmt.encode('utf-8'))
except Exception as e: except Exception as e:
logger.exception( logger.exception(
'Failed to load custom unit: %s - %s', cu.fmt_string(), e 'Failed to load custom unit: %s - %s', cu.fmt_string(), e
@@ -71,6 +124,9 @@ def reload_unit_registry():
# Once custom units are loaded, save registry # Once custom units are loaded, save registry
_unit_registry = reg _unit_registry = reg
# Update the unit registry hash
set_unit_registry_hash(hash_md5.hexdigest())
except Exception: except Exception:
# Database is not ready, or CustomUnit model is not available # Database is not ready, or CustomUnit model is not available
pass pass
+8 -4
View File
@@ -16,7 +16,7 @@ from django.conf import settings
from django.contrib.staticfiles.storage import StaticFilesStorage from django.contrib.staticfiles.storage import StaticFilesStorage
from django.core.exceptions import FieldError, ValidationError from django.core.exceptions import FieldError, ValidationError
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
from django.db.models.fields.files import ImageFieldFile from django.db.models.fields.files import FieldFile, ImageFieldFile
from django.http import StreamingHttpResponse from django.http import StreamingHttpResponse
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@@ -176,10 +176,14 @@ def constructPathString(path: list[str], max_chars: int = 250) -> str:
return pathstring return pathstring
def getMediaUrl(file: StdImageFieldFile | ImageFieldFile, name: str | None = None): def getMediaUrl(
file: FieldFile | ImageFieldFile | StdImageFieldFile, name: str | None = None
):
"""Return the qualified access path for the given file, under the media directory.""" """Return the qualified access path for the given file, under the media directory."""
if not isinstance(file, StdImageFieldFile): if not isinstance(file, (FieldFile, ImageFieldFile, StdImageFieldFile)):
raise ValueError('file_obj must be an instance of StdImageFieldFile') raise TypeError(
'file must be one of FileField, ImageFileField, StdImageFieldFile'
)
if name is not None: if name is not None:
file = regenerate_imagefile(file, name) file = regenerate_imagefile(file, name)
if settings.STORAGE_TARGET == StorageBackends.S3: if settings.STORAGE_TARGET == StorageBackends.S3:
+1 -1
View File
@@ -702,7 +702,7 @@ class TestHelpers(TestCase):
def testMediaUrl(self): def testMediaUrl(self):
"""Test getMediaUrl.""" """Test getMediaUrl."""
# Str should not work # Str should not work
with self.assertRaises(ValueError): with self.assertRaises(TypeError):
helpers.getMediaUrl('xx/yy.png') # type: ignore helpers.getMediaUrl('xx/yy.png') # type: ignore
# Correct usage # Correct usage
+17 -15
View File
@@ -39,6 +39,7 @@ from InvenTree.serializers import (
NotesFieldMixin, NotesFieldMixin,
enable_filter, enable_filter,
) )
from InvenTree.tasks import offload_task
from stock.generators import generate_batch_code from stock.generators import generate_batch_code
from stock.models import StockItem, StockLocation from stock.models import StockItem, StockLocation
from stock.serializers import ( from stock.serializers import (
@@ -51,6 +52,7 @@ from users.serializers import OwnerSerializer, UserSerializer
from .models import Build, BuildItem, BuildLine from .models import Build, BuildItem, BuildLine
from .status_codes import BuildStatus from .status_codes import BuildStatus
from .tasks import consume_build_item, consume_build_line
class BuildSerializer( class BuildSerializer(
@@ -1845,12 +1847,14 @@ class BuildConsumeSerializer(serializers.Serializer):
return data return data
@transaction.atomic
def save(self): def save(self):
"""Perform the stock consumption step.""" """Perform the stock consumption step."""
data = self.validated_data data = self.validated_data
request = self.context.get('request') request = self.context.get('request')
notes = data.get('notes', '') notes = data.get('notes', '')
# We may be passed either a list of BuildItem or BuildLine instances
items = data.get('items', []) items = data.get('items', [])
lines = data.get('lines', []) lines = data.get('lines', [])
@@ -1865,25 +1869,23 @@ class BuildConsumeSerializer(serializers.Serializer):
# Instead, it gets consumed when the output is completed # Instead, it gets consumed when the output is completed
continue continue
build_item.complete_allocation( # Offload a background task to consume this BuildItem
quantity=quantity, offload_task(
consume_build_item,
build_item.pk,
quantity,
notes=notes, notes=notes,
user=request.user if request else None, user_id=request.user.pk if request else None,
) )
# Process the provided BuildLine objects # Process the provided BuildLine objects
for line in lines: for line in lines:
build_line = line['build_line'] build_line = line['build_line']
# In this case, perform full consumption of all allocated stock # Offload a background task to consume this BuildLine
for item in build_line.allocations.all(): offload_task(
# If the build item is tracked into an output, we do not consume now consume_build_line,
# Instead, it gets consumed when the output is completed build_line.pk,
if item.install_into: notes=notes,
continue user_id=request.user.pk if request else None,
)
item.complete_allocation(
quantity=item.quantity,
notes=notes,
user=request.user if request else None,
)
+45
View File
@@ -39,6 +39,51 @@ def auto_allocate_build(build_id: int, **kwargs):
build_order.auto_allocate_stock(**kwargs) build_order.auto_allocate_stock(**kwargs)
@tracer.start_as_current_span('consume_build_item')
def consume_build_item(
item_id: str, quantity, notes: str = '', user_id: int | None = None
):
"""Consume stock against a particular BuildOrderLineItem allocation."""
from build.models import BuildItem
item = BuildItem.objects.filter(pk=item_id).first()
if not item:
logger.warning(
'Could not consume stock for BuildItem <%s> - BuildItem does not exist',
item_id,
)
return
item.complete_allocation(
quantity=quantity,
notes=notes,
user=User.objects.filter(pk=user_id).first() if user_id else None,
)
@tracer.start_as_current_span('consume_build_line')
def consume_build_line(line_id: int, notes: str = '', user_id: int | None = None):
"""Consume stock against a particular BuildOrderLineItem."""
from build.models import BuildLine
line_item = BuildLine.objects.filter(pk=line_id).first()
if not line_item:
logger.warning(
'Could not consume stock for LineItem <%s> - LineItem does not exist',
line_id,
)
return
for item in line_item.allocations.all():
item.complete_allocation(
quantity=item.quantity,
notes=notes,
user=User.objects.filter(pk=user_id).first() if user_id else None,
)
@tracer.start_as_current_span('complete_build_allocations') @tracer.start_as_current_span('complete_build_allocations')
def complete_build_allocations(build_id: int, user_id: int): def complete_build_allocations(build_id: int, user_id: int):
"""Complete build allocations for a specified BuildOrder.""" """Complete build allocations for a specified BuildOrder."""
+1 -1
View File
@@ -2039,7 +2039,7 @@ class Attachment(InvenTree.models.MetadataMixin, InvenTree.models.InvenTreeModel
if self.attachment: if self.attachment:
import InvenTree.helpers_model import InvenTree.helpers_model
media_url = InvenTree.helpers.getMediaUrl(self.attachment.url) media_url = InvenTree.helpers.getMediaUrl(self.attachment)
return InvenTree.helpers_model.construct_absolute_url(media_url) return InvenTree.helpers_model.construct_absolute_url(media_url)
return '' return ''
+15
View File
@@ -202,6 +202,21 @@ class AttachmentTest(InvenTreeAPITestCase):
self.assignRole('part.delete') self.assignRole('part.delete')
self.delete(url, expected_code=204) self.delete(url, expected_code=204)
def test_fully_qualified_url(self):
"""Test that the fully qualified URL is returned correctly."""
part = Part.objects.first()
attachment = Attachment.objects.create(
attachment=self.generate_file('test.txt'),
comment='Testing filename: test.txt',
model_type='part',
model_id=part.pk,
)
url = attachment.fully_qualified_url()
self.assertIs(type(url), str)
self.assertIn(f'/media/attachments/part/{part.pk}/test', url)
class SettingsTest(InvenTreeTestCase): class SettingsTest(InvenTreeTestCase):
"""Tests for the 'settings' model.""" """Tests for the 'settings' model."""
+13 -3
View File
@@ -309,13 +309,23 @@ class StatusCodeMixin:
return status is not None and status == self.get_custom_status() return status is not None and status == self.get_custom_status()
def set_status(self, status: int) -> bool: def set_status(self, status: int, custom_values=None) -> bool:
"""Set the status code for this object.""" """Set the status code for this object.
Arguments:
status: The status code to set
custom_values: Optional list of custom values to consider (can be used to avoid DB queries)
"""
if not self.status_class: if not self.status_class:
raise NotImplementedError('Status class not defined') raise NotImplementedError('Status class not defined')
base_values = self.status_class.values() base_values = self.status_class.values()
custom_value_set = self.status_class.custom_values()
custom_value_set = (
self.status_class.custom_values()
if custom_values is None
else custom_values
)
custom_field = f'{self.STATUS_FIELD}_custom_key' custom_field = f'{self.STATUS_FIELD}_custom_key'
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
+43 -18
View File
@@ -976,6 +976,9 @@ class PurchaseOrder(TotalPriceMixin, Order):
# Prefetch line item objects for DB efficiency # Prefetch line item objects for DB efficiency
line_items_ids = [item['line_item'].pk for item in items] line_items_ids = [item['line_item'].pk for item in items]
# Cache the custom status options for the StockItem model
custom_stock_status_values = stock.models.StockItem.STATUS_CLASS.custom_values()
line_items = PurchaseOrderLineItem.objects.filter( line_items = PurchaseOrderLineItem.objects.filter(
pk__in=line_items_ids pk__in=line_items_ids
).prefetch_related('part', 'part__part', 'order') ).prefetch_related('part', 'part__part', 'order')
@@ -1050,7 +1053,6 @@ class PurchaseOrder(TotalPriceMixin, Order):
'supplier_part': supplier_part, 'supplier_part': supplier_part,
'purchase_order': self, 'purchase_order': self,
'purchase_price': purchase_price, 'purchase_price': purchase_price,
'status': item.get('status', StockStatus.OK.value),
'location': stock_location, 'location': stock_location,
'quantity': 1 if serialize else stock_quantity, 'quantity': 1 if serialize else stock_quantity,
'batch': item.get('batch_code', ''), 'batch': item.get('batch_code', ''),
@@ -1059,6 +1061,9 @@ class PurchaseOrder(TotalPriceMixin, Order):
'packaging': item.get('packaging') or supplier_part.packaging, 'packaging': item.get('packaging') or supplier_part.packaging,
} }
# Extract the "status" field
status = item.get('status', StockStatus.OK.value)
# Check linked build order # Check linked build order
# This is for receiving against an *external* build order # This is for receiving against an *external* build order
if build_order := line.build_order: if build_order := line.build_order:
@@ -1099,11 +1104,14 @@ class PurchaseOrder(TotalPriceMixin, Order):
# Now, create the new stock items # Now, create the new stock items
if serialize: if serialize:
stock_items.extend( new_items = stock.models.StockItem._create_serial_numbers(
stock.models.StockItem._create_serial_numbers( serials=serials, **stock_data
serials=serials, **stock_data
)
) )
for item in new_items:
item.set_status(status, custom_values=custom_stock_status_values)
stock_items.append(item)
else: else:
new_item = stock.models.StockItem( new_item = stock.models.StockItem(
**stock_data, **stock_data,
@@ -1115,10 +1123,11 @@ class PurchaseOrder(TotalPriceMixin, Order):
rght=2, rght=2,
) )
new_item.set_status(status, custom_values=custom_stock_status_values)
if barcode: if barcode:
new_item.assign_barcode(barcode_data=barcode, save=False) new_item.assign_barcode(barcode_data=barcode, save=False)
# new_item.save()
bulk_create_items.append(new_item) bulk_create_items.append(new_item)
# Update the line item quantity # Update the line item quantity
@@ -1379,8 +1388,13 @@ class SalesOrder(TotalPriceMixin, Order):
return any(line.is_overallocated() for line in self.lines.all()) return any(line.is_overallocated() for line in self.lines.all())
def is_completed(self) -> bool: def is_completed(self) -> bool:
"""Check if this order is "shipped" (all line items delivered).""" """Check if this order is "shipped" (all line items delivered).
return all(line.is_completed() for line in self.lines.all())
Note: Any "virtual" parts are ignored in this calculation.
"""
lines = self.lines.all().filter(part__virtual=False)
return all(line.is_completed() for line in lines)
def can_complete( def can_complete(
self, raise_error: bool = False, allow_incomplete_lines: bool = False self, raise_error: bool = False, allow_incomplete_lines: bool = False
@@ -1415,10 +1429,15 @@ class SalesOrder(TotalPriceMixin, Order):
_('Order cannot be completed as there are incomplete allocations') _('Order cannot be completed as there are incomplete allocations')
) )
if not allow_incomplete_lines and self.pending_line_count > 0: if not allow_incomplete_lines:
raise ValidationError( pending_lines = self.pending_line_items().exclude(part__virtual=True)
_('Order cannot be completed as there are incomplete line items')
) if pending_lines.count() > 0:
raise ValidationError(
_(
'Order cannot be completed as there are incomplete line items'
)
)
except ValidationError as e: except ValidationError as e:
if raise_error: if raise_error:
@@ -1475,6 +1494,7 @@ class SalesOrder(TotalPriceMixin, Order):
trigger_event(SalesOrderEvents.HOLD, id=self.pk) trigger_event(SalesOrderEvents.HOLD, id=self.pk)
@transaction.atomic
def _action_complete(self, *args, **kwargs): def _action_complete(self, *args, **kwargs):
"""Mark this order as "complete.""" """Mark this order as "complete."""
user = kwargs.pop('user', None) user = kwargs.pop('user', None)
@@ -1486,6 +1506,16 @@ class SalesOrder(TotalPriceMixin, Order):
get_global_setting('SALESORDER_SHIP_COMPLETE') get_global_setting('SALESORDER_SHIP_COMPLETE')
) )
# Update line items
for line in self.lines.all():
# Mark any "virtual" parts as shipped at this point
if line.part and line.part.virtual and line.shipped != line.quantity:
line.shipped = line.quantity
line.save()
if line.part:
line.part.schedule_pricing_update(create=True)
if bypass_shipped or self.status == SalesOrderStatus.SHIPPED: if bypass_shipped or self.status == SalesOrderStatus.SHIPPED:
self.status = SalesOrderStatus.COMPLETE.value self.status = SalesOrderStatus.COMPLETE.value
else: else:
@@ -1497,11 +1527,6 @@ class SalesOrder(TotalPriceMixin, Order):
self.save() self.save()
# Schedule pricing update for any referenced parts
for line in self.lines.all():
if line.part:
line.part.schedule_pricing_update(create=True)
trigger_event(SalesOrderEvents.COMPLETED, id=self.pk) trigger_event(SalesOrderEvents.COMPLETED, id=self.pk)
return True return True
@@ -1565,7 +1590,7 @@ class SalesOrder(TotalPriceMixin, Order):
"""Attempt to transition to COMPLETED status.""" """Attempt to transition to COMPLETED status."""
return self.handle_transition( return self.handle_transition(
self.status, self.status,
SalesOrderStatus.COMPLETED.value, SalesOrderStatus.COMPLETE.value,
self, self,
self._action_complete, self._action_complete,
user=user, user=user,
+9 -1
View File
@@ -1226,6 +1226,8 @@ class PurchaseOrderReceiveTest(OrderTest):
def test_receive_large_quantity(self): def test_receive_large_quantity(self):
"""Test receipt of a large number of items.""" """Test receipt of a large number of items."""
from stock.status_codes import StockStatus
sp = SupplierPart.objects.first() sp = SupplierPart.objects.first()
# Create a new order # Create a new order
@@ -1256,7 +1258,12 @@ class PurchaseOrderReceiveTest(OrderTest):
url, url,
{ {
'items': [ 'items': [
{'line_item': line.pk, 'quantity': line.quantity} for line in lines {
'line_item': line.pk,
'quantity': line.quantity,
'status': StockStatus.QUARANTINED.value,
}
for line in lines
], ],
'location': location.pk, 'location': location.pk,
}, },
@@ -1269,6 +1276,7 @@ class PurchaseOrderReceiveTest(OrderTest):
for item in response: for item in response:
self.assertEqual(item['purchase_order'], po.pk) self.assertEqual(item['purchase_order'], po.pk)
self.assertEqual(item['status'], StockStatus.QUARANTINED)
# Check that the order has been completed # Check that the order has been completed
po.refresh_from_db() po.refresh_from_db()
@@ -488,3 +488,45 @@ class SalesOrderTest(InvenTreeTestCase):
p.set_metadata(k, k) p.set_metadata(k, k)
self.assertEqual(len(p.metadata.keys()), 4) self.assertEqual(len(p.metadata.keys()), 4)
def test_virtual_parts(self):
"""Test shipment of virtual parts against an order."""
vp = Part.objects.create(
name='Virtual Part',
salable=True,
virtual=True,
description='A virtual part that I sell',
)
so = SalesOrder.objects.create(
customer=self.customer,
reference='SO-VIRTUAL-1',
customer_reference='VIRT-001',
)
for qty in [5, 10, 15]:
SalesOrderLineItem.objects.create(order=so, part=vp, quantity=qty)
# Delete pending shipments (if any)
so.shipments.all().delete()
for line in so.lines.all():
self.assertEqual(line.part.virtual, True)
self.assertEqual(line.shipped, 0)
self.assertGreater(line.quantity, 0)
self.assertTrue(line.is_fully_allocated())
self.assertTrue(line.is_completed())
# Complete the order
so.ship_order(None)
so.refresh_from_db()
self.assertEqual(so.status, status.SalesOrderStatus.SHIPPED)
so.complete_order(None)
so.refresh_from_db()
self.assertEqual(so.status, status.SalesOrderStatus.COMPLETE)
# Ensure that virtual line item quantity values have been updated
for line in so.lines.all():
self.assertEqual(line.shipped, line.quantity)
+1 -1
View File
@@ -827,7 +827,7 @@ class StockItem(
super().save(*args, **kwargs) super().save(*args, **kwargs)
# If user information is provided, and no existing note exists, create one! # If user information is provided, and no existing note exists, create one!
if user and add_note and self.tracking_info.count() == 0: if add_note and self.tracking_info.count() == 0:
tracking_info = {'status': self.status} tracking_info = {'status': self.status}
self.add_tracking_entry( self.add_tracking_entry(
+10 -3
View File
@@ -1060,12 +1060,19 @@ class StockChangeStatusSerializer(serializers.Serializer):
# Instead of performing database updates for each item, # Instead of performing database updates for each item,
# perform bulk database updates (much more efficient) # perform bulk database updates (much more efficient)
# Pre-cache the custom status values (to reduce DB hits)
custom_status_codes = StockItem.STATUS_CLASS.custom_values()
for item in items: for item in items:
# Ignore items which are already in the desired status # Ignore items which are already in the desired status
if item.compare_status(status):
continue
item.set_status(status) # Careful check for custom status codes also
if item.compare_status(status):
custom_status = item.get_custom_status()
if status == custom_status or custom_status is None:
continue
item.set_status(status, custom_values=custom_status_codes)
item.save(add_note=False) item.save(add_note=False)
# Create a new transaction note for each item # Create a new transaction note for each item
+10 -4
View File
@@ -1701,12 +1701,18 @@ class StockItemTest(StockAPITestCase):
prt = Part.objects.first() prt = Part.objects.first()
# Number of items to create
N_ITEMS = 10
# Create a bunch of items # Create a bunch of items
items = [StockItem.objects.create(part=prt, quantity=10) for _ in range(10)] items = [
StockItem.objects.create(part=prt, quantity=10) for _ in range(N_ITEMS)
]
for item in items: for item in items:
item.refresh_from_db() item.refresh_from_db()
self.assertEqual(item.status, StockStatus.OK.value) self.assertEqual(item.status, StockStatus.OK.value)
self.assertEqual(item.tracking_info.count(), 1)
data = { data = {
'items': [item.pk for item in items], 'items': [item.pk for item in items],
@@ -1719,10 +1725,10 @@ class StockItemTest(StockAPITestCase):
for item in items: for item in items:
item.refresh_from_db() item.refresh_from_db()
self.assertEqual(item.status, StockStatus.DAMAGED.value) self.assertEqual(item.status, StockStatus.DAMAGED.value)
self.assertEqual(item.tracking_info.count(), 1) self.assertEqual(item.tracking_info.count(), 2)
# Same test, but with one item unchanged # Same test, but with one item unchanged
items[0].status = StockStatus.ATTENTION.value items[0].set_status(StockStatus.ATTENTION.value)
items[0].save() items[0].save()
data['status'] = StockStatus.ATTENTION.value data['status'] = StockStatus.ATTENTION.value
@@ -1732,7 +1738,7 @@ class StockItemTest(StockAPITestCase):
for item in items: for item in items:
item.refresh_from_db() item.refresh_from_db()
self.assertEqual(item.status, StockStatus.ATTENTION.value) self.assertEqual(item.status, StockStatus.ATTENTION.value)
self.assertEqual(item.tracking_info.count(), 2) self.assertEqual(item.tracking_info.count(), 3)
tracking = item.tracking_info.last() tracking = item.tracking_info.last()
self.assertEqual(tracking.tracking_type, StockHistoryCode.EDITED.value) self.assertEqual(tracking.tracking_type, StockHistoryCode.EDITED.value)
+96 -96
View File
@@ -230,99 +230,99 @@ click==8.3.0 \
--hash=sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc \ --hash=sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc \
--hash=sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4 --hash=sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4
# via pip-tools # via pip-tools
coverage[toml]==7.11.2 \ coverage[toml]==7.11.3 \
--hash=sha256:004bdc5985b86f565772af627925e368256ee2172623db10a0d78a3b53f20ef1 \ --hash=sha256:004cdcea3457c0ea3233622cd3464c1e32ebba9b41578421097402bee6461b63 \
--hash=sha256:03e7e7dc31a7deaebf121c3c3bd3c6442b7fbf50aca72aae2a1d08aa30ca2a20 \ --hash=sha256:0542ddf6107adbd2592f29da9f59f5d9cff7947b5bb4f734805085c327dcffaa \
--hash=sha256:07e14a4050525fd98bf3d793f229eb8b3ae81678f4031e38e6a18a068bd59fd4 \ --hash=sha256:05fd3fb6edff0c98874d752013588836f458261e5eba587afe4c547bba544afd \
--hash=sha256:0f4a958ff286038ac870f836351e9fb8912f1614d1cdbda200fc899235f7dc9b \ --hash=sha256:074e6a5cd38e06671580b4d872c1a67955d4e69639e4b04e87fc03b494c1f060 \
--hash=sha256:10f10c9acf584ef82bfaaa7296163bd11c7487237f1670e81fc2fa7e972be67b \ --hash=sha256:07bc7745c945a6d95676953e86ba7cebb9f11de7773951c387f4c07dc76d03f5 \
--hash=sha256:17047fb65fcd1ce8a2f97dd2247c2b59cb4bc8848b3911db02dcb05856f91b71 \ --hash=sha256:08c0bcf932e47795c49f0406054824b9d45671362dfc4269e0bc6e4bff010704 \
--hash=sha256:1ac3f647ecf25d883051ef42d38d823016e715b9f289f8c1768be5117075d1bd \ --hash=sha256:097acc18bedf2c6e3144eaf09b5f6034926c3c9bb9e10574ffd0942717232507 \
--hash=sha256:230317450af65a37c1fdbdd3546f7277e0c1c1b65e0d57409248e5dd0fa13493 \ --hash=sha256:0c986537abca9b064510f3fd104ba33e98d3036608c7f2f5537f869bc10e1ee5 \
--hash=sha256:2442afabe9e83b881be083238bb7cf5afd4a10e47f29b6094470338d2336b33c \ --hash=sha256:0dba4da36730e384669e05b765a2c49f39514dd3012fcc0398dd66fba8d746d5 \
--hash=sha256:2970c03fefee2a5f1aebc91201a0706a7d0061cc71ab452bb5c5345b7174a349 \ --hash=sha256:0e920567f8c3a3ce68ae5a42cf7c2dc4bb6cc389f18bff2235dd8c03fa405de5 \
--hash=sha256:2ca963994d28e44285dc104cf94b25d8a7fd0c6f87cf944f46a23f473910703f \ --hash=sha256:0f59387f5e6edbbffec2281affb71cdc85e0776c1745150a3ab9b6c1d016106b \
--hash=sha256:30c437e8b51ce081fe3903c9e368e85c9a803b093fd062c49215f3bf4fd1df37 \ --hash=sha256:12d821de7408292530b0d241468b698bce18dd12ecaf45316149f53877885f8c \
--hash=sha256:36c41bf2ee6f6062de8177e249fee17cd5c9662cd373f7a41e6468a34c5b9c0f \ --hash=sha256:13b2066303a1c1833c654d2af0455bb009b6e1727b3883c9964bc5c2f643c1d0 \
--hash=sha256:38a5509fe7fabb6fb3161059b947641753b6529150ef483fc01c4516a546f2ad \ --hash=sha256:1410bac9e98afd9623f53876fae7d8a5db9f5a0ac1c9e7c5188463cb4b3212e2 \
--hash=sha256:397778cf6d50df59c890bd3ac10acb5bf413388ff6a013305134f1403d5db648 \ --hash=sha256:1451464fd855d9bd000c19b71bb7dafea9ab815741fb0bd9e813d9b671462d6f \
--hash=sha256:3aa8c62460499e10ceac5ea61cc09c4f7ddcd8a68c6313cf08785ad353dfd311 \ --hash=sha256:149eccc85d48c8f06547534068c41d69a1a35322deaa4d69ba1561e2e9127e75 \
--hash=sha256:410cafc1aba1f7eb8c09823d5da381be30a2c9b3595758a4c176fcfc04732731 \ --hash=sha256:1e33d0bebf895c7a0905fcfaff2b07ab900885fc78bba2a12291a2cfbab014cc \
--hash=sha256:43ecf9dca4fcb3baf8a886019dd5ce663c95a5e1c5172719c414f0ebd9eeb785 \ --hash=sha256:200bb89fd2a8a07780eafcdff6463104dec459f3c838d980455cfa84f5e5e6e1 \
--hash=sha256:44b6e04bb94e59927a2807cd4de86386ce34248eaea95d9f1049a72f81828c38 \ --hash=sha256:2376e8a9c889016f25472c452389e98bc6e54a19570b107e27cde9d47f387b64 \
--hash=sha256:461577af3f8ad4da244a55af66c0731b68540ce571dbdc02598b5ec9e7a09e73 \ --hash=sha256:28c5251b3ab1d23e66f1130ca0c419747edfbcb4690de19467cd616861507af7 \
--hash=sha256:4648c90cf741fb61e142826db1557a44079de0ca868c5c5a363c53d852897e84 \ --hash=sha256:2ec27a7a991d229213c8070d31e3ecf44d005d96a9edc30c78eaeafaa421c001 \
--hash=sha256:4aa799c61869318d2b86c0d3c413d6805546aec42069f009cbb27df2eefb2790 \ --hash=sha256:305716afb19133762e8cf62745c46c4853ad6f9eeba54a593e373289e24ea237 \
--hash=sha256:4aaf2212302b6f748dde596424b0f08bc3e1285192104e2480f43d56b6824f35 \ --hash=sha256:31663572f20bf3406d7ac00d6981c7bbbcec302539d26b5ac596ca499664de31 \
--hash=sha256:4c4423ea9c28749080b41e18ec74d658e6c9f148a6b47e719f3d7f56197f8227 \ --hash=sha256:3224c7baf34e923ffc78cb45e793925539d640d42c96646db62dbd61bbcfa131 \
--hash=sha256:4d1ff4b87ad438148976f2215141a490ae000e878536370d53f8da8c59a175a6 \ --hash=sha256:351511ae28e2509c8d8cae5311577ea7dd511ab8e746ffc8814a0896c3d33fbe \
--hash=sha256:4f8f6bcaa7fe162460abb38f7a5dbfd7f47cfc51e2a0bf0d3ef9e51427298391 \ --hash=sha256:385977d94fc155f8731c895accdfcc3dd0d9dd9ef90d102969df95d3c637ab80 \
--hash=sha256:55ae008253df6000bc885a780c1b0e939bd8c932f41e16df1cfe19a00428a98a \ --hash=sha256:39764c6167c82d68a2d8c97c33dba45ec0ad9172570860e12191416f4f8e6e1b \
--hash=sha256:595c6bb2b565cc2d930ee634cae47fa959dfd24cc0e8ae4cf2b6e7e131e0d1f7 \ --hash=sha256:3e33a968672be1394eded257ec10d4acbb9af2ae263ba05a99ff901bb863557e \
--hash=sha256:5a02818ec44803e325d66bd022828212df934739b894d1699c9a05b9105d30f2 \ --hash=sha256:4234914b8c67238a3c4af2bba648dc716aa029ca44d01f3d51536d44ac16854f \
--hash=sha256:5b284931d57389ec97a63fb1edf91c68ec369cee44bc40b37b5c3985ba0a2914 \ --hash=sha256:426559f105f644b69290ea414e154a0d320c3ad8a2bb75e62884731f69cf8e2c \
--hash=sha256:5c31cdbb95ab0f4a60224a04efc43cfb406ce904f0b60fb6b2a72f37718ea5cb \ --hash=sha256:465695268414e149bab754c54b0c45c8ceda73dd4a5c3ba255500da13984b16d \
--hash=sha256:5db683000ff6217273071c752bd6a1d341b6dc5d6aaa56678c53577a4e70e78a \ --hash=sha256:4bec8c7160688bd5a34e65c82984b25409563134d63285d8943d0599efbc448e \
--hash=sha256:5f72a49504e1f35443b157d97997c9259a017384373eab52fd09b8ade2ae4674 \ --hash=sha256:4c5627429f7fbff4f4131cfdd6abd530734ef7761116811a707b88b7e205afd7 \
--hash=sha256:61d6a7cc1e7a7a761ac59dcc88cee54219fd4231face52bd1257cfd3df29ae9f \ --hash=sha256:4ca5f876bf41b24378ee67c41d688155f0e54cdc720de8ef9ad6544005899240 \
--hash=sha256:632904d126ca97e5d4ecf7e51ae8b20f086b6f002c6075adcfd4ff3a28574527 \ --hash=sha256:4d4ca49f5ba432b0755ebb0fc3a56be944a19a16bb33802264bbc7311622c0d1 \
--hash=sha256:6681164bc697b93676945c8c814b76ac72204c395e11b71ba796a93b33331c24 \ --hash=sha256:4ebcddfcdfb4c614233cff6e9a3967a09484114a8b2e4f2c7a62dc83676ba13f \
--hash=sha256:689d3b4dd0d4c912ed8bfd7a1b5ff2c5ecb1fa16571840573174704ff5437862 \ --hash=sha256:4f2bb4ee8dd40f9b2a80bb4adb2aecece9480ba1fa60d9382e8c8e0bd558e2eb \
--hash=sha256:6f70fa1ef17cba5dada94e144ea1b6e117d4f174666842d1da3aaf765d6eb477 \ --hash=sha256:56f909a40d68947ef726ce6a34eb38f0ed241ffbe55c5007c64e616663bcbafc \
--hash=sha256:72a3d109ac233666064d60b29ae5801dd28bc51d1990e69f183a2b91b92d4baf \ --hash=sha256:5b771b59ac0dfb7f139f70c85b42717ef400a6790abb6475ebac1ecee8de782f \
--hash=sha256:75ef769be19d69ea71b0417d7fbf090032c444792579cdf9b166346a340987d5 \ --hash=sha256:603c4414125fc9ae9000f17912dcfd3d3eb677d4e360b85206539240c96ea76e \
--hash=sha256:7e01ab8d69b6cffa2463e78a4d760a6b69dfebe5bf21837eabcc273655c7e7b3 \ --hash=sha256:60ca149a446da255d56c2a7a813b51a80d9497a62250532598d249b3cdb1a926 \
--hash=sha256:7ea36e981a8a591acdaa920704f8dc798f9fff356c97dbd5d5702046ae967ce1 \ --hash=sha256:68c4eb92997dbaaf839ea13527be463178ac0ddd37a7ac636b8bc11a51af2428 \
--hash=sha256:7f1aa017b47e1879d7bac50161b00d2b886f2ff3882fa09427119e1b3572ede1 \ --hash=sha256:6bb599052a974bb6cedfa114f9778fedfad66854107cf81397ec87cb9b8fbcf2 \
--hash=sha256:7f933bc1fead57373922e383d803e1dd5ec7b5a786c220161152ebee1aa3f006 \ --hash=sha256:6f033dec603eea88204589175782290a038b436105a8f3637a81c4359df27832 \
--hash=sha256:8014a28a37ffabf7da7107f4f154d68c6b89672f27fef835a0574591c5cd140b \ --hash=sha256:72c8b494bd20ae1c58528b97c4a67d5cfeafcb3845c73542875ecd43924296de \
--hash=sha256:805efa416085999da918f15f81b26636d8e79863e1fbac1495664686d1e6a6e9 \ --hash=sha256:77ffb3b7704eb7b9b3298a01fe4509cef70117a52d50bcba29cffc5f53dd326a \
--hash=sha256:811bff1f93566a8556a9aeb078bd82573e37f4d802a185fba4cbe75468615050 \ --hash=sha256:84b892e968164b7a0498ddc5746cdf4e985700b902128421bb5cec1080a6ee36 \
--hash=sha256:84e8e0f5ab5134a2d32d4ebadc18b433dbbeddd0b73481f816333b1edd3ff1c8 \ --hash=sha256:86d27d2dd7c7c5a44710565933c7dc9cd70e65ef97142e260d16d555667deef7 \
--hash=sha256:87d7c7b0b2279e174f36d276e2afb7bf16c9ea04e824d4fa277eea1854f4cfd4 \ --hash=sha256:876a3ee7fd2613eb79602e4cdb39deb6b28c186e76124c3f29e580099ec21a87 \
--hash=sha256:89565d7c9340858424a5ca3223bfefe449aeb116942cdc98cd76c07ca50e9db8 \ --hash=sha256:8bba7e4743e37484ae17d5c3b8eb1ce78b564cb91b7ace2e2182b25f0f764cb5 \
--hash=sha256:940d195f4c8ba3ec6e7c302c9f546cdbe63e57289ed535452bc52089b1634f1c \ --hash=sha256:8d16bbe566e16a71d123cd66382c1315fcd520c7573652a8074a8fe281b38c6a \
--hash=sha256:94ced4a29a6987af99faaa49a513bf8d0458e8af004c54174e05dd7a8a31c7d9 \ --hash=sha256:8d264402fc179776d43e557e1ca4a7d953020d3ee95f7ec19cc2c9d769277f06 \
--hash=sha256:9a6468e1a3a40d3d1f9120a9ff221d3eacef4540a6f819fff58868fe0bd44fa9 \ --hash=sha256:8f067ada2c333609b52835ca4d4868645d3b63ac04fb2b9a658c55bba7f667d3 \
--hash=sha256:9a95b7a6043b221ec1a0d4d5481e424272b37028353265fbe5fcd3768d652eb7 \ --hash=sha256:8f4cbfff5cf01fa07464439a8510affc9df281535f41a1f5312fbd2b59b4ab5c \
--hash=sha256:9f5f6ee021b3b25e748a9a053f3a8dd61a62b6689efd6425cb47e27360994903 \ --hash=sha256:900580bc99c145e2561ea91a2d207e639171870d8a18756eb57db944a017d4bb \
--hash=sha256:a35701fe0b5ee9d4b67d31aa76555237af32a36b0cf8dd33f8a74470cf7cd2f5 \ --hash=sha256:9061a3e3c92b27fd8036dafa26f25d95695b6aa2e4514ab16a254f297e664f83 \
--hash=sha256:a913b21f716aa05b149a8656e9e234d9da04bc1f9842136ad25a53172fecc20e \ --hash=sha256:90a96fcd824564eae6137ec2563bd061d49a32944858d4bdbae5c00fb10e76ac \
--hash=sha256:ae43149b7732df15c3ca9879b310c48b71d08cd8a7ba77fda7f9108f78499e93 \ --hash=sha256:9245bd392572b9f799261c4c9e7216bafc9405537d0f4ce3ad93afe081a12dc9 \
--hash=sha256:b4776c6555a9f378f37fa06408f2e1cc1d06e4c4e06adb3d157a4926b549efbe \ --hash=sha256:9799bd6a910961cb666196b8583ed0ee125fa225c6fdee2cbf00232b861f29d2 \
--hash=sha256:b7658f3d4f728092368c091c18efcfb679be9b612c93bfdf345f33635a325188 \ --hash=sha256:9a1d577c20b4334e5e814c3d5fe07fa4a8c3ae42a601945e8d7940bab811d0bd \
--hash=sha256:b7fc943097fa48de00d14d2a2f3bcebfede024e031d7cd96063fe135f8cbe96e \ --hash=sha256:a6b17c2b5e0b9bb7702449200f93e2d04cb04b1414c41424c08aa1e5d352da76 \
--hash=sha256:b9f28b900d96d83e2ae855b68d5cf5a704fa0b5e618999133fd2fb3bbe35ecb1 \ --hash=sha256:a730cd0824e8083989f304e97b3f884189efb48e2151e07f57e9e138ab104200 \
--hash=sha256:bc65e32fe5bb942f0f5247e1500e355cbbdf326181198f5e27e3bb3ddb81e203 \ --hash=sha256:a8258f10059b5ac837232c589a350a2df4a96406d6d5f2a09ec587cbdd539655 \
--hash=sha256:bee1911c44c52cad6b51d436aa8c6ff5ca5d414fa089c7444592df9e7b890be9 \ --hash=sha256:ab6212e62ea0e1006531a2234e209607f360d98d18d532c2fa8e403c1afbdd71 \
--hash=sha256:c4b1bea4c707f4c09f682fe0e646a114dfd068f627880d4a208850d01f8164ad \ --hash=sha256:abb903ffe46bd319d99979cdba350ae7016759bb69f47882242f7b93f3356055 \
--hash=sha256:c5769159986eb174f0f66d049a52da03f2d976ac1355679371f1269e83528599 \ --hash=sha256:abcea3b5f0dc44e1d01c27090bc32ce6ffb7aa665f884f1890710454113ea902 \
--hash=sha256:c65f4291aec39692a3bfbe1d92ae5bea58c16b5553fdf021de61c655d987233f \ --hash=sha256:ac5d5329c9c942bbe6295f4251b135d860ed9f86acd912d418dce186de7c19ac \
--hash=sha256:c7ea5dec77d79dabb7b5fc712c59361aac52e459cd22028480625c3c743323d0 \ --hash=sha256:adb9b7b42c802bd8cb3927de8c1c26368ce50c8fdaa83a9d8551384d77537044 \
--hash=sha256:c85f44ed4260221e46a4e9e8e8df4b359ab6c0a742c79e85d649779bcf77b534 \ --hash=sha256:ae12fe90b00b71a71b69f513773310782ce01d5f58d2ceb2b7c595ab9d222094 \
--hash=sha256:c8b9a7ebc6a29202fb095877fd8362aab09882894d1c950060c76d61fb116114 \ --hash=sha256:b5cd111d3ab7390be0c07ad839235d5ad54d2ca497b5f5db86896098a77180a4 \
--hash=sha256:cbffd1d5c5bf4c576ca247bf77646cdad4dced82928337eeb0b85e2b3be4d64b \ --hash=sha256:bb9d7efdb063903b3fdf77caec7b77c3066885068bdc0d44bc1b0c171033f944 \
--hash=sha256:d0e80c9946da61cc0bf55dfd90d65707acc1aa5bdcb551d4285ea8906255bb33 \ --hash=sha256:c0a3b6e32457535df0d41d2d895da46434706dd85dbaf53fbc0d3bd7d914b362 \
--hash=sha256:d30a717493583c2a83c99f195e934c073be7f4291b32b7352c246d52e43f6893 \ --hash=sha256:c381a252317f63ca0179d2c7918e83b99a4ff3101e1b24849b999a00f9cd4f86 \
--hash=sha256:d423991415f73a70c0a5f3e0a226cf4ab374dd0da7409978069b844df3d31582 \ --hash=sha256:c713c1c528284d636cd37723b0b4c35c11190da6f932794e145fc40f8210a14a \
--hash=sha256:d73da4893125e0671f762e408dea9957b2bda0036c9589c2fd258a6b870acbdb \ --hash=sha256:c8be5bfcdc7832011b2652db29ed7672ce9d353dd19bce5272ca33dbcf60aaa8 \
--hash=sha256:d752a8e398a19e2fb24781e4c73089bfeb417b6ac55f96c2c42cfe5bdb21cc18 \ --hash=sha256:c8f563b245b4ddb591e99f28e3cd140b85f114b38b7f95b2e42542f0603eb7d7 \
--hash=sha256:e3b92e10ca996b5421232dd6629b9933f97eb57ce374bca800ab56681fbeda2b \ --hash=sha256:ca90ef33a152205fb6f2f0c1f3e55c50df4ef049bb0940ebba666edd4cdebc55 \
--hash=sha256:e448ceee2fb880427eafc9a3f8e6162b2ac7cc3e9b30b85d6511f25cc8a11820 \ --hash=sha256:d60bf4d7f886989ddf80e121a7f4d140d9eac91f1d2385ce8eb6bda93d563297 \
--hash=sha256:e48b95abe2983be98cdf52900e07127eb7fe7067c87a700851f4f1f53d2b00e6 \ --hash=sha256:d8750dd20362a1b80e3cf84f58013d4672f89663aee457ea59336df50fab6739 \
--hash=sha256:e52a028a56889d3ad036c0420e866e4a69417d3203e2fc5f03dcb8841274b64c \ --hash=sha256:dd9ca2d44ed8018c90efb72f237a2a140325a4c3339971364d758e78b175f58e \
--hash=sha256:e7d3fccd5781c5d29ca0bd1ea272630f05cd40a71d419e7e6105c0991400eb14 \ --hash=sha256:e22539b676fafba17f0a90ac725f029a309eb6e483f364c86dcadee060429d46 \
--hash=sha256:e8eb6cbd7d3b238335b5da0f3ce281102435afb503be4d7bdd69eea3c700a952 \ --hash=sha256:e2a96fdc7643c9517a317553aca13b5cae9bad9a5f32f4654ce247ae4d321405 \
--hash=sha256:ea10a57568af7cf082a7a4d98a699f993652c2ffbdd5a6c9d63c9ca10b693b4d \ --hash=sha256:e5f4bfac975a2138215a38bda599ef00162e4143541cf7dd186da10a7f8e69f1 \
--hash=sha256:ea910cc737ee8553c81ad5c104bc5b135106ebb36f88be506c3493e001b4c733 \ --hash=sha256:e8feeb5e8705835f0622af0fe7ff8d5cb388948454647086494d6c41ec142c2e \
--hash=sha256:eaa2a5eeb82fa7a6a9cd65c4f968ee2a53839d451b4e88e060c67d87a0a40732 \ --hash=sha256:eb5069074db19a534de3859c43eec78e962d6d119f637c41c8e028c5ab3f59dd \
--hash=sha256:ed6ba9f1777fdd1c8e5650c7d123211fa484a187c61af4d82948dc5ee3c0afcc \ --hash=sha256:f0b4101e2b3c6c352ff1f70b3a6fcc7c17c1ab1a91ccb7a33013cb0782af9820 \
--hash=sha256:ef2d3081562cd83f97984a96e02e7a294efa28f58d5e7f4e28920f59fd752b41 \ --hash=sha256:f761dbcf45e9416ec4698e1a7649248005f0064ce3523a47402d1bff4af2779e \
--hash=sha256:f633da28958f57b846e955d28661b2b323d8ae84668756e1eea64045414dbe34 \ --hash=sha256:f9c96a29c6d65bd36a91f5634fef800212dff69dacdb44345c4c9783943ab0df \
--hash=sha256:f6b2498f86f2554ed6cb8df64201ee95b8c70fb77064a8b2ae8a7185e7a4a5f0 \ --hash=sha256:fb58da65e3339b3dbe266b607bb936efb983d86b00b03eb04c4ad5b442c58428 \
--hash=sha256:f6f985e175dfa1fb8c0a01f47186720ae25d5e20c181cc5f3b9eba95589b8148 \ --hash=sha256:fbffc22d80d86fbe456af9abb17f7a7766e7b2101f7edaacc3535501691563f7 \
--hash=sha256:f80cb5b328e870bf3df0568b41643a85ee4b8ccd219a096812389e39aa310ea4 \ --hash=sha256:fdc5255eb4815babcdf236fa1a806ccb546724c8a9b129fd1ea4a5448a0bf07c \
--hash=sha256:fd3f7cc6cb999e3eff91a2998a70c662b0fcd3c123d875766147c530ca0d3248 --hash=sha256:fe3425dc6021f906c6325d3c415e048e7cdb955505a94f1eb774dafc779ba203
# via -r src/backend/requirements-dev.in # via -r src/backend/requirements-dev.in
cryptography==46.0.3 \ cryptography==46.0.3 \
--hash=sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217 \ --hash=sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217 \
@@ -452,9 +452,9 @@ pip==25.3 \
--hash=sha256:8d0538dbbd7babbd207f261ed969c65de439f6bc9e5dbd3b3b9a77f25d95f343 \ --hash=sha256:8d0538dbbd7babbd207f261ed969c65de439f6bc9e5dbd3b3b9a77f25d95f343 \
--hash=sha256:9655943313a94722b7774661c21049070f6bbb0a1516bf02f7c8d5d9201514cd --hash=sha256:9655943313a94722b7774661c21049070f6bbb0a1516bf02f7c8d5d9201514cd
# via pip-tools # via pip-tools
pip-tools==7.5.1 \ pip-tools==7.5.2 \
--hash=sha256:a051a94794ba52df9acad2d7c9b0b09ae001617db458a543f8287fea7b89c2cf \ --hash=sha256:2d64d72da6a044da1110257d333960563d7a4743637e8617dd2610ae7b82d60f \
--hash=sha256:f5ff803823529edc0e6e40c86b1aa7da7266fb1078093c8beea4e5b77877036a --hash=sha256:2fe16db727bbe5bf28765aeb581e792e61be51fc275545ef6725374ad720a1ce
# via -r src/backend/requirements-dev.in # via -r src/backend/requirements-dev.in
platformdirs==4.5.0 \ platformdirs==4.5.0 \
--hash=sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312 \ --hash=sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312 \
+1 -1
View File
@@ -71,4 +71,4 @@ opentelemetry-instrumentation-psycopg
opentelemetry-instrumentation-pymysql opentelemetry-instrumentation-pymysql
# Pins # Pins
xmlsec==1.3.14 # 2025-06-02 pinned to avoid issues with builds - see https://github.com/inventree/InvenTree/pull/9713 xmlsec==1.3.17
+146 -94
View File
@@ -661,38 +661,38 @@ drf-spectacular==0.29.0 \
--hash=sha256:0a069339ea390ce7f14a75e8b5af4a0860a46e833fd4af027411a3e94fc1a0cc \ --hash=sha256:0a069339ea390ce7f14a75e8b5af4a0860a46e833fd4af027411a3e94fc1a0cc \
--hash=sha256:d1ee7c9535d89848affb4427347f7c4a22c5d22530b8842ef133d7b72e19b41a --hash=sha256:d1ee7c9535d89848affb4427347f7c4a22c5d22530b8842ef133d7b72e19b41a
# via -r src/backend/requirements.in # via -r src/backend/requirements.in
dulwich==0.24.8 \ dulwich==0.24.10 \
--hash=sha256:07aa6e7d41358fcba2a8ac53731e1b8ab201cac7a192ec678ef0da34c7643cf1 \ --hash=sha256:019af16c850ae85254289f9633a29dea02f45351c4182ea20b0c1394c074a13b \
--hash=sha256:0e9aacbbb0b0cf4b3fecac2c29ddd4d4e36f03ced30851889c193986e8bb327e \ --hash=sha256:0dfae8c59b97964a907fdf4c5809154a18fd8c55f2eb6d8fd1607464165a9aa2 \
--hash=sha256:108c74b329599931bfe66c4a34fb9312cd8136053cbfc04e7007e7c34081c6b7 \ --hash=sha256:0e1601789554e3d15b294356c78a5403521c27d5460e64dbbc44ffd5b10af4c3 \
--hash=sha256:138fd2480c1db43f372d52e4e6ed050c15f92ffbeeab9a0877dceb9801c65d2a \ --hash=sha256:15b32f8c3116a1c0a042dde8da96f65a607e263e860ee42b3d4a98ce2c2f4a06 \
--hash=sha256:16e335bce0d5192d476db0ca81de1f90fb56863ad7d0b985b0333a8194c73c64 \ --hash=sha256:1601bfea3906b52c924fae5b6ba32a0b087fb8fae927607e6b5381e6f7559611 \
--hash=sha256:17d8223cc69cf79ddd7a2f0893e223708f7efc2ea372f919ddcc0d852b3f5d06 \ --hash=sha256:1b19af8a3ab051003ba05f15fc5c0d6f0d427e795639490790f34ec0558e99e3 \
--hash=sha256:19855e8a0ce299cdcdafdc8bc4f6653bea9e02124a5022e13cda8103fb36912d \ --hash=sha256:1f511f7afe1f36e37193214e4e069685d7d0378e756cc96a2fcb138bdf9fefca \
--hash=sha256:19be46710a9d810a66d4a925754abf3818ba22d61134e7d7e1d7b1585c9445b6 \ --hash=sha256:2a56f9838e5d2414a2b57bab370b73b9803fefd98836ef841f0fd489b5cc1349 \
--hash=sha256:1bac020051cf228b33c787294e17ac80a284e028c3749437ee72577ee04e1cd9 \ --hash=sha256:30e028979b6fa7220c913da9c786026611c10746c06496149742602b36a11f6b \
--hash=sha256:267e79367cd6f438091248c1e826c6cf7abd84d5b641b9fe46fbc4d9119e11ed \ --hash=sha256:3581ae0af33f28e6c0834d2f41ca67ca81cd92a589e6a5f985e6c64373232958 \
--hash=sha256:2e3a9a713fda94f3216da4743db3cc8d670330f44c4d98580ac4600242dba2c4 \ --hash=sha256:393e9c3cdd382cff20b5beb66989376d6da69e3b0dfec046a884707ab5d27ac9 \
--hash=sha256:5593a7216b27412333b99b2e1851bcc2485485d5c4694430aa86d34a36f08d63 \ --hash=sha256:44f62e0244531a8c43ca7771e201ec9e7f6a2fb27f8c3c623939bc03c1f50423 \
--hash=sha256:6016e3f7a0f1dd5e19df14b772cb8f42bfde0cd55c504642c05e1e8328de21e3 \ --hash=sha256:470d6cd8207e1a5ff1fb34c4c6fac2ec9a96d618f7062e5fb96c5260927bb9a7 \
--hash=sha256:661af1fa3852d970fef68b0ab431f0bd488c3f94306e89244c173c4e6abb978e \ --hash=sha256:4914abb6408a719b7a1f7d9a182d1efd92c326e178b440faf582df50f9f032db \
--hash=sha256:6a51a41e858e0427b14bb19df7ac1207275dd6b5cc1976f54710bf15cb4c5614 \ --hash=sha256:4b5c225477a529e1d4a2b5e51272a418177e34803938391ce41b7573b2e5b0d0 \
--hash=sha256:6ffdd616135bcb31eb2edcccf82d4408720f1db69f596f687ffa2d26c2f5e6f4 \ --hash=sha256:5c724e5fc67c45f3c813f2630795ac388e3e6310534212f799a7a6bf230648c8 \
--hash=sha256:8ac85e3ea42878fa91b6a9282652327df54f5abea4aaf674036e1000608a15f0 \ --hash=sha256:6a25ca1605a94090514af408f9df64427281aefbb726f542e97d86d3a7c8ec18 \
--hash=sha256:8eca5242f8aed324394c95ecd13a0a66d2a0c31c2556f0a52e7bb8dd67edef20 \ --hash=sha256:752c32d517dc608dbb8414061eaaec8ac8a05591b29531f81a83336b018b26c6 \
--hash=sha256:97f7c64b02fbd366c36557aa2e8642fc686aaec7d6569bc1460d0b38a63169f9 \ --hash=sha256:843de5f678436a27b33aea0f2b87fd0453afdd0135f885a3ca44bc3147846dd2 \
--hash=sha256:a0a780b512d4144336eac2f1f6b982eb78c313442e88ba0db853a224e2b918ef \ --hash=sha256:858fae0c7121715282a993abb1919385a28e1a9c4f136f568748d283c2ba874f \
--hash=sha256:a6055b12cf2b90a0b4c21d38594f2681667d26bb0c135dfc36c2ea6de7458a37 \ --hash=sha256:8df79c8080471f363e4dfcfc4e0d2e61e6da73af1fd7d31cb6ae0d34af45a6b4 \
--hash=sha256:a60f8a5d718c7cc1f60bb931cc915311fd5198d85d68dde0ef3c569311c14a70 \ --hash=sha256:90028182b9a47ea4efed51c81298f3a98e279d7bf5c1f91c47101927a309ee45 \
--hash=sha256:b03474c16bcfa3524241b4ae89007931d79ac37c0b19e059133a6e63b7039845 \ --hash=sha256:90b24c0827299cfb53c4f4d4fedc811be5c4b10c11172ff6e5a5c52277fe0b3a \
--hash=sha256:b31e08bcd0a4eb29915987fa5273a827cccca7ee83eb27ef17bd5899f5668055 \ --hash=sha256:b715a9f85ed71bef8027275c1bded064e4925071ae8c8a8d9a20c67b31faf3cd \
--hash=sha256:c9f4748bbcca56fb57458c71c0d30e2351ac15e0583d428c739c09228be68f05 \ --hash=sha256:c262ffc94338999e7808b434dccafaccd572d03b42d4ef140059d4b7cad765a3 \
--hash=sha256:da03c7a6629b7ed37e7139739a175f2c9678080a45444418c54ab28d2ec6524b \ --hash=sha256:ce6e05ec50f258ccd14d83114eb32cc5bb241ae4a8c7199d014fd7568de285b1 \
--hash=sha256:ea6c63d3e40fc321ec7c5673b92be036b57aba7802c7e94a18f61451af382de0 \ --hash=sha256:d9793fc1e42149a650a017dc8ce38485368a41729b9937e1dfcfedd0591ebe9d \
--hash=sha256:ec0f62538b6fb26cdd1b2fb70788ccfdb17df26a4ba1ca70e623e196c4004f5c \ --hash=sha256:e2eda4a634d6f1ac4c0d4786f8772495c8840dfc2b3e595507376bf5e5b0f9c5 \
--hash=sha256:efbf0f29d8d3d56a098e2b4a9260bdfa5f313142180a882c7b28e648d9b5ca9e \ --hash=sha256:f102c38207540fa485e85e0b763ce3725a2d49d846dbf316ed271e27fd85ff21 \
--hash=sha256:f7519f3b8c66ba2e4ea66f47c2156a66cefedce2a121ac3227e030abe95698f3 \ --hash=sha256:f7bfa9f0bfae57685754b163eef6641609047460939d28052e3beeb63efa6795 \
--hash=sha256:f7a0d2cef91cf92a44071daa92639f648ab756d3db63a99e37d3a08ebacf69f3 --hash=sha256:fbf94fa73211d2f029751a72e1ca3a2fd35c6f5d9bb434acdf10a4a79ca322dd
# via -r src/backend/requirements.in # via -r src/backend/requirements.in
et-xmlfile==2.0.0 \ et-xmlfile==2.0.0 \
--hash=sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa \ --hash=sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa \
@@ -1832,9 +1832,9 @@ s3transfer==0.14.0 \
--hash=sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456 \ --hash=sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456 \
--hash=sha256:eff12264e7c8b4985074ccce27a3b38a485bb7f7422cc8046fee9be4983e4125 --hash=sha256:eff12264e7c8b4985074ccce27a3b38a485bb7f7422cc8046fee9be4983e4125
# via boto3 # via boto3
sentry-sdk==2.43.0 \ sentry-sdk==2.44.0 \
--hash=sha256:4aacafcf1756ef066d359ae35030881917160ba7f6fc3ae11e0e58b09edc2d5d \ --hash=sha256:5b1fe54dfafa332e900b07dd8f4dfe35753b64e78e7d9b1655a28fd3065e2493 \
--hash=sha256:52ed6e251c5d2c084224d73efee56b007ef5c2d408a4a071270e82131d336e20 --hash=sha256:9e36a0372b881e8f92fdbff4564764ce6cec4b7f25424d0a3a8d609c9e4651a7
# via # via
# -r src/backend/requirements.in # -r src/backend/requirements.in
# django-q-sentry # django-q-sentry
@@ -2026,65 +2026,117 @@ xlwt==1.3.0 \
--hash=sha256:a082260524678ba48a297d922cc385f58278b8aa68741596a87de01a9c628b2e \ --hash=sha256:a082260524678ba48a297d922cc385f58278b8aa68741596a87de01a9c628b2e \
--hash=sha256:c59912717a9b28f1a3c2a98fd60741014b06b043936dcecbc113eaaada156c88 --hash=sha256:c59912717a9b28f1a3c2a98fd60741014b06b043936dcecbc113eaaada156c88
# via tablib # via tablib
xmlsec==1.3.14 \ xmlsec==1.3.17 \
--hash=sha256:004e8a82e26728bf8a60f8ece1ef3ffafdac30ef538139dfe28870e8503ca64a \ --hash=sha256:00d43d4f68ac6b11f6e1e69bcb389495f54da77bf1168b4de08f4a7785e47bbb \
--hash=sha256:03ccba7dacf197850de954666af0221c740a5de631a80136362a1559223fab75 \ --hash=sha256:00d43d4f68ac6b11f6e1e69bcb389495f54da77bf1168b4de08f4a7785e47bbb \
--hash=sha256:0bae37b2920115cf00759ee9fb7841cbdebcef3a8a92734ab93ae8fa41ac581d \ --hash=sha256:040f28a7aacfdb467df46d423e4af05569e9376bc8c7f6416b0761e16a0e3d0b \
--hash=sha256:0be3b7a28e54a03b87faf07fb3c6dc3e50a2c79b686718c3ad08300b8bf6bb67 \ --hash=sha256:040f28a7aacfdb467df46d423e4af05569e9376bc8c7f6416b0761e16a0e3d0b \
--hash=sha256:1072878301cb9243a54679e0520e6a5be2266c07a28b0ecef9e029d05a90ffcd \ --hash=sha256:1a0b9a1dcda547e0340eefa6f4a04b87dbd9e40cd514487f347934f94fd559ab \
--hash=sha256:12d90059308bb0c1b94bde065784e6852999d08b91bcb2048c17e62b954acb07 \ --hash=sha256:1a0b9a1dcda547e0340eefa6f4a04b87dbd9e40cd514487f347934f94fd559ab \
--hash=sha256:147934bd39dfd840663fb6b920ea9201455fa886427975713f1b42d9f20b9b29 \ --hash=sha256:26cc3d81437b51839946d2e93d09371dfd73ed2831dc7e37eff0fb52fc33747c \
--hash=sha256:19c86bab1498e4c2e56d8e2c878f461ccb6e56b67fd7522b0c8fda46d8910781 \ --hash=sha256:26cc3d81437b51839946d2e93d09371dfd73ed2831dc7e37eff0fb52fc33747c \
--hash=sha256:1b9b5de6bc69fdec23147e5f712cb05dc86df105462f254f140d743cc680cc7b \ --hash=sha256:2aa5081e1e05dcb6029660ddad795c7daebb3c5771001f60850ab24a16a9cf5e \
--hash=sha256:1eb3dcf244a52f796377112d8f238dbb522eb87facffb498425dc8582a84a6bf \ --hash=sha256:2aa5081e1e05dcb6029660ddad795c7daebb3c5771001f60850ab24a16a9cf5e \
--hash=sha256:1fa1311f7489d050dde9028f5a2b5849c2927bb09c9a93491cb2f28fdc563912 \ --hash=sha256:320bf7162e2c442638233da9826af1476049999da1b474b5fe07c60952610131 \
--hash=sha256:1fe23c2dd5f5dbcb24f40e2c1061e2672a32aabee7cf8ac5337036a485607d72 \ --hash=sha256:320bf7162e2c442638233da9826af1476049999da1b474b5fe07c60952610131 \
--hash=sha256:204d3c586b8bd6f02a5d4c59850a8157205569d40c32567f49576fa5795d897d \ --hash=sha256:393eb5cc6b8d6e67c04fd64979ec30d766b3d226550b208803cd410d4b416d91 \
--hash=sha256:2401e162aaab7d9416c3405bac7a270e5f370988a0f1f46f0f29b735edba87e1 \ --hash=sha256:393eb5cc6b8d6e67c04fd64979ec30d766b3d226550b208803cd410d4b416d91 \
--hash=sha256:28cd9f513cf01dc0c5b9d9f0728714ecde2e7f46b3b6f63de91f4ae32f3008b3 \ --hash=sha256:3a53c14d4bc40b0f0fcc6d7908b88f3cbbcf36e25c392f796d88aee7dee5beea \
--hash=sha256:2f84a1c509c52773365645a87949081ee9ea9c535cd452048cc8ca4ad3b45666 \ --hash=sha256:3a53c14d4bc40b0f0fcc6d7908b88f3cbbcf36e25c392f796d88aee7dee5beea \
--hash=sha256:330147ce59fbe56a9be5b2085d739c55a569f112576b3f1b33681f87416eaf33 \ --hash=sha256:3a6ced8c7744e896cb5a9fd0156d204df3143a62bae11be91cab8e9743d40eec \
--hash=sha256:34c61ec0c0e70fda710290ae74b9efe1928d9242ed82c4eecf97aa696cff68e6 \ --hash=sha256:3a6ced8c7744e896cb5a9fd0156d204df3143a62bae11be91cab8e9743d40eec \
--hash=sha256:38e035bf48300b7dbde2dd01d3b8569f8584fc9c73809be13886e6b6c77b74fb \ --hash=sha256:3d1fc1fbe2e8585a3f468cf4154d0ec36cd95a15e68429ad8cc8ccd7c04e84ae \
--hash=sha256:48e894ad3e7de373f56efc09d6a56f7eae73a8dd4cec8943313134849e9c6607 \ --hash=sha256:3d1fc1fbe2e8585a3f468cf4154d0ec36cd95a15e68429ad8cc8ccd7c04e84ae \
--hash=sha256:4922afa9234d1c5763950b26c328a5320019e55eb6000272a79dfe54fee8e704 \ --hash=sha256:40fcb197bdb76301c82b10096c67cc44a6e887317b5b914dbbd166339d36fce4 \
--hash=sha256:4af81ce8044862ec865782efd353d22abdcd95b92364eef3c934de57ae6d5852 \ --hash=sha256:40fcb197bdb76301c82b10096c67cc44a6e887317b5b914dbbd166339d36fce4 \
--hash=sha256:4dea6df3ffcb65d0b215678c3a0fe7bbc66785d6eae81291296e372498bad43a \ --hash=sha256:46fc9c80e8898ad212b6ad389469bb82d1ab89b90555db42e3e73235b143739d \
--hash=sha256:4edd8db4df04bbac9c4a5ab4af855b74fe2bf2c248d07cac2e6d92a485f1a685 \ --hash=sha256:46fc9c80e8898ad212b6ad389469bb82d1ab89b90555db42e3e73235b143739d \
--hash=sha256:4fac2a787ae3b9fb761f9aec6b9f10f2d1c1b87abb574ebd8ff68435bdc97e3d \ --hash=sha256:4f2bf6bbf04f8a912483d268b4c2727d400d1806d054624da13bee4b9f6fa28a \
--hash=sha256:57fed3bc7943681c9ed4d2221600ab440f060d8d1a8f92f346f2b41effe175b8 \ --hash=sha256:4f2bf6bbf04f8a912483d268b4c2727d400d1806d054624da13bee4b9f6fa28a \
--hash=sha256:6566434e2e5c58e472362a6187f208601f1627a148683a6f92bd16479f1d9e20 \ --hash=sha256:5319d0bdaf9e597a0ba8dfb3840c4ae57e51f462e7620953f32b07df6267f2ba \
--hash=sha256:6679cec780386d848e7351d4b0de92c4483289ea4f0a2187e216159f939a4c6b \ --hash=sha256:5319d0bdaf9e597a0ba8dfb3840c4ae57e51f462e7620953f32b07df6267f2ba \
--hash=sha256:73eabf5ef58189d81655058cf328c1dfa9893d89f1bff5fc941481f08533f338 \ --hash=sha256:5346616e1fe1015f7800698c15225c7902f45db199e217af2039a21989aff7e9 \
--hash=sha256:774d5d1e45f07f953c1cc14fd055c1063f0725f7248b6b0e681f59fd8638934d \ --hash=sha256:5346616e1fe1015f7800698c15225c7902f45db199e217af2039a21989aff7e9 \
--hash=sha256:77749b338503fb6e151052c664064b34264f4168e2cb0cca1de78b7e5312a783 \ --hash=sha256:5616ad5016794b0dd41d03eef5b721e31bb306353226b25fc88fedb7d4f7c37e \
--hash=sha256:7799a9ff3593f9dd43464e18b1a621640bffc40456c47c23383727f937dca7fc \ --hash=sha256:5616ad5016794b0dd41d03eef5b721e31bb306353226b25fc88fedb7d4f7c37e \
--hash=sha256:7882963e9cb9c0bd0e8c2715a29159a366417ff4a30d8baf42b05bc5cf249446 \ --hash=sha256:593264c192d1836162d75478c8b1cb5874f3b69dcc5bdfac642a0933abefa93a \
--hash=sha256:7e8e0171916026cbe8e2022c959558d02086655fd3c3466f2bc0451b09cf9ee8 \ --hash=sha256:593264c192d1836162d75478c8b1cb5874f3b69dcc5bdfac642a0933abefa93a \
--hash=sha256:82ac81deb7d7bf5cc8a748148948e5df5386597ff43fb92ec651cc5c7addb0e7 \ --hash=sha256:5c3008b32a15d24b6c9da39bf6ede8dc3122570a640a73795d763aea55a2193e \
--hash=sha256:86ff7b2711557c1087b72b0a1a88d82eafbf2a6d38b97309a6f7101d4a7041c3 \ --hash=sha256:5c3008b32a15d24b6c9da39bf6ede8dc3122570a640a73795d763aea55a2193e \
--hash=sha256:934f804f2f895bcdb86f1eaee236b661013560ee69ec108d29cdd6e5f292a2d9 \ --hash=sha256:5c6d4b2ece9d109591d08128a1656b458e24d9eba6c02c32e93573e14eee2447 \
--hash=sha256:995e87acecc263a2f6f2aa3cc204268f651cac8f4d7a2047f11b2cd49979cc38 \ --hash=sha256:5c6d4b2ece9d109591d08128a1656b458e24d9eba6c02c32e93573e14eee2447 \
--hash=sha256:a487c3d144f791c32f5e560aa27a705fba23171728b8a8511f36de053ff6bc93 \ --hash=sha256:5d0e69291f90b28e9442d8e0e69d3e06cede8a3c44e856413fd284de81ce2888 \
--hash=sha256:a98eadfcb0c3b23ccceb7a2f245811f8d784bd287640dcfe696a26b9db1e2fc0 \ --hash=sha256:5d0e69291f90b28e9442d8e0e69d3e06cede8a3c44e856413fd284de81ce2888 \
--hash=sha256:ad1634cabe0915fe2a12e142db0ed2daf5be80cbe3891a2cecbba0750195cc6b \ --hash=sha256:64c1184d51c8a67e3d1eb3ac477e307a07e2b40fd03cd0c8084b147ea0f342db \
--hash=sha256:b109cdf717257fd4daa77c1d3ec8a3fb2a81318a6d06a36c55a8a53ae381ae5e \ --hash=sha256:64c1184d51c8a67e3d1eb3ac477e307a07e2b40fd03cd0c8084b147ea0f342db \
--hash=sha256:b6dd86f440fec9242515c64f0be93fec8b4289287db1f6de2651eee9995aaecb \ --hash=sha256:66fe5aaccf68fb85fe0b64277e3f594d6b01ddefb98ef1ceb0a666652d6ec580 \
--hash=sha256:b7ba2ea38e3d9efa520b14f3c0b7d99a7c055244ae5ba8bc9f4ca73b18f3a215 \ --hash=sha256:66fe5aaccf68fb85fe0b64277e3f594d6b01ddefb98ef1ceb0a666652d6ec580 \
--hash=sha256:ba3b39c493e3b04354615068a3218f30897fcc2f42c6d8986d0c1d63aca87782 \ --hash=sha256:672e41dc7962da4ce84b67aa1c3a008338e3b88332f5484b9911b91cee0997ed \
--hash=sha256:bd10ca3201f164482775a7ce61bf7ee9aade2e7d032046044dd0f6f52c91d79d \ --hash=sha256:672e41dc7962da4ce84b67aa1c3a008338e3b88332f5484b9911b91cee0997ed \
--hash=sha256:bddd2a2328b4e08c8a112e06cf2cd2b4d281f4ad94df15b4cef18f06cdc49d78 \ --hash=sha256:67717fe5151df68987a1387cba11ba28ce19b3bb9a2d10d650277cd910e510e7 \
--hash=sha256:c12900e1903e289deb84eb893dca88591d6884d3e3cda4fb711b8812118416e8 \ --hash=sha256:67717fe5151df68987a1387cba11ba28ce19b3bb9a2d10d650277cd910e510e7 \
--hash=sha256:c42735cc68fdb4c6065cf0a0701dfff3a12a1734c63a36376349af9a5481f27b \ --hash=sha256:6cbf8a556c74451a7455d506ce3edc81c7cd0f60895ef7792c77dc97434a8e82 \
--hash=sha256:c4d41c83c8a2b8d8030204391ebeb6174fbdb044f0331653c4b5a4ce4150bcc0 \ --hash=sha256:6cbf8a556c74451a7455d506ce3edc81c7cd0f60895ef7792c77dc97434a8e82 \
--hash=sha256:ce4e165a1436697e5e39587c4fba24db4545a5c9801e0d749f1afd09ad3ab901 \ --hash=sha256:6d1e9ff3398e87e3abb2ab8f1576c0761e5c5c18b086b41f2b39a3eb6b2f7e11 \
--hash=sha256:cf35a25be3eb6263b2e0544ba26294651113fab79064f994d347a2ca5973e8e2 \ --hash=sha256:6d1e9ff3398e87e3abb2ab8f1576c0761e5c5c18b086b41f2b39a3eb6b2f7e11 \
--hash=sha256:d0762f4232bce2c7f6c0af329db8b821b4460bbe123a2528fb5677d03db7a4b5 \ --hash=sha256:728058a1623a620811a3cdf2dd4894b5d9413ede20c8ddddf98fdea5eafe9529 \
--hash=sha256:dba457ff87c39cbae3c5020475a728d24bbd9d00376df9af9724cd3bb59ff07a \ --hash=sha256:728058a1623a620811a3cdf2dd4894b5d9413ede20c8ddddf98fdea5eafe9529 \
--hash=sha256:df4aa0782a53032fd35e18dcd6d328d6126324bfcfdef0cb5c2856f25b4b6f94 \ --hash=sha256:72fc6d336dd68d62822c6536ff4b2453fda94ea652eddb4a958ac97b16ac7001 \
--hash=sha256:e6cbc914d77678db0c8bc39e723d994174633d18f9d6be4665ec29cce978a96d \ --hash=sha256:72fc6d336dd68d62822c6536ff4b2453fda94ea652eddb4a958ac97b16ac7001 \
--hash=sha256:e732a75fcb6b84872b168f972fbbf3749baf76308635f14015d1d35ed0c5719c \ --hash=sha256:79b471fdd1d3a92b80907828eaa809f6e34023583488b1b8dc3f951529e7a2f8 \
--hash=sha256:ed4034939d8566ccdcd3b4e4f23c63fd807fb8763ae5668d59a19e11640a8242 --hash=sha256:79b471fdd1d3a92b80907828eaa809f6e34023583488b1b8dc3f951529e7a2f8 \
--hash=sha256:7a9f7f7c9ded817f8476b5d6762a831034a8c54783f6175a928f61d57bec78cb \
--hash=sha256:7a9f7f7c9ded817f8476b5d6762a831034a8c54783f6175a928f61d57bec78cb \
--hash=sha256:80fff2251d0e73714435b5860ce200990dffe85466dd91d08d75c4d64ee9967d \
--hash=sha256:80fff2251d0e73714435b5860ce200990dffe85466dd91d08d75c4d64ee9967d \
--hash=sha256:9877303e8c72d7aa2467d1af12e56d67b8fb50d324eda5848e0ec5ee2176aac5 \
--hash=sha256:9877303e8c72d7aa2467d1af12e56d67b8fb50d324eda5848e0ec5ee2176aac5 \
--hash=sha256:99b5b4b6fe232f2234bcec2bdd533b7ab7030b3ce6cfb8bd7153bf02441c8520 \
--hash=sha256:99b5b4b6fe232f2234bcec2bdd533b7ab7030b3ce6cfb8bd7153bf02441c8520 \
--hash=sha256:9bb6faa4ae0268204cfa6b0c0de1c9121eb606eea8c66c7d7ce62e89a17f9efa \
--hash=sha256:9bb6faa4ae0268204cfa6b0c0de1c9121eb606eea8c66c7d7ce62e89a17f9efa \
--hash=sha256:a1ef656421d01851618d0fe5518e57469159c14a48e05125f7bd3225631952f9 \
--hash=sha256:a1ef656421d01851618d0fe5518e57469159c14a48e05125f7bd3225631952f9 \
--hash=sha256:a3961102a6ba8250670814bd1086139fb918e03bf146ef85dc8b6084a9b027d1 \
--hash=sha256:a3961102a6ba8250670814bd1086139fb918e03bf146ef85dc8b6084a9b027d1 \
--hash=sha256:a603584ceee175036e1bccdbe65d551c0fff67343fd506bfa6cec52bc64d9a75 \
--hash=sha256:a603584ceee175036e1bccdbe65d551c0fff67343fd506bfa6cec52bc64d9a75 \
--hash=sha256:abd585642d1fb169dd014e1037dcebe21d12a06e71b6d8f0e37650a81d57d1f1 \
--hash=sha256:abd585642d1fb169dd014e1037dcebe21d12a06e71b6d8f0e37650a81d57d1f1 \
--hash=sha256:ac68a6ac9c0e74f6f3e476f5ac2271b1df7b349a4021b6b2cdfe32cc8fa4beec \
--hash=sha256:ac68a6ac9c0e74f6f3e476f5ac2271b1df7b349a4021b6b2cdfe32cc8fa4beec \
--hash=sha256:ac92a5d0104640e0ff6fd9ef82441005e08fef460fccd790240d3e39abd69ad3 \
--hash=sha256:ac92a5d0104640e0ff6fd9ef82441005e08fef460fccd790240d3e39abd69ad3 \
--hash=sha256:ae88c3aaab5704adfdbce913b3a18db1eb96c49c970657cc01c0d1c420ffdec3 \
--hash=sha256:ae88c3aaab5704adfdbce913b3a18db1eb96c49c970657cc01c0d1c420ffdec3 \
--hash=sha256:b3f306f5aef47336b8299d8dbee31fa0b2eba4579f9f41396070f7a97d0dcd49 \
--hash=sha256:b3f306f5aef47336b8299d8dbee31fa0b2eba4579f9f41396070f7a97d0dcd49 \
--hash=sha256:b4be73fbde421d6188300e02ad92d2d5435c708a35ede8124ebdf6b00330d7cb \
--hash=sha256:b4be73fbde421d6188300e02ad92d2d5435c708a35ede8124ebdf6b00330d7cb \
--hash=sha256:bc8d0a75a43a45349b37186ecce5ae028325ede47cbc217802ff3ef4db3f3cb9 \
--hash=sha256:bc8d0a75a43a45349b37186ecce5ae028325ede47cbc217802ff3ef4db3f3cb9 \
--hash=sha256:c4533780d91f547b841f2522a419da9f2cee2f906dbd4aa58083bc944526a45c \
--hash=sha256:c4533780d91f547b841f2522a419da9f2cee2f906dbd4aa58083bc944526a45c \
--hash=sha256:d360d4adfb53d3adeca398c225cb7e2a73a2246414455937082a1fa19bd8572b \
--hash=sha256:d360d4adfb53d3adeca398c225cb7e2a73a2246414455937082a1fa19bd8572b \
--hash=sha256:d4a7ee007c6b55f7621330aee8330ef2dafa4225fce554064571ca826beafe7e \
--hash=sha256:d4a7ee007c6b55f7621330aee8330ef2dafa4225fce554064571ca826beafe7e \
--hash=sha256:d586bb09f146235f82de624ff05fbca76f8aadc627eb9a072df1899317a1a9eb \
--hash=sha256:d586bb09f146235f82de624ff05fbca76f8aadc627eb9a072df1899317a1a9eb \
--hash=sha256:d862f023f56a49c06576be41dfaf213c9ac77e7a344e7f204278c365bb36d00e \
--hash=sha256:d862f023f56a49c06576be41dfaf213c9ac77e7a344e7f204278c365bb36d00e \
--hash=sha256:df4a8d7fef3ffe90e572400d47392ea480120e339c292f802830ed09d449e622 \
--hash=sha256:df4a8d7fef3ffe90e572400d47392ea480120e339c292f802830ed09d449e622 \
--hash=sha256:e2bf1d07c4f97afeb957f626b8c3ebb8cef300efa0cb95599e936c69a66a1b17 \
--hash=sha256:e2bf1d07c4f97afeb957f626b8c3ebb8cef300efa0cb95599e936c69a66a1b17 \
--hash=sha256:ea2d65749c4c6a35a3ba138debda2f910713a9f4f06b4647510c184c284d7c62 \
--hash=sha256:ea2d65749c4c6a35a3ba138debda2f910713a9f4f06b4647510c184c284d7c62 \
--hash=sha256:ed63cbd87dd69ebcf3a9f82d87b67818c9a7d656325dd4fb34d6c4dfbaa84017 \
--hash=sha256:ed63cbd87dd69ebcf3a9f82d87b67818c9a7d656325dd4fb34d6c4dfbaa84017 \
--hash=sha256:eee89c268a35f8a08a8e9abef6f466b97577e94f5cac8bf32c25e97cd5020097 \
--hash=sha256:eee89c268a35f8a08a8e9abef6f466b97577e94f5cac8bf32c25e97cd5020097 \
--hash=sha256:f3fac9ae679f66585925cc00c5f6839ae36c1d03157619571dee18acc05b9c01 \
--hash=sha256:f3fac9ae679f66585925cc00c5f6839ae36c1d03157619571dee18acc05b9c01
# via # via
# -r src/backend/requirements.in # -r src/backend/requirements.in
# python3-saml # python3-saml
+22
View File
@@ -7,6 +7,28 @@ import { cancelEvent } from './Events';
export const getBaseUrl = (): string => export const getBaseUrl = (): string =>
(window as any).INVENTREE_SETTINGS?.base_url || 'web'; (window as any).INVENTREE_SETTINGS?.base_url || 'web';
/**
* Returns the overview URL for a given model type.
* This is the UI URL, not the API URL.
*/
export function getOverviewUrl(model: ModelType, absolute?: boolean): string {
const modelInfo = ModelInformationDict[model];
if (modelInfo?.url_overview) {
const url = modelInfo.url_overview;
const base = getBaseUrl();
if (absolute && base) {
return `/${base}${url}`;
} else {
return url;
}
}
console.error(`No overview URL found for model ${model}`);
return '';
}
/** /**
* Returns the detail view URL for a given model type. * Returns the detail view URL for a given model type.
* This is the UI URL, not the API URL. * This is the UI URL, not the API URL.
+1
View File
@@ -7,6 +7,7 @@
[build.environment] [build.environment]
VITE_DEMO = "true" VITE_DEMO = "true"
PYTHON_VERSION = "3.11"
# Send requests to subpath # Send requests to subpath
+18 -11
View File
@@ -24,11 +24,7 @@ import { type NavigateFunction, useNavigate } from 'react-router-dom';
import { isTrue } from '@lib/functions/Conversion'; import { isTrue } from '@lib/functions/Conversion';
import { getDetailUrl } from '@lib/functions/Navigation'; import { getDetailUrl } from '@lib/functions/Navigation';
import type { import type { ApiFormFieldSet, ApiFormProps } from '@lib/types/Forms';
ApiFormFieldSet,
ApiFormFieldType,
ApiFormProps
} from '@lib/types/Forms';
import { useApi } from '../../contexts/ApiContext'; import { useApi } from '../../contexts/ApiContext';
import { import {
type NestedDict, type NestedDict,
@@ -46,9 +42,11 @@ import { ApiFormField } from './fields/ApiFormField';
export function OptionsApiForm({ export function OptionsApiForm({
props: _props, props: _props,
opened,
id: pId id: pId
}: Readonly<{ }: Readonly<{
props: ApiFormProps; props: ApiFormProps;
opened?: boolean;
id?: string; id?: string;
}>) { }>) {
const api = useApi(); const api = useApi();
@@ -75,24 +73,26 @@ export function OptionsApiForm({
); );
const optionsQuery = useQuery({ const optionsQuery = useQuery({
enabled: true, enabled: opened !== false && props.ignorePermissionCheck !== true,
refetchOnMount: false, refetchOnMount: false,
queryKey: [ queryKey: [
'form-options-data', 'form-options-data',
id, id,
opened,
props.ignorePermissionCheck,
props.method, props.method,
props.url, props.url,
props.pk, props.pk,
props.pathParams props.pathParams
], ],
queryFn: async () => { queryFn: async () => {
const response = await api.options(url); if (props.ignorePermissionCheck === true || opened === false) {
let fields: Record<string, ApiFormFieldType> | null = {}; return {};
if (!props.ignorePermissionCheck) {
fields = extractAvailableFields(response, props.method);
} }
return fields; return api.options(url).then((response: any) => {
return extractAvailableFields(response, props.method);
});
}, },
throwOnError: (error: any) => { throwOnError: (error: any) => {
if (error.response) { if (error.response) {
@@ -110,6 +110,13 @@ export function OptionsApiForm({
} }
}); });
// Refetch form options whenever the modal is opened
useEffect(() => {
if (opened !== false) {
optionsQuery.refetch();
}
}, [opened]);
const formProps: ApiFormProps = useMemo(() => { const formProps: ApiFormProps = useMemo(() => {
const _props = { ...props }; const _props = { ...props };
@@ -62,6 +62,13 @@ export function RelatedModelField({
const [isOpen, setIsOpen] = useState<boolean>(false); const [isOpen, setIsOpen] = useState<boolean>(false);
const [autoFilled, setAutoFilled] = useState<boolean>(false);
useEffect(() => {
// Reset auto-fill status when the form is reconstructed
setAutoFilled(false);
}, []);
// Auto-fill the field with data from the API // Auto-fill the field with data from the API
useEffect(() => { useEffect(() => {
// If there is *no value defined*, and autoFill is enabled, then fetch data from the API // If there is *no value defined*, and autoFill is enabled, then fetch data from the API
@@ -69,10 +76,17 @@ export function RelatedModelField({
return; return;
} }
// Return if the autofill has already been performed
if (autoFilled) {
return;
}
if (field.value != undefined) { if (field.value != undefined) {
return; return;
} }
setAutoFilled(true);
// Construct parameters for auto-filling the field // Construct parameters for auto-filling the field
const params = { const params = {
...(definition?.filters ?? {}), ...(definition?.filters ?? {}),
@@ -114,6 +128,7 @@ export function RelatedModelField({
} }
}); });
}, [ }, [
autoFilled,
definition.autoFill, definition.autoFill,
definition.api_url, definition.api_url,
definition.filters, definition.filters,
@@ -395,7 +410,14 @@ export function RelatedModelField({
menuPortalTarget={document.body} menuPortalTarget={document.body}
noOptionsMessage={() => t`No results found`} noOptionsMessage={() => t`No results found`}
menuPosition='fixed' menuPosition='fixed'
styles={{ menuPortal: (base: any) => ({ ...base, zIndex: 9999 }) }} styles={{
menuPortal: (base: any) => ({ ...base, zIndex: 9999 }),
clearIndicator: (base: any) => ({
...base,
color: 'red',
':hover': { color: 'red' }
})
}}
formatOptionLabel={(option: any) => formatOption(option)} formatOptionLabel={(option: any) => formatOption(option)}
theme={(theme) => { theme={(theme) => {
return { return {
@@ -1,6 +1,5 @@
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { TextInput, Tooltip } from '@mantine/core'; import { TextInput, Tooltip } from '@mantine/core';
import { useDebouncedValue } from '@mantine/hooks';
import { IconCopyCheck, IconX } from '@tabler/icons-react'; import { IconCopyCheck, IconX } from '@tabler/icons-react';
import { import {
type ReactNode, type ReactNode,
@@ -40,30 +39,26 @@ export default function TextField({
const { value } = useMemo(() => field, [field]); const { value } = useMemo(() => field, [field]);
const [rawText, setRawText] = useState<string>(value || ''); const [textValue, setTextValue] = useState<string>(value || '');
const [debouncedText] = useDebouncedValue(rawText, 100); const onTextChange = useCallback(
(value: any) => {
setTextValue(value);
onChange(value);
},
[onChange]
);
useEffect(() => { useEffect(() => {
setRawText(value || ''); setTextValue(value || '');
}, [value]); }, [value]);
const onTextChange = useCallback((value: any) => {
setRawText(value);
}, []);
useEffect(() => {
if (debouncedText !== value) {
onChange(debouncedText);
}
}, [debouncedText]);
// Construct a "right section" for the text field // Construct a "right section" for the text field
const textFieldRightSection: ReactNode = useMemo(() => { const textFieldRightSection: ReactNode = useMemo(() => {
if (definition.rightSection) { if (definition.rightSection) {
// Use the specified override value // Use the specified override value
return definition.rightSection; return definition.rightSection;
} else if (value) { } else if (textValue) {
if (!definition.required && !definition.disabled) { if (!definition.required && !definition.disabled) {
// Render a button to clear the text field // Render a button to clear the text field
return ( return (
@@ -78,7 +73,7 @@ export default function TextField({
); );
} }
} else if ( } else if (
!value && !textValue &&
definition.placeholder && definition.placeholder &&
placeholderAutofill && placeholderAutofill &&
!definition.disabled !definition.disabled
@@ -94,7 +89,7 @@ export default function TextField({
</Tooltip> </Tooltip>
); );
} }
}, [placeholderAutofill, definition, value]); }, [placeholderAutofill, definition, textValue]);
return ( return (
<TextInput <TextInput
@@ -103,19 +98,19 @@ export default function TextField({
id={fieldId} id={fieldId}
aria-label={`text-field-${field.name}`} aria-label={`text-field-${field.name}`}
type={definition.field_type} type={definition.field_type}
value={rawText || ''} value={textValue || ''}
error={definition.error ?? error?.message} error={definition.error ?? error?.message}
radius='sm' radius='sm'
onChange={(event) => onTextChange(event.currentTarget.value)} onChange={(event) => onTextChange(event.currentTarget.value)}
onBlur={(event) => { onBlur={(event) => {
if (event.currentTarget.value != value) { if (event.currentTarget.value != textValue) {
onChange(event.currentTarget.value); onTextChange(event.currentTarget.value);
} }
}} }}
onKeyDown={(event) => { onKeyDown={(event) => {
if (event.code === 'Enter') { if (event.code === 'Enter') {
// Bypass debounce on enter key // Bypass debounce on enter key
onChange(event.currentTarget.value); onTextChange(event.currentTarget.value);
} }
onKeyDown(event.code); onKeyDown(event.code);
}} }}
@@ -16,6 +16,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { apiUrl } from '@lib/functions/Api'; import { apiUrl } from '@lib/functions/Api';
import type { ApiFormFieldType } from '@lib/types/Forms'; import type { ApiFormFieldType } from '@lib/types/Forms';
import { useDebouncedValue } from '@mantine/hooks';
import { useApi } from '../../contexts/ApiContext'; import { useApi } from '../../contexts/ApiContext';
import type { ImportSessionState } from '../../hooks/UseImportSession'; import type { ImportSessionState } from '../../hooks/UseImportSession';
import { StandaloneField } from '../forms/StandaloneField'; import { StandaloneField } from '../forms/StandaloneField';
@@ -83,6 +84,14 @@ function ImporterDefaultField({
}) { }) {
const api = useApi(); const api = useApi();
const [rawValue, setRawValue] = useState<any>('');
const fieldType: string = useMemo(() => {
return session.availableFields[fieldName]?.type;
}, [fieldName, session.availableFields]);
const [value] = useDebouncedValue(rawValue, fieldType == 'string' ? 500 : 10);
const onChange = useCallback( const onChange = useCallback(
(value: any) => { (value: any) => {
// Update the default value for the field // Update the default value for the field
@@ -105,6 +114,11 @@ function ImporterDefaultField({
[fieldName, session, session.fieldDefaults] [fieldName, session, session.fieldDefaults]
); );
// Update the default value after the debounced value changes
useEffect(() => {
onChange(value);
}, [value]);
const fieldDef: ApiFormFieldType = useMemo(() => { const fieldDef: ApiFormFieldType = useMemo(() => {
let def: any = session.availableFields[fieldName]; let def: any = session.availableFields[fieldName];
@@ -114,7 +128,10 @@ function ImporterDefaultField({
value: session.fieldDefaults[fieldName], value: session.fieldDefaults[fieldName],
field_type: def.type, field_type: def.type,
description: def.help_text, description: def.help_text,
onValueChange: onChange required: false,
onValueChange: (value: string) => {
setRawValue(value);
}
}; };
} }
@@ -215,7 +215,7 @@ export function RenderInlineModel({
return ( return (
<Group gap='xs' justify='space-between' title={tooltip}> <Group gap='xs' justify='space-between' title={tooltip}>
<Group gap='xs' justify='left' wrap='nowrap'> <Group gap='xs' justify='left'>
{prefix} {prefix}
{image && <Thumbnail src={image} size={18} />} {image && <Thumbnail src={image} size={18} />}
{url ? ( {url ? (
+3 -3
View File
@@ -517,7 +517,7 @@ function BuildAllocateLineRow({
field_type: 'related field', field_type: 'related field',
api_url: apiUrl(ApiEndpoints.stock_item_list), api_url: apiUrl(ApiEndpoints.stock_item_list),
model: ModelType.stockitem, model: ModelType.stockitem,
autoFill: !!output?.serial, autoFill: !output || !!output?.serial,
autoFillFilters: { autoFillFilters: {
serial: output?.serial serial: output?.serial
}, },
@@ -814,7 +814,7 @@ export function useConsumeBuildItemsForm({
url: ApiEndpoints.build_order_consume, url: ApiEndpoints.build_order_consume,
pk: buildId, pk: buildId,
title: t`Consume Stock`, title: t`Consume Stock`,
successMessage: t`Stock items consumed`, successMessage: t`Stock items scheduled to be consumed`,
onFormSuccess: onFormSuccess, onFormSuccess: onFormSuccess,
size: '80%', size: '80%',
fields: consumeFields, fields: consumeFields,
@@ -915,7 +915,7 @@ export function useConsumeBuildLinesForm({
url: ApiEndpoints.build_order_consume, url: ApiEndpoints.build_order_consume,
pk: buildId, pk: buildId,
title: t`Consume Stock`, title: t`Consume Stock`,
successMessage: t`Stock items consumed`, successMessage: t`Stock items scheduled to be consumed`,
onFormSuccess: onFormSuccess, onFormSuccess: onFormSuccess,
fields: consumeFields, fields: consumeFields,
initialData: { initialData: {
+46 -29
View File
@@ -20,6 +20,9 @@ export function usePartFields({
}): ApiFormFieldSet { }): ApiFormFieldSet {
const settings = useGlobalSettingsState(); const settings = useGlobalSettingsState();
const [virtual, setVirtual] = useState<boolean>(false);
const [purchaseable, setPurchaseable] = useState<boolean>(false);
return useMemo(() => { return useMemo(() => {
const fields: ApiFormFieldSet = { const fields: ApiFormFieldSet = {
category: { category: {
@@ -62,9 +65,19 @@ export function usePartFields({
is_template: {}, is_template: {},
testable: {}, testable: {},
trackable: {}, trackable: {},
purchaseable: {}, purchaseable: {
value: purchaseable,
onValueChange: (value: boolean) => {
setPurchaseable(value);
}
},
salable: {}, salable: {},
virtual: {}, virtual: {
value: virtual,
onValueChange: (value: boolean) => {
setVirtual(value);
}
},
locked: {}, locked: {},
active: {}, active: {},
starred: { starred: {
@@ -80,33 +93,37 @@ export function usePartFields({
if (create) { if (create) {
fields.copy_category_parameters = {}; fields.copy_category_parameters = {};
fields.initial_stock = { if (!virtual) {
icon: <IconPackages />, fields.initial_stock = {
children: { icon: <IconPackages />,
quantity: { children: {
value: 0 quantity: {
}, value: 0
location: {} },
} location: {}
}; }
};
}
fields.initial_supplier = { if (purchaseable) {
icon: <IconBuildingStore />, fields.initial_supplier = {
children: { icon: <IconBuildingStore />,
supplier: { children: {
filters: { supplier: {
is_supplier: true filters: {
} is_supplier: true
}, }
sku: {}, },
manufacturer: { sku: {},
filters: { manufacturer: {
is_manufacturer: true filters: {
} is_manufacturer: true
}, }
mpn: {} },
} mpn: {}
}; }
};
}
} }
// Additional fields for part duplication // Additional fields for part duplication
@@ -159,7 +176,7 @@ export function usePartFields({
} }
return fields; return fields;
}, [create, duplicatePartInstance, settings]); }, [virtual, purchaseable, create, duplicatePartInstance, settings]);
} }
/** /**
+5 -1
View File
@@ -125,8 +125,11 @@ export function useSalesOrderLineItemFields({
) )
.sort((a: any, b: any) => a.quantity - b.quantity); .sort((a: any, b: any) => a.quantity - b.quantity);
if (applicablePriceBreaks.length) if (applicablePriceBreaks.length) {
setSalePrice(applicablePriceBreaks[0].price); setSalePrice(applicablePriceBreaks[0].price);
} else {
setSalePrice('');
}
}, [part, quantity, partCurrency, create]); }, [part, quantity, partCurrency, create]);
return useMemo(() => { return useMemo(() => {
@@ -184,6 +187,7 @@ function SalesOrderAllocateLineRow({
field_type: 'related field', field_type: 'related field',
api_url: apiUrl(ApiEndpoints.stock_item_list), api_url: apiUrl(ApiEndpoints.stock_item_list),
model: ModelType.stockitem, model: ModelType.stockitem,
autoFill: true,
filters: { filters: {
available: true, available: true,
part_detail: true, part_detail: true,
+6 -2
View File
@@ -1,7 +1,7 @@
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { Alert, Divider, Stack } from '@mantine/core'; import { Alert, Divider, Stack } from '@mantine/core';
import { useId } from '@mantine/hooks'; import { useId } from '@mantine/hooks';
import { useEffect, useMemo, useRef } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import type { import type {
ApiFormModalProps, ApiFormModalProps,
@@ -50,14 +50,18 @@ export function useApiFormModal(props: ApiFormModalProps) {
[props] [props]
); );
const [isOpen, setIsOpen] = useState<boolean>(false);
const modal = useModal({ const modal = useModal({
id: modalId, id: modalId,
title: formProps.title, title: formProps.title,
onOpen: () => { onOpen: () => {
setIsOpen(true);
modalState.setModalOpen(modalId, true); modalState.setModalOpen(modalId, true);
formProps.onOpen?.(); formProps.onOpen?.();
}, },
onClose: () => { onClose: () => {
setIsOpen(false);
modalState.setModalOpen(modalId, false); modalState.setModalOpen(modalId, false);
formProps.onClose?.(); formProps.onClose?.();
}, },
@@ -66,7 +70,7 @@ export function useApiFormModal(props: ApiFormModalProps) {
children: ( children: (
<Stack gap={'xs'}> <Stack gap={'xs'}>
<Divider /> <Divider />
<OptionsApiForm props={formProps} id={modalId} /> <OptionsApiForm props={formProps} id={modalId} opened={isOpen} />
</Stack> </Stack>
) )
}); });
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

Some files were not shown because too many files have changed in this diff Show More