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

[API] Sales order filters #8331

Merged
merged 6 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion src/backend/InvenTree/InvenTree/api_version.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
"""InvenTree API version information."""

# InvenTree API version
INVENTREE_API_VERSION = 270
INVENTREE_API_VERSION = 271

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


INVENTREE_API_TEXT = """

v271 - 2024-10-22 : https://github.com/inventree/InvenTree/pull/8331
- Fixes for SalesOrderLineItem endpoints

v270 - 2024-10-19 : https://github.com/inventree/InvenTree/pull/8307
- Adds missing date fields from order API endpoint(s)

Expand Down
23 changes: 21 additions & 2 deletions src/backend/InvenTree/order/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -771,12 +771,29 @@ class Meta:
queryset=Part.objects.all(), field_name='part', label=_('Part')
)

completed = rest_filters.BooleanFilter(label='completed', method='filter_completed')
allocated = rest_filters.BooleanFilter(
label=_('Allocated'), method='filter_allocated'
)

def filter_allocated(self, queryset, name, value):
"""Filter by lines which are 'allocated'.

A line is 'allocated' when allocated >= quantity
"""
q = Q(allocated__gte=F('quantity'))

if str2bool(value):
return queryset.filter(q)
return queryset.exclude(q)

completed = rest_filters.BooleanFilter(
label=_('Completed'), method='filter_completed'
)

def filter_completed(self, queryset, name, value):
"""Filter by lines which are "completed".

