Merge branch 'master' into block-notes
This commit is contained in:
commit
fd3d64de99
|
|
@ -174,7 +174,7 @@ jobs:
|
|||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # pin@v4.0.0
|
||||
- name: Set up cosign
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # pin@v4.1.1
|
||||
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # pin@v4.1.2
|
||||
- name: Check if Dockerhub login is required
|
||||
id: docker_login
|
||||
run: |
|
||||
|
|
|
|||
|
|
@ -222,7 +222,7 @@ jobs:
|
|||
echo "Downloaded api.yaml"
|
||||
- name: Running OpenAPI Spec diff action
|
||||
id: breaking_changes
|
||||
uses: oasdiff/oasdiff-action/diff@e7609f171f253c3e2be2a2e5c508f749ca476a07 # pin@main
|
||||
uses: oasdiff/oasdiff-action/diff@6147a58e5d1249a12f42fc864ab791d571a30015 # pin@main
|
||||
with:
|
||||
base: "api.yaml"
|
||||
revision: "src/backend/InvenTree/schema.yml"
|
||||
|
|
@ -363,7 +363,7 @@ jobs:
|
|||
pip install .
|
||||
if: needs.paths-filter.outputs.submit-performance == 'true'
|
||||
- name: Performance Reporting
|
||||
uses: CodSpeedHQ/action@c381be0bfd20e844fb45594f6aa182ffcd94545c # pin@v4.15.0
|
||||
uses: CodSpeedHQ/action@3194d9a39c4d46684cb44bf7207fc56626aad8fd # pin@v4.15.1
|
||||
# check if we are in inventree/inventree - reporting only works in that OIDC context
|
||||
if: github.repository == 'inventree/InvenTree' && needs.paths-filter.outputs.submit-performance == 'true'
|
||||
with:
|
||||
|
|
@ -454,7 +454,7 @@ jobs:
|
|||
env:
|
||||
node_version: '>=24'
|
||||
- name: Performance Reporting
|
||||
uses: CodSpeedHQ/action@c381be0bfd20e844fb45594f6aa182ffcd94545c # pin@v4.15.0
|
||||
uses: CodSpeedHQ/action@3194d9a39c4d46684cb44bf7207fc56626aad8fd # pin@v4.15.1
|
||||
with:
|
||||
mode: walltime
|
||||
run: inv dev.test --pytest
|
||||
|
|
|
|||
|
|
@ -67,6 +67,6 @@ jobs:
|
|||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||
uses: github/codeql-action/upload-sarif@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
|
|
|||
|
|
@ -209,6 +209,7 @@ Configuration of stock item options
|
|||
| Name | Description | Default | Units |
|
||||
| ---- | ----------- | ------- | ----- |
|
||||
{{ globalsetting("SERIAL_NUMBER_GLOBALLY_UNIQUE") }}
|
||||
{{ globalsetting("STOCK_ALLOW_EDIT_SERIAL") }}
|
||||
{{ globalsetting("STOCK_ALLOW_DELETE_SERIALIZED") }}
|
||||
{{ globalsetting("STOCK_DELETE_DEPLETED_DEFAULT") }}
|
||||
{{ globalsetting("STOCK_BATCH_CODE_TEMPLATE") }}
|
||||
|
|
|
|||
|
|
@ -142,6 +142,8 @@ Note that any serial number adjustments are subject to the same validation rules
|
|||
|
||||
{{ image("stock/serial_edit_error.png", title="Error while editing a serial number") }}
|
||||
|
||||
!!! info "Disable Serial Number Editing"
|
||||
If you wish to prevent users from editing serial numbers, this can be achieved by disabling the `Allow Edit Serial Number` setting in the system settings view.
|
||||
|
||||
#### Plugin Support
|
||||
|
||||
|
|
|
|||
|
|
@ -480,9 +480,9 @@ pygments==2.20.0 \
|
|||
# via
|
||||
# mkdocs-material
|
||||
# rich
|
||||
pymdown-extensions==10.21.2 \
|
||||
--hash=sha256:5c0fd2a2bea14eb39af8ff284f1066d898ab2187d81b889b75d46d4348c01638 \
|
||||
--hash=sha256:c3f55a5b8a1d0edf6699e35dcbea71d978d34ff3fa79f3d807b8a5b3fa90fbdc
|
||||
pymdown-extensions==10.21.3 \
|
||||
--hash=sha256:72cfcf55f07aea0d4af2c4f11dd4e52466ddfb1bb819673146398e0bd3a77354 \
|
||||
--hash=sha256:d7a5d08014fc571e80ca21dd6f854e31f94c489800350564d55d15b3c41e76b6
|
||||
# via
|
||||
# mkdocs-material
|
||||
# mkdocs-mermaid2-plugin
|
||||
|
|
|
|||
|
|
@ -696,6 +696,12 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
|
|||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
'STOCK_ALLOW_EDIT_SERIAL': {
|
||||
'name': _('Allow Edit Serial Number'),
|
||||
'description': _('Allow editing of serial number for stock items'),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
'STOCK_ALLOW_DELETE_SERIALIZED': {
|
||||
'name': _('Delete Serialized Stock'),
|
||||
'description': _('Allow deletion of stock items which have a serial number'),
|
||||
|
|
|
|||
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
|
|
@ -2,14 +2,13 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import decimal
|
||||
import hashlib
|
||||
import inspect
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from decimal import ROUND_HALF_UP, Decimal, InvalidOperation
|
||||
from typing import TypedDict, cast
|
||||
|
||||
from django.conf import settings
|
||||
|
|
@ -2053,8 +2052,9 @@ class Part(
|
|||
'part', 'sub_part'
|
||||
)
|
||||
|
||||
for item in bom_items:
|
||||
item.validate_hash(valid=valid)
|
||||
if valid:
|
||||
for item in bom_items:
|
||||
item.validate_hash(valid=True)
|
||||
|
||||
self.bom_validated = valid
|
||||
self.bom_checksum = self.get_bom_hash() if valid else ''
|
||||
|
|
@ -2234,8 +2234,8 @@ class Part(
|
|||
logger.warning('WARNING: BomItem ID %s contains itself in BOM', item.pk)
|
||||
continue
|
||||
|
||||
q = decimal.Decimal(quantity)
|
||||
i = decimal.Decimal(item.quantity)
|
||||
q = Decimal(quantity)
|
||||
i = Decimal(item.quantity)
|
||||
|
||||
prices = item.sub_part.get_price_range(
|
||||
q * i, internal=internal, purchase=purchase
|
||||
|
|
@ -3935,14 +3935,18 @@ class BomItem(InvenTree.models.MetadataMixin, InvenTree.models.InvenTreeModel):
|
|||
'raw_amount': _('Quantity must be greater than zero')
|
||||
})
|
||||
|
||||
self.quantity = Decimal(quantity.magnitude)
|
||||
# Normalize the quantity, to maximum 5 decimal places
|
||||
quantity = Decimal(quantity.magnitude)
|
||||
|
||||
except ValidationError as e:
|
||||
raise ValidationError({'raw_amount': e.messages})
|
||||
|
||||
# Ensure that the raw_amount is converted to a Decimal value
|
||||
# and quantized to a maximum of 5 decimal places (to avoid floating point issues)
|
||||
try:
|
||||
self.quantity = Decimal(self.quantity)
|
||||
self.quantity = Decimal(quantity).quantize(
|
||||
Decimal('0.00001'), rounding=ROUND_HALF_UP
|
||||
)
|
||||
except InvalidOperation:
|
||||
msg = _('Invalid quantity provided')
|
||||
raise ValidationError({'quantity': msg, 'raw_amount': msg})
|
||||
|
|
|
|||
|
|
@ -450,6 +450,81 @@ class StockItem(
|
|||
|
||||
order_insertion_by = ['part']
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Save this StockItem to the database.
|
||||
|
||||
Performs a number of checks:
|
||||
- Unique serial number requirement
|
||||
- Adds a transaction note when the item is first created.
|
||||
"""
|
||||
self.validate_unique()
|
||||
self.clean()
|
||||
self.update_serial_number()
|
||||
|
||||
user = kwargs.pop('user', None)
|
||||
|
||||
if user is None:
|
||||
user = getattr(self, '_user', None)
|
||||
|
||||
# If 'add_note = False' specified, then no tracking note will be added for item creation
|
||||
add_note = kwargs.pop('add_note', True)
|
||||
|
||||
notes = kwargs.pop('notes', '')
|
||||
|
||||
if self.pk:
|
||||
# StockItem has already been saved
|
||||
|
||||
# Check if "interesting" fields have been changed
|
||||
# (we wish to record these as historical records)
|
||||
|
||||
try:
|
||||
old = StockItem.objects.get(pk=self.pk)
|
||||
old_custom_status = old.get_custom_status()
|
||||
custom_status = self.get_custom_status()
|
||||
|
||||
deltas = {}
|
||||
|
||||
# Status changed?
|
||||
if old.status != self.status:
|
||||
# Custom status changed?
|
||||
# Matches custom status tracking behavior of StockChangeStatusSerializer
|
||||
if old_custom_status != custom_status:
|
||||
deltas['status'] = custom_status
|
||||
deltas['status_logical'] = self.status
|
||||
else:
|
||||
deltas['status'] = self.status
|
||||
deltas['status_logical'] = self.status
|
||||
|
||||
if old_custom_status:
|
||||
deltas['old_status'] = old_custom_status
|
||||
deltas['old_status_logical'] = old.status
|
||||
else:
|
||||
deltas['old_status'] = old.status
|
||||
deltas['old_status_logical'] = old.status
|
||||
|
||||
if add_note and len(deltas) > 0:
|
||||
self.add_tracking_entry(
|
||||
StockHistoryCode.EDITED, user, deltas=deltas, notes=notes
|
||||
)
|
||||
|
||||
except (ValueError, StockItem.DoesNotExist):
|
||||
pass
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
# If user information is provided, and no existing note exists, create one!
|
||||
if add_note and self.tracking_info.count() == 0:
|
||||
tracking_info = {'status': self.status}
|
||||
|
||||
self.add_tracking_entry(
|
||||
StockHistoryCode.CREATED,
|
||||
user,
|
||||
deltas=tracking_info,
|
||||
notes=notes,
|
||||
location=self.location,
|
||||
quantity=float(self.quantity),
|
||||
)
|
||||
|
||||
def delete(self, ignore_serial_check: bool = False, **kwargs):
|
||||
"""Custom delete method for StockItem model.
|
||||
|
||||
|
|
@ -785,82 +860,6 @@ class StockItem(
|
|||
"""Return the 'previous' stock item (based on serial number)."""
|
||||
return self.get_next_serialized_item(reverse=True)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Save this StockItem to the database.
|
||||
|
||||
Performs a number of checks:
|
||||
- Unique serial number requirement
|
||||
- Adds a transaction note when the item is first created.
|
||||
"""
|
||||
self.validate_unique()
|
||||
self.clean()
|
||||
|
||||
self.update_serial_number()
|
||||
|
||||
user = kwargs.pop('user', None)
|
||||
|
||||
if user is None:
|
||||
user = getattr(self, '_user', None)
|
||||
|
||||
# If 'add_note = False' specified, then no tracking note will be added for item creation
|
||||
add_note = kwargs.pop('add_note', True)
|
||||
|
||||
notes = kwargs.pop('notes', '')
|
||||
|
||||
if self.pk:
|
||||
# StockItem has already been saved
|
||||
|
||||
# Check if "interesting" fields have been changed
|
||||
# (we wish to record these as historical records)
|
||||
|
||||
try:
|
||||
old = StockItem.objects.get(pk=self.pk)
|
||||
old_custom_status = old.get_custom_status()
|
||||
custom_status = self.get_custom_status()
|
||||
|
||||
deltas = {}
|
||||
|
||||
# Status changed?
|
||||
if old.status != self.status:
|
||||
# Custom status changed?
|
||||
# Matches custom status tracking behavior of StockChangeStatusSerializer
|
||||
if old_custom_status != custom_status:
|
||||
deltas['status'] = custom_status
|
||||
deltas['status_logical'] = self.status
|
||||
else:
|
||||
deltas['status'] = self.status
|
||||
deltas['status_logical'] = self.status
|
||||
|
||||
if old_custom_status:
|
||||
deltas['old_status'] = old_custom_status
|
||||
deltas['old_status_logical'] = old.status
|
||||
else:
|
||||
deltas['old_status'] = old.status
|
||||
deltas['old_status_logical'] = old.status
|
||||
|
||||
if add_note and len(deltas) > 0:
|
||||
self.add_tracking_entry(
|
||||
StockHistoryCode.EDITED, user, deltas=deltas, notes=notes
|
||||
)
|
||||
|
||||
except (ValueError, StockItem.DoesNotExist):
|
||||
pass
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
# If user information is provided, and no existing note exists, create one!
|
||||
if add_note and self.tracking_info.count() == 0:
|
||||
tracking_info = {'status': self.status}
|
||||
|
||||
self.add_tracking_entry(
|
||||
StockHistoryCode.CREATED,
|
||||
user,
|
||||
deltas=tracking_info,
|
||||
notes=notes,
|
||||
location=self.location,
|
||||
quantity=float(self.quantity),
|
||||
)
|
||||
|
||||
@property
|
||||
def status_label(self):
|
||||
"""Return label."""
|
||||
|
|
@ -937,6 +936,17 @@ class StockItem(
|
|||
if type(self.batch) is str:
|
||||
self.batch = self.batch.strip()
|
||||
|
||||
if not get_global_setting('STOCK_ALLOW_EDIT_SERIAL'):
|
||||
deltas = self.get_field_deltas()
|
||||
|
||||
# Prevent editing of serial numbers if the item already has a serial number assigned
|
||||
if 'serial' in deltas and deltas['serial']['old'] not in [None, '']:
|
||||
raise ValidationError({
|
||||
'serial': _(
|
||||
'Editing of serial numbers is not allowed - this item has already been assigned a serial number'
|
||||
)
|
||||
})
|
||||
|
||||
# Custom validation of batch code
|
||||
self.validate_batch_code()
|
||||
|
||||
|
|
|
|||
|
|
@ -1644,6 +1644,36 @@ class StockItemTest(StockAPITestCase):
|
|||
)
|
||||
self.assertEqual(trackable_part.get_stock_count(), 10)
|
||||
|
||||
def test_edit_serial(self):
|
||||
"""Test that we can edit serial numbers via the API."""
|
||||
item = StockItem.objects.create(
|
||||
part=Part.objects.filter(trackable=True).first(),
|
||||
quantity=1,
|
||||
location=StockLocation.objects.first(),
|
||||
)
|
||||
|
||||
set_global_setting('STOCK_ALLOW_EDIT_SERIAL', False)
|
||||
|
||||
url = reverse('api-stock-detail', kwargs={'pk': item.pk})
|
||||
|
||||
# Edit the serial number
|
||||
# This should succeed, as the initial serial number is blank
|
||||
response = self.patch(url, {'serial': '54321'}, expected_code=200)
|
||||
self.assertEqual(response.data['serial'], '54321')
|
||||
|
||||
# Edit it again - this time, should fail as the serial number is already set
|
||||
response = self.patch(url, {'serial': '98765'}, expected_code=400)
|
||||
self.assertIn('Editing of serial numbers is not allowed', str(response.data))
|
||||
|
||||
# Ensure that changing a different field does not cause an error
|
||||
response = self.patch(url, {'batch': 'abcde'}, expected_code=200)
|
||||
self.assertEqual(response.data['batch'], 'abcde')
|
||||
|
||||
# Adjust the setting to allow serial editing
|
||||
set_global_setting('STOCK_ALLOW_EDIT_SERIAL', True)
|
||||
response = self.patch(url, {'serial': '98765'}, expected_code=200)
|
||||
self.assertEqual(response.data['serial'], '98765')
|
||||
|
||||
def test_default_expiry(self):
|
||||
"""Test that the "default_expiry" functionality works via the API.
|
||||
|
||||
|
|
|
|||
|
|
@ -232,6 +232,7 @@ export function useStockFields({
|
|||
serial: {
|
||||
placeholderAutofill: true,
|
||||
placeholder: serialGenerator.result,
|
||||
disabled: !create && !globalSettings.isSet('STOCK_ALLOW_EDIT_SERIAL'),
|
||||
hidden:
|
||||
create ||
|
||||
partInstance.trackable == false ||
|
||||
|
|
|
|||
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
|
|
@ -251,6 +251,7 @@ export default function SystemSettings() {
|
|||
<GlobalSettingList
|
||||
keys={[
|
||||
'SERIAL_NUMBER_GLOBALLY_UNIQUE',
|
||||
'STOCK_ALLOW_EDIT_SERIAL',
|
||||
'STOCK_ALLOW_DELETE_SERIALIZED',
|
||||
'STOCK_DELETE_DEPLETED_DEFAULT',
|
||||
'STOCK_BATCH_CODE_TEMPLATE',
|
||||
|
|
|
|||
Loading…
Reference in New Issue