Skip to content

Commit

Permalink
Adds "active" field for Company model (#7024)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
SchrodingersGat authored Apr 20, 2024
1 parent 2632bcf commit 2fe0eef
Show file tree
Hide file tree
Showing 41 changed files with 927 additions and 390 deletions.
3 changes: 1 addition & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ Run the following commands from the top-level project directory:

```
$ git clone https://github.com/inventree/inventree
$ cd inventree/docs
$ pip install -r requirements.txt
$ pip install -r docs/requirements.txt
```

## Serve Locally
Expand Down
Binary file added docs/docs/assets/images/order/company_disable.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 33 additions & 1 deletion docs/docs/order/company.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ External companies are represented by the *Company* database model. Each company
- [Manufacturer](#manufacturers)

!!! tip Multi Purpose
A company may be allocated to multiple categories
A company may be allocated to multiple categories, for example, a company may be both a supplier and a customer.

### Edit Company

Expand All @@ -20,6 +20,20 @@ To edit a company, click on the <span class='fas fa-edit'>Edit Company</span> ic
!!! warning "Permission Required"
The edit button will not be available to users who do not have the required permissions to edit the company

### Disable Company

Rather than deleting a company, it is possible to disable it. This will prevent the company from being used in new orders, but will not remove it from the database. Additionally, any existing orders associated with the company (and other linked items such as supplier parts, for a supplier) will remain intact. Unless the company is re-enabled, it will not be available for selection in new orders.

It is recommended to disable a company rather than deleting it, as this will preserve the integrity of historical data.

To disable a company, simply edit the company details and set the `active` attribute to `False`:

{% with id="company_disable", url="order/company_disable.png", description="Disable Company" %}
{% include "img.html" %}
{% endwith %}

To re-enable a company, simply follow the same process and set the `active` attribute to `True`.

### Delete Company

To delete a company, click on the <span class='fas fa-trash-alt'></span> icon under the actions menu. Confirm the deletion using the checkbox then click on <span class="badge inventree confirm">Submit</span>
Expand Down Expand Up @@ -193,6 +207,24 @@ To edit a supplier part, first access the supplier part detail page with one of

After the supplier part details are loaded, click on the <span class='fas fa-edit'></span> icon next to the supplier part image. Edit the supplier part information then click on <span class="badge inventree confirm">Submit</span>

#### Disable Supplier Part

Supplier parts can be individually disabled - for example, if a supplier part is no longer available for purchase. By disabling the part in the InvenTree system, it will no longer be available for selection in new purchase orders. However, any existing purchase orders which reference the supplier part will remain intact.

The "active" status of a supplier part is clearly visible within the user interface:

{% with id="supplier_part_disable", url="order/disable_supplier_part.png", description="Disable Supplier Part" %}
{% include "img.html" %}
{% endwith %}

To change the "active" status of a supplier part, simply edit the supplier part details and set the `active` attribute:

{% with id="supplier_part_disable_edit", url="order/disable_supplier_part_edit.png", description="Disable Supplier Part" %}
{% include "img.html" %}
{% endwith %}

It is recommended to disable a supplier part rather than deleting it, as this will preserve the integrity of historical data.

#### Delete Supplier Part

To delete a supplier part, first access the supplier part detail page like in the [Edit Supplier Part](#edit-supplier-part) section.
Expand Down
6 changes: 5 additions & 1 deletion src/backend/InvenTree/InvenTree/api_version.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
17 changes: 10 additions & 7 deletions src/backend/InvenTree/InvenTree/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -1087,14 +1087,17 @@
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'
Expand Down
30 changes: 26 additions & 4 deletions src/backend/InvenTree/company/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -58,11 +59,17 @@ def get_queryset(self):

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'

Expand Down Expand Up @@ -153,7 +160,13 @@ class Meta:
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):
Expand Down Expand Up @@ -301,8 +314,16 @@ class Meta:
'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(
Expand Down Expand Up @@ -378,6 +399,7 @@ def get_serializer(self, *args, **kwargs):
'part',
'supplier',
'manufacturer',
'active',
'MPN',
'packaging',
'pack_quantity',
Expand Down
23 changes: 23 additions & 0 deletions src/backend/InvenTree/company/migrations/0069_company_active.py
Original file line number Diff line number Diff line change
@@ -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'),
),
]
12 changes: 12 additions & 0 deletions src/backend/InvenTree/company/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -155,6 +156,10 @@ def get_api_url():
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'),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -802,6 +808,12 @@ def save(self, *args, **kwargs):
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,
Expand Down
20 changes: 14 additions & 6 deletions src/backend/InvenTree/company/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,17 @@ class Meta:
"""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)
Expand Down Expand Up @@ -118,6 +123,7 @@ class Meta:
'contact',
'link',
'image',
'active',
'is_customer',
'is_manufacturer',
'is_supplier',
Expand Down Expand Up @@ -308,6 +314,7 @@ class Meta:
'description',
'in_stock',
'link',
'active',
'manufacturer',
'manufacturer_detail',
'manufacturer_part',
Expand Down Expand Up @@ -371,8 +378,9 @@ def __init__(self, *args, **kwargs):
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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@

{% block heading %}
{% trans "Company" %}: {{ company.name }}
{% if not company.active %}
&ensp;
<div class='badge rounded-pill bg-danger'>
{% trans 'Inactive' %}
</div>
{% endif %}
{% endblock heading %}

{% block actions %}
Expand Down
71 changes: 71 additions & 0 deletions src/backend/InvenTree/company/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from rest_framework import status

from InvenTree.unit_test import InvenTreeAPITestCase
from part.models import Part

from .models import Address, Company, Contact, ManufacturerPart, SupplierPart

Expand Down Expand Up @@ -131,6 +132,32 @@ def test_company_create(self):

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."""
Expand Down Expand Up @@ -528,6 +555,50 @@ def test_available(self):
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."""
Expand Down
Loading

0 comments on commit 2fe0eef

Please sign in to comment.