Skip to content

Commit

Permalink
Sales order improvements (#8445)
Browse files Browse the repository at this point in the history
* Migration for SalesOrderAllocation

- Allow allocation against order with null shipment

* Enhaced query efficiency

* Further API cleanup

* Adjust serializer

* PUI updates

* Enable editing of allocation shipment

* Improve shipment filtering

* Add sub-table for salesorderlineitem

* Add helper method to SalesOrder to return pending SalesOrderAllocations

* Fix for CUI

* Update form for CUI

* Prevent SalesOrder completion with incomplete allocations

* Fixes for StockItem API

* Frontend refactoring

* Code cleanup

* Annotate shipment information to SalesOrder API endpoint

* Update frontend PUI

* Additional filtering for SalesOrderAllocation

* Bump API version

* Hide panel based on user permissions

* js linting

* Unit test fix

* Update playwright tests

* Revert diff

* Disable playwright test (temporary)

* View output from build table
  • Loading branch information
SchrodingersGat authored Nov 8, 2024
1 parent 656950a commit 2c294d6
Show file tree
Hide file tree
Showing 22 changed files with 475 additions and 242 deletions.
8 changes: 7 additions & 1 deletion src/backend/InvenTree/InvenTree/api_version.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
"""InvenTree API version information."""

# InvenTree API version
INVENTREE_API_VERSION = 277
INVENTREE_API_VERSION = 278

"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""


INVENTREE_API_TEXT = """
v278 - 2024-11-07 : https://github.com/inventree/InvenTree/pull/8445
- Updates to the SalesOrder API endpoints
- Add "shipment count" information to the SalesOrder API endpoints
- Allow null value for SalesOrderAllocation.shipment field
- Additional filtering options for allocation endpoints
v277 - 2024-11-01 : https://github.com/inventree/InvenTree/pull/8278
- Allow build order list to be filtered by "outstanding" (alias for "active")
Expand Down
31 changes: 27 additions & 4 deletions src/backend/InvenTree/order/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -978,7 +978,7 @@ class Meta:
"""Metaclass options."""

model = models.SalesOrderAllocation
fields = ['shipment', 'item']
fields = ['shipment', 'line', 'item']

order = rest_filters.ModelChoiceFilter(
queryset=models.SalesOrder.objects.all(),
Expand Down Expand Up @@ -1034,6 +1034,16 @@ def filter_outstanding(self, queryset, name, value):
line__order__status__in=SalesOrderStatusGroups.OPEN,
)

assigned_to_shipment = rest_filters.BooleanFilter(
label=_('Has Shipment'), method='filter_assigned_to_shipment'
)

def filter_assigned_to_shipment(self, queryset, name, value):
"""Filter by whether or not the allocation has been assigned to a shipment."""
if str2bool(value):
return queryset.exclude(shipment=None)
return queryset.filter(shipment=None)


class SalesOrderAllocationMixin:
"""Mixin class for SalesOrderAllocation endpoints."""
Expand All @@ -1049,12 +1059,16 @@ def get_queryset(self, *args, **kwargs):
'item',
'item__sales_order',
'item__part',
'line__part',
'item__location',
'line__order',
'line__part',
'line__order__responsible',
'line__order__project_code',
'line__order__project_code__responsible',
'shipment',
'shipment__order',
)
'shipment__checked_by',
).select_related('line__part__pricing_data', 'item__part__pricing_data')

return queryset

Expand All @@ -1065,14 +1079,23 @@ class SalesOrderAllocationList(SalesOrderAllocationMixin, ListAPI):
filterset_class = SalesOrderAllocationFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS

ordering_fields = ['quantity', 'part', 'serial', 'batch', 'location', 'order']
ordering_fields = [
'quantity',
'part',
'serial',
'batch',
'location',
'order',
'shipment_date',
]

ordering_field_aliases = {
'part': 'item__part__name',
'serial': ['item__serial_int', 'item__serial'],
'batch': 'item__batch',
'location': 'item__location__name',
'order': 'line__order__reference',
'shipment_date': 'shipment__shipment_date',
}

search_fields = {'item__part__name', 'item__serial', 'item__batch'}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 4.2.16 on 2024-11-06 04:46

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('order', '0102_purchaseorder_destination_and_more'),
]

operations = [
migrations.AlterField(
model_name='salesorderallocation',
name='shipment',
field=models.ForeignKey(blank=True, help_text='Sales order shipment reference', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='allocations', to='order.salesordershipment', verbose_name='Shipment'),
),
]
31 changes: 30 additions & 1 deletion src/backend/InvenTree/order/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1105,6 +1105,11 @@ def can_complete(self, raise_error=False, allow_incomplete_lines=False):
_('Order cannot be completed as there are incomplete shipments')
)

if self.pending_allocation_count > 0:
raise ValidationError(
_('Order cannot be completed as there are incomplete allocations')
)

if not allow_incomplete_lines and self.pending_line_count > 0:
raise ValidationError(
_('Order cannot be completed as there are incomplete line items')
Expand Down Expand Up @@ -1297,6 +1302,23 @@ def pending_shipments(self):
"""Return a queryset of the pending shipments for this order."""
return self.shipments.filter(shipment_date=None)

def allocations(self):
"""Return a queryset of all allocations for this order."""
return SalesOrderAllocation.objects.filter(line__order=self)

def pending_allocations(self):
"""Return a queryset of any pending allocations for this order.
Allocations are pending if:
a) They are not associated with a SalesOrderShipment
b) The linked SalesOrderShipment has not been shipped
"""
Q1 = Q(shipment=None)
Q2 = Q(shipment__shipment_date=None)

return self.allocations().filter(Q1 | Q2).distinct()

@property
def shipment_count(self):
"""Return the total number of shipments associated with this order."""
Expand All @@ -1312,6 +1334,11 @@ def pending_shipment_count(self):
"""Return the number of pending shipments associated with this order."""
return self.pending_shipments().count()

@property
def pending_allocation_count(self):
"""Return the number of pending (non-shipped) allocations."""
return self.pending_allocations().count()


@receiver(post_save, sender=SalesOrder, dispatch_uid='sales_order_post_save')
def after_save_sales_order(sender, instance: SalesOrder, created: bool, **kwargs):
Expand Down Expand Up @@ -2030,7 +2057,7 @@ def clean(self):
if self.item.serial and self.quantity != 1:
errors['quantity'] = _('Quantity must be 1 for serialized stock item')

if self.line.order != self.shipment.order:
if self.shipment and self.line.order != self.shipment.order:
errors['line'] = _('Sales order does not match shipment')
errors['shipment'] = _('Shipment does not match sales order')

Expand All @@ -2047,6 +2074,8 @@ def clean(self):
shipment = models.ForeignKey(
SalesOrderShipment,
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='allocations',
verbose_name=_('Shipment'),
help_text=_('Sales order shipment reference'),
Expand Down
61 changes: 45 additions & 16 deletions src/backend/InvenTree/order/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -990,6 +990,8 @@ class Meta:
'shipment_date',
'total_price',
'order_currency',
'shipments_count',
'completed_shipments_count',
])

read_only_fields = ['status', 'creation_date', 'shipment_date']
Expand Down Expand Up @@ -1035,12 +1037,26 @@ def annotate_queryset(queryset):
)
)

# Annotate shipment details
queryset = queryset.annotate(
shipments_count=SubqueryCount('shipments'),
completed_shipments_count=SubqueryCount(
'shipments', filter=Q(shipment_date__isnull=False)
),
)

return queryset

customer_detail = CompanyBriefSerializer(
source='customer', many=False, read_only=True
)

shipments_count = serializers.IntegerField(read_only=True, label=_('Shipments'))

completed_shipments_count = serializers.IntegerField(
read_only=True, label=_('Completed Shipments')
)


class SalesOrderIssueSerializer(OrderAdjustSerializer):
"""Serializer for issuing a SalesOrder."""
Expand Down Expand Up @@ -1246,6 +1262,15 @@ class Meta:
'notes',
]

def __init__(self, *args, **kwargs):
"""Initialization routine for the serializer."""
order_detail = kwargs.pop('order_detail', True)

super().__init__(*args, **kwargs)

if not order_detail:
self.fields.pop('order_detail', None)

@staticmethod
def annotate_queryset(queryset):
"""Annotate the queryset with extra information."""
Expand Down Expand Up @@ -1276,22 +1301,26 @@ class Meta:

fields = [
'pk',
'item',
'quantity',
'shipment',
# Annotated read-only fields
'line',
'customer_detail',
'part',
'order',
'serial',
'quantity',
'location',
'location_detail',
'item',
# Extra detail fields
'item_detail',
'order',
'order_detail',
'part',
'part_detail',
'shipment',
'order_detail',
'customer_detail',
'location_detail',
'shipment_detail',
]

read_only_fields = ['line', '']

def __init__(self, *args, **kwargs):
"""Initialization routine for the serializer."""
order_detail = kwargs.pop('order_detail', False)
Expand Down Expand Up @@ -1341,7 +1370,7 @@ def __init__(self, *args, **kwargs):
)

shipment_detail = SalesOrderShipmentSerializer(
source='shipment', many=False, read_only=True
source='shipment', order_detail=False, many=False, read_only=True
)


Expand Down Expand Up @@ -1596,8 +1625,8 @@ def validate_line_item(self, line_item):
shipment = serializers.PrimaryKeyRelatedField(
queryset=order.models.SalesOrderShipment.objects.all(),
many=False,
allow_null=False,
required=True,
required=False,
allow_null=True,
label=_('Shipment'),
)

Expand All @@ -1609,10 +1638,10 @@ def validate_shipment(self, shipment):
"""
order = self.context['order']

if shipment.shipment_date is not None:
if shipment and shipment.shipment_date is not None:
raise ValidationError(_('Shipment has already been shipped'))

if shipment.order != order:
if shipment and shipment.order != order:
raise ValidationError(_('Shipment is not associated with this order'))

return shipment
Expand Down Expand Up @@ -1720,8 +1749,8 @@ class Meta:
shipment = serializers.PrimaryKeyRelatedField(
queryset=order.models.SalesOrderShipment.objects.all(),
many=False,
allow_null=False,
required=True,
required=False,
allow_null=True,
label=_('Shipment'),
)

Expand Down Expand Up @@ -1756,7 +1785,7 @@ def save(self):
data = self.validated_data

items = data['items']
shipment = data['shipment']
shipment = data.get('shipment')

with transaction.atomic():
for entry in items:
Expand Down
1 change: 0 additions & 1 deletion src/backend/InvenTree/order/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1877,7 +1877,6 @@ def test_invalid(self):
response = self.post(self.url, {}, expected_code=400)

self.assertIn('This field is required', str(response.data['items']))
self.assertIn('This field is required', str(response.data['shipment']))

# Test with a single line items
line = self.order.lines.first()
Expand Down
Loading

0 comments on commit 2c294d6

Please sign in to comment.