Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sales order improvements #8445

Merged
merged 29 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
717489d
Migration for SalesOrderAllocation
SchrodingersGat Nov 6, 2024
aebbfc3
Enhaced query efficiency
SchrodingersGat Nov 6, 2024
7cea6f5
Further API cleanup
SchrodingersGat Nov 6, 2024
9d264ad
Adjust serializer
SchrodingersGat Nov 6, 2024
afaa26c
PUI updates
SchrodingersGat Nov 6, 2024
3d6b844
Enable editing of allocation shipment
SchrodingersGat Nov 6, 2024
d5b1968
Improve shipment filtering
SchrodingersGat Nov 6, 2024
22b04f6
Add sub-table for salesorderlineitem
SchrodingersGat Nov 7, 2024
8232717
Add helper method to SalesOrder to return pending SalesOrderAllocations
SchrodingersGat Nov 7, 2024
db33126
Fix for CUI
SchrodingersGat Nov 7, 2024
e09ab1f
Update form for CUI
SchrodingersGat Nov 7, 2024
52bdfec
Prevent SalesOrder completion with incomplete allocations
SchrodingersGat Nov 7, 2024
7aa8742
Fixes for StockItem API
SchrodingersGat Nov 7, 2024
e598931
Frontend refactoring
SchrodingersGat Nov 7, 2024
623acbf
Code cleanup
SchrodingersGat Nov 7, 2024
c2cddd9
Annotate shipment information to SalesOrder API endpoint
SchrodingersGat Nov 7, 2024
97e0a45
Update frontend PUI
SchrodingersGat Nov 7, 2024
35597c9
Additional filtering for SalesOrderAllocation
SchrodingersGat Nov 7, 2024
11c6194
Bump API version
SchrodingersGat Nov 7, 2024
2d1c5bb
Hide panel based on user permissions
SchrodingersGat Nov 7, 2024
a62b505
Merge branch 'master' into sales-orders
SchrodingersGat Nov 7, 2024
e4ea932
js linting
SchrodingersGat Nov 7, 2024
a065125
Merge branch 'sales-orders' of github.com:SchrodingersGat/InvenTree i…
SchrodingersGat Nov 7, 2024
0b7af8d
Unit test fix
SchrodingersGat Nov 7, 2024
deccf12
Update playwright tests
SchrodingersGat Nov 7, 2024
cb03da6
Merge branch 'master' into sales-orders
SchrodingersGat Nov 8, 2024
138616d
Revert diff
SchrodingersGat Nov 8, 2024
18115d0
Disable playwright test (temporary)
SchrodingersGat Nov 8, 2024
ed4e593
View output from build table
SchrodingersGat Nov 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading