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:
Oliver 2024-04-20 23:18:25 +10:00 committed by GitHub
parent 2632bcfbbc
commit 2fe0eefa8f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 927 additions and 390 deletions

View File

@ -12,8 +12,7 @@ Run the following commands from the top-level project directory:
``` ```
$ git clone https://github.com/inventree/inventree $ git clone https://github.com/inventree/inventree
$ cd inventree/docs $ pip install -r docs/requirements.txt
$ pip install -r requirements.txt
``` ```
## Serve Locally ## Serve Locally

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -11,7 +11,7 @@ External companies are represented by the *Company* database model. Each company
- [Manufacturer](#manufacturers) - [Manufacturer](#manufacturers)
!!! tip Multi Purpose !!! 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 ### Edit Company
@ -20,6 +20,20 @@ To edit a company, click on the <span class='fas fa-edit'>Edit Company</span> ic
!!! warning "Permission Required" !!! warning "Permission Required"
The edit button will not be available to users who do not have the required permissions to edit the company 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 ### 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> 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>
@ -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> 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 #### 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. To delete a supplier part, first access the supplier part detail page like in the [Edit Supplier Part](#edit-supplier-part) section.

View File

@ -1,11 +1,15 @@
"""InvenTree API version information.""" """InvenTree API version information."""
# InvenTree API version # 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.""" """Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """ 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 v189 - 2024-04-19 : https://github.com/inventree/InvenTree/pull/7066
- Adds "currency" field to CompanyBriefSerializer class - Adds "currency" field to CompanyBriefSerializer class

View File

@ -1087,14 +1087,17 @@ CSRF_TRUSTED_ORIGINS = get_setting(
if SITE_URL and SITE_URL not in CSRF_TRUSTED_ORIGINS: if SITE_URL and SITE_URL not in CSRF_TRUSTED_ORIGINS:
CSRF_TRUSTED_ORIGINS.append(SITE_URL) CSRF_TRUSTED_ORIGINS.append(SITE_URL)
if not TESTING and len(CSRF_TRUSTED_ORIGINS) == 0: if DEBUG:
if DEBUG: for origin in [
logger.warning( 'http://localhost',
'No CSRF_TRUSTED_ORIGINS specified. Defaulting to http://* for debug mode. This is not recommended for production use' 'http://*.localhost' 'http://*localhost:8000',
) 'http://*localhost:5173',
CSRF_TRUSTED_ORIGINS = ['http://*'] ]:
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 # Server thread cannot run without CSRF_TRUSTED_ORIGINS
logger.error( logger.error(
'No CSRF_TRUSTED_ORIGINS specified. Please provide a list of trusted origins, or specify INVENTREE_SITE_URL' 'No CSRF_TRUSTED_ORIGINS specified. Please provide a list of trusted origins, or specify INVENTREE_SITE_URL'

View File

@ -2,6 +2,7 @@
from django.db.models import Q from django.db.models import Q
from django.urls import include, path, re_path 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 from django_filters import rest_framework as rest_filters
@ -58,11 +59,17 @@ class CompanyList(ListCreateAPI):
filter_backends = SEARCH_ORDER_FILTER 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'] search_fields = ['name', 'description', 'website']
ordering_fields = ['name', 'parts_supplied', 'parts_manufactured'] ordering_fields = ['active', 'name', 'parts_supplied', 'parts_manufactured']
ordering = 'name' ordering = 'name'
@ -153,7 +160,13 @@ class ManufacturerPartFilter(rest_filters.FilterSet):
fields = ['manufacturer', 'MPN', 'part', 'tags__name', 'tags__slug'] fields = ['manufacturer', 'MPN', 'part', 'tags__name', 'tags__slug']
# Filter by 'active' status of linked part # 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): class ManufacturerPartList(ListCreateDestroyAPIView):
@ -301,8 +314,16 @@ class SupplierPartFilter(rest_filters.FilterSet):
'tags__slug', 'tags__slug',
] ]
active = rest_filters.BooleanFilter(label=_('Supplier Part is Active'))
# Filter by 'active' status of linked part # 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 # Filter by the 'MPN' of linked manufacturer part
MPN = rest_filters.CharFilter( MPN = rest_filters.CharFilter(
@ -378,6 +399,7 @@ class SupplierPartList(ListCreateDestroyAPIView):
'part', 'part',
'supplier', 'supplier',
'manufacturer', 'manufacturer',
'active',
'MPN', 'MPN',
'packaging', 'packaging',
'pack_quantity', 'pack_quantity',

View File

@ -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'),
),
]

View File

@ -81,6 +81,7 @@ class Company(
link: Secondary URL e.g. for link to internal Wiki page link: Secondary URL e.g. for link to internal Wiki page
image: Company image / logo image: Company image / logo
notes: Extra notes about the company notes: Extra notes about the company
active: boolean value, is this company active
is_customer: boolean value, is this company a customer is_customer: boolean value, is this company a customer
is_supplier: boolean value, is this company a supplier is_supplier: boolean value, is this company a supplier
is_manufacturer: boolean value, is this company a manufacturer is_manufacturer: boolean value, is this company a manufacturer
@ -155,6 +156,10 @@ class Company(
verbose_name=_('Image'), verbose_name=_('Image'),
) )
active = models.BooleanField(
default=True, verbose_name=_('Active'), help_text=_('Is this company active?')
)
is_customer = models.BooleanField( is_customer = models.BooleanField(
default=False, default=False,
verbose_name=_('is customer'), verbose_name=_('is customer'),
@ -654,6 +659,7 @@ class SupplierPart(
part: Link to the master Part (Obsolete) part: Link to the master Part (Obsolete)
source_item: The sourcing item linked to this SupplierPart instance source_item: The sourcing item linked to this SupplierPart instance
supplier: Company that supplies this SupplierPart object supplier: Company that supplies this SupplierPart object
active: Boolean value, is this supplier part active
SKU: Stock keeping unit (supplier part number) SKU: Stock keeping unit (supplier part number)
link: Link to external website for this supplier part link: Link to external website for this supplier part
description: Descriptive notes field description: Descriptive notes field
@ -802,6 +808,12 @@ class SupplierPart(
help_text=_('Supplier stock keeping unit'), 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( manufacturer_part = models.ForeignKey(
ManufacturerPart, ManufacturerPart,
on_delete=models.CASCADE, on_delete=models.CASCADE,

View File

@ -42,12 +42,17 @@ class CompanyBriefSerializer(InvenTreeModelSerializer):
"""Metaclass options.""" """Metaclass options."""
model = Company model = Company
fields = ['pk', 'url', 'name', 'description', 'image', 'thumbnail', 'currency'] fields = [
'pk',
'active',
'name',
'description',
'image',
'thumbnail',
'currency',
]
read_only_fields = ['currency'] read_only_fields = ['currency']
url = serializers.CharField(source='get_absolute_url', read_only=True)
image = InvenTreeImageSerializerField(read_only=True) image = InvenTreeImageSerializerField(read_only=True)
thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True) thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True)
@ -118,6 +123,7 @@ class CompanySerializer(RemoteImageMixin, InvenTreeModelSerializer):
'contact', 'contact',
'link', 'link',
'image', 'image',
'active',
'is_customer', 'is_customer',
'is_manufacturer', 'is_manufacturer',
'is_supplier', 'is_supplier',
@ -308,6 +314,7 @@ class SupplierPartSerializer(InvenTreeTagModelSerializer):
'description', 'description',
'in_stock', 'in_stock',
'link', 'link',
'active',
'manufacturer', 'manufacturer',
'manufacturer_detail', 'manufacturer_detail',
'manufacturer_part', 'manufacturer_part',
@ -371,8 +378,9 @@ class SupplierPartSerializer(InvenTreeTagModelSerializer):
self.fields.pop('pretty_name') self.fields.pop('pretty_name')
# Annotated field showing total in-stock quantity # Annotated field showing total in-stock quantity
in_stock = serializers.FloatField(read_only=True) in_stock = serializers.FloatField(read_only=True, label=_('In Stock'))
available = serializers.FloatField(required=False)
available = serializers.FloatField(required=False, label=_('Available'))
pack_quantity_native = serializers.FloatField(read_only=True) pack_quantity_native = serializers.FloatField(read_only=True)

View File

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

View File

@ -5,6 +5,7 @@ from django.urls import reverse
from rest_framework import status from rest_framework import status
from InvenTree.unit_test import InvenTreeAPITestCase from InvenTree.unit_test import InvenTreeAPITestCase
from part.models import Part
from .models import Address, Company, Contact, ManufacturerPart, SupplierPart from .models import Address, Company, Contact, ManufacturerPart, SupplierPart
@ -131,6 +132,32 @@ class CompanyTest(InvenTreeAPITestCase):
self.assertTrue('currency' in response.data) 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): class ContactTest(InvenTreeAPITestCase):
"""Tests for the Contact models.""" """Tests for the Contact models."""
@ -528,6 +555,50 @@ class SupplierPartTest(InvenTreeAPITestCase):
self.assertEqual(sp.available, 999) self.assertEqual(sp.available, 999)
self.assertIsNotNone(sp.availability_updated) 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): class CompanyMetadataAPITest(InvenTreeAPITestCase):
"""Unit tests for the various metadata endpoints of API.""" """Unit tests for the various metadata endpoints of API."""

View File

@ -426,7 +426,8 @@ function companyFormFields() {
}, },
is_supplier: {}, is_supplier: {},
is_manufacturer: {}, is_manufacturer: {},
is_customer: {} is_customer: {},
active: {},
}; };
} }
@ -517,6 +518,15 @@ function loadCompanyTable(table, url, options={}) {
field: 'description', field: 'description',
title: '{% trans "Description" %}', title: '{% trans "Description" %}',
}, },
{
field: 'active',
title: '{% trans "Active" %}',
sortable: true,
switchable: true,
formatter: function(value) {
return yesNoLabel(value);
}
},
{ {
field: 'website', field: 'website',
title: '{% trans "Website" %}', title: '{% trans "Website" %}',

View File

@ -791,6 +791,10 @@ function getContactFilters() {
// Return a dictionary of filters for the "company" table // Return a dictionary of filters for the "company" table
function getCompanyFilters() { function getCompanyFilters() {
return { return {
active: {
type: 'bool',
title: '{% trans "Active" %}'
},
is_manufacturer: { is_manufacturer: {
type: 'bool', type: 'bool',
title: '{% trans "Manufacturer" %}', title: '{% trans "Manufacturer" %}',

View File

@ -107,6 +107,8 @@ export function OptionsApiForm({
const optionsQuery = useQuery({ const optionsQuery = useQuery({
enabled: true, enabled: true,
refetchOnMount: false,
refetchOnWindowFocus: false,
queryKey: [ queryKey: [
'form-options-data', 'form-options-data',
id, id,
@ -181,21 +183,26 @@ export function ApiForm({
props: ApiFormProps; props: ApiFormProps;
optionsLoading: boolean; optionsLoading: boolean;
}) { }) {
const fields: ApiFormFieldSet = useMemo(() => {
return props.fields ?? {};
}, [props.fields]);
const defaultValues: FieldValues = useMemo(() => { const defaultValues: FieldValues = useMemo(() => {
let defaultValuesMap = mapFields(props.fields ?? {}, (_path, field) => { let defaultValuesMap = mapFields(fields ?? {}, (_path, field) => {
return field.value ?? field.default ?? undefined; return field.value ?? field.default ?? undefined;
}); });
// If the user has specified initial data, use that instead // If the user has specified initial data, that overrides default values
// But, *only* for the fields we have specified
if (props.initialData) { if (props.initialData) {
defaultValuesMap = { Object.keys(props.initialData).map((key) => {
...defaultValuesMap, if (key in defaultValuesMap) {
...props.initialData defaultValuesMap[key] =
}; props?.initialData?.[key] ?? defaultValuesMap[key];
}
});
} }
// Update the form values, but only for the fields specified for this form
return defaultValuesMap; return defaultValuesMap;
}, [props.fields, props.initialData]); }, [props.fields, props.initialData]);
@ -260,14 +267,22 @@ export function ApiForm({
}; };
// Process API response // Process API response
const initialData: any = processFields( const initialData: any = processFields(fields, response.data);
props.fields ?? {},
response.data
);
// Update form values, but only for the fields specified for this form // Update form values, but only for the fields specified for this form
form.reset(initialData); form.reset(initialData);
// Update the field references, too
Object.keys(fields).forEach((fieldName) => {
if (fieldName in initialData) {
let field = fields[fieldName] ?? {};
fields[fieldName] = {
...field,
value: initialData[fieldName]
};
}
});
return response; return response;
} catch (error) { } catch (error) {
console.error('Error fetching initial data:', error); console.error('Error fetching initial data:', error);
@ -301,12 +316,12 @@ export function ApiForm({
initialDataQuery.isFetching || initialDataQuery.isFetching ||
optionsLoading || optionsLoading ||
isSubmitting || isSubmitting ||
!props.fields, !fields,
[ [
isFormLoading, isFormLoading,
initialDataQuery.isFetching, initialDataQuery.isFetching,
isSubmitting, isSubmitting,
props.fields, fields,
optionsLoading optionsLoading
] ]
); );
@ -319,7 +334,7 @@ export function ApiForm({
if (!focusField) { if (!focusField) {
// If a focus field is not specified, then focus on the first available field // If a focus field is not specified, then focus on the first available field
Object.entries(props.fields ?? {}).forEach(([fieldName, field]) => { Object.entries(fields).forEach(([fieldName, field]) => {
if (focusField || field.read_only || field.disabled || field.hidden) { if (focusField || field.read_only || field.disabled || field.hidden) {
return; return;
} }
@ -334,7 +349,7 @@ export function ApiForm({
form.setFocus(focusField); form.setFocus(focusField);
setInitialFocus(focusField); setInitialFocus(focusField);
}, [props.focus, props.fields, form.setFocus, isLoading, initialFocus]); }, [props.focus, fields, form.setFocus, isLoading, initialFocus]);
const submitForm: SubmitHandler<FieldValues> = async (data) => { const submitForm: SubmitHandler<FieldValues> = async (data) => {
setNonFieldErrors([]); setNonFieldErrors([]);
@ -342,7 +357,7 @@ export function ApiForm({
let method = props.method?.toLowerCase() ?? 'get'; let method = props.method?.toLowerCase() ?? 'get';
let hasFiles = false; let hasFiles = false;
mapFields(props.fields ?? {}, (_path, field) => { mapFields(fields, (_path, field) => {
if (field.field_type === 'file upload') { if (field.field_type === 'file upload') {
hasFiles = true; hasFiles = true;
} }
@ -474,16 +489,14 @@ export function ApiForm({
<FormProvider {...form}> <FormProvider {...form}>
<Stack spacing="xs"> <Stack spacing="xs">
{!optionsLoading && {!optionsLoading &&
Object.entries(props.fields ?? {}).map( Object.entries(fields).map(([fieldName, field]) => (
([fieldName, field]) => ( <ApiFormField
<ApiFormField key={fieldName}
key={fieldName} fieldName={fieldName}
fieldName={fieldName} definition={field}
definition={field} control={form.control}
control={form.control} />
/> ))}
)
)}
</Stack> </Stack>
</FormProvider> </FormProvider>
{props.postFormContent} {props.postFormContent}

View File

@ -15,6 +15,7 @@ import { useMemo } from 'react';
import { Control, FieldValues, useController } from 'react-hook-form'; import { Control, FieldValues, useController } from 'react-hook-form';
import { ModelType } from '../../../enums/ModelType'; import { ModelType } from '../../../enums/ModelType';
import { isTrue } from '../../../functions/conversion';
import { ChoiceField } from './ChoiceField'; import { ChoiceField } from './ChoiceField';
import DateField from './DateField'; import DateField from './DateField';
import { NestedObjectField } from './NestedObjectField'; import { NestedObjectField } from './NestedObjectField';
@ -210,7 +211,7 @@ export function ApiFormField({
id={fieldId} id={fieldId}
radius="lg" radius="lg"
size="sm" size="sm"
checked={value ?? false} checked={isTrue(value)}
error={error?.message} error={error?.message}
onChange={(event) => onChange(event.currentTarget.checked)} onChange={(event) => onChange(event.currentTarget.checked)}
/> />

View File

@ -74,13 +74,7 @@ export const StatusRenderer = ({
}) => { }) => {
const statusCodeList = useGlobalStatusState.getState().status; const statusCodeList = useGlobalStatusState.getState().status;
if (status === undefined) { if (status === undefined || statusCodeList === undefined) {
console.log('StatusRenderer: status is undefined');
return null;
}
if (statusCodeList === undefined) {
console.log('StatusRenderer: statusCodeList is undefined');
return null; return null;
} }

View File

@ -7,57 +7,64 @@ import {
IconUser, IconUser,
IconUsersGroup IconUsersGroup
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useMemo } from 'react';
import { ApiFormFieldSet } from '../components/forms/fields/ApiFormField'; import { ApiFormFieldSet } from '../components/forms/fields/ApiFormField';
/** /**
* Field set for BuildOrder forms * Field set for BuildOrder forms
*/ */
export function buildOrderFields(): ApiFormFieldSet { export function useBuildOrderFields({
return { create
reference: {}, }: {
part: { create: boolean;
filters: { }): ApiFormFieldSet {
assembly: true, return useMemo(() => {
virtual: false return {
reference: {},
part: {
filters: {
assembly: true,
virtual: false
}
},
title: {},
quantity: {},
project_code: {
icon: <IconList />
},
priority: {},
parent: {
icon: <IconSitemap />,
filters: {
part_detail: true
}
},
sales_order: {
icon: <IconTruckDelivery />
},
batch: {},
target_date: {
icon: <IconCalendar />
},
take_from: {},
destination: {
filters: {
structural: false
}
},
link: {
icon: <IconLink />
},
issued_by: {
icon: <IconUser />
},
responsible: {
icon: <IconUsersGroup />,
filters: {
is_active: true
}
} }
}, };
title: {}, }, [create]);
quantity: {},
project_code: {
icon: <IconList />
},
priority: {},
parent: {
icon: <IconSitemap />,
filters: {
part_detail: true
}
},
sales_order: {
icon: <IconTruckDelivery />
},
batch: {},
target_date: {
icon: <IconCalendar />
},
take_from: {},
destination: {
filters: {
structural: false
}
},
link: {
icon: <IconLink />
},
issued_by: {
icon: <IconUser />
},
responsible: {
icon: <IconUsersGroup />,
filters: {
is_active: true
}
}
};
} }

View File

@ -10,34 +10,21 @@ import {
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { ApiFormFieldSet } from '../components/forms/fields/ApiFormField'; import {
ApiFormAdjustFilterType,
ApiFormFieldSet
} from '../components/forms/fields/ApiFormField';
/** /**
* Field set for SupplierPart instance * Field set for SupplierPart instance
*/ */
export function useSupplierPartFields({ export function useSupplierPartFields() {
partPk,
supplierPk,
hidePart
}: {
partPk?: number;
supplierPk?: number;
hidePart?: boolean;
}) {
const [part, setPart] = useState<number | undefined>(partPk);
useEffect(() => {
setPart(partPk);
}, [partPk]);
return useMemo(() => { return useMemo(() => {
const fields: ApiFormFieldSet = { const fields: ApiFormFieldSet = {
part: { part: {
hidden: hidePart,
value: part,
onValueChange: setPart,
filters: { filters: {
purchaseable: true purchaseable: true,
active: true
} }
}, },
manufacturer_part: { manufacturer_part: {
@ -45,15 +32,18 @@ export function useSupplierPartFields({
part_detail: true, part_detail: true,
manufacturer_detail: true manufacturer_detail: true
}, },
adjustFilters: (filters: any) => { adjustFilters: (adjust: ApiFormAdjustFilterType) => {
if (part) { return {
filters.part = part; ...adjust.filters,
} part: adjust.data.part
};
return filters; }
},
supplier: {
filters: {
active: true
} }
}, },
supplier: {},
SKU: { SKU: {
icon: <IconHash /> icon: <IconHash />
}, },
@ -67,15 +57,12 @@ export function useSupplierPartFields({
pack_quantity: {}, pack_quantity: {},
packaging: { packaging: {
icon: <IconPackage /> icon: <IconPackage />
} },
active: {}
}; };
if (supplierPk !== undefined) {
fields.supplier.value = supplierPk;
}
return fields; return fields;
}, [part]); }, []);
} }
export function useManufacturerPartFields() { export function useManufacturerPartFields() {
@ -125,6 +112,7 @@ export function companyFields(): ApiFormFieldSet {
}, },
is_supplier: {}, is_supplier: {},
is_manufacturer: {}, is_manufacturer: {},
is_customer: {} is_customer: {},
active: {}
}; };
} }

View File

@ -37,8 +37,12 @@ import { apiUrl } from '../states/ApiState';
* Construct a set of fields for creating / editing a PurchaseOrderLineItem instance * Construct a set of fields for creating / editing a PurchaseOrderLineItem instance
*/ */
export function usePurchaseOrderLineItemFields({ export function usePurchaseOrderLineItemFields({
supplierId,
orderId,
create create
}: { }: {
supplierId?: number;
orderId?: number;
create?: boolean; create?: boolean;
}) { }) {
const [purchasePrice, setPurchasePrice] = useState<string>(''); const [purchasePrice, setPurchasePrice] = useState<string>('');
@ -60,16 +64,20 @@ export function usePurchaseOrderLineItemFields({
filters: { filters: {
supplier_detail: true supplier_detail: true
}, },
hidden: true disabled: true
}, },
part: { part: {
filters: { filters: {
part_detail: true, part_detail: true,
supplier_detail: true supplier_detail: true,
active: true,
part_active: true
}, },
adjustFilters: (value: ApiFormAdjustFilterType) => { adjustFilters: (adjust: ApiFormAdjustFilterType) => {
// TODO: Adjust part based on the supplier associated with the supplier return {
return value.filters; ...adjust.filters,
supplier: supplierId
};
} }
}, },
quantity: {}, quantity: {},
@ -105,7 +113,7 @@ export function usePurchaseOrderLineItemFields({
} }
return fields; return fields;
}, [create, autoPricing, purchasePrice]); }, [create, orderId, supplierId, autoPricing, purchasePrice]);
return fields; return fields;
} }
@ -113,50 +121,53 @@ export function usePurchaseOrderLineItemFields({
/** /**
* Construct a set of fields for creating / editing a PurchaseOrder instance * Construct a set of fields for creating / editing a PurchaseOrder instance
*/ */
export function purchaseOrderFields(): ApiFormFieldSet { export function usePurchaseOrderFields(): ApiFormFieldSet {
return { return useMemo(() => {
reference: { return {
icon: <IconHash /> reference: {
}, icon: <IconHash />
description: {}, },
supplier: { description: {},
filters: { supplier: {
is_supplier: true filters: {
is_supplier: true,
active: true
}
},
supplier_reference: {},
project_code: {
icon: <IconList />
},
order_currency: {
icon: <IconCoins />
},
target_date: {
icon: <IconCalendar />
},
link: {},
contact: {
icon: <IconUser />,
adjustFilters: (value: ApiFormAdjustFilterType) => {
return {
...value.filters,
company: value.data.supplier
};
}
},
address: {
icon: <IconAddressBook />,
adjustFilters: (value: ApiFormAdjustFilterType) => {
return {
...value.filters,
company: value.data.supplier
};
}
},
responsible: {
icon: <IconUsers />
} }
}, };
supplier_reference: {}, }, []);
project_code: {
icon: <IconList />
},
order_currency: {
icon: <IconCoins />
},
target_date: {
icon: <IconCalendar />
},
link: {},
contact: {
icon: <IconUser />,
adjustFilters: (value: ApiFormAdjustFilterType) => {
return {
...value.filters,
company: value.data.supplier
};
}
},
address: {
icon: <IconAddressBook />,
adjustFilters: (value: ApiFormAdjustFilterType) => {
return {
...value.filters,
company: value.data.supplier
};
}
},
responsible: {
icon: <IconUsers />
}
};
} }
/** /**

View File

@ -1,44 +1,89 @@
import { IconAddressBook, IconUser, IconUsers } from '@tabler/icons-react'; import { IconAddressBook, IconUser, IconUsers } from '@tabler/icons-react';
import { useMemo } from 'react';
import { import {
ApiFormAdjustFilterType, ApiFormAdjustFilterType,
ApiFormFieldSet ApiFormFieldSet
} from '../components/forms/fields/ApiFormField'; } from '../components/forms/fields/ApiFormField';
export function salesOrderFields(): ApiFormFieldSet { export function useSalesOrderFields(): ApiFormFieldSet {
return { return useMemo(() => {
reference: {}, return {
description: {}, reference: {},
customer: { description: {},
filters: { customer: {
is_customer: true filters: {
is_customer: true,
active: true
}
},
customer_reference: {},
project_code: {},
order_currency: {},
target_date: {},
link: {},
contact: {
icon: <IconUser />,
adjustFilters: (value: ApiFormAdjustFilterType) => {
return {
...value.filters,
company: value.data.customer
};
}
},
address: {
icon: <IconAddressBook />,
adjustFilters: (value: ApiFormAdjustFilterType) => {
return {
...value.filters,
company: value.data.customer
};
}
},
responsible: {
icon: <IconUsers />
} }
}, };
customer_reference: {}, }, []);
project_code: {}, }
order_currency: {},
target_date: {}, export function useReturnOrderFields(): ApiFormFieldSet {
link: {}, return useMemo(() => {
contact: { return {
icon: <IconUser />, reference: {},
adjustFilters: (value: ApiFormAdjustFilterType) => { description: {},
return { customer: {
...value.filters, filters: {
company: value.data.customer is_customer: true,
}; active: true
} }
}, },
address: { customer_reference: {},
icon: <IconAddressBook />, project_code: {},
adjustFilters: (value: ApiFormAdjustFilterType) => { order_currency: {},
return { target_date: {},
...value.filters, link: {},
company: value.data.customer contact: {
}; icon: <IconUser />,
} adjustFilters: (value: ApiFormAdjustFilterType) => {
}, return {
responsible: { ...value.filters,
icon: <IconUsers /> company: value.data.customer
} };
}; }
},
address: {
icon: <IconAddressBook />,
adjustFilters: (value: ApiFormAdjustFilterType) => {
return {
...value.filters,
company: value.data.customer
};
}
},
responsible: {
icon: <IconUsers />
}
};
}, []);
} }

View File

@ -39,7 +39,7 @@ export function useStockFields({
const fields: ApiFormFieldSet = { const fields: ApiFormFieldSet = {
part: { part: {
value: part, value: part,
hidden: !create, disabled: !create,
onValueChange: (change) => { onValueChange: (change) => {
setPart(change); setPart(change);
// TODO: implement remaining functionality from old stock.py // TODO: implement remaining functionality from old stock.py
@ -57,12 +57,12 @@ export function useStockFields({
supplier_detail: true, supplier_detail: true,
...(part ? { part } : {}) ...(part ? { part } : {})
}, },
adjustFilters: (value: ApiFormAdjustFilterType) => { adjustFilters: (adjust: ApiFormAdjustFilterType) => {
if (value.data.part) { if (adjust.data.part) {
value.filters['part'] = value.data.part; adjust.filters['part'] = adjust.data.part;
} }
return value.filters; return adjust.filters;
} }
}, },
use_pack_size: { use_pack_size: {
@ -137,29 +137,6 @@ export function useCreateStockItem() {
}); });
} }
/**
* Launch a form to edit an existing StockItem instance
* @param item : primary key of the StockItem to edit
*/
export function useEditStockItem({
item_id,
callback
}: {
item_id: number;
callback?: () => void;
}) {
const fields = useStockFields({ create: false });
return useEditApiFormModal({
url: ApiEndpoints.stock_item_list,
pk: item_id,
fields: fields,
title: t`Edit Stock Item`,
successMessage: t`Stock item updated`,
onFormSuccess: callback
});
}
function StockItemDefaultMove({ function StockItemDefaultMove({
stockItem, stockItem,
value value

View File

@ -35,8 +35,7 @@ import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType'; import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles'; import { UserRoles } from '../../enums/Roles';
import { buildOrderFields } from '../../forms/BuildForms'; import { useBuildOrderFields } from '../../forms/BuildForms';
import { partCategoryFields } from '../../forms/PartForms';
import { useEditApiFormModal } from '../../hooks/UseForm'; import { useEditApiFormModal } from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
import { apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
@ -280,11 +279,13 @@ export default function BuildDetail() {
]; ];
}, [build, id]); }, [build, id]);
const buildOrderFields = useBuildOrderFields({ create: false });
const editBuild = useEditApiFormModal({ const editBuild = useEditApiFormModal({
url: ApiEndpoints.build_order_list, url: ApiEndpoints.build_order_list,
pk: build.pk, pk: build.pk,
title: t`Edit Build Order`, title: t`Edit Build Order`,
fields: buildOrderFields(), fields: buildOrderFields,
onFormSuccess: () => { onFormSuccess: () => {
refreshInstance(); refreshInstance();
} }

View File

@ -15,10 +15,11 @@ import {
IconTruckReturn, IconTruckReturn,
IconUsersGroup IconUsersGroup
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useMemo } from 'react'; import { ReactNode, useMemo } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { DetailsField, DetailsTable } from '../../components/details/Details'; import { DetailsField, DetailsTable } from '../../components/details/Details';
import DetailsBadge from '../../components/details/DetailsBadge';
import { DetailsImage } from '../../components/details/DetailsImage'; import { DetailsImage } from '../../components/details/DetailsImage';
import { ItemDetailsGrid } from '../../components/details/ItemDetails'; import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import { import {
@ -293,6 +294,12 @@ export default function CompanyDetail(props: CompanyDetailProps) {
]; ];
}, [id, company, user]); }, [id, company, user]);
const badges: ReactNode[] = useMemo(() => {
return [
<DetailsBadge label={t`Inactive`} color="red" visible={!company.active} />
];
}, [company]);
return ( return (
<> <>
{editCompany.modal} {editCompany.modal}
@ -304,6 +311,7 @@ export default function CompanyDetail(props: CompanyDetailProps) {
actions={companyActions} actions={companyActions}
imageUrl={company.image} imageUrl={company.image}
breadcrumbs={props.breadcrumbs} breadcrumbs={props.breadcrumbs}
badges={badges}
/> />
<PanelGroup pageKey="company" panels={companyPanels} /> <PanelGroup pageKey="company" panels={companyPanels} />
</Stack> </Stack>

View File

@ -7,10 +7,11 @@ import {
IconPackages, IconPackages,
IconShoppingCart IconShoppingCart
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useMemo } from 'react'; import { ReactNode, useMemo } from 'react';
import { useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { DetailsField, DetailsTable } from '../../components/details/Details'; import { DetailsField, DetailsTable } from '../../components/details/Details';
import DetailsBadge from '../../components/details/DetailsBadge';
import { DetailsImage } from '../../components/details/DetailsImage'; import { DetailsImage } from '../../components/details/DetailsImage';
import { ItemDetailsGrid } from '../../components/details/ItemDetails'; import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import { import {
@ -25,7 +26,11 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType'; import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles'; import { UserRoles } from '../../enums/Roles';
import { useSupplierPartFields } from '../../forms/CompanyForms'; import { useSupplierPartFields } from '../../forms/CompanyForms';
import { useEditApiFormModal } from '../../hooks/UseForm'; import { getDetailUrl } from '../../functions/urls';
import {
useCreateApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
import { apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
@ -38,6 +43,8 @@ export default function SupplierPartDetail() {
const user = useUserState(); const user = useUserState();
const navigate = useNavigate();
const { const {
instance: supplierPart, instance: supplierPart,
instanceQuery, instanceQuery,
@ -245,7 +252,8 @@ export default function SupplierPartDetail() {
icon={<IconDots />} icon={<IconDots />}
actions={[ actions={[
DuplicateItemAction({ DuplicateItemAction({
hidden: !user.hasAddRole(UserRoles.purchase_order) hidden: !user.hasAddRole(UserRoles.purchase_order),
onClick: () => duplicateSupplierPart.open()
}), }),
EditItemAction({ EditItemAction({
hidden: !user.hasChangeRole(UserRoles.purchase_order), hidden: !user.hasChangeRole(UserRoles.purchase_order),
@ -259,19 +267,30 @@ export default function SupplierPartDetail() {
]; ];
}, [user]); }, [user]);
const editSupplierPartFields = useSupplierPartFields({ const supplierPartFields = useSupplierPartFields();
hidePart: true,
partPk: supplierPart?.pk
});
const editSuppliertPart = useEditApiFormModal({ const editSuppliertPart = useEditApiFormModal({
url: ApiEndpoints.supplier_part_list, url: ApiEndpoints.supplier_part_list,
pk: supplierPart?.pk, pk: supplierPart?.pk,
title: t`Edit Supplier Part`, title: t`Edit Supplier Part`,
fields: editSupplierPartFields, fields: supplierPartFields,
onFormSuccess: refreshInstance onFormSuccess: refreshInstance
}); });
const duplicateSupplierPart = useCreateApiFormModal({
url: ApiEndpoints.supplier_part_list,
title: t`Add Supplier Part`,
fields: supplierPartFields,
initialData: {
...supplierPart
},
onFormSuccess: (response: any) => {
if (response.pk) {
navigate(getDetailUrl(ModelType.supplierpart, response.pk));
}
}
});
const breadcrumbs = useMemo(() => { const breadcrumbs = useMemo(() => {
return [ return [
{ {
@ -285,15 +304,27 @@ export default function SupplierPartDetail() {
]; ];
}, [supplierPart]); }, [supplierPart]);
const badges: ReactNode[] = useMemo(() => {
return [
<DetailsBadge
label={t`Inactive`}
color="red"
visible={!supplierPart.active}
/>
];
}, [supplierPart]);
return ( return (
<> <>
{editSuppliertPart.modal} {editSuppliertPart.modal}
{duplicateSupplierPart.modal}
<Stack spacing="xs"> <Stack spacing="xs">
<LoadingOverlay visible={instanceQuery.isFetching} /> <LoadingOverlay visible={instanceQuery.isFetching} />
<PageDetail <PageDetail
title={t`Supplier Part`} title={t`Supplier Part`}
subtitle={`${supplierPart.SKU} - ${supplierPart?.part_detail?.name}`} subtitle={`${supplierPart.SKU} - ${supplierPart?.part_detail?.name}`}
breadcrumbs={breadcrumbs} breadcrumbs={breadcrumbs}
badges={badges}
actions={supplierPartActions} actions={supplierPartActions}
imageUrl={supplierPart?.part_detail?.thumbnail} imageUrl={supplierPart?.part_detail?.thumbnail}
/> />

View File

@ -1,13 +1,5 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core';
Badge,
Grid,
Group,
LoadingOverlay,
Skeleton,
Stack,
Text
} from '@mantine/core';
import { import {
IconBookmarks, IconBookmarks,
IconBuilding, IconBuilding,
@ -32,13 +24,11 @@ import {
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useSuspenseQuery } from '@tanstack/react-query'; import { useSuspenseQuery } from '@tanstack/react-query';
import { ReactNode, useMemo, useState } from 'react'; import { ReactNode, useMemo, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { api } from '../../App'; import { api } from '../../App';
import { DetailsField, DetailsTable } from '../../components/details/Details'; import { DetailsField, DetailsTable } from '../../components/details/Details';
import DetailsBadge, { import DetailsBadge from '../../components/details/DetailsBadge';
DetailsBadgeProps
} from '../../components/details/DetailsBadge';
import { DetailsImage } from '../../components/details/DetailsImage'; import { DetailsImage } from '../../components/details/DetailsImage';
import { ItemDetailsGrid } from '../../components/details/ItemDetails'; import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import { PartIcons } from '../../components/details/PartIcons'; import { PartIcons } from '../../components/details/PartIcons';
@ -68,7 +58,10 @@ import {
} from '../../forms/StockForms'; } from '../../forms/StockForms';
import { InvenTreeIcon } from '../../functions/icons'; import { InvenTreeIcon } from '../../functions/icons';
import { getDetailUrl } from '../../functions/urls'; import { getDetailUrl } from '../../functions/urls';
import { useEditApiFormModal } from '../../hooks/UseForm'; import {
useCreateApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
import { apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
@ -93,6 +86,7 @@ export default function PartDetail() {
const { id } = useParams(); const { id } = useParams();
const user = useUserState(); const user = useUserState();
const navigate = useNavigate();
const [treeOpen, setTreeOpen] = useState(false); const [treeOpen, setTreeOpen] = useState(false);
@ -664,7 +658,8 @@ export default function PartDetail() {
label={t`In Production` + `: ${part.building}`} label={t`In Production` + `: ${part.building}`}
color="blue" color="blue"
visible={part.building > 0} visible={part.building > 0}
/> />,
<DetailsBadge label={t`Inactive`} color="red" visible={!part.active} />
]; ];
}, [part, instanceQuery]); }, [part, instanceQuery]);
@ -678,6 +673,20 @@ export default function PartDetail() {
onFormSuccess: refreshInstance onFormSuccess: refreshInstance
}); });
const duplicatePart = useCreateApiFormModal({
url: ApiEndpoints.part_list,
title: t`Add Part`,
fields: partFields,
initialData: {
...part
},
onFormSuccess: (response: any) => {
if (response.pk) {
navigate(getDetailUrl(ModelType.part, response.pk));
}
}
});
const stockActionProps: StockOperationProps = useMemo(() => { const stockActionProps: StockOperationProps = useMemo(() => {
return { return {
pk: part.pk, pk: part.pk,
@ -695,10 +704,10 @@ export default function PartDetail() {
actions={[ actions={[
ViewBarcodeAction({}), ViewBarcodeAction({}),
LinkBarcodeAction({ LinkBarcodeAction({
hidden: part?.barcode_hash hidden: part?.barcode_hash || !user.hasChangeRole(UserRoles.part)
}), }),
UnlinkBarcodeAction({ UnlinkBarcodeAction({
hidden: !part?.barcode_hash hidden: !part?.barcode_hash || !user.hasChangeRole(UserRoles.part)
}) })
]} ]}
/>, />,
@ -737,7 +746,8 @@ export default function PartDetail() {
icon={<IconDots />} icon={<IconDots />}
actions={[ actions={[
DuplicateItemAction({ DuplicateItemAction({
hidden: !user.hasAddRole(UserRoles.part) hidden: !user.hasAddRole(UserRoles.part),
onClick: () => duplicatePart.open()
}), }),
EditItemAction({ EditItemAction({
hidden: !user.hasChangeRole(UserRoles.part), hidden: !user.hasChangeRole(UserRoles.part),
@ -753,6 +763,7 @@ export default function PartDetail() {
return ( return (
<> <>
{duplicatePart.modal}
{editPart.modal} {editPart.modal}
<Stack spacing="xs"> <Stack spacing="xs">
<LoadingOverlay visible={instanceQuery.isFetching} /> <LoadingOverlay visible={instanceQuery.isFetching} />

View File

@ -30,7 +30,7 @@ import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType'; import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles'; import { UserRoles } from '../../enums/Roles';
import { purchaseOrderFields } from '../../forms/PurchaseOrderForms'; import { usePurchaseOrderFields } from '../../forms/PurchaseOrderForms';
import { useEditApiFormModal } from '../../hooks/UseForm'; import { useEditApiFormModal } from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
import { apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
@ -60,11 +60,13 @@ export default function PurchaseOrderDetail() {
refetchOnMount: true refetchOnMount: true
}); });
const purchaseOrderFields = usePurchaseOrderFields();
const editPurchaseOrder = useEditApiFormModal({ const editPurchaseOrder = useEditApiFormModal({
url: ApiEndpoints.purchase_order_list, url: ApiEndpoints.purchase_order_list,
pk: id, pk: id,
title: t`Edit Purchase Order`, title: t`Edit Purchase Order`,
fields: purchaseOrderFields(), fields: purchaseOrderFields,
onFormSuccess: () => { onFormSuccess: () => {
refreshInstance(); refreshInstance();
} }
@ -227,7 +229,12 @@ export default function PurchaseOrderDetail() {
name: 'line-items', name: 'line-items',
label: t`Line Items`, label: t`Line Items`,
icon: <IconList />, icon: <IconList />,
content: <PurchaseOrderLineItemTable orderId={Number(id)} /> content: (
<PurchaseOrderLineItemTable
orderId={Number(id)}
supplierId={Number(order.supplier)}
/>
)
}, },
{ {
name: 'received-stock', name: 'received-stock',
@ -269,7 +276,6 @@ export default function PurchaseOrderDetail() {
}, [order, id]); }, [order, id]);
const poActions = useMemo(() => { const poActions = useMemo(() => {
// TODO: Disable certain actions based on user permissions
return [ return [
<BarcodeActionDropdown <BarcodeActionDropdown
actions={[ actions={[
@ -288,11 +294,14 @@ export default function PurchaseOrderDetail() {
icon={<IconDots />} icon={<IconDots />}
actions={[ actions={[
EditItemAction({ EditItemAction({
hidden: !user.hasChangeRole(UserRoles.purchase_order),
onClick: () => { onClick: () => {
editPurchaseOrder.open(); editPurchaseOrder.open();
} }
}), }),
DeleteItemAction({}) DeleteItemAction({
hidden: !user.hasDeleteRole(UserRoles.purchase_order)
})
]} ]}
/> />
]; ];

View File

@ -1,6 +1,7 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core'; import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core';
import { import {
IconDots,
IconInfoCircle, IconInfoCircle,
IconList, IconList,
IconNotes, IconNotes,
@ -12,6 +13,11 @@ import { useParams } from 'react-router-dom';
import { DetailsField, DetailsTable } from '../../components/details/Details'; import { DetailsField, DetailsTable } from '../../components/details/Details';
import { DetailsImage } from '../../components/details/DetailsImage'; import { DetailsImage } from '../../components/details/DetailsImage';
import { ItemDetailsGrid } from '../../components/details/ItemDetails'; import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import {
ActionDropdown,
DeleteItemAction,
EditItemAction
} from '../../components/items/ActionDropdown';
import { PageDetail } from '../../components/nav/PageDetail'; import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { StatusRenderer } from '../../components/render/StatusRenderer'; import { StatusRenderer } from '../../components/render/StatusRenderer';
@ -19,8 +25,11 @@ import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType'; import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles'; import { UserRoles } from '../../enums/Roles';
import { useReturnOrderFields } from '../../forms/SalesOrderForms';
import { useEditApiFormModal } from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
import { apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
import { AttachmentTable } from '../../tables/general/AttachmentTable'; import { AttachmentTable } from '../../tables/general/AttachmentTable';
/** /**
@ -29,7 +38,13 @@ import { AttachmentTable } from '../../tables/general/AttachmentTable';
export default function ReturnOrderDetail() { export default function ReturnOrderDetail() {
const { id } = useParams(); const { id } = useParams();
const { instance: order, instanceQuery } = useInstance({ const user = useUserState();
const {
instance: order,
instanceQuery,
refreshInstance
} = useInstance({
endpoint: ApiEndpoints.return_order_list, endpoint: ApiEndpoints.return_order_list,
pk: id, pk: id,
params: { params: {
@ -233,8 +248,43 @@ export default function ReturnOrderDetail() {
]; ];
}, [order, instanceQuery]); }, [order, instanceQuery]);
const returnOrderFields = useReturnOrderFields();
const editReturnOrder = useEditApiFormModal({
url: ApiEndpoints.return_order_list,
pk: order.pk,
title: t`Edit Return Order`,
fields: returnOrderFields,
onFormSuccess: () => {
refreshInstance();
}
});
const orderActions = useMemo(() => {
return [
<ActionDropdown
key="order-actions"
tooltip={t`Order Actions`}
icon={<IconDots />}
actions={[
EditItemAction({
hidden: !user.hasChangeRole(UserRoles.return_order),
onClick: () => {
editReturnOrder.open();
}
}),
DeleteItemAction({
hidden: !user.hasDeleteRole(UserRoles.return_order)
// TODO: Delete?
})
]}
/>
];
}, [user]);
return ( return (
<> <>
{editReturnOrder.modal}
<Stack spacing="xs"> <Stack spacing="xs">
<LoadingOverlay visible={instanceQuery.isFetching} /> <LoadingOverlay visible={instanceQuery.isFetching} />
<PageDetail <PageDetail
@ -242,6 +292,7 @@ export default function ReturnOrderDetail() {
subtitle={order.description} subtitle={order.description}
imageUrl={order.customer_detail?.image} imageUrl={order.customer_detail?.image}
badges={orderBadges} badges={orderBadges}
actions={orderActions}
breadcrumbs={[{ name: t`Sales`, url: '/sales/' }]} breadcrumbs={[{ name: t`Sales`, url: '/sales/' }]}
/> />
<PanelGroup pageKey="returnorder" panels={orderPanels} /> <PanelGroup pageKey="returnorder" panels={orderPanels} />

View File

@ -1,6 +1,7 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core'; import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core';
import { import {
IconDots,
IconInfoCircle, IconInfoCircle,
IconList, IconList,
IconNotes, IconNotes,
@ -15,6 +16,11 @@ import { useParams } from 'react-router-dom';
import { DetailsField, DetailsTable } from '../../components/details/Details'; import { DetailsField, DetailsTable } from '../../components/details/Details';
import { DetailsImage } from '../../components/details/DetailsImage'; import { DetailsImage } from '../../components/details/DetailsImage';
import { ItemDetailsGrid } from '../../components/details/ItemDetails'; import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import {
ActionDropdown,
DeleteItemAction,
EditItemAction
} from '../../components/items/ActionDropdown';
import { PageDetail } from '../../components/nav/PageDetail'; import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { StatusRenderer } from '../../components/render/StatusRenderer'; import { StatusRenderer } from '../../components/render/StatusRenderer';
@ -22,8 +28,11 @@ import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType'; import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles'; import { UserRoles } from '../../enums/Roles';
import { useSalesOrderFields } from '../../forms/SalesOrderForms';
import { useEditApiFormModal } from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
import { apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
import { BuildOrderTable } from '../../tables/build/BuildOrderTable'; import { BuildOrderTable } from '../../tables/build/BuildOrderTable';
import { AttachmentTable } from '../../tables/general/AttachmentTable'; import { AttachmentTable } from '../../tables/general/AttachmentTable';
@ -33,7 +42,13 @@ import { AttachmentTable } from '../../tables/general/AttachmentTable';
export default function SalesOrderDetail() { export default function SalesOrderDetail() {
const { id } = useParams(); const { id } = useParams();
const { instance: order, instanceQuery } = useInstance({ const user = useUserState();
const {
instance: order,
instanceQuery,
refreshInstance
} = useInstance({
endpoint: ApiEndpoints.sales_order_list, endpoint: ApiEndpoints.sales_order_list,
pk: id, pk: id,
params: { params: {
@ -185,6 +200,18 @@ export default function SalesOrderDetail() {
); );
}, [order, instanceQuery]); }, [order, instanceQuery]);
const salesOrderFields = useSalesOrderFields();
const editSalesOrder = useEditApiFormModal({
url: ApiEndpoints.sales_order_list,
pk: order.pk,
title: t`Edit Sales Order`,
fields: salesOrderFields,
onFormSuccess: () => {
refreshInstance();
}
});
const orderPanels: PanelType[] = useMemo(() => { const orderPanels: PanelType[] = useMemo(() => {
return [ return [
{ {
@ -245,6 +272,28 @@ export default function SalesOrderDetail() {
]; ];
}, [order, id]); }, [order, id]);
const soActions = useMemo(() => {
return [
<ActionDropdown
key="order-actions"
tooltip={t`Order Actions`}
icon={<IconDots />}
actions={[
EditItemAction({
hidden: !user.hasChangeRole(UserRoles.sales_order),
onClick: () => {
editSalesOrder.open();
}
}),
DeleteItemAction({
hidden: !user.hasDeleteRole(UserRoles.sales_order)
// TODO: Delete?
})
]}
/>
];
}, [user]);
const orderBadges: ReactNode[] = useMemo(() => { const orderBadges: ReactNode[] = useMemo(() => {
return instanceQuery.isLoading return instanceQuery.isLoading
? [] ? []
@ -259,6 +308,7 @@ export default function SalesOrderDetail() {
return ( return (
<> <>
{editSalesOrder.modal}
<Stack spacing="xs"> <Stack spacing="xs">
<LoadingOverlay visible={instanceQuery.isFetching} /> <LoadingOverlay visible={instanceQuery.isFetching} />
<PageDetail <PageDetail
@ -266,6 +316,7 @@ export default function SalesOrderDetail() {
subtitle={order.description} subtitle={order.description}
imageUrl={order.customer_detail?.image} imageUrl={order.customer_detail?.image}
badges={orderBadges} badges={orderBadges}
actions={soActions}
breadcrumbs={[{ name: t`Sales`, url: '/sales/' }]} breadcrumbs={[{ name: t`Sales`, url: '/sales/' }]}
/> />
<PanelGroup pageKey="salesorder" panels={orderPanels} /> <PanelGroup pageKey="salesorder" panels={orderPanels} />

View File

@ -23,7 +23,7 @@ import {
IconSitemap IconSitemap
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { ReactNode, useMemo, useState } from 'react'; import { ReactNode, useMemo, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { DetailsField, DetailsTable } from '../../components/details/Details'; import { DetailsField, DetailsTable } from '../../components/details/Details';
import DetailsBadge from '../../components/details/DetailsBadge'; import DetailsBadge from '../../components/details/DetailsBadge';
@ -33,6 +33,7 @@ import {
ActionDropdown, ActionDropdown,
BarcodeActionDropdown, BarcodeActionDropdown,
DeleteItemAction, DeleteItemAction,
DuplicateItemAction,
EditItemAction, EditItemAction,
LinkBarcodeAction, LinkBarcodeAction,
UnlinkBarcodeAction, UnlinkBarcodeAction,
@ -50,12 +51,16 @@ import {
StockOperationProps, StockOperationProps,
useAddStockItem, useAddStockItem,
useCountStockItem, useCountStockItem,
useEditStockItem,
useRemoveStockItem, useRemoveStockItem,
useStockFields,
useTransferStockItem useTransferStockItem
} from '../../forms/StockForms'; } from '../../forms/StockForms';
import { InvenTreeIcon } from '../../functions/icons'; import { InvenTreeIcon } from '../../functions/icons';
import { getDetailUrl } from '../../functions/urls'; import { getDetailUrl } from '../../functions/urls';
import {
useCreateApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
import { apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
@ -69,6 +74,8 @@ export default function StockDetail() {
const user = useUserState(); const user = useUserState();
const navigate = useNavigate();
const [treeOpen, setTreeOpen] = useState(false); const [treeOpen, setTreeOpen] = useState(false);
const { const {
@ -349,9 +356,30 @@ export default function StockDetail() {
[stockitem] [stockitem]
); );
const editStockItem = useEditStockItem({ const editStockItemFields = useStockFields({ create: false });
item_id: stockitem.pk,
callback: () => refreshInstance() const editStockItem = useEditApiFormModal({
url: ApiEndpoints.stock_item_list,
pk: stockitem.pk,
title: t`Edit Stock Item`,
fields: editStockItemFields,
onFormSuccess: refreshInstance
});
const duplicateStockItemFields = useStockFields({ create: true });
const duplicateStockItem = useCreateApiFormModal({
url: ApiEndpoints.stock_item_list,
title: t`Add Stock Item`,
fields: duplicateStockItemFields,
initialData: {
...stockitem
},
onFormSuccess: (response: any) => {
if (response.pk) {
navigate(getDetailUrl(ModelType.stockitem, response.pk));
}
}
}); });
const stockActionProps: StockOperationProps = useMemo(() => { const stockActionProps: StockOperationProps = useMemo(() => {
@ -368,15 +396,17 @@ export default function StockDetail() {
const transferStockItem = useTransferStockItem(stockActionProps); const transferStockItem = useTransferStockItem(stockActionProps);
const stockActions = useMemo( const stockActions = useMemo(
() => /* TODO: Disable actions based on user permissions*/ [ () => [
<BarcodeActionDropdown <BarcodeActionDropdown
actions={[ actions={[
ViewBarcodeAction({}), ViewBarcodeAction({}),
LinkBarcodeAction({ LinkBarcodeAction({
hidden: stockitem?.barcode_hash hidden:
stockitem?.barcode_hash || !user.hasChangeRole(UserRoles.stock)
}), }),
UnlinkBarcodeAction({ UnlinkBarcodeAction({
hidden: !stockitem?.barcode_hash hidden:
!stockitem?.barcode_hash || !user.hasChangeRole(UserRoles.stock)
}) })
]} ]}
/>, />,
@ -425,16 +455,20 @@ export default function StockDetail() {
/>, />,
<ActionDropdown <ActionDropdown
key="stock" key="stock"
// tooltip={t`Stock Actions`} tooltip={t`Stock Item Actions`}
icon={<IconDots />} icon={<IconDots />}
actions={[ actions={[
{ DuplicateItemAction({
name: t`Duplicate`, hidden: !user.hasAddRole(UserRoles.stock),
tooltip: t`Duplicate stock item`, onClick: () => duplicateStockItem.open()
icon: <IconCopy /> }),
}, EditItemAction({
EditItemAction({}), hidden: !user.hasChangeRole(UserRoles.stock),
DeleteItemAction({}) onClick: () => editStockItem.open()
}),
DeleteItemAction({
hidden: !user.hasDeleteRole(UserRoles.stock)
})
]} ]}
/> />
], ],
@ -489,6 +523,7 @@ export default function StockDetail() {
/> />
<PanelGroup pageKey="stockitem" panels={stockPanels} /> <PanelGroup pageKey="stockitem" panels={stockPanels} />
{editStockItem.modal} {editStockItem.modal}
{duplicateStockItem.modal}
{countStockItem.modal} {countStockItem.modal}
{addStockItem.modal} {addStockItem.modal}
{removeStockItem.modal} {removeStockItem.modal}

View File

@ -10,7 +10,7 @@ import { renderDate } from '../../defaults/formatters';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType'; import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles'; import { UserRoles } from '../../enums/Roles';
import { buildOrderFields } from '../../forms/BuildForms'; import { useBuildOrderFields } from '../../forms/BuildForms';
import { getDetailUrl } from '../../functions/urls'; import { getDetailUrl } from '../../functions/urls';
import { useCreateApiFormModal } from '../../hooks/UseForm'; import { useCreateApiFormModal } from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable'; import { useTable } from '../../hooks/UseTable';
@ -135,10 +135,12 @@ export function BuildOrderTable({
const table = useTable('buildorder'); const table = useTable('buildorder');
const buildOrderFields = useBuildOrderFields({ create: true });
const newBuild = useCreateApiFormModal({ const newBuild = useCreateApiFormModal({
url: ApiEndpoints.build_order_list, url: ApiEndpoints.build_order_list,
title: t`Add Build Order`, title: t`Add Build Order`,
fields: buildOrderFields(), fields: buildOrderFields,
initialData: { initialData: {
part: partId, part: partId,
sales_order: salesOrderId, sales_order: salesOrderId,

View File

@ -1,5 +1,6 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Group, Text } from '@mantine/core'; import { Group, Text } from '@mantine/core';
import { access } from 'fs';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
@ -13,7 +14,8 @@ import { useCreateApiFormModal } from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable'; import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
import { DescriptionColumn } from '../ColumnRenderers'; import { BooleanColumn, DescriptionColumn } from '../ColumnRenderers';
import { TableFilter } from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable'; import { InvenTreeTable } from '../InvenTreeTable';
/** /**
@ -51,6 +53,12 @@ export function CompanyTable({
} }
}, },
DescriptionColumn({}), DescriptionColumn({}),
BooleanColumn({
accessor: 'active',
title: t`Active`,
sortable: true,
switchable: true
}),
{ {
accessor: 'website', accessor: 'website',
sortable: false sortable: false
@ -73,6 +81,31 @@ export function CompanyTable({
} }
}); });
const tableFilters: TableFilter[] = useMemo(() => {
return [
{
name: 'active',
label: t`Active`,
description: t`Show active companies`
},
{
name: 'is_supplier',
label: t`Supplier`,
description: t`Show companies which are suppliers`
},
{
name: 'is_manufacturer',
label: t`Manufacturer`,
description: t`Show companies which are manufacturers`
},
{
name: 'is_customer',
label: t`Customer`,
description: t`Show companies which are customers`
}
];
}, []);
const tableActions = useMemo(() => { const tableActions = useMemo(() => {
const can_add = const can_add =
user.hasAddRole(UserRoles.purchase_order) || user.hasAddRole(UserRoles.purchase_order) ||
@ -98,6 +131,7 @@ export function CompanyTable({
params: { params: {
...params ...params
}, },
tableFilters: tableFilters,
tableActions: tableActions, tableActions: tableActions,
onRowClick: (row: any) => { onRowClick: (row: any) => {
if (row.pk) { if (row.pk) {

View File

@ -63,9 +63,7 @@ export function ContactTable({
}; };
}, []); }, []);
const [selectedContact, setSelectedContact] = useState<number | undefined>( const [selectedContact, setSelectedContact] = useState<number>(0);
undefined
);
const editContact = useEditApiFormModal({ const editContact = useEditApiFormModal({
url: ApiEndpoints.contact_list, url: ApiEndpoints.contact_list,

View File

@ -44,9 +44,11 @@ import { TableHoverCard } from '../TableHoverCard';
*/ */
export function PurchaseOrderLineItemTable({ export function PurchaseOrderLineItemTable({
orderId, orderId,
supplierId,
params params
}: { }: {
orderId: number; orderId: number;
supplierId?: number;
params?: any; params?: any;
}) { }) {
const table = useTable('purchase-order-line-item'); const table = useTable('purchase-order-line-item');
@ -67,7 +69,7 @@ export function PurchaseOrderLineItemTable({
return [ return [
{ {
accessor: 'part', accessor: 'part',
title: t`Part`, title: t`Internal Part`,
sortable: true, sortable: true,
switchable: false, switchable: false,
render: (record: any) => { render: (record: any) => {
@ -183,25 +185,35 @@ export function PurchaseOrderLineItemTable({
]; ];
}, [orderId, user]); }, [orderId, user]);
const addPurchaseOrderFields = usePurchaseOrderLineItemFields({
create: true,
orderId: orderId,
supplierId: supplierId
});
const [initialData, setInitialData] = useState({});
const newLine = useCreateApiFormModal({ const newLine = useCreateApiFormModal({
url: ApiEndpoints.purchase_order_line_list, url: ApiEndpoints.purchase_order_line_list,
title: t`Add Line Item`, title: t`Add Line Item`,
fields: usePurchaseOrderLineItemFields({ create: true }), fields: addPurchaseOrderFields,
initialData: { initialData: initialData,
order: orderId
},
onFormSuccess: table.refreshTable onFormSuccess: table.refreshTable
}); });
const [selectedLine, setSelectedLine] = useState<number | undefined>( const [selectedLine, setSelectedLine] = useState<number>(0);
undefined
); const editPurchaseOrderFields = usePurchaseOrderLineItemFields({
create: false,
orderId: orderId,
supplierId: supplierId
});
const editLine = useEditApiFormModal({ const editLine = useEditApiFormModal({
url: ApiEndpoints.purchase_order_line_list, url: ApiEndpoints.purchase_order_line_list,
pk: selectedLine, pk: selectedLine,
title: t`Edit Line Item`, title: t`Edit Line Item`,
fields: usePurchaseOrderLineItemFields({}), fields: editPurchaseOrderFields,
onFormSuccess: table.refreshTable onFormSuccess: table.refreshTable
}); });
@ -235,7 +247,11 @@ export function PurchaseOrderLineItemTable({
} }
}), }),
RowDuplicateAction({ RowDuplicateAction({
hidden: !user.hasAddRole(UserRoles.purchase_order) hidden: !user.hasAddRole(UserRoles.purchase_order),
onClick: () => {
setInitialData({ ...record });
newLine.open();
}
}), }),
RowDeleteAction({ RowDeleteAction({
hidden: !user.hasDeleteRole(UserRoles.purchase_order), hidden: !user.hasDeleteRole(UserRoles.purchase_order),
@ -254,7 +270,12 @@ export function PurchaseOrderLineItemTable({
return [ return [
<AddItemButton <AddItemButton
tooltip={t`Add line item`} tooltip={t`Add line item`}
onClick={() => newLine.open()} onClick={() => {
setInitialData({
order: orderId
});
newLine.open();
}}
hidden={!user?.hasAddRole(UserRoles.purchase_order)} hidden={!user?.hasAddRole(UserRoles.purchase_order)}
/>, />,
<ActionButton <ActionButton

View File

@ -8,7 +8,7 @@ import { formatCurrency } from '../../defaults/formatters';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType'; import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles'; import { UserRoles } from '../../enums/Roles';
import { purchaseOrderFields } from '../../forms/PurchaseOrderForms'; import { usePurchaseOrderFields } from '../../forms/PurchaseOrderForms';
import { getDetailUrl } from '../../functions/urls'; import { getDetailUrl } from '../../functions/urls';
import { useCreateApiFormModal } from '../../hooks/UseForm'; import { useCreateApiFormModal } from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable'; import { useTable } from '../../hooks/UseTable';
@ -106,10 +106,12 @@ export function PurchaseOrderTable({
]; ];
}, []); }, []);
const purchaseOrderFields = usePurchaseOrderFields();
const newPurchaseOrder = useCreateApiFormModal({ const newPurchaseOrder = useCreateApiFormModal({
url: ApiEndpoints.purchase_order_list, url: ApiEndpoints.purchase_order_list,
title: t`Add Purchase Order`, title: t`Add Purchase Order`,
fields: purchaseOrderFields(), fields: purchaseOrderFields,
initialData: { initialData: {
supplier: supplierId supplier: supplierId
}, },

View File

@ -1,6 +1,6 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Text } from '@mantine/core'; import { Text } from '@mantine/core';
import { ReactNode, useCallback, useMemo } from 'react'; import { ReactNode, useCallback, useMemo, useState } from 'react';
import { AddItemButton } from '../../components/buttons/AddItemButton'; import { AddItemButton } from '../../components/buttons/AddItemButton';
import { Thumbnail } from '../../components/images/Thumbnail'; import { Thumbnail } from '../../components/images/Thumbnail';
@ -9,17 +9,23 @@ import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles'; import { UserRoles } from '../../enums/Roles';
import { useSupplierPartFields } from '../../forms/CompanyForms'; import { useSupplierPartFields } from '../../forms/CompanyForms';
import { openDeleteApiForm, openEditApiForm } from '../../functions/forms'; import { openDeleteApiForm, openEditApiForm } from '../../functions/forms';
import { useCreateApiFormModal } from '../../hooks/UseForm'; import {
useCreateApiFormModal,
useDeleteApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable'; import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
import { TableColumn } from '../Column'; import { TableColumn } from '../Column';
import { import {
BooleanColumn,
DescriptionColumn, DescriptionColumn,
LinkColumn, LinkColumn,
NoteColumn, NoteColumn,
PartColumn PartColumn
} from '../ColumnRenderers'; } from '../ColumnRenderers';
import { TableFilter } from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable'; import { InvenTreeTable } from '../InvenTreeTable';
import { RowDeleteAction, RowEditAction } from '../RowActions'; import { RowDeleteAction, RowEditAction } from '../RowActions';
import { TableHoverCard } from '../TableHoverCard'; import { TableHoverCard } from '../TableHoverCard';
@ -88,6 +94,12 @@ export function SupplierPartTable({ params }: { params: any }): ReactNode {
title: t`MPN`, title: t`MPN`,
render: (record: any) => record?.manufacturer_part_detail?.MPN render: (record: any) => record?.manufacturer_part_detail?.MPN
}, },
BooleanColumn({
accessor: 'active',
title: t`Active`,
sortable: true,
switchable: true
}),
{ {
accessor: 'in_stock', accessor: 'in_stock',
sortable: true sortable: true
@ -145,35 +157,67 @@ export function SupplierPartTable({ params }: { params: any }): ReactNode {
]; ];
}, [params]); }, [params]);
const addSupplierPartFields = useSupplierPartFields({ const supplierPartFields = useSupplierPartFields();
partPk: params?.part,
supplierPk: params?.supplier, const addSupplierPart = useCreateApiFormModal({
hidePart: true url: ApiEndpoints.supplier_part_list,
title: t`Add Supplier Part`,
fields: supplierPartFields,
initialData: {
part: params?.part,
supplier: params?.supplier
},
onFormSuccess: table.refreshTable,
successMessage: t`Supplier part created`
}); });
const { modal: addSupplierPartModal, open: openAddSupplierPartForm } =
useCreateApiFormModal({
url: ApiEndpoints.supplier_part_list,
title: t`Add Supplier Part`,
fields: addSupplierPartFields,
onFormSuccess: table.refreshTable,
successMessage: t`Supplier part created`
});
// Table actions
const tableActions = useMemo(() => { const tableActions = useMemo(() => {
// TODO: Hide actions based on user permissions
return [ return [
<AddItemButton <AddItemButton
tooltip={t`Add supplier part`} tooltip={t`Add supplier part`}
onClick={openAddSupplierPartForm} onClick={() => addSupplierPart.open()}
hidden={!user.hasAddRole(UserRoles.purchase_order)}
/> />
]; ];
}, [user]); }, [user]);
const editSupplierPartFields = useSupplierPartFields({ const tableFilters: TableFilter[] = useMemo(() => {
hidePart: true, return [
partPk: params?.part {
name: 'active',
label: t`Active`,
description: t`Show active supplier parts`
},
{
name: 'part_active',
label: t`Active Part`,
description: t`Show active internal parts`
},
{
name: 'supplier_active',
label: t`Active Supplier`,
description: t`Show active suppliers`
}
];
}, []);
const editSupplierPartFields = useSupplierPartFields();
const [selectedSupplierPart, setSelectedSupplierPart] = useState<number>(0);
const editSupplierPart = useEditApiFormModal({
url: ApiEndpoints.supplier_part_list,
pk: selectedSupplierPart,
title: t`Edit Supplier Part`,
fields: editSupplierPartFields,
onFormSuccess: () => table.refreshTable()
});
const deleteSupplierPart = useDeleteApiFormModal({
url: ApiEndpoints.supplier_part_list,
pk: selectedSupplierPart,
title: t`Delete Supplier Part`,
onFormSuccess: () => table.refreshTable()
}); });
// Row action callback // Row action callback
@ -183,29 +227,15 @@ export function SupplierPartTable({ params }: { params: any }): ReactNode {
RowEditAction({ RowEditAction({
hidden: !user.hasChangeRole(UserRoles.purchase_order), hidden: !user.hasChangeRole(UserRoles.purchase_order),
onClick: () => { onClick: () => {
record.pk && setSelectedSupplierPart(record.pk);
openEditApiForm({ editSupplierPart.open();
url: ApiEndpoints.supplier_part_list,
pk: record.pk,
title: t`Edit Supplier Part`,
fields: editSupplierPartFields,
onFormSuccess: table.refreshTable,
successMessage: t`Supplier part updated`
});
} }
}), }),
RowDeleteAction({ RowDeleteAction({
hidden: !user.hasDeleteRole(UserRoles.purchase_order), hidden: !user.hasDeleteRole(UserRoles.purchase_order),
onClick: () => { onClick: () => {
record.pk && setSelectedSupplierPart(record.pk);
openDeleteApiForm({ deleteSupplierPart.open();
url: ApiEndpoints.supplier_part_list,
pk: record.pk,
title: t`Delete Supplier Part`,
successMessage: t`Supplier part deleted`,
onFormSuccess: table.refreshTable,
preFormWarning: t`Are you sure you want to remove this supplier part?`
});
} }
}) })
]; ];
@ -215,7 +245,9 @@ export function SupplierPartTable({ params }: { params: any }): ReactNode {
return ( return (
<> <>
{addSupplierPartModal} {addSupplierPart.modal}
{editSupplierPart.modal}
{deleteSupplierPart.modal}
<InvenTreeTable <InvenTreeTable
url={apiUrl(ApiEndpoints.supplier_part_list)} url={apiUrl(ApiEndpoints.supplier_part_list)}
tableState={table} tableState={table}
@ -229,6 +261,7 @@ export function SupplierPartTable({ params }: { params: any }): ReactNode {
}, },
rowActions: rowActions, rowActions: rowActions,
tableActions: tableActions, tableActions: tableActions,
tableFilters: tableFilters,
modelType: ModelType.supplierpart modelType: ModelType.supplierpart
}} }}
/> />

View File

@ -1,5 +1,6 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { AddItemButton } from '../../components/buttons/AddItemButton'; import { AddItemButton } from '../../components/buttons/AddItemButton';
import { Thumbnail } from '../../components/images/Thumbnail'; import { Thumbnail } from '../../components/images/Thumbnail';
@ -7,7 +8,10 @@ import { formatCurrency } from '../../defaults/formatters';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType'; import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles'; import { UserRoles } from '../../enums/Roles';
import { useReturnOrderFields } from '../../forms/SalesOrderForms';
import { notYetImplemented } from '../../functions/notifications'; import { notYetImplemented } from '../../functions/notifications';
import { getDetailUrl } from '../../functions/urls';
import { useCreateApiFormModal } from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable'; import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
@ -33,6 +37,7 @@ import { InvenTreeTable } from '../InvenTreeTable';
export function ReturnOrderTable({ params }: { params?: any }) { export function ReturnOrderTable({ params }: { params?: any }) {
const table = useTable('return-orders'); const table = useTable('return-orders');
const user = useUserState(); const user = useUserState();
const navigate = useNavigate();
const tableFilters: TableFilter[] = useMemo(() => { const tableFilters: TableFilter[] = useMemo(() => {
return [ return [
@ -48,10 +53,6 @@ export function ReturnOrderTable({ params }: { params?: any }) {
]; ];
}, []); }, []);
// TODO: Row actions
// TODO: Table actions (e.g. create new return order)
const tableColumns = useMemo(() => { const tableColumns = useMemo(() => {
return [ return [
ReferenceColumn(), ReferenceColumn(),
@ -94,34 +95,48 @@ export function ReturnOrderTable({ params }: { params?: any }) {
]; ];
}, []); }, []);
const addReturnOrder = useCallback(() => { const returnOrderFields = useReturnOrderFields();
notYetImplemented();
}, []); const newReturnOrder = useCreateApiFormModal({
url: ApiEndpoints.return_order_list,
title: t`Add Return Order`,
fields: returnOrderFields,
onFormSuccess: (response) => {
if (response.pk) {
navigate(getDetailUrl(ModelType.returnorder, response.pk));
} else {
table.refreshTable();
}
}
});
const tableActions = useMemo(() => { const tableActions = useMemo(() => {
return [ return [
<AddItemButton <AddItemButton
tooltip={t`Add Return Order`} tooltip={t`Add Return Order`}
onClick={addReturnOrder} onClick={() => newReturnOrder.open()}
hidden={!user.hasAddRole(UserRoles.sales_order)} hidden={!user.hasAddRole(UserRoles.return_order)}
/> />
]; ];
}, [user]); }, [user]);
return ( return (
<InvenTreeTable <>
url={apiUrl(ApiEndpoints.return_order_list)} {newReturnOrder.modal}
tableState={table} <InvenTreeTable
columns={tableColumns} url={apiUrl(ApiEndpoints.return_order_list)}
props={{ tableState={table}
params: { columns={tableColumns}
...params, props={{
customer_detail: true params: {
}, ...params,
tableFilters: tableFilters, customer_detail: true
tableActions: tableActions, },
modelType: ModelType.returnorder tableFilters: tableFilters,
}} tableActions: tableActions,
/> modelType: ModelType.returnorder
}}
/>
</>
); );
} }

View File

@ -8,7 +8,7 @@ import { formatCurrency } from '../../defaults/formatters';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType'; import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles'; import { UserRoles } from '../../enums/Roles';
import { salesOrderFields } from '../../forms/SalesOrderForms'; import { useSalesOrderFields } from '../../forms/SalesOrderForms';
import { getDetailUrl } from '../../functions/urls'; import { getDetailUrl } from '../../functions/urls';
import { useCreateApiFormModal } from '../../hooks/UseForm'; import { useCreateApiFormModal } from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable'; import { useTable } from '../../hooks/UseTable';
@ -61,10 +61,12 @@ export function SalesOrderTable({
]; ];
}, []); }, []);
const salesOrderFields = useSalesOrderFields();
const newSalesOrder = useCreateApiFormModal({ const newSalesOrder = useCreateApiFormModal({
url: ApiEndpoints.sales_order_list, url: ApiEndpoints.sales_order_list,
title: t`Add Sales Order`, title: t`Add Sales Order`,
fields: salesOrderFields(), fields: salesOrderFields,
initialData: { initialData: {
customer: customerId customer: customerId
}, },

View File

@ -279,9 +279,7 @@ export default function StockItemTestResultTable({
successMessage: t`Test result added` successMessage: t`Test result added`
}); });
const [selectedTest, setSelectedTest] = useState<number | undefined>( const [selectedTest, setSelectedTest] = useState<number>(0);
undefined
);
const editTestModal = useEditApiFormModal({ const editTestModal = useEditApiFormModal({
url: ApiEndpoints.stock_test_result_list, url: ApiEndpoints.stock_test_result_list,

View File

@ -67,6 +67,11 @@ test('PUI - Purchasing', async ({ page }) => {
.click(); .click();
await page.getByRole('menuitem', { name: 'Edit' }).click(); await page.getByRole('menuitem', { name: 'Edit' }).click();
await page.getByLabel('Address title *').waitFor(); await page.getByLabel('Address title *').waitFor();
// Read the current value of the cell, to ensure we always *change* it!
const value = await page.getByLabel('Line 2').inputValue();
await page.getByLabel('Line 2').fill(value == 'old' ? 'new' : 'old');
await page.getByRole('button', { name: 'Submit' }).isEnabled(); await page.getByRole('button', { name: 'Submit' }).isEnabled();
await page.getByRole('button', { name: 'Submit' }).click(); await page.getByRole('button', { name: 'Submit' }).click();
await page.getByRole('tab', { name: 'Details' }).waitFor(); await page.getByRole('tab', { name: 'Details' }).waitFor();