mirror of
https://github.com/inventree/InvenTree.git
synced 2025-10-30 12:45:42 +00:00
SalesOrderShipment address (#10650)
* Adds "shipment_address" attribute to the SalesOrderShipment model: - Allows different addresses for each shipment - Defaults to the order shipment address (if not specified) * Add unit testing for field validation * Update SalesOrderShipment serializer * Edit shipment address in UI * Render date on shipment page * Improve address rendering * Update docs * Bump API version * Update CHANGELOG.md * Fix API version
This commit is contained in:
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Added support for Redis ACL user-based authentication in [#10551](https://github.com/inventree/InvenTree/pull/10551)
|
- Added support for Redis ACL user-based authentication in [#10551](https://github.com/inventree/InvenTree/pull/10551)
|
||||||
- Expose stock adjustment forms to the UI plugin context in [#10584](https://github.com/inventree/InvenTree/pull/10584)
|
- Expose stock adjustment forms to the UI plugin context in [#10584](https://github.com/inventree/InvenTree/pull/10584)
|
||||||
- Allow stock adjustments for "in production" items in [#10600](https://github.com/inventree/InvenTree/pull/10600)
|
- Allow stock adjustments for "in production" items in [#10600](https://github.com/inventree/InvenTree/pull/10600)
|
||||||
|
- Adds optional shipping address against individual sales order shipments in [#10650](https://github.com/inventree/InvenTree/pull/10650)
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
title: Sales Orders
|
title: Sales Orders
|
||||||
---
|
---
|
||||||
@@ -59,6 +60,10 @@ Sales Order Status supports [custom states](../concepts/custom_states.md).
|
|||||||
|
|
||||||
The currency code can be specified for an individual sales order. If not specified, the default currency specified against the [customer](./customer.md) will be used.
|
The currency code can be specified for an individual sales order. If not specified, the default currency specified against the [customer](./customer.md) will be used.
|
||||||
|
|
||||||
|
### Sales Order Address
|
||||||
|
|
||||||
|
A sales order can have a specific shipping address assigned to it. The shipping address can be selected from the list of addresses assigned to the [customer](./customer.md) which is linked to the sales order.
|
||||||
|
|
||||||
## Create a Sales Order
|
## Create a Sales Order
|
||||||
|
|
||||||
Once the sales order page is loaded, click on <span class="badge inventree add">{{ icon("plus-circle") }} New Sales Order</span> which opens the "Create Sales Order" form.
|
Once the sales order page is loaded, click on <span class="badge inventree add">{{ icon("plus-circle") }} New Sales Order</span> which opens the "Create Sales Order" form.
|
||||||
@@ -148,7 +153,6 @@ By default, completed orders are not exported. These can be included by appendin
|
|||||||
|
|
||||||
## Sales Order Shipments
|
## Sales Order Shipments
|
||||||
|
|
||||||
|
|
||||||
Shipments are used to track sales items when they are shipped to customers. Multiple shipments can be created against a [Sales Order](./sales_order.md), allowing line items to be sent to customers in multiple deliveries.
|
Shipments are used to track sales items when they are shipped to customers. Multiple shipments can be created against a [Sales Order](./sales_order.md), allowing line items to be sent to customers in multiple deliveries.
|
||||||
|
|
||||||
On the main Sales Order detail page, the order shipments are split into two categories, *Pending Shipments* and *Completed Shipments*:
|
On the main Sales Order detail page, the order shipments are split into two categories, *Pending Shipments* and *Completed Shipments*:
|
||||||
@@ -185,6 +189,10 @@ Each shipment provides the following data fields:
|
|||||||
|
|
||||||
A unique number for the shipment, used to identify each shipment within a sales order. By default, this value starts at `1` for the first shipment (for each order) and automatically increments for each new shipment.
|
A unique number for the shipment, used to identify each shipment within a sales order. By default, this value starts at `1` for the first shipment (for each order) and automatically increments for each new shipment.
|
||||||
|
|
||||||
|
#### Shipment Address
|
||||||
|
|
||||||
|
A shipping address can be optionally specified for an individual shipment. If not specified, the [shipping address assigned to the sales order](#sales-order-address) will be used.
|
||||||
|
|
||||||
#### Tracking Number
|
#### Tracking Number
|
||||||
|
|
||||||
An optional field used to store tracking number information for the shipment.
|
An optional field used to store tracking number information for the shipment.
|
||||||
@@ -197,6 +205,7 @@ An optional field used to store an invoice reference for the shipment.
|
|||||||
|
|
||||||
An optional URL field which can be used to provide a link to an external URL.
|
An optional URL field which can be used to provide a link to an external URL.
|
||||||
|
|
||||||
|
|
||||||
All these fields can be edited by the user:
|
All these fields can be edited by the user:
|
||||||
|
|
||||||
{{ image("order/edit_shipment.png", "Edit shipment") }}
|
{{ image("order/edit_shipment.png", "Edit shipment") }}
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 414
|
INVENTREE_API_VERSION = 415
|
||||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||||
|
|
||||||
INVENTREE_API_TEXT = """
|
INVENTREE_API_TEXT = """
|
||||||
|
|
||||||
v414 -> 2025-06-20 : https://github.com/inventree/InvenTree/pull/10629
|
v415 -> 2025-10-22 : https://github.com/inventree/InvenTree/pull/10650
|
||||||
|
- Adds "shipment_address" fields to the SalesOrderShipment API endpoints
|
||||||
|
|
||||||
|
v414 -> 2025-10-20 : https://github.com/inventree/InvenTree/pull/10629
|
||||||
- Add enums for all ordering fields in schema - no functional changes
|
- Add enums for all ordering fields in schema - no functional changes
|
||||||
|
|
||||||
v413 -> 2025-10-20 : https://github.com/inventree/InvenTree/pull/10624
|
v413 -> 2025-10-20 : https://github.com/inventree/InvenTree/pull/10624
|
||||||
@@ -15,10 +18,10 @@ v413 -> 2025-10-20 : https://github.com/inventree/InvenTree/pull/10624
|
|||||||
v412 -> 2025-10-19 : https://github.com/inventree/InvenTree/pull/10549
|
v412 -> 2025-10-19 : https://github.com/inventree/InvenTree/pull/10549
|
||||||
- added a new query parameter for the PartsList api: price_breaks (default: false)
|
- added a new query parameter for the PartsList api: price_breaks (default: false)
|
||||||
|
|
||||||
v411 -> 2025-06-19 : https://github.com/inventree/InvenTree/pull/10630
|
v411 -> 2025-10-19 : https://github.com/inventree/InvenTree/pull/10630
|
||||||
- Editorialy changes to machine api - no functional changes
|
- Editorial changes to machine api - no functional changes
|
||||||
|
|
||||||
v410 -> 2025-06-12 : https://github.com/inventree/InvenTree/pull/9761
|
v410 -> 2025-10-12 : https://github.com/inventree/InvenTree/pull/9761
|
||||||
- Add supplier search and import API endpoints
|
- Add supplier search and import API endpoints
|
||||||
- Add part parameter bulk create API endpoint
|
- Add part parameter bulk create API endpoint
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 4.2.25 on 2025-10-22 03:04
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("company", "0075_company_tax_id"),
|
||||||
|
("order", "0112_alter_salesorderlineitem_part"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="salesordershipment",
|
||||||
|
name="shipment_address",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
help_text="Shipping address for this shipment",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="+",
|
||||||
|
to="company.address",
|
||||||
|
verbose_name="Address",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -2137,6 +2137,7 @@ class SalesOrderShipmentReportContext(report.mixins.BaseReportContext):
|
|||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
allocations: QuerySet of SalesOrderAllocation objects
|
allocations: QuerySet of SalesOrderAllocation objects
|
||||||
|
address: The shipping address for this shipment (or order)
|
||||||
order: The associated SalesOrder object
|
order: The associated SalesOrder object
|
||||||
reference: Shipment reference string
|
reference: Shipment reference string
|
||||||
shipment: The SalesOrderShipment object itself
|
shipment: The SalesOrderShipment object itself
|
||||||
@@ -2147,6 +2148,7 @@ class SalesOrderShipmentReportContext(report.mixins.BaseReportContext):
|
|||||||
allocations: report.mixins.QuerySet['SalesOrderAllocation']
|
allocations: report.mixins.QuerySet['SalesOrderAllocation']
|
||||||
order: 'SalesOrder'
|
order: 'SalesOrder'
|
||||||
reference: str
|
reference: str
|
||||||
|
address: 'Address'
|
||||||
shipment: 'SalesOrderShipment'
|
shipment: 'SalesOrderShipment'
|
||||||
tracking_number: str
|
tracking_number: str
|
||||||
title: str
|
title: str
|
||||||
@@ -2168,6 +2170,7 @@ class SalesOrderShipment(
|
|||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
order: SalesOrder reference
|
order: SalesOrder reference
|
||||||
|
shipment_address: Shipping address for this shipment (optional)
|
||||||
shipment_date: Date this shipment was "shipped" (or null)
|
shipment_date: Date this shipment was "shipped" (or null)
|
||||||
checked_by: User reference field indicating who checked this order
|
checked_by: User reference field indicating who checked this order
|
||||||
reference: Custom reference text for this shipment (e.g. consignment number?)
|
reference: Custom reference text for this shipment (e.g. consignment number?)
|
||||||
@@ -2186,6 +2189,16 @@ class SalesOrderShipment(
|
|||||||
unique_together = ['order', 'reference']
|
unique_together = ['order', 'reference']
|
||||||
verbose_name = _('Sales Order Shipment')
|
verbose_name = _('Sales Order Shipment')
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
"""Custom clean method for the SalesOrderShipment class."""
|
||||||
|
super().clean()
|
||||||
|
|
||||||
|
if self.order and self.shipment_address:
|
||||||
|
if self.shipment_address.company != self.order.customer:
|
||||||
|
raise ValidationError({
|
||||||
|
'shipment_address': _('Shipment address must match the customer')
|
||||||
|
})
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_api_url():
|
def get_api_url():
|
||||||
"""Return the API URL associated with the SalesOrderShipment model."""
|
"""Return the API URL associated with the SalesOrderShipment model."""
|
||||||
@@ -2196,6 +2209,7 @@ class SalesOrderShipment(
|
|||||||
return {
|
return {
|
||||||
'allocations': self.allocations,
|
'allocations': self.allocations,
|
||||||
'order': self.order,
|
'order': self.order,
|
||||||
|
'address': self.address,
|
||||||
'reference': self.reference,
|
'reference': self.reference,
|
||||||
'shipment': self,
|
'shipment': self,
|
||||||
'tracking_number': self.tracking_number,
|
'tracking_number': self.tracking_number,
|
||||||
@@ -2212,6 +2226,16 @@ class SalesOrderShipment(
|
|||||||
help_text=_('Sales Order'),
|
help_text=_('Sales Order'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
shipment_address = models.ForeignKey(
|
||||||
|
Address,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name=_('Address'),
|
||||||
|
help_text=_('Shipping address for this shipment'),
|
||||||
|
related_name='+',
|
||||||
|
)
|
||||||
|
|
||||||
shipment_date = models.DateField(
|
shipment_date = models.DateField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
@@ -2267,6 +2291,14 @@ class SalesOrderShipment(
|
|||||||
max_length=2000,
|
max_length=2000,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def address(self) -> Address:
|
||||||
|
"""Return the shipping address for this shipment.
|
||||||
|
|
||||||
|
If no specific shipment address is assigned, return the address from the order.
|
||||||
|
"""
|
||||||
|
return self.shipment_address or self.order.address
|
||||||
|
|
||||||
def is_complete(self):
|
def is_complete(self):
|
||||||
"""Return True if this shipment has already been completed."""
|
"""Return True if this shipment has already been completed."""
|
||||||
return self.shipment_date is not None
|
return self.shipment_date is not None
|
||||||
|
|||||||
@@ -1226,9 +1226,9 @@ class SalesOrderShipmentSerializer(
|
|||||||
fields = [
|
fields = [
|
||||||
'pk',
|
'pk',
|
||||||
'order',
|
'order',
|
||||||
'order_detail',
|
|
||||||
'allocated_items',
|
'allocated_items',
|
||||||
'shipment_date',
|
'shipment_date',
|
||||||
|
'shipment_address',
|
||||||
'delivery_date',
|
'delivery_date',
|
||||||
'checked_by',
|
'checked_by',
|
||||||
'reference',
|
'reference',
|
||||||
@@ -1237,6 +1237,10 @@ class SalesOrderShipmentSerializer(
|
|||||||
'barcode_hash',
|
'barcode_hash',
|
||||||
'link',
|
'link',
|
||||||
'notes',
|
'notes',
|
||||||
|
# Extra detail fields
|
||||||
|
'checked_by_detail',
|
||||||
|
'order_detail',
|
||||||
|
'shipment_address_detail',
|
||||||
]
|
]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -1253,6 +1257,13 @@ class SalesOrderShipmentSerializer(
|
|||||||
read_only=True, allow_null=True, label=_('Allocated Items')
|
read_only=True, allow_null=True, label=_('Allocated Items')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
checked_by_detail = enable_filter(
|
||||||
|
UserSerializer(
|
||||||
|
source='checked_by', many=False, read_only=True, allow_null=True
|
||||||
|
),
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
|
||||||
order_detail = enable_filter(
|
order_detail = enable_filter(
|
||||||
SalesOrderSerializer(
|
SalesOrderSerializer(
|
||||||
source='order', read_only=True, allow_null=True, many=False
|
source='order', read_only=True, allow_null=True, many=False
|
||||||
@@ -1260,6 +1271,13 @@ class SalesOrderShipmentSerializer(
|
|||||||
True,
|
True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
shipment_address_detail = enable_filter(
|
||||||
|
AddressBriefSerializer(
|
||||||
|
source='shipment_address', many=False, read_only=True, allow_null=True
|
||||||
|
),
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SalesOrderAllocationSerializer(
|
class SalesOrderAllocationSerializer(
|
||||||
FilterableSerializerMixin, InvenTreeModelSerializer
|
FilterableSerializerMixin, InvenTreeModelSerializer
|
||||||
|
|||||||
@@ -348,6 +348,58 @@ class SalesOrderTest(InvenTreeTestCase):
|
|||||||
self.assertIsNone(self.shipment.delivery_date)
|
self.assertIsNone(self.shipment.delivery_date)
|
||||||
self.assertFalse(self.shipment.is_delivered())
|
self.assertFalse(self.shipment.is_delivered())
|
||||||
|
|
||||||
|
def test_shipment_address(self):
|
||||||
|
"""Unit tests for SalesOrderShipment address field."""
|
||||||
|
shipment = SalesOrderShipment.objects.first()
|
||||||
|
self.assertIsNotNone(shipment)
|
||||||
|
|
||||||
|
# Set an address for the order
|
||||||
|
address_1 = Address.objects.create(
|
||||||
|
company=shipment.order.customer, title='Order Address', line1='123 Test St'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Save the address against the order
|
||||||
|
shipment.order.address = address_1
|
||||||
|
shipment.order.clean()
|
||||||
|
shipment.order.save()
|
||||||
|
|
||||||
|
# By default, no address set
|
||||||
|
self.assertIsNone(shipment.shipment_address)
|
||||||
|
|
||||||
|
# But, the 'address' accessor defaults to the order address
|
||||||
|
self.assertIsNotNone(shipment.address)
|
||||||
|
self.assertEqual(shipment.address, shipment.order.address)
|
||||||
|
|
||||||
|
# Set a custom address for the shipment
|
||||||
|
address_2 = Address.objects.create(
|
||||||
|
company=shipment.order.customer,
|
||||||
|
title='Shipment Address',
|
||||||
|
line1='456 Another St',
|
||||||
|
)
|
||||||
|
|
||||||
|
shipment.shipment_address = address_2
|
||||||
|
shipment.clean()
|
||||||
|
shipment.save()
|
||||||
|
|
||||||
|
self.assertEqual(shipment.address, shipment.shipment_address)
|
||||||
|
self.assertNotEqual(shipment.address, shipment.order.address)
|
||||||
|
|
||||||
|
# Check that the shipment_address validation works
|
||||||
|
other_company = Company.objects.exclude(pk=shipment.order.customer.pk).first()
|
||||||
|
self.assertIsNotNone(other_company)
|
||||||
|
|
||||||
|
address_2.company = other_company
|
||||||
|
address_2.save()
|
||||||
|
shipment.refresh_from_db()
|
||||||
|
|
||||||
|
# This should error out (address company does not match customer)
|
||||||
|
with self.assertRaises(ValidationError) as err:
|
||||||
|
shipment.clean()
|
||||||
|
|
||||||
|
self.assertIn(
|
||||||
|
'Shipment address must match the customer', err.exception.messages
|
||||||
|
)
|
||||||
|
|
||||||
def test_overdue_notification(self):
|
def test_overdue_notification(self):
|
||||||
"""Test overdue sales order notification."""
|
"""Test overdue sales order notification."""
|
||||||
self.ensurePluginsLoaded()
|
self.ensurePluginsLoaded()
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
import { Text } from '@mantine/core';
|
||||||
|
|
||||||
import { ModelType } from '@lib/enums/ModelType';
|
import { ModelType } from '@lib/enums/ModelType';
|
||||||
import { getDetailUrl } from '@lib/functions/Navigation';
|
import { getDetailUrl } from '@lib/functions/Navigation';
|
||||||
import { type InstanceRenderInterface, RenderInlineModel } from './Instance';
|
import { type InstanceRenderInterface, RenderInlineModel } from './Instance';
|
||||||
@@ -22,9 +24,19 @@ export function RenderAddress({
|
|||||||
.join(', ');
|
.join(', ');
|
||||||
|
|
||||||
const primary: string = instance.title || text;
|
const primary: string = instance.title || text;
|
||||||
const secondary: string = instance.title ? text : '';
|
const secondary: string = !!instance.title ? text : '';
|
||||||
|
|
||||||
return <RenderInlineModel primary={primary} secondary={secondary} />;
|
const suffix: ReactNode = instance.primary ? (
|
||||||
|
<Text size='xs'>Primary Address</Text>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RenderInlineModel
|
||||||
|
primary={primary}
|
||||||
|
secondary={secondary}
|
||||||
|
suffix={suffix}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -372,8 +372,10 @@ export function useSalesOrderAllocateSerialsFields({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useSalesOrderShipmentFields({
|
export function useSalesOrderShipmentFields({
|
||||||
|
customerId,
|
||||||
pending
|
pending
|
||||||
}: {
|
}: {
|
||||||
|
customerId: number;
|
||||||
pending?: boolean;
|
pending?: boolean;
|
||||||
}): ApiFormFieldSet {
|
}): ApiFormFieldSet {
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
@@ -388,11 +390,18 @@ export function useSalesOrderShipmentFields({
|
|||||||
delivery_date: {
|
delivery_date: {
|
||||||
hidden: pending ?? true
|
hidden: pending ?? true
|
||||||
},
|
},
|
||||||
|
shipment_address: {
|
||||||
|
placeholder: t`Leave blank to use the order address`,
|
||||||
|
filters: {
|
||||||
|
company: customerId,
|
||||||
|
ordering: '-primary'
|
||||||
|
}
|
||||||
|
},
|
||||||
tracking_number: {},
|
tracking_number: {},
|
||||||
invoice_number: {},
|
invoice_number: {},
|
||||||
link: {}
|
link: {}
|
||||||
};
|
};
|
||||||
}, [pending]);
|
}, [customerId, pending]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSalesOrderShipmentCompleteFields({
|
export function useSalesOrderShipmentCompleteFields({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { Accordion, Grid, Skeleton, Stack } from '@mantine/core';
|
import { Accordion, Grid, Skeleton, Stack, Text } from '@mantine/core';
|
||||||
import { IconInfoCircle, IconList } from '@tabler/icons-react';
|
import { IconInfoCircle, IconList } from '@tabler/icons-react';
|
||||||
import { type ReactNode, useMemo } from 'react';
|
import { type ReactNode, useMemo } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
@@ -194,8 +194,12 @@ export default function ReturnOrderDetail() {
|
|||||||
name: 'address',
|
name: 'address',
|
||||||
label: t`Return Address`,
|
label: t`Return Address`,
|
||||||
icon: 'address',
|
icon: 'address',
|
||||||
hidden: !order.address_detail,
|
value_formatter: () =>
|
||||||
value_formatter: () => <RenderAddress instance={order.address_detail} />
|
order.address_detail ? (
|
||||||
|
<RenderAddress instance={order.address_detail} />
|
||||||
|
) : (
|
||||||
|
<Text size='sm' c='red'>{t`Not specified`}</Text>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { Accordion, Grid, Skeleton, Stack } from '@mantine/core';
|
import { Accordion, Grid, Skeleton, Stack, Text } from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
IconBookmark,
|
IconBookmark,
|
||||||
IconInfoCircle,
|
IconInfoCircle,
|
||||||
@@ -187,8 +187,12 @@ export default function SalesOrderDetail() {
|
|||||||
name: 'address',
|
name: 'address',
|
||||||
label: t`Shipping Address`,
|
label: t`Shipping Address`,
|
||||||
icon: 'address',
|
icon: 'address',
|
||||||
hidden: !order.address_detail,
|
value_formatter: () =>
|
||||||
value_formatter: () => <RenderAddress instance={order.address_detail} />
|
order.address_detail ? (
|
||||||
|
<RenderAddress instance={order.address_detail} />
|
||||||
|
) : (
|
||||||
|
<Text size='sm' c='red'>{t`Not specified`}</Text>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
@@ -391,7 +395,12 @@ export default function SalesOrderDetail() {
|
|||||||
name: 'shipments',
|
name: 'shipments',
|
||||||
label: t`Shipments`,
|
label: t`Shipments`,
|
||||||
icon: <IconTruckDelivery />,
|
icon: <IconTruckDelivery />,
|
||||||
content: <SalesOrderShipmentTable orderId={order.pk} />
|
content: (
|
||||||
|
<SalesOrderShipmentTable
|
||||||
|
orderId={order.pk}
|
||||||
|
customerId={order.customer}
|
||||||
|
/>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'allocations',
|
name: 'allocations',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { Grid, Skeleton, Stack } from '@mantine/core';
|
import { Grid, Skeleton, Stack, Text } from '@mantine/core';
|
||||||
import { IconBookmark, IconInfoCircle } from '@tabler/icons-react';
|
import { IconBookmark, IconInfoCircle } from '@tabler/icons-react';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
@@ -30,6 +30,7 @@ import AttachmentPanel from '../../components/panels/AttachmentPanel';
|
|||||||
import NotesPanel from '../../components/panels/NotesPanel';
|
import NotesPanel from '../../components/panels/NotesPanel';
|
||||||
import type { PanelType } from '../../components/panels/Panel';
|
import type { PanelType } from '../../components/panels/Panel';
|
||||||
import { PanelGroup } from '../../components/panels/PanelGroup';
|
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||||
|
import { RenderAddress } from '../../components/render/Company';
|
||||||
import { formatDate } from '../../defaults/formatters';
|
import { formatDate } from '../../defaults/formatters';
|
||||||
import {
|
import {
|
||||||
useSalesOrderShipmentCompleteFields,
|
useSalesOrderShipmentCompleteFields,
|
||||||
@@ -104,6 +105,18 @@ export default function SalesOrderShipmentDetail() {
|
|||||||
model_field: 'name',
|
model_field: 'name',
|
||||||
hidden: !data.customer
|
hidden: !data.customer
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
external: true,
|
||||||
|
name: 'link',
|
||||||
|
label: t`Link`,
|
||||||
|
copy: true,
|
||||||
|
hidden: !shipment.link
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Top right: Shipment information
|
||||||
|
const tr: DetailsField[] = [
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
name: 'customer_reference',
|
name: 'customer_reference',
|
||||||
@@ -119,16 +132,6 @@ export default function SalesOrderShipmentDetail() {
|
|||||||
label: t`Shipment Reference`,
|
label: t`Shipment Reference`,
|
||||||
copy: true
|
copy: true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
name: 'allocated_items',
|
|
||||||
icon: 'packages',
|
|
||||||
label: t`Allocated Items`
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// Top right: Shipment information
|
|
||||||
const tr: DetailsField[] = [
|
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
name: 'tracking_number',
|
name: 'tracking_number',
|
||||||
@@ -144,6 +147,38 @@ export default function SalesOrderShipmentDetail() {
|
|||||||
icon: 'serial',
|
icon: 'serial',
|
||||||
value_formatter: () => shipment.invoice_number || '---',
|
value_formatter: () => shipment.invoice_number || '---',
|
||||||
copy: !!shipment.invoice_number
|
copy: !!shipment.invoice_number
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const address: any =
|
||||||
|
shipment.shipment_address_detail || shipment.order_detail?.address_detail;
|
||||||
|
|
||||||
|
const bl: DetailsField[] = [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'address',
|
||||||
|
label: t`Shipping Address`,
|
||||||
|
icon: 'address',
|
||||||
|
value_formatter: () =>
|
||||||
|
address ? (
|
||||||
|
<RenderAddress
|
||||||
|
instance={
|
||||||
|
shipment.shipment_address_detail ||
|
||||||
|
shipment.order_detail?.address_detail
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text size='sm' c='red'>{t`Not specified`}</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const br: DetailsField[] = [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'allocated_items',
|
||||||
|
icon: 'packages',
|
||||||
|
label: t`Allocated Items`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
@@ -160,14 +195,6 @@ export default function SalesOrderShipmentDetail() {
|
|||||||
icon: 'calendar',
|
icon: 'calendar',
|
||||||
value_formatter: () => formatDate(shipment.delivery_date),
|
value_formatter: () => formatDate(shipment.delivery_date),
|
||||||
hidden: !shipment.delivery_date
|
hidden: !shipment.delivery_date
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'link',
|
|
||||||
external: true,
|
|
||||||
name: 'link',
|
|
||||||
label: t`Link`,
|
|
||||||
copy: true,
|
|
||||||
hidden: !shipment.link
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -192,6 +219,8 @@ export default function SalesOrderShipmentDetail() {
|
|||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
</Grid>
|
</Grid>
|
||||||
<DetailsTable fields={tr} item={data} />
|
<DetailsTable fields={tr} item={data} />
|
||||||
|
<DetailsTable fields={bl} item={data} />
|
||||||
|
<DetailsTable fields={br} item={data} />
|
||||||
</ItemDetailsGrid>
|
</ItemDetailsGrid>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -232,7 +261,8 @@ export default function SalesOrderShipmentDetail() {
|
|||||||
}, [isPending, shipment, detailsPanel]);
|
}, [isPending, shipment, detailsPanel]);
|
||||||
|
|
||||||
const editShipmentFields = useSalesOrderShipmentFields({
|
const editShipmentFields = useSalesOrderShipmentFields({
|
||||||
pending: isPending
|
pending: isPending,
|
||||||
|
customerId: shipment.order_detail?.customer
|
||||||
});
|
});
|
||||||
|
|
||||||
const editShipment = useEditApiFormModal({
|
const editShipment = useEditApiFormModal({
|
||||||
|
|||||||
@@ -33,8 +33,10 @@ import { DateColumn, LinkColumn } from '../ColumnRenderers';
|
|||||||
import { InvenTreeTable } from '../InvenTreeTable';
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
|
|
||||||
export default function SalesOrderShipmentTable({
|
export default function SalesOrderShipmentTable({
|
||||||
|
customerId,
|
||||||
orderId
|
orderId
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
|
customerId: number;
|
||||||
orderId: number;
|
orderId: number;
|
||||||
}>) {
|
}>) {
|
||||||
const user = useUserState();
|
const user = useUserState();
|
||||||
@@ -43,9 +45,13 @@ export default function SalesOrderShipmentTable({
|
|||||||
|
|
||||||
const [selectedShipment, setSelectedShipment] = useState<any>({});
|
const [selectedShipment, setSelectedShipment] = useState<any>({});
|
||||||
|
|
||||||
const newShipmentFields = useSalesOrderShipmentFields({});
|
const newShipmentFields = useSalesOrderShipmentFields({
|
||||||
|
customerId: customerId
|
||||||
|
});
|
||||||
|
|
||||||
const editShipmentFields = useSalesOrderShipmentFields({});
|
const editShipmentFields = useSalesOrderShipmentFields({
|
||||||
|
customerId: customerId
|
||||||
|
});
|
||||||
|
|
||||||
const completeShipmentFields = useSalesOrderShipmentCompleteFields({});
|
const completeShipmentFields = useSalesOrderShipmentCompleteFields({});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user