mirror of
https://github.com/inventree/InvenTree.git
synced 2026-05-22 01:06:50 +00:00
BOM Ruleset (#11825)
* Add BOM role * Adjust UI permissions * Adjust docs * Add data migratoin * Specify role for BOM validation * Tweak old migrati * Fix role_required * Update API version and CHANGELOG Co-authored-by: Copilot <copilot@github.com> * Update unit tests Co-authored-by: Copilot <copilot@github.com> --------- Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 480
|
||||
INVENTREE_API_VERSION = 481
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
v481 -> 2026-04-28 : https://github.com/inventree/InvenTree/pull/11825
|
||||
- Adds new "bom" ruleset and associated permissions for BOM management, separate from the "part" ruleset which remains focused on part management
|
||||
|
||||
v480 -> 2026-04-27 : https://github.com/inventree/InvenTree/pull/11816
|
||||
- The "issued_by" field on the Build API endpoint is now read-only, and is automatically set to the current user when a build is created
|
||||
|
||||
|
||||
@@ -599,6 +599,7 @@ class PartValidateBOM(RetrieveUpdateAPI):
|
||||
|
||||
queryset = Part.objects.all()
|
||||
serializer_class = part_serializers.PartBomValidateSerializer
|
||||
role_required = 'bom.change'
|
||||
|
||||
@extend_schema(
|
||||
responses={
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# Generated by Django 5.2.13 on 2026-04-27 22:33
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def update_bom_role(apps, schema_editor):
|
||||
"""Update the ruleset for the BOM role.
|
||||
|
||||
As the 'bom' role was previously covered by the 'part' role,
|
||||
we need to update the ruleset to include the correct models.
|
||||
|
||||
"""
|
||||
from django.contrib.auth.models import Group
|
||||
from users.ruleset import RuleSetEnum
|
||||
from users.models import RuleSet
|
||||
|
||||
# For each existing group, create a new 'bom' ruleset
|
||||
for group in Group.objects.all():
|
||||
# Find a matching 'part' ruleset for this group
|
||||
if ruleset := RuleSet.objects.filter(name=RuleSetEnum.PART, group=group).first():
|
||||
# A 'part' ruleset exists for this group - create a new 'bom' ruleset based on this
|
||||
ruleset.pk = None # Create a new instance
|
||||
ruleset.name = RuleSetEnum.BOM
|
||||
ruleset.save()
|
||||
|
||||
else:
|
||||
# No 'part' ruleset exists for this group - create a default 'bom' ruleset
|
||||
RuleSet.objects.create(
|
||||
name=RuleSetEnum.BOM,
|
||||
group=group
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("part", "0147_remove_part_default_supplier"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(update_bom_role, reverse_code=migrations.RunPython.noop),
|
||||
]
|
||||
@@ -1010,7 +1010,7 @@ class PartAPITest(PartAPITestBase):
|
||||
filters = {'active': True, 'assembly': True, 'bom_valid': True}
|
||||
|
||||
# Initially, there are no parts with a valid BOM
|
||||
response = self.get(url, filters)
|
||||
response = self.get(url, filters, expected_code=200)
|
||||
|
||||
self.assertEqual(len(response.data), 0)
|
||||
|
||||
@@ -1038,6 +1038,14 @@ class PartAPITest(PartAPITestBase):
|
||||
|
||||
# Test the BOM validation API endpoint
|
||||
bom_url = reverse('api-part-bom-validate', kwargs={'pk': assembly.pk})
|
||||
|
||||
# Initially, we do not have the required role permissions
|
||||
self.get(bom_url, expected_code=403)
|
||||
|
||||
# Add required role
|
||||
self.assignRole('bom.add')
|
||||
|
||||
# Now we should be able to validate the BOM via the API
|
||||
data = self.get(bom_url, expected_code=200).data
|
||||
|
||||
self.assertEqual(data['bom_validated'], True)
|
||||
@@ -2913,6 +2921,12 @@ class BomItemTest(InvenTreeAPITestCase):
|
||||
# Count first, operate directly on Model
|
||||
countbefore = BomItemSubstitute.objects.count()
|
||||
|
||||
# Initially, the user does not have the required permissions
|
||||
self.get(url, expected_code=403)
|
||||
|
||||
# Assign the permission to view the substitute list
|
||||
self.assignRole('bom.add')
|
||||
|
||||
# Now, make sure API returns the same count
|
||||
response = self.get(url, expected_code=200)
|
||||
self.assertEqual(len(response.data), countbefore)
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
# Generated by Django 4.2.12 on 2024-05-23 16:40
|
||||
|
||||
import structlog
|
||||
|
||||
from importlib import import_module
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
logger = structlog.getLogger('inventree')
|
||||
|
||||
|
||||
def clear_sessions(apps, schema_editor): # pragma: no cover
|
||||
"""Clear all user sessions."""
|
||||
|
||||
@@ -16,7 +21,7 @@ def clear_sessions(apps, schema_editor): # pragma: no cover
|
||||
try:
|
||||
engine = import_module(settings.SESSION_ENGINE)
|
||||
engine.SessionStore.clear_expired()
|
||||
print('\nCleared all user sessions to deal with GHSA-2crp-q9pc-457j')
|
||||
logger.info('Cleared all user sessions to deal with GHSA-2crp-q9pc-457j')
|
||||
except Exception:
|
||||
# Database may not be ready yet, so this does not matter anyhow
|
||||
pass
|
||||
|
||||
@@ -15,6 +15,7 @@ _roles = {
|
||||
'part': 'Role Parts',
|
||||
'stock_location': 'Role Stock Locations',
|
||||
'stock': 'Role Stock Items',
|
||||
'bom': 'Role Bills of Material',
|
||||
'build': 'Role Build Orders',
|
||||
'purchase_order': 'Role Purchase Orders',
|
||||
'sales_order': 'Role Sales Orders',
|
||||
|
||||
@@ -12,6 +12,7 @@ class RuleSetEnum(StringEnum):
|
||||
ADMIN = 'admin'
|
||||
PART_CATEGORY = 'part_category'
|
||||
PART = 'part'
|
||||
BOM = 'bom'
|
||||
STOCK_LOCATION = 'stock_location'
|
||||
STOCK = 'stock'
|
||||
BUILD = 'build'
|
||||
@@ -26,6 +27,7 @@ RULESET_CHOICES = [
|
||||
(RuleSetEnum.ADMIN, _('Admin')),
|
||||
(RuleSetEnum.PART_CATEGORY, _('Part Categories')),
|
||||
(RuleSetEnum.PART, _('Parts')),
|
||||
(RuleSetEnum.BOM, _('Bills of Material')),
|
||||
(RuleSetEnum.STOCK_LOCATION, _('Stock Locations')),
|
||||
(RuleSetEnum.STOCK, _('Stock Items')),
|
||||
(RuleSetEnum.BUILD, _('Build Orders')),
|
||||
@@ -94,6 +96,18 @@ def get_ruleset_models() -> dict:
|
||||
'django_mailbox_messageattachment',
|
||||
'django_mailbox_message',
|
||||
],
|
||||
RuleSetEnum.BOM: ['part_bomitem', 'part_bomitemsubstitute'],
|
||||
RuleSetEnum.BUILD: [
|
||||
'part_part',
|
||||
'part_partcategory',
|
||||
'part_bomitem',
|
||||
'part_bomitemsubstitute',
|
||||
'build_build',
|
||||
'build_builditem',
|
||||
'build_buildline',
|
||||
'stock_stockitem',
|
||||
'stock_stocklocation',
|
||||
],
|
||||
RuleSetEnum.PART_CATEGORY: [
|
||||
'part_partcategory',
|
||||
'part_partcategoryparametertemplate',
|
||||
@@ -102,8 +116,6 @@ def get_ruleset_models() -> dict:
|
||||
RuleSetEnum.PART: [
|
||||
'part_part',
|
||||
'part_partpricing',
|
||||
'part_bomitem',
|
||||
'part_bomitemsubstitute',
|
||||
'part_partsellpricebreak',
|
||||
'part_partinternalpricebreak',
|
||||
'part_parttesttemplate',
|
||||
@@ -120,17 +132,6 @@ def get_ruleset_models() -> dict:
|
||||
'stock_stockitemtracking',
|
||||
'stock_stockitemtestresult',
|
||||
],
|
||||
RuleSetEnum.BUILD: [
|
||||
'part_part',
|
||||
'part_partcategory',
|
||||
'part_bomitem',
|
||||
'part_bomitemsubstitute',
|
||||
'build_build',
|
||||
'build_builditem',
|
||||
'build_buildline',
|
||||
'stock_stockitem',
|
||||
'stock_stocklocation',
|
||||
],
|
||||
RuleSetEnum.PURCHASE_ORDER: [
|
||||
'company_company',
|
||||
'company_contact',
|
||||
|
||||
Reference in New Issue
Block a user