2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-14 02:55:41 +00:00

Adds "active" field for Company model (#7024)

* Add "active" field to Company model

* Expose 'active' parameter to API

* Fix default value

* Add 'active' column to PUI

* Update PUI table

* Update company detail pages

* Update API filters for SupplierPart and ManufacturerPart

* Bump API version

* Update order forms

* Add edit action to SalesOrderDetail page

* Enable editing of ReturnOrder

* Typo fix

* Adds explicit "active" field to SupplierPart model

* More updates

- Add "inactive" badge to SupplierPart page
- Update SupplierPartTable
- Update backend API fields

* Update ReturnOrderTable

- Also some refactoring

* Impove usePurchaseOrderLineItemFields hook

* Cleanup

* Implement duplicate action for SupplierPart

* Fix for ApiForm

- Only override initialValues for specified fields

* Allow edit and duplicate of StockItem

* Fix for ApiForm

- Default values were overriding initial data

* Add duplicate part option

* Cleanup ApiForm

- Cache props.fields

* Fix unused import

* More fixes

* Add unit tests

* Allow ordering company by 'active' status

* Update docs

* Merge migrations

* Fix for serializers.py

* Force new form value

* Remove debug call

* Further unit test fixes

* Update default CSRF_TRUSTED_ORIGINS values

* Reduce debug output
This commit is contained in:
Oliver
2024-04-20 23:18:25 +10:00
committed by GitHub
parent 2632bcfbbc
commit 2fe0eefa8f
41 changed files with 927 additions and 390 deletions

View File

@ -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

View File

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

View File

@ -2,6 +2,7 @@
from django.db.models import Q
from django.urls import include, path, re_path
from django.utils.translation import gettext_lazy as _
from django_filters import rest_framework as rest_filters
@ -58,11 +59,17 @@ class CompanyList(ListCreateAPI):
filter_backends = SEARCH_ORDER_FILTER
filterset_fields = ['is_customer', 'is_manufacturer', 'is_supplier', 'name']
filterset_fields = [
'is_customer',
'is_manufacturer',
'is_supplier',
'name',
'active',
]
search_fields = ['name', 'description', 'website']
ordering_fields = ['name', 'parts_supplied', 'parts_manufactured']
ordering_fields = ['active', 'name', 'parts_supplied', 'parts_manufactured']
ordering = 'name'
@ -153,7 +160,13 @@ class ManufacturerPartFilter(rest_filters.FilterSet):
fields = ['manufacturer', 'MPN', 'part', 'tags__name', 'tags__slug']
# Filter by 'active' status of linked part
active = rest_filters.BooleanFilter(field_name='part__active')
part_active = rest_filters.BooleanFilter(
field_name='part__active', label=_('Part is Active')
)
manufacturer_active = rest_filters.BooleanFilter(
field_name='manufacturer__active', label=_('Manufacturer is Active')
)
class ManufacturerPartList(ListCreateDestroyAPIView):
@ -301,8 +314,16 @@ class SupplierPartFilter(rest_filters.FilterSet):
'tags__slug',
]
active = rest_filters.BooleanFilter(label=_('Supplier Part is Active'))
# Filter by 'active' status of linked part
active = rest_filters.BooleanFilter(field_name='part__active')
part_active = rest_filters.BooleanFilter(
field_name='part__active', label=_('Internal Part is Active')
)
supplier_active = rest_filters.BooleanFilter(
field_name='supplier__active', label=_('Supplier is Active')
)
# Filter by the 'MPN' of linked manufacturer part
MPN = rest_filters.CharFilter(
@ -378,6 +399,7 @@ class SupplierPartList(ListCreateDestroyAPIView):
'part',
'supplier',
'manufacturer',
'active',
'MPN',
'packaging',
'pack_quantity',

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@ from django.urls import reverse
from rest_framework import status
from InvenTree.unit_test import InvenTreeAPITestCase
from part.models import Part
from .models import Address, Company, Contact, ManufacturerPart, SupplierPart
@ -131,6 +132,32 @@ class CompanyTest(InvenTreeAPITestCase):
self.assertTrue('currency' in response.data)
def test_company_active(self):
"""Test that the 'active' value and filter works."""
Company.objects.filter(active=False).update(active=True)
n = Company.objects.count()
url = reverse('api-company-list')
self.assertEqual(
len(self.get(url, data={'active': True}, expected_code=200).data), n
)
self.assertEqual(
len(self.get(url, data={'active': False}, expected_code=200).data), 0
)
# Set one company to inactive
c = Company.objects.first()
c.active = False
c.save()
self.assertEqual(
len(self.get(url, data={'active': True}, expected_code=200).data), n - 1
)
self.assertEqual(
len(self.get(url, data={'active': False}, expected_code=200).data), 1
)
class ContactTest(InvenTreeAPITestCase):
"""Tests for the Contact models."""
@ -528,6 +555,50 @@ class SupplierPartTest(InvenTreeAPITestCase):
self.assertEqual(sp.available, 999)
self.assertIsNotNone(sp.availability_updated)
def test_active(self):
"""Test that 'active' status filtering works correctly."""
url = reverse('api-supplier-part-list')
# Create a new company, which is inactive
company = Company.objects.create(
name='Inactive Company', is_supplier=True, active=False
)
part = Part.objects.filter(purchaseable=True).first()
# Create some new supplier part objects, *some* of which are inactive
for idx in range(10):
SupplierPart.objects.create(
part=part,
supplier=company,
SKU=f'CMP-{company.pk}-SKU-{idx}',
active=(idx % 2 == 0),
)
n = SupplierPart.objects.count()
# List *all* supplier parts
self.assertEqual(len(self.get(url, data={}, expected_code=200).data), n)
# List only active supplier parts (all except 5 from the new supplier)
self.assertEqual(
len(self.get(url, data={'active': True}, expected_code=200).data), n - 5
)
# List only from 'active' suppliers (all except this new supplier)
self.assertEqual(
len(self.get(url, data={'supplier_active': True}, expected_code=200).data),
n - 10,
)
# List active parts from inactive suppliers (only 5 from the new supplier)
response = self.get(
url, data={'supplier_active': False, 'active': True}, expected_code=200
)
self.assertEqual(len(response.data), 5)
for result in response.data:
self.assertEqual(result['supplier'], company.pk)
class CompanyMetadataAPITest(InvenTreeAPITestCase):
"""Unit tests for the various metadata endpoints of API."""

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,57 +7,64 @@ import {
IconUser,
IconUsersGroup
} from '@tabler/icons-react';
import { useMemo } from 'react';
import { ApiFormFieldSet } from '../components/forms/fields/ApiFormField';
/**
* Field set for BuildOrder forms
*/
export function buildOrderFields(): ApiFormFieldSet {
return {
reference: {},
part: {
filters: {
assembly: true,
virtual: false
export function useBuildOrderFields({
create
}: {
create: boolean;
}): ApiFormFieldSet {
return useMemo(() => {
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: {},
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
}
}
};
};
}, [create]);
}

View File

@ -10,34 +10,21 @@ import {
} from '@tabler/icons-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
*/
export function useSupplierPartFields({
partPk,
supplierPk,
hidePart
}: {
partPk?: number;
supplierPk?: number;
hidePart?: boolean;
}) {
const [part, setPart] = useState<number | undefined>(partPk);
useEffect(() => {
setPart(partPk);
}, [partPk]);
export function useSupplierPartFields() {
return useMemo(() => {
const fields: ApiFormFieldSet = {
part: {
hidden: hidePart,
value: part,
onValueChange: setPart,
filters: {
purchaseable: true
purchaseable: true,
active: true
}
},
manufacturer_part: {
@ -45,15 +32,18 @@ export function useSupplierPartFields({
part_detail: true,
manufacturer_detail: true
},
adjustFilters: (filters: any) => {
if (part) {
filters.part = part;
}
return filters;
adjustFilters: (adjust: ApiFormAdjustFilterType) => {
return {
...adjust.filters,
part: adjust.data.part
};
}
},
supplier: {
filters: {
active: true
}
},
supplier: {},
SKU: {
icon: <IconHash />
},
@ -67,15 +57,12 @@ export function useSupplierPartFields({
pack_quantity: {},
packaging: {
icon: <IconPackage />
}
},
active: {}
};
if (supplierPk !== undefined) {
fields.supplier.value = supplierPk;
}
return fields;
}, [part]);
}, []);
}
export function useManufacturerPartFields() {
@ -125,6 +112,7 @@ export function companyFields(): ApiFormFieldSet {
},
is_supplier: {},
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
*/
export function usePurchaseOrderLineItemFields({
supplierId,
orderId,
create
}: {
supplierId?: number;
orderId?: number;
create?: boolean;
}) {
const [purchasePrice, setPurchasePrice] = useState<string>('');
@ -60,16 +64,20 @@ export function usePurchaseOrderLineItemFields({
filters: {
supplier_detail: true
},
hidden: true
disabled: true
},
part: {
filters: {
part_detail: true,
supplier_detail: true
supplier_detail: true,
active: true,
part_active: true
},
adjustFilters: (value: ApiFormAdjustFilterType) => {
// TODO: Adjust part based on the supplier associated with the supplier
return value.filters;
adjustFilters: (adjust: ApiFormAdjustFilterType) => {
return {
...adjust.filters,
supplier: supplierId
};
}
},
quantity: {},
@ -105,7 +113,7 @@ export function usePurchaseOrderLineItemFields({
}
return fields;
}, [create, autoPricing, purchasePrice]);
}, [create, orderId, supplierId, autoPricing, purchasePrice]);
return fields;
}
@ -113,50 +121,53 @@ export function usePurchaseOrderLineItemFields({
/**
* Construct a set of fields for creating / editing a PurchaseOrder instance
*/
export function purchaseOrderFields(): ApiFormFieldSet {
return {
reference: {
icon: <IconHash />
},
description: {},
supplier: {
filters: {
is_supplier: true
export function usePurchaseOrderFields(): ApiFormFieldSet {
return useMemo(() => {
return {
reference: {
icon: <IconHash />
},
description: {},
supplier: {
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 { useMemo } from 'react';
import {
ApiFormAdjustFilterType,
ApiFormFieldSet
} from '../components/forms/fields/ApiFormField';
export function salesOrderFields(): ApiFormFieldSet {
return {
reference: {},
description: {},
customer: {
filters: {
is_customer: true
export function useSalesOrderFields(): ApiFormFieldSet {
return useMemo(() => {
return {
reference: {},
description: {},
customer: {
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: {},
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 />
}
};
};
}, []);
}
export function useReturnOrderFields(): ApiFormFieldSet {
return useMemo(() => {
return {
reference: {},
description: {},
customer: {
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 />
}
};
}, []);
}

View File

@ -39,7 +39,7 @@ export function useStockFields({
const fields: ApiFormFieldSet = {
part: {
value: part,
hidden: !create,
disabled: !create,
onValueChange: (change) => {
setPart(change);
// TODO: implement remaining functionality from old stock.py
@ -57,12 +57,12 @@ export function useStockFields({
supplier_detail: true,
...(part ? { part } : {})
},
adjustFilters: (value: ApiFormAdjustFilterType) => {
if (value.data.part) {
value.filters['part'] = value.data.part;
adjustFilters: (adjust: ApiFormAdjustFilterType) => {
if (adjust.data.part) {
adjust.filters['part'] = adjust.data.part;
}
return value.filters;
return adjust.filters;
}
},
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({
stockItem,
value

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import { t } from '@lingui/macro';
import { Group, Text } from '@mantine/core';
import { access } from 'fs';
import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
@ -13,7 +14,8 @@ import { useCreateApiFormModal } from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
import { DescriptionColumn } from '../ColumnRenderers';
import { BooleanColumn, DescriptionColumn } from '../ColumnRenderers';
import { TableFilter } from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
/**
@ -51,6 +53,12 @@ export function CompanyTable({
}
},
DescriptionColumn({}),
BooleanColumn({
accessor: 'active',
title: t`Active`,
sortable: true,
switchable: true
}),
{
accessor: 'website',
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 can_add =
user.hasAddRole(UserRoles.purchase_order) ||
@ -98,6 +131,7 @@ export function CompanyTable({
params: {
...params
},
tableFilters: tableFilters,
tableActions: tableActions,
onRowClick: (row: any) => {
if (row.pk) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -67,6 +67,11 @@ test('PUI - Purchasing', async ({ page }) => {
.click();
await page.getByRole('menuitem', { name: 'Edit' }).click();
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' }).click();
await page.getByRole('tab', { name: 'Details' }).waitFor();