mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Add Metadata to more models (#4898)
* Update models: add MetadataMixin * Fix name of model in Metadata API definition * Add API endpoints * Update API version * Fix syntax * Add API endpoint for RO, RO line, RO line extra item * Add Metadata to Contacts * Fix link in API version * Fix name of model * Fix error? * Fix error? * Fix all errors, hopefully.. * Add tests for order, line, extraline metadata Extend for PO, SO * Add tests for metadata for Company-related models * Fix spelling * Consolidate metadata test for all part models into one test * Add test for all Stock metadata * Update stock test_api * Add all metadata tests for orders * Fix various errors in tests * Fix model name * Add migration files * Update tests for metadata * Resolve conflict around API version number * Rename migration file * Rename migration file * Will Contact edit endpoint work better? * Revert changes in URL definitions * Remove test, duplicate * Fix tests with fixed PK, not from fixtures, to use a dynamic PK * Fix migration overlap
This commit is contained in:
parent
c0dafe155f
commit
1d85b70313
@ -2,11 +2,15 @@
|
||||
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 118
|
||||
INVENTREE_API_VERSION = 119
|
||||
|
||||
"""
|
||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||
|
||||
|
||||
v119 -> 2023-06-01 : https://github.com/inventree/InvenTree/pull/4898
|
||||
- Add Metadata to: Part test templates, Part parameters, Part category parameter templates, BOM item substitute, Part relateds, Stock item test result
|
||||
|
||||
v118 -> 2023-06-01 : https://github.com/inventree/InvenTree/pull/4935
|
||||
- Adds extra fields for the PartParameterTemplate model
|
||||
|
||||
|
@ -561,7 +561,10 @@ company_api_urls = [
|
||||
])),
|
||||
|
||||
re_path(r'^contact/', include([
|
||||
path('<int:pk>/', ContactDetail.as_view(), name='api-contact-detail'),
|
||||
re_path(r'^(?P<pk>\d+)/?', include([
|
||||
re_path('^metadata/', MetadataView.as_view(), {'model': Contact}, name='api-contact-metadata'),
|
||||
re_path('^.*$', ContactDetail.as_view(), name='api-contact-detail'),
|
||||
])),
|
||||
re_path(r'^.*$', ContactList.as_view(), name='api-contact-list'),
|
||||
])),
|
||||
|
||||
|
9
InvenTree/company/fixtures/contact.yaml
Normal file
9
InvenTree/company/fixtures/contact.yaml
Normal file
@ -0,0 +1,9 @@
|
||||
# Sample contact data
|
||||
- model: company.contact
|
||||
pk: 1
|
||||
fields:
|
||||
name: Johnny Rotten
|
||||
company: 1
|
||||
phone: 001234567
|
||||
email: none@example.com
|
||||
role: Rockstar
|
18
InvenTree/company/migrations/0062_contact_metadata.py
Normal file
18
InvenTree/company/migrations/0062_contact_metadata.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.19 on 2023-05-25 16:06
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('company', '0061_remove_supplierpart_pack_size'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='contact',
|
||||
name='metadata',
|
||||
field=models.JSONField(blank=True, help_text='JSON metadata field, for use by external plugins', null=True, verbose_name='Plugin Metadata'),
|
||||
),
|
||||
]
|
@ -225,7 +225,7 @@ class CompanyAttachment(InvenTreeAttachment):
|
||||
)
|
||||
|
||||
|
||||
class Contact(models.Model):
|
||||
class Contact(MetadataMixin, models.Model):
|
||||
"""A Contact represents a person who works at a particular company. A Company may have zero or more associated Contact objects.
|
||||
|
||||
Attributes:
|
||||
|
@ -6,7 +6,7 @@ from rest_framework import status
|
||||
|
||||
from InvenTree.unit_test import InvenTreeAPITestCase
|
||||
|
||||
from .models import Company, Contact, SupplierPart
|
||||
from .models import Company, Contact, ManufacturerPart, SupplierPart
|
||||
|
||||
|
||||
class CompanyTest(InvenTreeAPITestCase):
|
||||
@ -233,7 +233,10 @@ class ContactTest(InvenTreeAPITestCase):
|
||||
def test_edit(self):
|
||||
"""Test that we can edit a Contact via the API"""
|
||||
|
||||
url = reverse('api-contact-detail', kwargs={'pk': 1})
|
||||
# Get the first contact
|
||||
contact = Contact.objects.first()
|
||||
# Use this contact in the tests
|
||||
url = reverse('api-contact-detail', kwargs={'pk': contact.pk})
|
||||
|
||||
# Retrieve detail view
|
||||
data = self.get(url, expected_code=200).data
|
||||
@ -259,13 +262,16 @@ class ContactTest(InvenTreeAPITestCase):
|
||||
expected_code=200
|
||||
)
|
||||
|
||||
contact = Contact.objects.get(pk=1)
|
||||
# Get the contact again
|
||||
contact = Contact.objects.first()
|
||||
self.assertEqual(contact.role, 'x')
|
||||
|
||||
def test_delete(self):
|
||||
"""Tests that we can delete a Contact via the API"""
|
||||
|
||||
url = reverse('api-contact-detail', kwargs={'pk': 6})
|
||||
# Get the last contact
|
||||
contact = Contact.objects.first()
|
||||
url = reverse('api-contact-detail', kwargs={'pk': contact.pk})
|
||||
|
||||
# Delete (without required permissions)
|
||||
self.delete(url, expected_code=403)
|
||||
@ -490,3 +496,63 @@ class SupplierPartTest(InvenTreeAPITestCase):
|
||||
sp = SupplierPart.objects.get(pk=response.data['pk'])
|
||||
self.assertEqual(sp.available, 999)
|
||||
self.assertIsNotNone(sp.availability_updated)
|
||||
|
||||
|
||||
class CompanyMetadataAPITest(InvenTreeAPITestCase):
|
||||
"""Unit tests for the various metadata endpoints of API."""
|
||||
|
||||
fixtures = [
|
||||
'category',
|
||||
'part',
|
||||
'location',
|
||||
'company',
|
||||
'contact',
|
||||
'manufacturer_part',
|
||||
'supplier_part',
|
||||
]
|
||||
|
||||
roles = [
|
||||
'company.change',
|
||||
'purchase_order.change',
|
||||
'part.change',
|
||||
]
|
||||
|
||||
def metatester(self, apikey, model):
|
||||
"""Generic tester"""
|
||||
|
||||
modeldata = model.objects.first()
|
||||
|
||||
# Useless test unless a model object is found
|
||||
self.assertIsNotNone(modeldata)
|
||||
|
||||
url = reverse(apikey, kwargs={'pk': modeldata.pk})
|
||||
|
||||
# Metadata is initially null
|
||||
self.assertIsNone(modeldata.metadata)
|
||||
|
||||
numstr = f'12{len(apikey)}'
|
||||
|
||||
self.patch(
|
||||
url,
|
||||
{
|
||||
'metadata': {
|
||||
f'abc-{numstr}': f'xyz-{apikey}-{numstr}',
|
||||
}
|
||||
},
|
||||
expected_code=200
|
||||
)
|
||||
|
||||
# Refresh
|
||||
modeldata.refresh_from_db()
|
||||
self.assertEqual(modeldata.get_metadata(f'abc-{numstr}'), f'xyz-{apikey}-{numstr}')
|
||||
|
||||
def test_metadata(self):
|
||||
"""Test all endpoints"""
|
||||
|
||||
for apikey, model in {
|
||||
'api-manufacturer-part-metadata': ManufacturerPart,
|
||||
'api-supplier-part-metadata': SupplierPart,
|
||||
'api-company-metadata': Company,
|
||||
'api-contact-metadata': Contact,
|
||||
}.items():
|
||||
self.metatester(apikey, model)
|
||||
|
@ -51,3 +51,19 @@
|
||||
description: 'RMA from a customer'
|
||||
customer: 5
|
||||
status: 10 # Pending
|
||||
|
||||
# 1 x R_4K7_0603
|
||||
- model: order.returnorderlineitem
|
||||
pk: 1
|
||||
fields:
|
||||
order: 6
|
||||
item: 1008
|
||||
quantity: 1
|
||||
|
||||
# An extra line item
|
||||
- model: order.returnorderextraline
|
||||
pk: 1
|
||||
fields:
|
||||
order: 6
|
||||
reference: 'Freight cost'
|
||||
quantity: 1
|
||||
|
@ -37,3 +37,26 @@
|
||||
description: "One sales order, please"
|
||||
customer: 5
|
||||
status: 60 # Returned
|
||||
|
||||
# 1 x R_4K7_0603
|
||||
- model: order.salesorderlineitem
|
||||
pk: 1
|
||||
fields:
|
||||
order: 5
|
||||
part: 5
|
||||
quantity: 1
|
||||
|
||||
# An extra line item
|
||||
- model: order.salesorderextraline
|
||||
pk: 1
|
||||
fields:
|
||||
order: 5
|
||||
reference: 'Freight cost'
|
||||
quantity: 1
|
||||
|
||||
# Shipment
|
||||
- model: order.salesordershipment
|
||||
pk: 1
|
||||
fields:
|
||||
order: 1
|
||||
reference: "Test Shipment, must be present for metadata test"
|
||||
|
@ -509,23 +509,6 @@ class PurchaseOrderTest(OrderTest):
|
||||
|
||||
self.assertEqual(po.status, PurchaseOrderStatus.PLACED)
|
||||
|
||||
def test_po_metadata(self):
|
||||
"""Test the 'metadata' endpoint for the PurchaseOrder model"""
|
||||
url = reverse('api-po-metadata', kwargs={'pk': 1})
|
||||
|
||||
self.patch(
|
||||
url,
|
||||
{
|
||||
'metadata': {
|
||||
'yam': 'yum',
|
||||
}
|
||||
},
|
||||
expected_code=200
|
||||
)
|
||||
|
||||
order = models.PurchaseOrder.objects.get(pk=1)
|
||||
self.assertEqual(order.get_metadata('yam'), 'yum')
|
||||
|
||||
def test_po_calendar(self):
|
||||
"""Test the calendar export endpoint"""
|
||||
|
||||
@ -1374,23 +1357,6 @@ class SalesOrderTest(OrderTest):
|
||||
|
||||
self.assertEqual(so.status, SalesOrderStatus.CANCELLED)
|
||||
|
||||
def test_so_metadata(self):
|
||||
"""Test the 'metadata' API endpoint for the SalesOrder model"""
|
||||
url = reverse('api-so-metadata', kwargs={'pk': 1})
|
||||
|
||||
self.patch(
|
||||
url,
|
||||
{
|
||||
'metadata': {
|
||||
'xyz': 'abc',
|
||||
}
|
||||
},
|
||||
expected_code=200
|
||||
)
|
||||
|
||||
order = models.SalesOrder.objects.get(pk=1)
|
||||
self.assertEqual(order.get_metadata('xyz'), 'abc')
|
||||
|
||||
def test_so_calendar(self):
|
||||
"""Test the calendar export endpoint"""
|
||||
|
||||
@ -1887,6 +1853,9 @@ class SalesOrderAllocateTest(OrderTest):
|
||||
"""Test the SalesOrderShipment list API endpoint"""
|
||||
url = reverse('api-so-shipment-list')
|
||||
|
||||
# Count before creation
|
||||
countbefore = models.SalesOrderShipment.objects.count()
|
||||
|
||||
# Create some new shipments via the API
|
||||
for order in models.SalesOrder.objects.all():
|
||||
|
||||
@ -1916,7 +1885,7 @@ class SalesOrderAllocateTest(OrderTest):
|
||||
# List *all* shipments
|
||||
response = self.get(url, expected_code=200)
|
||||
|
||||
self.assertEqual(len(response.data), 1 + 3 * models.SalesOrder.objects.count())
|
||||
self.assertEqual(len(response.data), countbefore + 3 * models.SalesOrder.objects.count())
|
||||
|
||||
|
||||
class ReturnOrderTests(InvenTreeAPITestCase):
|
||||
@ -2212,3 +2181,71 @@ class ReturnOrderTests(InvenTreeAPITestCase):
|
||||
response = self.get(url, expected_code=200, format=None)
|
||||
calendar = Calendar.from_ical(response.content)
|
||||
self.assertIsInstance(calendar, Calendar)
|
||||
|
||||
|
||||
class OrderMetadataAPITest(InvenTreeAPITestCase):
|
||||
"""Unit tests for the various metadata endpoints of API."""
|
||||
|
||||
fixtures = [
|
||||
'category',
|
||||
'part',
|
||||
'company',
|
||||
'location',
|
||||
'supplier_part',
|
||||
'stock',
|
||||
'order',
|
||||
'sales_order',
|
||||
'return_order',
|
||||
]
|
||||
|
||||
roles = [
|
||||
'purchase_order.change',
|
||||
'sales_order.change',
|
||||
'return_order.change',
|
||||
]
|
||||
|
||||
def metatester(self, apikey, model):
|
||||
"""Generic tester"""
|
||||
|
||||
modeldata = model.objects.first()
|
||||
|
||||
# Useless test unless a model object is found
|
||||
self.assertIsNotNone(modeldata)
|
||||
|
||||
url = reverse(apikey, kwargs={'pk': modeldata.pk})
|
||||
|
||||
# Metadata is initially null
|
||||
self.assertIsNone(modeldata.metadata)
|
||||
|
||||
numstr = f'12{len(apikey)}'
|
||||
|
||||
self.patch(
|
||||
url,
|
||||
{
|
||||
'metadata': {
|
||||
f'abc-{numstr}': f'xyz-{apikey}-{numstr}',
|
||||
}
|
||||
},
|
||||
expected_code=200
|
||||
)
|
||||
|
||||
# Refresh
|
||||
modeldata.refresh_from_db()
|
||||
self.assertEqual(modeldata.get_metadata(f'abc-{numstr}'), f'xyz-{apikey}-{numstr}')
|
||||
|
||||
def test_metadata(self):
|
||||
"""Test all endpoints"""
|
||||
|
||||
for apikey, model in {
|
||||
'api-po-metadata': models.PurchaseOrder,
|
||||
'api-po-line-metadata': models.PurchaseOrderLineItem,
|
||||
'api-po-extra-line-metadata': models.PurchaseOrderExtraLine,
|
||||
'api-so-shipment-metadata': models.SalesOrderShipment,
|
||||
'api-so-metadata': models.SalesOrder,
|
||||
'api-so-line-metadata': models.SalesOrderLineItem,
|
||||
'api-so-extra-line-metadata': models.SalesOrderExtraLine,
|
||||
'api-return-order-metadata': models.ReturnOrder,
|
||||
'api-return-order-line-metadata': models.ReturnOrderLineItem,
|
||||
'api-return-order-extra-line-metadata': models.ReturnOrderExtraLine,
|
||||
}.items():
|
||||
self.metatester(apikey, model)
|
||||
|
@ -1892,7 +1892,10 @@ part_api_urls = [
|
||||
re_path(r'^tree/', CategoryTree.as_view(), name='api-part-category-tree'),
|
||||
|
||||
re_path(r'^parameters/', include([
|
||||
re_path(r'^(?P<pk>\d+)/', CategoryParameterDetail.as_view(), name='api-part-category-parameter-detail'),
|
||||
re_path(r'^(?P<pk>\d+)/', include([
|
||||
re_path(r'^metadata/', MetadataView.as_view(), {'model': PartCategoryParameterTemplate}, name='api-part-category-parameter-metadata'),
|
||||
re_path(r'^.*$', CategoryParameterDetail.as_view(), name='api-part-category-parameter-detail'),
|
||||
])),
|
||||
re_path(r'^.*$', CategoryParameterList.as_view(), name='api-part-category-parameter-list'),
|
||||
])),
|
||||
|
||||
@ -1910,7 +1913,10 @@ part_api_urls = [
|
||||
|
||||
# Base URL for PartTestTemplate API endpoints
|
||||
re_path(r'^test-template/', include([
|
||||
path(r'<int:pk>/', PartTestTemplateDetail.as_view(), name='api-part-test-template-detail'),
|
||||
path(r'<int:pk>/', include([
|
||||
re_path(r'^metadata/', MetadataView.as_view(), {'model': PartTestTemplate}, name='api-part-test-template-metadata'),
|
||||
re_path(r'^.*$', PartTestTemplateDetail.as_view(), name='api-part-test-template-detail'),
|
||||
])),
|
||||
path('', PartTestTemplateList.as_view(), name='api-part-test-template-list'),
|
||||
])),
|
||||
|
||||
@ -1934,7 +1940,10 @@ part_api_urls = [
|
||||
|
||||
# Base URL for PartRelated API endpoints
|
||||
re_path(r'^related/', include([
|
||||
path(r'<int:pk>/', PartRelatedDetail.as_view(), name='api-part-related-detail'),
|
||||
path(r'<int:pk>/', include([
|
||||
re_path(r'^metadata/', MetadataView.as_view(), {'model': PartRelated}, name='api-part-related-metadata'),
|
||||
re_path(r'^.*$', PartRelatedDetail.as_view(), name='api-part-related-detail'),
|
||||
])),
|
||||
re_path(r'^.*$', PartRelatedList.as_view(), name='api-part-related-list'),
|
||||
])),
|
||||
|
||||
@ -1948,7 +1957,10 @@ part_api_urls = [
|
||||
re_path(r'^.*$', PartParameterTemplateList.as_view(), name='api-part-parameter-template-list'),
|
||||
])),
|
||||
|
||||
path(r'<int:pk>/', PartParameterDetail.as_view(), name='api-part-parameter-detail'),
|
||||
path(r'<int:pk>/', include([
|
||||
re_path(r'^metadata/?', MetadataView.as_view(), {'model': PartParameter}, name='api-part-parameter-metadata'),
|
||||
re_path(r'^.*$', PartParameterDetail.as_view(), name='api-part-parameter-detail'),
|
||||
])),
|
||||
re_path(r'^.*$', PartParameterList.as_view(), name='api-part-parameter-list'),
|
||||
])),
|
||||
|
||||
@ -2012,7 +2024,10 @@ bom_api_urls = [
|
||||
re_path(r'^substitute/', include([
|
||||
|
||||
# Detail view
|
||||
path(r'<int:pk>/', BomItemSubstituteDetail.as_view(), name='api-bom-substitute-detail'),
|
||||
path(r'<int:pk>/', include([
|
||||
re_path(r'^metadata/?', MetadataView.as_view(), {'model': BomItemSubstitute}, name='api-bom-substitute-metadata'),
|
||||
re_path(r'^.*$', BomItemSubstituteDetail.as_view(), name='api-bom-substitute-detail'),
|
||||
])),
|
||||
|
||||
# Catch all
|
||||
re_path(r'^.*$', BomItemSubstituteList.as_view(), name='api-bom-substitute-list'),
|
||||
|
@ -50,3 +50,9 @@
|
||||
part: 101
|
||||
sub_part: 100
|
||||
quantity: 10
|
||||
|
||||
- model: part.bomitemsubstitute
|
||||
pk: 1
|
||||
fields:
|
||||
part: 5
|
||||
bom_item: 6
|
||||
|
@ -188,3 +188,9 @@
|
||||
level: 0
|
||||
lft: 0
|
||||
rght: 0
|
||||
|
||||
- model: part.partrelated
|
||||
pk: 1
|
||||
fields:
|
||||
part_1: 10003
|
||||
part_2: 10004
|
||||
|
38
InvenTree/part/migrations/0112_auto_20230525_1606.py
Normal file
38
InvenTree/part/migrations/0112_auto_20230525_1606.py
Normal file
@ -0,0 +1,38 @@
|
||||
# Generated by Django 3.2.19 on 2023-05-25 16:06
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('part', '0111_auto_20230521_1350'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='bomitemsubstitute',
|
||||
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='partcategoryparametertemplate',
|
||||
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='partparameter',
|
||||
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='partrelated',
|
||||
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='parttesttemplate',
|
||||
name='metadata',
|
||||
field=models.JSONField(blank=True, help_text='JSON metadata field, for use by external plugins', null=True, verbose_name='Plugin Metadata'),
|
||||
),
|
||||
]
|
@ -6,7 +6,7 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('part', '0111_auto_20230521_1350'),
|
||||
('part', '0112_auto_20230525_1606'),
|
||||
]
|
||||
|
||||
operations = [
|
@ -3194,7 +3194,7 @@ class PartCategoryStar(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('User'), related_name='starred_categories')
|
||||
|
||||
|
||||
class PartTestTemplate(models.Model):
|
||||
class PartTestTemplate(MetadataMixin, models.Model):
|
||||
"""A PartTestTemplate defines a 'template' for a test which is required to be run against a StockItem (an instance of the Part).
|
||||
|
||||
The test template applies "recursively" to part variants, allowing tests to be
|
||||
@ -3443,7 +3443,7 @@ def post_save_part_parameter_template(sender, instance, created, **kwargs):
|
||||
)
|
||||
|
||||
|
||||
class PartParameter(models.Model):
|
||||
class PartParameter(MetadataMixin, models.Model):
|
||||
"""A PartParameter is a specific instance of a PartParameterTemplate. It assigns a particular parameter <key:value> pair to a part.
|
||||
|
||||
Attributes:
|
||||
@ -3559,7 +3559,7 @@ class PartParameter(models.Model):
|
||||
return part_parameter
|
||||
|
||||
|
||||
class PartCategoryParameterTemplate(models.Model):
|
||||
class PartCategoryParameterTemplate(MetadataMixin, models.Model):
|
||||
"""A PartCategoryParameterTemplate creates a unique relationship between a PartCategory and a PartParameterTemplate.
|
||||
|
||||
Multiple PartParameterTemplate instances can be associated to a PartCategory to drive a default list of parameter templates attached to a Part instance upon creation.
|
||||
@ -3999,7 +3999,7 @@ def update_pricing_after_delete(sender, instance, **kwargs):
|
||||
instance.part.schedule_pricing_update(create=False)
|
||||
|
||||
|
||||
class BomItemSubstitute(models.Model):
|
||||
class BomItemSubstitute(MetadataMixin, models.Model):
|
||||
"""A BomItemSubstitute provides a specification for alternative parts, which can be used in a bill of materials.
|
||||
|
||||
Attributes:
|
||||
@ -4058,7 +4058,7 @@ class BomItemSubstitute(models.Model):
|
||||
)
|
||||
|
||||
|
||||
class PartRelated(models.Model):
|
||||
class PartRelated(MetadataMixin, models.Model):
|
||||
"""Store and handle related parts (eg. mating connector, crimps, etc.)."""
|
||||
|
||||
class Meta:
|
||||
|
@ -22,8 +22,9 @@ from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatus,
|
||||
StockStatus)
|
||||
from InvenTree.unit_test import InvenTreeAPITestCase
|
||||
from part.models import (BomItem, BomItemSubstitute, Part, PartCategory,
|
||||
PartCategoryParameterTemplate, PartParameterTemplate,
|
||||
PartRelated, PartStocktake)
|
||||
PartCategoryParameterTemplate, PartParameter,
|
||||
PartParameterTemplate, PartRelated, PartStocktake,
|
||||
PartTestTemplate)
|
||||
from stock.models import StockItem, StockLocation
|
||||
|
||||
|
||||
@ -159,26 +160,6 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
|
||||
# Annotation should include parts from all sub-categories
|
||||
self.assertEqual(response.data['part_count'], 100)
|
||||
|
||||
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')
|
||||
|
||||
def test_category_parameters(self):
|
||||
"""Test that the PartCategoryParameterTemplate API function work"""
|
||||
|
||||
@ -1665,56 +1646,6 @@ class PartDetailTests(PartAPITestBase):
|
||||
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 PartListTests(PartAPITestBase):
|
||||
"""Unit tests for the Part List API endpoint"""
|
||||
@ -2536,9 +2467,13 @@ class BomItemTest(InvenTreeAPITestCase):
|
||||
url = reverse('api-bom-substitute-list')
|
||||
stock_url = reverse('api-stock-list')
|
||||
|
||||
# Initially we have no substitute parts
|
||||
# Initially we may have substitute parts
|
||||
# Count first, operate directly on Model
|
||||
countbefore = BomItemSubstitute.objects.count()
|
||||
|
||||
# Now, make sure API returns the same count
|
||||
response = self.get(url, expected_code=200)
|
||||
self.assertEqual(len(response.data), 0)
|
||||
self.assertEqual(len(response.data), countbefore)
|
||||
|
||||
# BOM item we are interested in
|
||||
bom_item = BomItem.objects.get(pk=1)
|
||||
@ -2594,9 +2529,9 @@ class BomItemTest(InvenTreeAPITestCase):
|
||||
|
||||
self.assertEqual(len(response.data), n_items + ii + 1)
|
||||
|
||||
# There should now be 5 substitute parts available in the database
|
||||
# There should now be 5 more substitute parts available in the database
|
||||
response = self.get(url, expected_code=200)
|
||||
self.assertEqual(len(response.data), 5)
|
||||
self.assertEqual(len(response.data), countbefore + 5)
|
||||
|
||||
# The BomItem detail endpoint should now also reflect the substitute data
|
||||
data = self.get(
|
||||
@ -3050,3 +2985,71 @@ class PartStocktakeTest(InvenTreeAPITestCase):
|
||||
InvenTreeSetting.set_setting('STOCKTAKE_ENABLE', True, None)
|
||||
response = self.post(url, data={}, expected_code=400)
|
||||
self.assertIn('Background worker check failed', str(response.data))
|
||||
|
||||
|
||||
class PartMetadataAPITest(InvenTreeAPITestCase):
|
||||
"""Unit tests for the various metadata endpoints of API."""
|
||||
|
||||
fixtures = [
|
||||
'category',
|
||||
'part',
|
||||
'params',
|
||||
'location',
|
||||
'bom',
|
||||
'company',
|
||||
'test_templates',
|
||||
'manufacturer_part',
|
||||
'supplier_part',
|
||||
'order',
|
||||
'stock',
|
||||
]
|
||||
|
||||
roles = [
|
||||
'part.change',
|
||||
'part_category.change',
|
||||
]
|
||||
|
||||
def metatester(self, apikey, model):
|
||||
"""Generic tester"""
|
||||
|
||||
modeldata = model.objects.first()
|
||||
|
||||
# Useless test unless a model object is found
|
||||
self.assertIsNotNone(modeldata)
|
||||
|
||||
url = reverse(apikey, kwargs={'pk': modeldata.pk})
|
||||
|
||||
# Metadata is initially null
|
||||
self.assertIsNone(modeldata.metadata)
|
||||
|
||||
numstr = randint(100, 900)
|
||||
|
||||
self.patch(
|
||||
url,
|
||||
{
|
||||
'metadata': {
|
||||
f'abc-{numstr}': f'xyz-{apikey}-{numstr}',
|
||||
}
|
||||
},
|
||||
expected_code=200
|
||||
)
|
||||
|
||||
# Refresh
|
||||
modeldata.refresh_from_db()
|
||||
self.assertEqual(modeldata.get_metadata(f'abc-{numstr}'), f'xyz-{apikey}-{numstr}')
|
||||
|
||||
def test_metadata(self):
|
||||
"""Test all endpoints"""
|
||||
|
||||
for apikey, model in {
|
||||
'api-part-category-parameter-metadata': PartCategoryParameterTemplate,
|
||||
'api-part-category-metadata': PartCategory,
|
||||
'api-part-test-template-metadata': PartTestTemplate,
|
||||
'api-part-related-metadata': PartRelated,
|
||||
'api-part-parameter-template-metadata': PartParameterTemplate,
|
||||
'api-part-parameter-metadata': PartParameter,
|
||||
'api-part-metadata': Part,
|
||||
'api-bom-substitute-metadata': BomItemSubstitute,
|
||||
'api-bom-item-metadata': BomItem,
|
||||
}.items():
|
||||
self.metatester(apikey, model)
|
||||
|
@ -294,8 +294,10 @@ class PartTest(TestCase):
|
||||
"""Unit tests for the PartRelated model"""
|
||||
|
||||
# Create a part relationship
|
||||
# Count before creation
|
||||
countbefore = PartRelated.objects.count()
|
||||
PartRelated.objects.create(part_1=self.r1, part_2=self.r2)
|
||||
self.assertEqual(PartRelated.objects.count(), 1)
|
||||
self.assertEqual(PartRelated.objects.count(), countbefore + 1)
|
||||
|
||||
# Creating a duplicate part relationship should fail
|
||||
with self.assertRaises(ValidationError):
|
||||
@ -321,7 +323,7 @@ class PartTest(TestCase):
|
||||
# Delete a part, ensure the relationship also gets deleted
|
||||
self.r1.delete()
|
||||
|
||||
self.assertEqual(PartRelated.objects.count(), 0)
|
||||
self.assertEqual(PartRelated.objects.count(), countbefore)
|
||||
self.assertEqual(len(self.r2.get_related_parts()), 0)
|
||||
|
||||
# Add multiple part relationships to self.r2
|
||||
@ -330,12 +332,12 @@ class PartTest(TestCase):
|
||||
|
||||
n = Part.objects.count() - 1
|
||||
|
||||
self.assertEqual(PartRelated.objects.count(), n)
|
||||
self.assertEqual(PartRelated.objects.count(), n + countbefore)
|
||||
self.assertEqual(len(self.r2.get_related_parts()), n)
|
||||
|
||||
# Deleting r2 should remove *all* relationships
|
||||
# Deleting r2 should remove *all* newly created relationships
|
||||
self.r2.delete()
|
||||
self.assertEqual(PartRelated.objects.count(), 0)
|
||||
self.assertEqual(PartRelated.objects.count(), countbefore)
|
||||
|
||||
def test_stocktake(self):
|
||||
"""Test for adding stocktake data"""
|
||||
|
@ -1430,7 +1430,10 @@ stock_api_urls = [
|
||||
|
||||
# StockItemTestResult API endpoints
|
||||
re_path(r'^test/', include([
|
||||
path(r'<int:pk>/', StockItemTestResultDetail.as_view(), name='api-stock-test-result-detail'),
|
||||
path(r'<int:pk>/', include([
|
||||
re_path(r'^metadata/', MetadataView.as_view(), {'model': StockItemTestResult}, name='api-stock-test-result-metadata'),
|
||||
re_path(r'^.*$', StockItemTestResultDetail.as_view(), name='api-stock-test-result-detail'),
|
||||
])),
|
||||
re_path(r'^.*$', StockItemTestResultList.as_view(), name='api-stock-test-result-list'),
|
||||
])),
|
||||
|
||||
|
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.19 on 2023-05-25 16:06
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('stock', '0100_stockitem_consumed_by'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='stockitemtestresult',
|
||||
name='metadata',
|
||||
field=models.JSONField(blank=True, help_text='JSON metadata field, for use by external plugins', null=True, verbose_name='Plugin Metadata'),
|
||||
),
|
||||
]
|
@ -2170,7 +2170,7 @@ def rename_stock_item_test_result_attachment(instance, filename):
|
||||
return os.path.join('stock_files', str(instance.stock_item.pk), os.path.basename(filename))
|
||||
|
||||
|
||||
class StockItemTestResult(models.Model):
|
||||
class StockItemTestResult(MetadataMixin, models.Model):
|
||||
"""A StockItemTestResult records results of custom tests against individual StockItem objects.
|
||||
|
||||
This is useful for tracking unit acceptance tests, and particularly useful when integrated
|
||||
|
@ -1693,3 +1693,62 @@ class StockMergeTest(StockAPITestCase):
|
||||
|
||||
# Total number of stock items has been reduced!
|
||||
self.assertEqual(StockItem.objects.filter(part=self.part).count(), n - 2)
|
||||
|
||||
|
||||
class StockMetadataAPITest(InvenTreeAPITestCase):
|
||||
"""Unit tests for the various metadata endpoints of API."""
|
||||
|
||||
fixtures = [
|
||||
'category',
|
||||
'part',
|
||||
'bom',
|
||||
'company',
|
||||
'location',
|
||||
'supplier_part',
|
||||
'stock',
|
||||
'stock_tests',
|
||||
]
|
||||
|
||||
roles = [
|
||||
'stock.change',
|
||||
'stock_location.change',
|
||||
]
|
||||
|
||||
def metatester(self, apikey, model):
|
||||
"""Generic tester"""
|
||||
|
||||
modeldata = model.objects.first()
|
||||
|
||||
# Useless test unless a model object is found
|
||||
self.assertIsNotNone(modeldata)
|
||||
|
||||
url = reverse(apikey, kwargs={'pk': modeldata.pk})
|
||||
|
||||
# Metadata is initially null
|
||||
self.assertIsNone(modeldata.metadata)
|
||||
|
||||
numstr = f'12{len(apikey)}'
|
||||
|
||||
self.patch(
|
||||
url,
|
||||
{
|
||||
'metadata': {
|
||||
f'abc-{numstr}': f'xyz-{apikey}-{numstr}',
|
||||
}
|
||||
},
|
||||
expected_code=200
|
||||
)
|
||||
|
||||
# Refresh
|
||||
modeldata.refresh_from_db()
|
||||
self.assertEqual(modeldata.get_metadata(f'abc-{numstr}'), f'xyz-{apikey}-{numstr}')
|
||||
|
||||
def test_metadata(self):
|
||||
"""Test all endpoints"""
|
||||
|
||||
for apikey, model in {
|
||||
'api-location-metadata': StockLocation,
|
||||
'api-stock-test-result-metadata': StockItemTestResult,
|
||||
'api-stock-item-metadata': StockItem,
|
||||
}.items():
|
||||
self.metatester(apikey, model)
|
||||
|
Loading…
Reference in New Issue
Block a user