A line is completed when shipped >= quantity
A line is 'completed' when shipped >= quantity
"""
q = Q(shipped__gte=F('quantity'))

Expand Down Expand Up @@ -855,6 +872,8 @@ class SalesOrderLineItemList(
'part',
'part__name',
'quantity',
'allocated',
'shipped',
'reference',
'sale_price',
'target_date',
Expand Down
14 changes: 12 additions & 2 deletions src/backend/InvenTree/order/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@
Value,
When,
)
from django.db.models.functions import Coalesce
from django.utils.translation import gettext_lazy as _

from rest_framework import serializers
from rest_framework.serializers import ValidationError
from sql_util.utils import SubqueryCount
from sql_util.utils import SubqueryCount, SubquerySum

import order.models
import part.filters as part_filters
Expand Down Expand Up @@ -1165,6 +1166,15 @@ def annotate_queryset(queryset):
building=part_filters.annotate_in_production_quantity(reference='part__')
)

# Annotate total 'allocated' stock quantity
queryset = queryset.annotate(
allocated=Coalesce(
SubquerySum('allocations__quantity'),
Decimal(0),
output_field=models.DecimalField(),
)
)

return queryset

order_detail = SalesOrderSerializer(source='order', many=False, read_only=True)
Expand All @@ -1182,7 +1192,7 @@ def annotate_queryset(queryset):

quantity = InvenTreeDecimalField()

allocated = serializers.FloatField(source='allocated_quantity', read_only=True)
allocated = serializers.FloatField(read_only=True)

shipped = InvenTreeDecimalField(read_only=True)

Expand Down
88 changes: 87 additions & 1 deletion src/backend/InvenTree/order/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1683,10 +1683,96 @@ def test_so_line_list(self):
self.filter({'has_pricing': 1}, 0)
self.filter({'has_pricing': 0}, n)

# Filter by has_pricing status
# Filter by 'completed' status
self.filter({'completed': 1}, 0)
self.filter({'completed': 0}, n)

# Filter by 'allocated' status
self.filter({'allocated': 'true'}, 0)
self.filter({'allocated': 'false'}, n)

def test_so_line_allocated_filters(self):
"""Test filtering by allocation status for a SalesOrderLineItem."""
self.assignRole('sales_order.add')

# Crete a new SalesOrder via the API
response = self.post(
reverse('api-so-list'),
{
'customer': Company.objects.filter(is_customer=True).first().pk,
'reference': 'SO-12345',
'description': 'Test Sales Order',
},
)

order_id = response.data['pk']
order = models.SalesOrder.objects.get(pk=order_id)

so_line_url = reverse('api-so-line-list')

# Initially, there should be no line items against this order
response = self.get(so_line_url, {'order': order_id})

self.assertEqual(len(response.data), 0)

parts = [25, 50, 100]

# Let's create some new line items
for part_id in parts:
self.post(so_line_url, {'order': order_id, 'part': part_id, 'quantity': 10})

# Should be three items now
response = self.get(so_line_url, {'order': order_id})

self.assertEqual(len(response.data), 3)

for item in response.data:
# Check that the line item has been created
self.assertEqual(item['order'], order_id)

# Check that the line quantities are correct
self.assertEqual(item['quantity'], 10)
self.assertEqual(item['allocated'], 0)
self.assertEqual(item['shipped'], 0)

# Initial API filters should return no results
self.filter({'order': order_id, 'allocated': 1}, 0)
self.filter({'order': order_id, 'completed': 1}, 0)

# Create a new shipment against this SalesOrder
shipment = models.SalesOrderShipment.objects.create(
order=order, reference='SHIP-12345'
)

# Next, allocate stock against 2 line items
for item in parts[:2]:
p = Part.objects.get(pk=item)
s = StockItem.objects.create(part=p, quantity=100)
l = models.SalesOrderLineItem.objects.filter(order=order, part=p).first()

# Allocate against the API
self.post(
reverse('api-so-allocate', kwargs={'pk': order.pk}),
{
'items': [{'line_item': l.pk, 'stock_item': s.pk, 'quantity': 10}],
'shipment': shipment.pk,
},
)

# Filter by 'fully allocated' status
self.filter({'order': order_id, 'allocated': 1}, 2)
self.filter({'order': order_id, 'allocated': 0}, 1)

self.filter({'order': order_id, 'completed': 1}, 0)
self.filter({'order': order_id, 'completed': 0}, 3)

# Finally, mark this shipment as 'shipped'
self.post(reverse('api-so-shipment-ship', kwargs={'pk': shipment.pk}), {})

# Filter by 'completed' status
self.filter({'order': order_id, 'completed': 1}, 2)
self.filter({'order': order_id, 'completed': 0}, 1)


class SalesOrderDownloadTest(OrderTest):
"""Unit tests for downloading SalesOrder data via the API endpoint."""
Expand Down
19 changes: 19 additions & 0 deletions src/frontend/src/tables/sales/SalesOrderLineItemTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
import { TableColumn } from '../Column';
import { DateColumn, LinkColumn, PartColumn } from '../ColumnRenderers';
import { TableFilter } from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
import {
RowAction,
Expand Down Expand Up @@ -161,6 +162,7 @@ export default function SalesOrderLineItemTable({
},
{
accessor: 'allocated',
sortable: true,
render: (record: any) => (
<ProgressBar
progressLabel={true}
Expand All @@ -171,6 +173,7 @@ export default function SalesOrderLineItemTable({
},
{
accessor: 'shipped',
sortable: true,
render: (record: any) => (
<ProgressBar
progressLabel={true}
Expand Down Expand Up @@ -266,6 +269,21 @@ export default function SalesOrderLineItemTable({
}
});

const tableFilters: TableFilter[] = useMemo(() => {
return [
{
name: 'allocated',
label: t`Allocated`,
description: t`Show lines which are fully allocated`
},
{
name: 'completed',
label: t`Completed`,
description: t`Show lines which are completed`
}
];
}, []);

const tableActions = useMemo(() => {
return [
<AddItemButton
Expand Down Expand Up @@ -404,6 +422,7 @@ export default function SalesOrderLineItemTable({
},
rowActions: rowActions,
tableActions: tableActions,
tableFilters: tableFilters,
modelType: ModelType.part,
modelField: 'part'
}}
Expand Down
3 changes: 3 additions & 0 deletions src/frontend/tests/pages/pui_stock.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { test } from '../baseFixtures.js';

Check failure on line 1 in src/frontend/tests/pages/pui_stock.spec.ts

View workflow job for this annotation

GitHub Actions / Tests - Platform UI

[chromium] › pages/pui_stock.spec.ts:52:1 › Stock - Serial Numbers

1) [chromium] › pages/pui_stock.spec.ts:52:1 › Stock - Serial Numbers ──────────────────────────── Test timeout of 90000ms exceeded.

Check failure on line 1 in src/frontend/tests/pages/pui_stock.spec.ts

View workflow job for this annotation

GitHub Actions / Tests - Platform UI

[firefox] › pages/pui_stock.spec.ts:52:1 › Stock - Serial Numbers

2) [firefox] › pages/pui_stock.spec.ts:52:1 › Stock - Serial Numbers ───────────────────────────── Test timeout of 90000ms exceeded.
import { baseUrl } from '../defaults.js';
import { doQuickLogin } from '../login.js';

Expand Down Expand Up @@ -77,13 +77,16 @@
await page.getByLabel('action-button-add-stock-item').click();

// Initially fill with invalid serial/quantity combinations
await page.getByLabel('text-field-serial_numbers').fill('200-250');

Check failure on line 80 in src/frontend/tests/pages/pui_stock.spec.ts

View workflow job for this annotation

GitHub Actions / Tests - Platform UI

[chromium] › pages/pui_stock.spec.ts:52:1 › Stock - Serial Numbers

1) [chromium] › pages/pui_stock.spec.ts:52:1 › Stock - Serial Numbers ──────────────────────────── Error: locator.fill: Test timeout of 90000ms exceeded. Call log: - waiting for getByLabel('text-field-serial_numbers') 78 | 79 | // Initially fill with invalid serial/quantity combinations > 80 | await page.getByLabel('text-field-serial_numbers').fill('200-250'); | ^ 81 | await page.getByLabel('number-field-quantity').fill('10'); 82 | 83 | // Add delay to account to field debounce at /home/runner/work/InvenTree/InvenTree/src/frontend/tests/pages/pui_stock.spec.ts:80:54
await page.getByLabel('number-field-quantity').fill('10');

// Add delay to account to field debounce
await page.waitForTimeout(250);

await page.getByRole('button', { name: 'Submit' }).click();

// Expected error messages
await page.getByText('Errors exist for one or more form fields').waitFor();

Check failure on line 89 in src/frontend/tests/pages/pui_stock.spec.ts

View workflow job for this annotation

GitHub Actions / Tests - Platform UI

[firefox] › pages/pui_stock.spec.ts:52:1 › Stock - Serial Numbers

2) [firefox] › pages/pui_stock.spec.ts:52:1 › Stock - Serial Numbers ───────────────────────────── Error: locator.waitFor: Test timeout of 90000ms exceeded. Call log: - waiting for getByText('Errors exist for one or more form fields') to be visible 87 | 88 | // Expected error messages > 89 | await page.getByText('Errors exist for one or more form fields').waitFor(); | ^ 90 | await page 91 | .getByText(/exceeds allowed quantity/) 92 | .first() at /home/runner/work/InvenTree/InvenTree/src/frontend/tests/pages/pui_stock.spec.ts:89:68
await page
.getByText(/exceeds allowed quantity/)
.first()
Expand Down
Loading