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