mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-12 01:55:39 +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