mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 13:15:43 +00:00 
			
		
		
		
	Adds "active" field for Company model (#7024)
* Add "active" field to Company model * Expose 'active' parameter to API * Fix default value * Add 'active' column to PUI * Update PUI table * Update company detail pages * Update API filters for SupplierPart and ManufacturerPart * Bump API version * Update order forms * Add edit action to SalesOrderDetail page * Enable editing of ReturnOrder * Typo fix * Adds explicit "active" field to SupplierPart model * More updates - Add "inactive" badge to SupplierPart page - Update SupplierPartTable - Update backend API fields * Update ReturnOrderTable - Also some refactoring * Impove usePurchaseOrderLineItemFields hook * Cleanup * Implement duplicate action for SupplierPart * Fix for ApiForm - Only override initialValues for specified fields * Allow edit and duplicate of StockItem * Fix for ApiForm - Default values were overriding initial data * Add duplicate part option * Cleanup ApiForm - Cache props.fields * Fix unused import * More fixes * Add unit tests * Allow ordering company by 'active' status * Update docs * Merge migrations * Fix for serializers.py * Force new form value * Remove debug call * Further unit test fixes * Update default CSRF_TRUSTED_ORIGINS values * Reduce debug output
This commit is contained in:
		| @@ -1,11 +1,15 @@ | ||||
| """InvenTree API version information.""" | ||||
|  | ||||
| # InvenTree API version | ||||
| INVENTREE_API_VERSION = 189 | ||||
| INVENTREE_API_VERSION = 190 | ||||
| """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" | ||||
|  | ||||
| INVENTREE_API_TEXT = """ | ||||
|  | ||||
| v190 - 2024-04-19 : https://github.com/inventree/InvenTree/pull/7024 | ||||
|     - Adds "active" field to the Company API endpoints | ||||
|     - Allow company list to be filtered by "active" status | ||||
|  | ||||
| v189 - 2024-04-19 : https://github.com/inventree/InvenTree/pull/7066 | ||||
|     - Adds "currency" field to CompanyBriefSerializer class | ||||
|  | ||||
|   | ||||
| @@ -1087,14 +1087,17 @@ CSRF_TRUSTED_ORIGINS = get_setting( | ||||
| if SITE_URL and SITE_URL not in CSRF_TRUSTED_ORIGINS: | ||||
|     CSRF_TRUSTED_ORIGINS.append(SITE_URL) | ||||
|  | ||||
| if not TESTING and len(CSRF_TRUSTED_ORIGINS) == 0: | ||||
|     if DEBUG: | ||||
|         logger.warning( | ||||
|             'No CSRF_TRUSTED_ORIGINS specified. Defaulting to http://* for debug mode. This is not recommended for production use' | ||||
|         ) | ||||
|         CSRF_TRUSTED_ORIGINS = ['http://*'] | ||||
| if DEBUG: | ||||
|     for origin in [ | ||||
|         'http://localhost', | ||||
|         'http://*.localhost' 'http://*localhost:8000', | ||||
|         'http://*localhost:5173', | ||||
|     ]: | ||||
|         if origin not in CSRF_TRUSTED_ORIGINS: | ||||
|             CSRF_TRUSTED_ORIGINS.append(origin) | ||||
|  | ||||
|     elif isInMainThread(): | ||||
| if not TESTING and len(CSRF_TRUSTED_ORIGINS) == 0: | ||||
|     if isInMainThread(): | ||||
|         # Server thread cannot run without CSRF_TRUSTED_ORIGINS | ||||
|         logger.error( | ||||
|             'No CSRF_TRUSTED_ORIGINS specified. Please provide a list of trusted origins, or specify INVENTREE_SITE_URL' | ||||
|   | ||||
| @@ -2,6 +2,7 @@ | ||||
|  | ||||
| from django.db.models import Q | ||||
| from django.urls import include, path, re_path | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
|  | ||||
| from django_filters import rest_framework as rest_filters | ||||
|  | ||||
| @@ -58,11 +59,17 @@ class CompanyList(ListCreateAPI): | ||||
|  | ||||
|     filter_backends = SEARCH_ORDER_FILTER | ||||
|  | ||||
|     filterset_fields = ['is_customer', 'is_manufacturer', 'is_supplier', 'name'] | ||||
|     filterset_fields = [ | ||||
|         'is_customer', | ||||
|         'is_manufacturer', | ||||
|         'is_supplier', | ||||
|         'name', | ||||
|         'active', | ||||
|     ] | ||||
|  | ||||
|     search_fields = ['name', 'description', 'website'] | ||||
|  | ||||
|     ordering_fields = ['name', 'parts_supplied', 'parts_manufactured'] | ||||
|     ordering_fields = ['active', 'name', 'parts_supplied', 'parts_manufactured'] | ||||
|  | ||||
|     ordering = 'name' | ||||
|  | ||||
| @@ -153,7 +160,13 @@ class ManufacturerPartFilter(rest_filters.FilterSet): | ||||
|         fields = ['manufacturer', 'MPN', 'part', 'tags__name', 'tags__slug'] | ||||
|  | ||||
|     # Filter by 'active' status of linked part | ||||
|     active = rest_filters.BooleanFilter(field_name='part__active') | ||||
|     part_active = rest_filters.BooleanFilter( | ||||
|         field_name='part__active', label=_('Part is Active') | ||||
|     ) | ||||
|  | ||||
|     manufacturer_active = rest_filters.BooleanFilter( | ||||
|         field_name='manufacturer__active', label=_('Manufacturer is Active') | ||||
|     ) | ||||
|  | ||||
|  | ||||
| class ManufacturerPartList(ListCreateDestroyAPIView): | ||||
| @@ -301,8 +314,16 @@ class SupplierPartFilter(rest_filters.FilterSet): | ||||
|             'tags__slug', | ||||
|         ] | ||||
|  | ||||
|     active = rest_filters.BooleanFilter(label=_('Supplier Part is Active')) | ||||
|  | ||||
|     # Filter by 'active' status of linked part | ||||
|     active = rest_filters.BooleanFilter(field_name='part__active') | ||||
|     part_active = rest_filters.BooleanFilter( | ||||
|         field_name='part__active', label=_('Internal Part is Active') | ||||
|     ) | ||||
|  | ||||
|     supplier_active = rest_filters.BooleanFilter( | ||||
|         field_name='supplier__active', label=_('Supplier is Active') | ||||
|     ) | ||||
|  | ||||
|     # Filter by the 'MPN' of linked manufacturer part | ||||
|     MPN = rest_filters.CharFilter( | ||||
| @@ -378,6 +399,7 @@ class SupplierPartList(ListCreateDestroyAPIView): | ||||
|         'part', | ||||
|         'supplier', | ||||
|         'manufacturer', | ||||
|         'active', | ||||
|         'MPN', | ||||
|         'packaging', | ||||
|         'pack_quantity', | ||||
|   | ||||
| @@ -0,0 +1,23 @@ | ||||
| # Generated by Django 4.2.11 on 2024-04-15 14:42 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('company', '0068_auto_20231120_1108'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='company', | ||||
|             name='active', | ||||
|             field=models.BooleanField(default=True, help_text='Is this company active?', verbose_name='Active'), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='supplierpart', | ||||
|             name='active', | ||||
|             field=models.BooleanField(default=True, help_text='Is this supplier part active?', verbose_name='Active'), | ||||
|         ), | ||||
|     ] | ||||
| @@ -81,6 +81,7 @@ class Company( | ||||
|         link: Secondary URL e.g. for link to internal Wiki page | ||||
|         image: Company image / logo | ||||
|         notes: Extra notes about the company | ||||
|         active: boolean value, is this company active | ||||
|         is_customer: boolean value, is this company a customer | ||||
|         is_supplier: boolean value, is this company a supplier | ||||
|         is_manufacturer: boolean value, is this company a manufacturer | ||||
| @@ -155,6 +156,10 @@ class Company( | ||||
|         verbose_name=_('Image'), | ||||
|     ) | ||||
|  | ||||
|     active = models.BooleanField( | ||||
|         default=True, verbose_name=_('Active'), help_text=_('Is this company active?') | ||||
|     ) | ||||
|  | ||||
|     is_customer = models.BooleanField( | ||||
|         default=False, | ||||
|         verbose_name=_('is customer'), | ||||
| @@ -654,6 +659,7 @@ class SupplierPart( | ||||
|         part: Link to the master Part (Obsolete) | ||||
|         source_item: The sourcing item linked to this SupplierPart instance | ||||
|         supplier: Company that supplies this SupplierPart object | ||||
|         active: Boolean value, is this supplier part active | ||||
|         SKU: Stock keeping unit (supplier part number) | ||||
|         link: Link to external website for this supplier part | ||||
|         description: Descriptive notes field | ||||
| @@ -802,6 +808,12 @@ class SupplierPart( | ||||
|         help_text=_('Supplier stock keeping unit'), | ||||
|     ) | ||||
|  | ||||
|     active = models.BooleanField( | ||||
|         default=True, | ||||
|         verbose_name=_('Active'), | ||||
|         help_text=_('Is this supplier part active?'), | ||||
|     ) | ||||
|  | ||||
|     manufacturer_part = models.ForeignKey( | ||||
|         ManufacturerPart, | ||||
|         on_delete=models.CASCADE, | ||||
|   | ||||
| @@ -42,12 +42,17 @@ class CompanyBriefSerializer(InvenTreeModelSerializer): | ||||
|         """Metaclass options.""" | ||||
|  | ||||
|         model = Company | ||||
|         fields = ['pk', 'url', 'name', 'description', 'image', 'thumbnail', 'currency'] | ||||
|  | ||||
|         fields = [ | ||||
|             'pk', | ||||
|             'active', | ||||
|             'name', | ||||
|             'description', | ||||
|             'image', | ||||
|             'thumbnail', | ||||
|             'currency', | ||||
|         ] | ||||
|         read_only_fields = ['currency'] | ||||
|  | ||||
|     url = serializers.CharField(source='get_absolute_url', read_only=True) | ||||
|  | ||||
|     image = InvenTreeImageSerializerField(read_only=True) | ||||
|  | ||||
|     thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True) | ||||
| @@ -118,6 +123,7 @@ class CompanySerializer(RemoteImageMixin, InvenTreeModelSerializer): | ||||
|             'contact', | ||||
|             'link', | ||||
|             'image', | ||||
|             'active', | ||||
|             'is_customer', | ||||
|             'is_manufacturer', | ||||
|             'is_supplier', | ||||
| @@ -308,6 +314,7 @@ class SupplierPartSerializer(InvenTreeTagModelSerializer): | ||||
|             'description', | ||||
|             'in_stock', | ||||
|             'link', | ||||
|             'active', | ||||
|             'manufacturer', | ||||
|             'manufacturer_detail', | ||||
|             'manufacturer_part', | ||||
| @@ -371,8 +378,9 @@ class SupplierPartSerializer(InvenTreeTagModelSerializer): | ||||
|             self.fields.pop('pretty_name') | ||||
|  | ||||
|     # Annotated field showing total in-stock quantity | ||||
|     in_stock = serializers.FloatField(read_only=True) | ||||
|     available = serializers.FloatField(required=False) | ||||
|     in_stock = serializers.FloatField(read_only=True, label=_('In Stock')) | ||||
|  | ||||
|     available = serializers.FloatField(required=False, label=_('Available')) | ||||
|  | ||||
|     pack_quantity_native = serializers.FloatField(read_only=True) | ||||
|  | ||||
|   | ||||
| @@ -10,6 +10,12 @@ | ||||
|  | ||||
| {% block heading %} | ||||
| {% trans "Company" %}: {{ company.name }} | ||||
| {% if not company.active %} | ||||
|   | ||||
| <div class='badge rounded-pill bg-danger'> | ||||
|     {% trans 'Inactive' %} | ||||
| </div> | ||||
| {% endif %} | ||||
| {% endblock heading %} | ||||
|  | ||||
| {% block actions %} | ||||
|   | ||||
| @@ -5,6 +5,7 @@ from django.urls import reverse | ||||
| from rest_framework import status | ||||
|  | ||||
| from InvenTree.unit_test import InvenTreeAPITestCase | ||||
| from part.models import Part | ||||
|  | ||||
| from .models import Address, Company, Contact, ManufacturerPart, SupplierPart | ||||
|  | ||||
| @@ -131,6 +132,32 @@ class CompanyTest(InvenTreeAPITestCase): | ||||
|  | ||||
|         self.assertTrue('currency' in response.data) | ||||
|  | ||||
|     def test_company_active(self): | ||||
|         """Test that the 'active' value and filter works.""" | ||||
|         Company.objects.filter(active=False).update(active=True) | ||||
|         n = Company.objects.count() | ||||
|  | ||||
|         url = reverse('api-company-list') | ||||
|  | ||||
|         self.assertEqual( | ||||
|             len(self.get(url, data={'active': True}, expected_code=200).data), n | ||||
|         ) | ||||
|         self.assertEqual( | ||||
|             len(self.get(url, data={'active': False}, expected_code=200).data), 0 | ||||
|         ) | ||||
|  | ||||
|         # Set one company to inactive | ||||
|         c = Company.objects.first() | ||||
|         c.active = False | ||||
|         c.save() | ||||
|  | ||||
|         self.assertEqual( | ||||
|             len(self.get(url, data={'active': True}, expected_code=200).data), n - 1 | ||||
|         ) | ||||
|         self.assertEqual( | ||||
|             len(self.get(url, data={'active': False}, expected_code=200).data), 1 | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class ContactTest(InvenTreeAPITestCase): | ||||
|     """Tests for the Contact models.""" | ||||
| @@ -528,6 +555,50 @@ class SupplierPartTest(InvenTreeAPITestCase): | ||||
|         self.assertEqual(sp.available, 999) | ||||
|         self.assertIsNotNone(sp.availability_updated) | ||||
|  | ||||
|     def test_active(self): | ||||
|         """Test that 'active' status filtering works correctly.""" | ||||
|         url = reverse('api-supplier-part-list') | ||||
|  | ||||
|         # Create a new company, which is inactive | ||||
|         company = Company.objects.create( | ||||
|             name='Inactive Company', is_supplier=True, active=False | ||||
|         ) | ||||
|  | ||||
|         part = Part.objects.filter(purchaseable=True).first() | ||||
|  | ||||
|         # Create some new supplier part objects, *some* of which are inactive | ||||
|         for idx in range(10): | ||||
|             SupplierPart.objects.create( | ||||
|                 part=part, | ||||
|                 supplier=company, | ||||
|                 SKU=f'CMP-{company.pk}-SKU-{idx}', | ||||
|                 active=(idx % 2 == 0), | ||||
|             ) | ||||
|  | ||||
|         n = SupplierPart.objects.count() | ||||
|  | ||||
|         # List *all* supplier parts | ||||
|         self.assertEqual(len(self.get(url, data={}, expected_code=200).data), n) | ||||
|  | ||||
|         # List only active supplier parts (all except 5 from the new supplier) | ||||
|         self.assertEqual( | ||||
|             len(self.get(url, data={'active': True}, expected_code=200).data), n - 5 | ||||
|         ) | ||||
|  | ||||
|         # List only from 'active' suppliers (all except this new supplier) | ||||
|         self.assertEqual( | ||||
|             len(self.get(url, data={'supplier_active': True}, expected_code=200).data), | ||||
|             n - 10, | ||||
|         ) | ||||
|  | ||||
|         # List active parts from inactive suppliers (only 5 from the new supplier) | ||||
|         response = self.get( | ||||
|             url, data={'supplier_active': False, 'active': True}, expected_code=200 | ||||
|         ) | ||||
|         self.assertEqual(len(response.data), 5) | ||||
|         for result in response.data: | ||||
|             self.assertEqual(result['supplier'], company.pk) | ||||
|  | ||||
|  | ||||
| class CompanyMetadataAPITest(InvenTreeAPITestCase): | ||||
|     """Unit tests for the various metadata endpoints of API.""" | ||||
|   | ||||
| @@ -426,7 +426,8 @@ function companyFormFields() { | ||||
|         }, | ||||
|         is_supplier: {}, | ||||
|         is_manufacturer: {}, | ||||
|         is_customer: {} | ||||
|         is_customer: {}, | ||||
|         active: {}, | ||||
|     }; | ||||
| } | ||||
|  | ||||
| @@ -517,6 +518,15 @@ function loadCompanyTable(table, url, options={}) { | ||||
|             field: 'description', | ||||
|             title: '{% trans "Description" %}', | ||||
|         }, | ||||
|         { | ||||
|             field: 'active', | ||||
|             title: '{% trans "Active" %}', | ||||
|             sortable: true, | ||||
|             switchable: true, | ||||
|             formatter: function(value) { | ||||
|                 return yesNoLabel(value); | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             field: 'website', | ||||
|             title: '{% trans "Website" %}', | ||||
|   | ||||
| @@ -791,6 +791,10 @@ function getContactFilters() { | ||||
| // Return a dictionary of filters for the "company" table | ||||
| function getCompanyFilters() { | ||||
|     return { | ||||
|         active: { | ||||
|             type: 'bool', | ||||
|             title: '{% trans "Active" %}' | ||||
|         }, | ||||
|         is_manufacturer: { | ||||
|             type: 'bool', | ||||
|             title: '{% trans "Manufacturer" %}', | ||||
|   | ||||
		Reference in New Issue
	
	Block a user