mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +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:
parent
2632bcfbbc
commit
2fe0eefa8f
@ -12,8 +12,7 @@ Run the following commands from the top-level project directory:
|
||||
|
||||
```
|
||||
$ git clone https://github.com/inventree/inventree
|
||||
$ cd inventree/docs
|
||||
$ pip install -r requirements.txt
|
||||
$ pip install -r docs/requirements.txt
|
||||
```
|
||||
|
||||
## Serve Locally
|
||||
|
BIN
docs/docs/assets/images/order/company_disable.png
Normal file
BIN
docs/docs/assets/images/order/company_disable.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
BIN
docs/docs/assets/images/order/disable_supplier_part.png
Normal file
BIN
docs/docs/assets/images/order/disable_supplier_part.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 56 KiB |
BIN
docs/docs/assets/images/order/disable_supplier_part_edit.png
Normal file
BIN
docs/docs/assets/images/order/disable_supplier_part_edit.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
@ -11,7 +11,7 @@ External companies are represented by the *Company* database model. Each company
|
||||
- [Manufacturer](#manufacturers)
|
||||
|
||||
!!! tip Multi Purpose
|
||||
A company may be allocated to multiple categories
|
||||
A company may be allocated to multiple categories, for example, a company may be both a supplier and a customer.
|
||||
|
||||
### Edit Company
|
||||
|
||||
@ -20,6 +20,20 @@ To edit a company, click on the <span class='fas fa-edit'>Edit Company</span> ic
|
||||
!!! warning "Permission Required"
|
||||
The edit button will not be available to users who do not have the required permissions to edit the company
|
||||
|
||||
### Disable Company
|
||||
|
||||
Rather than deleting a company, it is possible to disable it. This will prevent the company from being used in new orders, but will not remove it from the database. Additionally, any existing orders associated with the company (and other linked items such as supplier parts, for a supplier) will remain intact. Unless the company is re-enabled, it will not be available for selection in new orders.
|
||||
|
||||
It is recommended to disable a company rather than deleting it, as this will preserve the integrity of historical data.
|
||||
|
||||
To disable a company, simply edit the company details and set the `active` attribute to `False`:
|
||||
|
||||
{% with id="company_disable", url="order/company_disable.png", description="Disable Company" %}
|
||||
{% include "img.html" %}
|
||||
{% endwith %}
|
||||
|
||||
To re-enable a company, simply follow the same process and set the `active` attribute to `True`.
|
||||
|
||||
### Delete Company
|
||||
|
||||
To delete a company, click on the <span class='fas fa-trash-alt'></span> icon under the actions menu. Confirm the deletion using the checkbox then click on <span class="badge inventree confirm">Submit</span>
|
||||
@ -193,6 +207,24 @@ To edit a supplier part, first access the supplier part detail page with one of
|
||||
|
||||
After the supplier part details are loaded, click on the <span class='fas fa-edit'></span> icon next to the supplier part image. Edit the supplier part information then click on <span class="badge inventree confirm">Submit</span>
|
||||
|
||||
#### Disable Supplier Part
|
||||
|
||||
Supplier parts can be individually disabled - for example, if a supplier part is no longer available for purchase. By disabling the part in the InvenTree system, it will no longer be available for selection in new purchase orders. However, any existing purchase orders which reference the supplier part will remain intact.
|
||||
|
||||
The "active" status of a supplier part is clearly visible within the user interface:
|
||||
|
||||
{% with id="supplier_part_disable", url="order/disable_supplier_part.png", description="Disable Supplier Part" %}
|
||||
{% include "img.html" %}
|
||||
{% endwith %}
|
||||
|
||||
To change the "active" status of a supplier part, simply edit the supplier part details and set the `active` attribute:
|
||||
|
||||
{% with id="supplier_part_disable_edit", url="order/disable_supplier_part_edit.png", description="Disable Supplier Part" %}
|
||||
{% include "img.html" %}
|
||||
{% endwith %}
|
||||
|
||||
It is recommended to disable a supplier part rather than deleting it, as this will preserve the integrity of historical data.
|
||||
|
||||
#### Delete Supplier Part
|
||||
|
||||
To delete a supplier part, first access the supplier part detail page like in the [Edit Supplier Part](#edit-supplier-part) section.
|
||||
|
@ -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();
|
||||
|
Loading…
Reference in New Issue
Block a user