mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-03 22:55:43 +00:00 
			
		
		
		
	Merge pull request #2957 from SchrodingersGat/locate-mixin
Adds plugin mixin to "locate" items
This commit is contained in:
		@@ -44,6 +44,7 @@ from stock.models import StockItem, StockLocation
 | 
			
		||||
from common.models import InvenTreeSetting
 | 
			
		||||
from build.models import Build, BuildItem
 | 
			
		||||
import order.models
 | 
			
		||||
from plugin.serializers import MetadataSerializer
 | 
			
		||||
 | 
			
		||||
from . import serializers as part_serializers
 | 
			
		||||
 | 
			
		||||
@@ -203,6 +204,15 @@ class CategoryDetail(generics.RetrieveUpdateDestroyAPIView):
 | 
			
		||||
        return response
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CategoryMetadata(generics.RetrieveUpdateAPIView):
 | 
			
		||||
    """API endpoint for viewing / updating PartCategory metadata"""
 | 
			
		||||
 | 
			
		||||
    def get_serializer(self, *args, **kwargs):
 | 
			
		||||
        return MetadataSerializer(PartCategory, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    queryset = PartCategory.objects.all()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CategoryParameterList(generics.ListAPIView):
 | 
			
		||||
    """ API endpoint for accessing a list of PartCategoryParameterTemplate objects.
 | 
			
		||||
 | 
			
		||||
@@ -587,6 +597,17 @@ class PartScheduling(generics.RetrieveAPIView):
 | 
			
		||||
        return Response(schedule)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PartMetadata(generics.RetrieveUpdateAPIView):
 | 
			
		||||
    """
 | 
			
		||||
    API endpoint for viewing / updating Part metadata
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def get_serializer(self, *args, **kwargs):
 | 
			
		||||
        return MetadataSerializer(Part, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    queryset = Part.objects.all()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PartSerialNumberDetail(generics.RetrieveAPIView):
 | 
			
		||||
    """
 | 
			
		||||
    API endpoint for returning extra serial number information about a particular part
 | 
			
		||||
@@ -1912,7 +1933,15 @@ part_api_urls = [
 | 
			
		||||
        re_path(r'^tree/', CategoryTree.as_view(), name='api-part-category-tree'),
 | 
			
		||||
        re_path(r'^parameters/', CategoryParameterList.as_view(), name='api-part-category-parameter-list'),
 | 
			
		||||
 | 
			
		||||
        re_path(r'^(?P<pk>\d+)/?', CategoryDetail.as_view(), name='api-part-category-detail'),
 | 
			
		||||
        # Category detail endpoints
 | 
			
		||||
        re_path(r'^(?P<pk>\d+)/', include([
 | 
			
		||||
 | 
			
		||||
            re_path(r'^metadata/', CategoryMetadata.as_view(), name='api-part-category-metadata'),
 | 
			
		||||
 | 
			
		||||
            # PartCategory detail endpoint
 | 
			
		||||
            re_path(r'^.*$', CategoryDetail.as_view(), name='api-part-category-detail'),
 | 
			
		||||
        ])),
 | 
			
		||||
 | 
			
		||||
        path('', CategoryList.as_view(), name='api-part-category-list'),
 | 
			
		||||
    ])),
 | 
			
		||||
 | 
			
		||||
@@ -1973,6 +2002,9 @@ part_api_urls = [
 | 
			
		||||
        # Endpoint for validating a BOM for the specific Part
 | 
			
		||||
        re_path(r'^bom-validate/', PartValidateBOM.as_view(), name='api-part-bom-validate'),
 | 
			
		||||
 | 
			
		||||
        # Part metadata
 | 
			
		||||
        re_path(r'^metadata/', PartMetadata.as_view(), name='api-part-metadata'),
 | 
			
		||||
 | 
			
		||||
        # Part detail endpoint
 | 
			
		||||
        re_path(r'^.*$', PartDetail.as_view(), name='api-part-detail'),
 | 
			
		||||
    ])),
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										23
									
								
								InvenTree/part/migrations/0076_auto_20220516_0819.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								InvenTree/part/migrations/0076_auto_20220516_0819.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
# Generated by Django 3.2.13 on 2022-05-16 08:19
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('part', '0075_auto_20211128_0151'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='part',
 | 
			
		||||
            name='metadata',
 | 
			
		||||
            field=models.JSONField(blank=True, help_text='JSON metadata field, for use by external plugins', null=True, verbose_name='Plugin Metadata'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='partcategory',
 | 
			
		||||
            name='metadata',
 | 
			
		||||
            field=models.JSONField(blank=True, help_text='JSON metadata field, for use by external plugins', null=True, verbose_name='Plugin Metadata'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -46,29 +46,29 @@ from common.models import InvenTreeSetting
 | 
			
		||||
 | 
			
		||||
from InvenTree import helpers
 | 
			
		||||
from InvenTree import validators
 | 
			
		||||
from InvenTree.models import InvenTreeTree, InvenTreeAttachment, DataImportMixin
 | 
			
		||||
from InvenTree.fields import InvenTreeURLField
 | 
			
		||||
from InvenTree.helpers import decimal2string, normalize, decimal2money
 | 
			
		||||
 | 
			
		||||
import InvenTree.ready
 | 
			
		||||
import InvenTree.tasks
 | 
			
		||||
 | 
			
		||||
from InvenTree.fields import InvenTreeURLField
 | 
			
		||||
from InvenTree.helpers import decimal2string, normalize, decimal2money
 | 
			
		||||
from InvenTree.models import InvenTreeTree, InvenTreeAttachment, DataImportMixin
 | 
			
		||||
from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus, SalesOrderStatus
 | 
			
		||||
 | 
			
		||||
import common.models
 | 
			
		||||
from build import models as BuildModels
 | 
			
		||||
from order import models as OrderModels
 | 
			
		||||
from company.models import SupplierPart
 | 
			
		||||
import part.settings as part_settings
 | 
			
		||||
from stock import models as StockModels
 | 
			
		||||
 | 
			
		||||
import common.models
 | 
			
		||||
 | 
			
		||||
import part.settings as part_settings
 | 
			
		||||
from plugin.models import MetadataMixin
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
logger = logging.getLogger("inventree")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PartCategory(InvenTreeTree):
 | 
			
		||||
class PartCategory(MetadataMixin, InvenTreeTree):
 | 
			
		||||
    """ PartCategory provides hierarchical organization of Part objects.
 | 
			
		||||
 | 
			
		||||
    Attributes:
 | 
			
		||||
@@ -327,7 +327,7 @@ class PartManager(TreeManager):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@cleanup.ignore
 | 
			
		||||
class Part(MPTTModel):
 | 
			
		||||
class Part(MetadataMixin, MPTTModel):
 | 
			
		||||
    """ The Part object represents an abstract part, the 'concept' of an actual entity.
 | 
			
		||||
 | 
			
		||||
    An actual physical instance of a Part is a StockItem which is treated separately.
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,85 @@ import build.models
 | 
			
		||||
import order.models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PartCategoryAPITest(InvenTreeAPITestCase):
 | 
			
		||||
    """Unit tests for the PartCategory API"""
 | 
			
		||||
 | 
			
		||||
    fixtures = [
 | 
			
		||||
        'category',
 | 
			
		||||
        'part',
 | 
			
		||||
        'location',
 | 
			
		||||
        'bom',
 | 
			
		||||
        'company',
 | 
			
		||||
        'test_templates',
 | 
			
		||||
        'manufacturer_part',
 | 
			
		||||
        'supplier_part',
 | 
			
		||||
        'order',
 | 
			
		||||
        'stock',
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    roles = [
 | 
			
		||||
        'part.change',
 | 
			
		||||
        'part.add',
 | 
			
		||||
        'part.delete',
 | 
			
		||||
        'part_category.change',
 | 
			
		||||
        'part_category.add',
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    def test_category_list(self):
 | 
			
		||||
 | 
			
		||||
        # List all part categories
 | 
			
		||||
        url = reverse('api-part-category-list')
 | 
			
		||||
 | 
			
		||||
        response = self.get(url, expected_code=200)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(len(response.data), 8)
 | 
			
		||||
 | 
			
		||||
        # Filter by parent, depth=1
 | 
			
		||||
        response = self.get(
 | 
			
		||||
            url,
 | 
			
		||||
            {
 | 
			
		||||
                'parent': 1,
 | 
			
		||||
                'cascade': False,
 | 
			
		||||
            },
 | 
			
		||||
            expected_code=200
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(len(response.data), 3)
 | 
			
		||||
 | 
			
		||||
        # Filter by parent, cascading
 | 
			
		||||
        response = self.get(
 | 
			
		||||
            url,
 | 
			
		||||
            {
 | 
			
		||||
                'parent': 1,
 | 
			
		||||
                'cascade': True,
 | 
			
		||||
            },
 | 
			
		||||
            expected_code=200,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(len(response.data), 5)
 | 
			
		||||
 | 
			
		||||
    def test_category_metadata(self):
 | 
			
		||||
        """Test metadata endpoint for the PartCategory"""
 | 
			
		||||
 | 
			
		||||
        cat = PartCategory.objects.get(pk=1)
 | 
			
		||||
 | 
			
		||||
        cat.metadata = {
 | 
			
		||||
            'foo': 'bar',
 | 
			
		||||
            'water': 'melon',
 | 
			
		||||
            'abc': 'xyz',
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        cat.set_metadata('abc', 'ABC')
 | 
			
		||||
 | 
			
		||||
        response = self.get(reverse('api-part-category-metadata', kwargs={'pk': 1}), expected_code=200)
 | 
			
		||||
 | 
			
		||||
        metadata = response.data['metadata']
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(metadata['foo'], 'bar')
 | 
			
		||||
        self.assertEqual(metadata['water'], 'melon')
 | 
			
		||||
        self.assertEqual(metadata['abc'], 'ABC')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PartOptionsAPITest(InvenTreeAPITestCase):
 | 
			
		||||
    """
 | 
			
		||||
    Tests for the various OPTIONS endpoints in the /part/ API
 | 
			
		||||
@@ -1021,6 +1100,59 @@ class PartDetailTests(InvenTreeAPITestCase):
 | 
			
		||||
        self.assertEqual(data['in_stock'], 9000)
 | 
			
		||||
        self.assertEqual(data['unallocated_stock'], 9000)
 | 
			
		||||
 | 
			
		||||
    def test_part_metadata(self):
 | 
			
		||||
        """
 | 
			
		||||
        Tests for the part metadata endpoint
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        url = reverse('api-part-metadata', kwargs={'pk': 1})
 | 
			
		||||
 | 
			
		||||
        part = Part.objects.get(pk=1)
 | 
			
		||||
 | 
			
		||||
        # Metadata is initially null
 | 
			
		||||
        self.assertIsNone(part.metadata)
 | 
			
		||||
 | 
			
		||||
        part.metadata = {'foo': 'bar'}
 | 
			
		||||
        part.save()
 | 
			
		||||
 | 
			
		||||
        response = self.get(url, expected_code=200)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(response.data['metadata']['foo'], 'bar')
 | 
			
		||||
 | 
			
		||||
        # Add more data via the API
 | 
			
		||||
        # Using the 'patch' method causes the new data to be merged in
 | 
			
		||||
        self.patch(
 | 
			
		||||
            url,
 | 
			
		||||
            {
 | 
			
		||||
                'metadata': {
 | 
			
		||||
                    'hello': 'world',
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            expected_code=200
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        part.refresh_from_db()
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(part.metadata['foo'], 'bar')
 | 
			
		||||
        self.assertEqual(part.metadata['hello'], 'world')
 | 
			
		||||
 | 
			
		||||
        # Now, issue a PUT request (existing data will be replacted)
 | 
			
		||||
        self.put(
 | 
			
		||||
            url,
 | 
			
		||||
            {
 | 
			
		||||
                'metadata': {
 | 
			
		||||
                    'x': 'y'
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            expected_code=200
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        part.refresh_from_db()
 | 
			
		||||
 | 
			
		||||
        self.assertFalse('foo' in part.metadata)
 | 
			
		||||
        self.assertFalse('hello' in part.metadata)
 | 
			
		||||
        self.assertEqual(part.metadata['x'], 'y')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PartAPIAggregationTest(InvenTreeAPITestCase):
 | 
			
		||||
    """
 | 
			
		||||
 
 | 
			
		||||
@@ -199,7 +199,7 @@ class PartTest(TestCase):
 | 
			
		||||
        with self.assertRaises(ValidationError):
 | 
			
		||||
            part_2.validate_unique()
 | 
			
		||||
 | 
			
		||||
    def test_metadata(self):
 | 
			
		||||
    def test_attributes(self):
 | 
			
		||||
        self.assertEqual(self.r1.name, 'R_2K2_0805')
 | 
			
		||||
        self.assertEqual(self.r1.get_absolute_url(), '/part/3/')
 | 
			
		||||
 | 
			
		||||
@@ -245,6 +245,24 @@ class PartTest(TestCase):
 | 
			
		||||
        self.assertEqual(float(self.r1.get_internal_price(1)), 0.08)
 | 
			
		||||
        self.assertEqual(float(self.r1.get_internal_price(10)), 0.5)
 | 
			
		||||
 | 
			
		||||
    def test_metadata(self):
 | 
			
		||||
        """Unit tests for the Part metadata field"""
 | 
			
		||||
 | 
			
		||||
        p = Part.objects.get(pk=1)
 | 
			
		||||
        self.assertIsNone(p.metadata)
 | 
			
		||||
 | 
			
		||||
        self.assertIsNone(p.get_metadata('test'))
 | 
			
		||||
        self.assertEqual(p.get_metadata('test', backup_value=123), 123)
 | 
			
		||||
 | 
			
		||||
        # Test update via the set_metadata() method
 | 
			
		||||
        p.set_metadata('test', 3)
 | 
			
		||||
        self.assertEqual(p.get_metadata('test'), 3)
 | 
			
		||||
 | 
			
		||||
        for k in ['apple', 'banana', 'carrot', 'carrot', 'banana']:
 | 
			
		||||
            p.set_metadata(k, k)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(len(p.metadata.keys()), 4)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestTemplateTest(TestCase):
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user