mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Add 'Tag' management (#4367)
* 'Tag' management Fixes #83 * Add for ManufacturerPart, SupplierPart * Add tags for StockLocation, StockItem * fix serializer definition * add migrations * update pre-commit * bump dependencies * revert updates * set version for bugbear * remove bugbear * readd bugbear remove isort * and remove bugbear again * remove bugbear * make tag fields not required * add ruleset * Merge migrations * fix migrations * add unittest for detail * test tag add * order api * reduce database access * add tag modification test * use overriden serializer to ensuer the manager is always available * fix typo * fix serializer * increae query thershold by 1 * move tag serializer * fix migrations * content_types are changing between tests - removing them * remove unneeded fixture * Add basic docs * bump API version * add api access to the docs * add python code * Add tags to search and filters for all models
This commit is contained in:
parent
baaa147fd0
commit
f5c2591fd4
@ -2,11 +2,17 @@
|
||||
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 110
|
||||
INVENTREE_API_VERSION = 111
|
||||
|
||||
"""
|
||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||
|
||||
v111 -> 2023-05-02 : https://github.com/inventree/InvenTree/pull/4367
|
||||
- Adds tags to the Part serializer
|
||||
- Adds tags to the SupplierPart serializer
|
||||
- Adds tags to the ManufacturerPart serializer
|
||||
- Adds tags to the StockItem serializer
|
||||
- Adds tags to the StockLocation serializer
|
||||
v110 -> 2023-04-26 : https://github.com/inventree/InvenTree/pull/4698
|
||||
- Adds 'order_currency' field for PurchaseOrder / SalesOrder endpoints
|
||||
|
||||
|
@ -19,6 +19,7 @@ from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import empty
|
||||
from rest_framework.serializers import DecimalField
|
||||
from rest_framework.utils import model_meta
|
||||
from taggit.serializers import TaggitSerializer
|
||||
|
||||
from common.models import InvenTreeSetting
|
||||
from common.settings import currency_code_default, currency_code_mappings
|
||||
@ -264,6 +265,28 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
|
||||
return data
|
||||
|
||||
|
||||
class InvenTreeTaggitSerializer(TaggitSerializer):
|
||||
"""Updated from https://github.com/glemmaPaul/django-taggit-serializer."""
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
"""Overriden update method to readd the tagmanager."""
|
||||
to_be_tagged, validated_data = self._pop_tags(validated_data)
|
||||
|
||||
tag_object = super().update(instance, validated_data)
|
||||
|
||||
for key in to_be_tagged.keys():
|
||||
# readd the tagmanager
|
||||
new_tagobject = tag_object.__class__.objects.get(id=tag_object.id)
|
||||
setattr(tag_object, key, getattr(new_tagobject, key))
|
||||
|
||||
return self._save_tags(tag_object, to_be_tagged)
|
||||
|
||||
|
||||
class InvenTreeTagModelSerializer(InvenTreeTaggitSerializer, InvenTreeModelSerializer):
|
||||
"""Combination of InvenTreeTaggitSerializer and InvenTreeModelSerializer."""
|
||||
pass
|
||||
|
||||
|
||||
class UserSerializer(InvenTreeModelSerializer):
|
||||
"""Serializer for a User."""
|
||||
|
||||
|
@ -225,6 +225,7 @@ INSTALLED_APPS = [
|
||||
'django_q',
|
||||
'formtools', # Form wizard tools
|
||||
'dbbackup', # Backups - django-dbbackup
|
||||
'taggit', # Tagging
|
||||
|
||||
'allauth', # Base app for SSO
|
||||
'allauth.account', # Extend user with accounts
|
||||
|
@ -146,6 +146,8 @@ class ManufacturerPartFilter(rest_filters.FilterSet):
|
||||
'manufacturer',
|
||||
'MPN',
|
||||
'part',
|
||||
'tags__name',
|
||||
'tags__slug',
|
||||
]
|
||||
|
||||
# Filter by 'active' status of linked part
|
||||
@ -163,6 +165,7 @@ class ManufacturerPartList(ListCreateDestroyAPIView):
|
||||
'part',
|
||||
'manufacturer',
|
||||
'supplier_parts',
|
||||
'tags',
|
||||
)
|
||||
|
||||
serializer_class = ManufacturerPartSerializer
|
||||
@ -193,6 +196,8 @@ class ManufacturerPartList(ListCreateDestroyAPIView):
|
||||
'part__IPN',
|
||||
'part__name',
|
||||
'part__description',
|
||||
'tags__name',
|
||||
'tags__slug',
|
||||
]
|
||||
|
||||
|
||||
@ -303,6 +308,8 @@ class SupplierPartFilter(rest_filters.FilterSet):
|
||||
'part',
|
||||
'manufacturer_part',
|
||||
'SKU',
|
||||
'tags__name',
|
||||
'tags__slug',
|
||||
]
|
||||
|
||||
# Filter by 'active' status of linked part
|
||||
@ -323,7 +330,9 @@ class SupplierPartList(ListCreateDestroyAPIView):
|
||||
- POST: Create a new SupplierPart object
|
||||
"""
|
||||
|
||||
queryset = SupplierPart.objects.all()
|
||||
queryset = SupplierPart.objects.all().prefetch_related(
|
||||
'tags',
|
||||
)
|
||||
filterset_class = SupplierPartFilter
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
@ -403,6 +412,8 @@ class SupplierPartList(ListCreateDestroyAPIView):
|
||||
'part__name',
|
||||
'part__description',
|
||||
'part__keywords',
|
||||
'tags__name',
|
||||
'tags__slug',
|
||||
]
|
||||
|
||||
|
||||
|
25
InvenTree/company/migrations/0057_auto_20230427_2033.py
Normal file
25
InvenTree/company/migrations/0057_auto_20230427_2033.py
Normal file
@ -0,0 +1,25 @@
|
||||
# Generated by Django 3.2.18 on 2023-04-27 20:33
|
||||
|
||||
from django.db import migrations
|
||||
import taggit.managers
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('taggit', '0005_auto_20220424_2025'),
|
||||
('company', '0056_alter_company_notes'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='manufacturerpart',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='supplierpart',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
|
||||
),
|
||||
]
|
@ -15,6 +15,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from moneyed import CURRENCIES
|
||||
from stdimage.models import StdImageField
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
import common.models
|
||||
import common.settings
|
||||
@ -310,6 +311,8 @@ class ManufacturerPart(MetadataMixin, models.Model):
|
||||
help_text=_('Manufacturer part description')
|
||||
)
|
||||
|
||||
tags = TaggableManager()
|
||||
|
||||
@classmethod
|
||||
def create(cls, part, manufacturer, mpn, description, link=None):
|
||||
"""Check if ManufacturerPart instance does not already exist then create it."""
|
||||
@ -445,6 +448,7 @@ class SupplierPart(MetadataMixin, InvenTreeBarcodeMixin, common.models.MetaMixin
|
||||
db_table = 'part_supplierpart'
|
||||
|
||||
objects = SupplierPartManager()
|
||||
tags = TaggableManager()
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
|
@ -7,6 +7,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from rest_framework import serializers
|
||||
from sql_util.utils import SubqueryCount
|
||||
from taggit.serializers import TagListSerializerField
|
||||
|
||||
import part.filters
|
||||
from InvenTree.serializers import (InvenTreeAttachmentSerializer,
|
||||
@ -14,7 +15,9 @@ from InvenTree.serializers import (InvenTreeAttachmentSerializer,
|
||||
InvenTreeDecimalField,
|
||||
InvenTreeImageSerializerField,
|
||||
InvenTreeModelSerializer,
|
||||
InvenTreeMoneySerializer, RemoteImageMixin)
|
||||
InvenTreeMoneySerializer,
|
||||
InvenTreeTagModelSerializer,
|
||||
RemoteImageMixin)
|
||||
from part.serializers import PartBriefSerializer
|
||||
|
||||
from .models import (Company, CompanyAttachment, Contact, ManufacturerPart,
|
||||
@ -149,7 +152,7 @@ class ContactSerializer(InvenTreeModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class ManufacturerPartSerializer(InvenTreeModelSerializer):
|
||||
class ManufacturerPartSerializer(InvenTreeTagModelSerializer):
|
||||
"""Serializer for ManufacturerPart object."""
|
||||
|
||||
class Meta:
|
||||
@ -166,8 +169,12 @@ class ManufacturerPartSerializer(InvenTreeModelSerializer):
|
||||
'description',
|
||||
'MPN',
|
||||
'link',
|
||||
|
||||
'tags',
|
||||
]
|
||||
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize this serializer with extra detail fields as required"""
|
||||
part_detail = kwargs.pop('part_detail', True)
|
||||
@ -236,7 +243,7 @@ class ManufacturerPartParameterSerializer(InvenTreeModelSerializer):
|
||||
manufacturer_part_detail = ManufacturerPartSerializer(source='manufacturer_part', many=False, read_only=True)
|
||||
|
||||
|
||||
class SupplierPartSerializer(InvenTreeModelSerializer):
|
||||
class SupplierPartSerializer(InvenTreeTagModelSerializer):
|
||||
"""Serializer for SupplierPart object."""
|
||||
|
||||
class Meta:
|
||||
@ -267,6 +274,8 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
|
||||
'supplier_detail',
|
||||
'url',
|
||||
'updated',
|
||||
|
||||
'tags',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
@ -274,6 +283,8 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
|
||||
'barcode_hash',
|
||||
]
|
||||
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize this serializer with extra detail fields as required"""
|
||||
|
||||
|
@ -970,6 +970,10 @@ class PartFilter(rest_filters.FilterSet):
|
||||
|
||||
virtual = rest_filters.BooleanFilter()
|
||||
|
||||
tags_name = rest_filters.CharFilter(field_name='tags__name', lookup_expr='iexact')
|
||||
|
||||
tags_slug = rest_filters.CharFilter(field_name='tags__slug', lookup_expr='iexact')
|
||||
|
||||
|
||||
class PartMixin:
|
||||
"""Mixin class for Part API endpoints"""
|
||||
@ -1240,6 +1244,8 @@ class PartList(PartMixin, APIDownloadMixin, ListCreateAPI):
|
||||
'category__name',
|
||||
'manufacturer_parts__MPN',
|
||||
'supplier_parts__SKU',
|
||||
'tags__name',
|
||||
'tags__slug',
|
||||
]
|
||||
|
||||
|
||||
|
20
InvenTree/part/migrations/0106_part_tags.py
Normal file
20
InvenTree/part/migrations/0106_part_tags.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Generated by Django 3.2.18 on 2023-04-27 20:33
|
||||
|
||||
from django.db import migrations
|
||||
import taggit.managers
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('taggit', '0005_auto_20220424_2025'),
|
||||
('part', '0105_alter_part_notes'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='part',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
|
||||
),
|
||||
]
|
@ -31,6 +31,7 @@ from mptt.exceptions import InvalidMove
|
||||
from mptt.managers import TreeManager
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
from stdimage.models import StdImageField
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
import common.models
|
||||
import common.settings
|
||||
@ -336,6 +337,7 @@ class PartManager(TreeManager):
|
||||
'category__parent',
|
||||
'stock_items',
|
||||
'builds',
|
||||
'tags',
|
||||
)
|
||||
|
||||
|
||||
@ -378,6 +380,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
||||
"""
|
||||
|
||||
objects = PartManager()
|
||||
tags = TaggableManager()
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defines extra model properties"""
|
||||
|
@ -15,6 +15,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from rest_framework import serializers
|
||||
from sql_util.utils import SubqueryCount, SubquerySum
|
||||
from taggit.serializers import TagListSerializerField
|
||||
|
||||
import common.models
|
||||
import company.models
|
||||
@ -31,8 +32,9 @@ from InvenTree.serializers import (DataFileExtractSerializer,
|
||||
InvenTreeDecimalField,
|
||||
InvenTreeImageSerializerField,
|
||||
InvenTreeModelSerializer,
|
||||
InvenTreeMoneySerializer, RemoteImageMixin,
|
||||
UserSerializer)
|
||||
InvenTreeMoneySerializer,
|
||||
InvenTreeTagModelSerializer,
|
||||
RemoteImageMixin, UserSerializer)
|
||||
from InvenTree.status_codes import BuildStatus
|
||||
from InvenTree.tasks import offload_task
|
||||
|
||||
@ -403,7 +405,7 @@ class InitialSupplierSerializer(serializers.Serializer):
|
||||
return data
|
||||
|
||||
|
||||
class PartSerializer(RemoteImageMixin, InvenTreeModelSerializer):
|
||||
class PartSerializer(RemoteImageMixin, InvenTreeTagModelSerializer):
|
||||
"""Serializer for complete detail information of a part.
|
||||
|
||||
Used when displaying all details of a single component.
|
||||
@ -464,13 +466,17 @@ class PartSerializer(RemoteImageMixin, InvenTreeModelSerializer):
|
||||
'duplicate',
|
||||
'initial_stock',
|
||||
'initial_supplier',
|
||||
'copy_category_parameters'
|
||||
'copy_category_parameters',
|
||||
|
||||
'tags',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
'barcode_hash',
|
||||
]
|
||||
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Custom initialization method for PartSerializer:
|
||||
|
||||
|
@ -1425,6 +1425,7 @@ class PartDetailTests(PartAPITestBase):
|
||||
'name': 'my test api part',
|
||||
'description': 'a part created with the API',
|
||||
'category': 1,
|
||||
'tags': '["tag1", "tag2"]',
|
||||
}
|
||||
)
|
||||
|
||||
@ -1438,6 +1439,8 @@ class PartDetailTests(PartAPITestBase):
|
||||
part = Part.objects.get(pk=pk)
|
||||
|
||||
self.assertEqual(part.name, 'my test api part')
|
||||
self.assertEqual(part.tags.count(), 2)
|
||||
self.assertEqual([a.name for a in part.tags.order_by('slug')], ['tag1', 'tag2'])
|
||||
|
||||
# Edit the part
|
||||
url = reverse('api-part-detail', kwargs={'pk': pk})
|
||||
@ -1468,6 +1471,13 @@ class PartDetailTests(PartAPITestBase):
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Try to remove a tag
|
||||
response = self.patch(url, {
|
||||
'tags': ['tag1',],
|
||||
})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data['tags'], ['tag1'])
|
||||
|
||||
# Try to remove the part
|
||||
response = self.delete(url)
|
||||
|
||||
|
@ -152,7 +152,7 @@ class CategoryTest(TestCase):
|
||||
def test_parameters(self):
|
||||
"""Test that the Category parameters are correctly fetched."""
|
||||
# Check number of SQL queries to iterate other parameters
|
||||
with self.assertNumQueries(8):
|
||||
with self.assertNumQueries(9):
|
||||
# Prefetch: 3 queries (parts, parameters and parameters_template)
|
||||
fasteners = self.fasteners.prefetch_parts_parameters()
|
||||
# Iterate through all parts and parameters
|
||||
|
@ -214,7 +214,9 @@ class StockLocationList(APIDownloadMixin, ListCreateAPI):
|
||||
- POST: Create a new StockLocation
|
||||
"""
|
||||
|
||||
queryset = StockLocation.objects.all()
|
||||
queryset = StockLocation.objects.all().prefetch_related(
|
||||
'tags',
|
||||
)
|
||||
serializer_class = StockSerializers.LocationSerializer
|
||||
|
||||
def download_queryset(self, queryset, export_format):
|
||||
@ -300,11 +302,15 @@ class StockLocationList(APIDownloadMixin, ListCreateAPI):
|
||||
'name',
|
||||
'structural',
|
||||
'external',
|
||||
'tags__name',
|
||||
'tags__slug',
|
||||
]
|
||||
|
||||
search_fields = [
|
||||
'name',
|
||||
'description',
|
||||
'tags__name',
|
||||
'tags__slug',
|
||||
]
|
||||
|
||||
ordering_fields = [
|
||||
@ -351,6 +357,8 @@ class StockFilter(rest_filters.FilterSet):
|
||||
'customer',
|
||||
'sales_order',
|
||||
'purchase_order',
|
||||
'tags__name',
|
||||
'tags__slug',
|
||||
]
|
||||
|
||||
# Relationship filters
|
||||
@ -811,7 +819,8 @@ class StockList(APIDownloadMixin, ListCreateDestroyAPIView):
|
||||
queryset = queryset.prefetch_related(
|
||||
'part',
|
||||
'part__category',
|
||||
'location'
|
||||
'location',
|
||||
'tags',
|
||||
)
|
||||
|
||||
return queryset
|
||||
@ -1035,6 +1044,8 @@ class StockList(APIDownloadMixin, ListCreateDestroyAPIView):
|
||||
'part__IPN',
|
||||
'part__description',
|
||||
'location__name',
|
||||
'tags__name',
|
||||
'tags__slug',
|
||||
]
|
||||
|
||||
|
||||
|
25
InvenTree/stock/migrations/0098_auto_20230427_2033.py
Normal file
25
InvenTree/stock/migrations/0098_auto_20230427_2033.py
Normal file
@ -0,0 +1,25 @@
|
||||
# Generated by Django 3.2.18 on 2023-04-27 20:33
|
||||
|
||||
from django.db import migrations
|
||||
import taggit.managers
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('taggit', '0005_auto_20220424_2025'),
|
||||
('stock', '0097_alter_stockitem_notes'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='stockitem',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='stocklocation',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
|
||||
),
|
||||
]
|
@ -21,6 +21,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from jinja2 import Template
|
||||
from mptt.managers import TreeManager
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
import common.models
|
||||
import InvenTree.helpers
|
||||
@ -53,6 +54,8 @@ class StockLocation(InvenTreeBarcodeMixin, MetadataMixin, InvenTreeTree):
|
||||
verbose_name = _('Stock Location')
|
||||
verbose_name_plural = _('Stock Locations')
|
||||
|
||||
tags = TaggableManager()
|
||||
|
||||
def delete_recursive(self, *args, **kwargs):
|
||||
"""This function handles the recursive deletion of sub-locations depending on kwargs contents"""
|
||||
delete_stock_items = kwargs.get('delete_stock_items', False)
|
||||
@ -321,6 +324,8 @@ class StockItem(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, commo
|
||||
}
|
||||
}
|
||||
|
||||
tags = TaggableManager()
|
||||
|
||||
# A Query filter which will be re-used in multiple places to determine if a StockItem is actually "in stock"
|
||||
IN_STOCK_FILTER = Q(
|
||||
quantity__gt=0,
|
||||
|
@ -12,6 +12,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
from rest_framework.serializers import ValidationError
|
||||
from sql_util.utils import SubqueryCount, SubquerySum
|
||||
from taggit.serializers import TagListSerializerField
|
||||
|
||||
import common.models
|
||||
import company.models
|
||||
@ -76,7 +77,7 @@ class StockItemSerializerBrief(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
return value
|
||||
|
||||
|
||||
class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
class StockItemSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
|
||||
"""Serializer for a StockItem.
|
||||
|
||||
- Includes serialization for the linked part
|
||||
@ -123,6 +124,8 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
'updated',
|
||||
'purchase_price',
|
||||
'purchase_price_currency',
|
||||
|
||||
'tags',
|
||||
]
|
||||
|
||||
"""
|
||||
@ -236,6 +239,8 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
purchase_order_reference = serializers.CharField(source='purchase_order.reference', read_only=True)
|
||||
sales_order_reference = serializers.CharField(source='sales_order.reference', read_only=True)
|
||||
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Add detail fields."""
|
||||
part_detail = kwargs.pop('part_detail', False)
|
||||
@ -566,7 +571,7 @@ class LocationTreeSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class LocationSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
class LocationSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
|
||||
"""Detailed information about a stock location."""
|
||||
|
||||
class Meta:
|
||||
@ -587,6 +592,8 @@ class LocationSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
'icon',
|
||||
'structural',
|
||||
'external',
|
||||
|
||||
'tags',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
@ -610,6 +617,8 @@ class LocationSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
|
||||
level = serializers.IntegerField(read_only=True)
|
||||
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
|
||||
class StockItemAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSerializer):
|
||||
"""Serializer for StockItemAttachment model."""
|
||||
|
@ -79,6 +79,8 @@ class RuleSet(models.Model):
|
||||
'plugin_pluginsetting',
|
||||
'plugin_notificationusersetting',
|
||||
'common_newsfeedentry',
|
||||
'taggit_tag',
|
||||
'taggit_taggeditem',
|
||||
],
|
||||
'part_category': [
|
||||
'part_partcategory',
|
||||
|
61
docs/docs/extend/plugins/tags.md
Normal file
61
docs/docs/extend/plugins/tags.md
Normal file
@ -0,0 +1,61 @@
|
||||
---
|
||||
title: Item Tags
|
||||
---
|
||||
|
||||
## Tags
|
||||
|
||||
Several models in InvenTree can be tagged with arbitrary tags. Tags are useful for grouping items together. This can be used to mark items with a plugin or to group items together for a particular theme. Tags are meant to be used by programms and are not visible to the end user.
|
||||
Tags are shared between all models that can be tagged.
|
||||
|
||||
The following models can be tagged:
|
||||
- [Parts](../../part/part.md) and [Supplier Parts](../../order/company#supplier-parts)/[Manufacturer Part](../../order/company#manufacturer-parts)
|
||||
- [Stock Items](../../stock/stock.md#stock-item) / [Stock Location](../../stock/stock.md#stock-location)
|
||||
|
||||
|
||||
## Accessing Tags
|
||||
|
||||
### Plugin Access
|
||||
|
||||
The `tags` field can be accessed and updated directly from custom plugin code, as follows:
|
||||
|
||||
```python
|
||||
from part.models import Part
|
||||
|
||||
# Show tags for a particular Part instance
|
||||
part = Part.objects.get(pk=100)
|
||||
print(part.tags)
|
||||
|
||||
> {['Tag1', 'Another Tag']}
|
||||
|
||||
# Tags can also be accessed via tags.all()
|
||||
print(part.tags.all())
|
||||
|
||||
> {['Tag1', 'Another Tag']}
|
||||
|
||||
# Add tag
|
||||
part.tags.add('Tag 2')
|
||||
print(part.tags)
|
||||
|
||||
> {['Tag1', 'Tag 2', 'Another Tag']}
|
||||
|
||||
# Remove tag
|
||||
part.tags.remove('Tag1')
|
||||
print(part.tags)
|
||||
|
||||
> {['Tag 2', 'Another Tag']}
|
||||
|
||||
# Filter by tags
|
||||
Part.objects.filter(tags__name__in=["Tag1", "Tag 2"]).distinct()
|
||||
```
|
||||
|
||||
### API Access
|
||||
|
||||
For models which provide tags, access is also provided via the API. The tags are exposed via the detail endpoint for the models starting from version 111.
|
||||
|
||||
Tags can be cached via PATCH or POST requests. The tags are provided as a json formatted list of strings. The tags are note case sensitive and must be unique across the instance - else the exsisting tag gets assigned. The tags are not sorted and the order is not guaranteed.
|
||||
|
||||
```json
|
||||
{
|
||||
"tags": '["foo", "bar"]'
|
||||
}
|
||||
```
|
@ -166,6 +166,7 @@ nav:
|
||||
- Installation: extend/plugins/install.md
|
||||
- Developing a Plugin: extend/how_to_plugin.md
|
||||
- Model Meatadata: extend/plugins/metadata.md
|
||||
- Tags: extend/plugins/tags.md
|
||||
- Plugin Mixins:
|
||||
- Action Mixin: extend/plugins/action.md
|
||||
- API Mixin: extend/plugins/api.md
|
||||
|
@ -23,6 +23,7 @@ django-q-sentry # sentry.io integration for django-q
|
||||
django-sql-utils # Advanced query annotation / aggregation
|
||||
django-sslserver # Secure HTTP development server
|
||||
django-stdimage<6.0.0 # Advanced ImageField management # FIXED 2022-06-29 6.0.0 breaks serialization for django-q
|
||||
django-taggit # Tagging support
|
||||
django-user-sessions # user sessions in DB
|
||||
django-weasyprint # django weasyprint integration
|
||||
djangorestframework # DRF framework
|
||||
|
@ -68,6 +68,7 @@ django==3.2.18
|
||||
# django-sql-utils
|
||||
# django-sslserver
|
||||
# django-stdimage
|
||||
# django-taggit
|
||||
# django-user-sessions
|
||||
# django-weasyprint
|
||||
# django-xforwardedfor-middleware
|
||||
@ -125,6 +126,8 @@ django-sslserver==0.22
|
||||
# via -r requirements.in
|
||||
django-stdimage==5.3.0
|
||||
# via -r requirements.in
|
||||
django-taggit==3.1.0
|
||||
# via -r requirements.in
|
||||
django-user-sessions==2.0.0
|
||||
# via -r requirements.in
|
||||
django-weasyprint==2.2.0
|
||||
|
Loading…
Reference in New Issue
Block a user