2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-01 11:10:54 +00:00

Negative annotations (#9909)

* Fix for "annotated_scheduled_to_build_quantity"

* Further annotation updates

* Add unit testing for annotation

* Tweak unit test

* Cast annotated expression

* Specific fields required to work

* Refactor other instances

* Update API docs
This commit is contained in:
Oliver
2025-07-01 08:57:27 +10:00
committed by GitHub
parent e4102f98cb
commit 83b524f808
8 changed files with 210 additions and 80 deletions

View File

@ -1,12 +1,15 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 358
INVENTREE_API_VERSION = 359
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v359 -> 2025-07-01 : https://github.com/inventree/InvenTree/pull/9909
- Fixes annotated types for various part fields
v358 -> 2025-06-25 : https://github.com/inventree/InvenTree/pull/9857
- Provide list of generated stock items against "StockItemSerialize" API endpoint
- Provide list of generated stock items against "StockList" API endpoint

View File

@ -15,7 +15,7 @@ from django.db.models import (
Value,
When,
)
from django.db.models.functions import Coalesce
from django.db.models.functions import Coalesce, Greatest
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
@ -1566,11 +1566,15 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
# Calculate 'available_stock' based on previously annotated fields
queryset = queryset.annotate(
available_stock=ExpressionWrapper(
available_stock=Greatest(
ExpressionWrapper(
F('total_stock')
- F('allocated_to_sales_orders')
- F('allocated_to_build_orders'),
output_field=models.DecimalField(),
),
0,
output_field=models.DecimalField(),
)
)
@ -1603,11 +1607,15 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
# Calculate 'available_substitute_stock' field
queryset = queryset.annotate(
available_substitute_stock=ExpressionWrapper(
available_substitute_stock=Greatest(
ExpressionWrapper(
F('substitute_stock')
- F('substitute_build_allocations')
- F('substitute_sales_allocations'),
output_field=models.DecimalField(),
),
0,
output_field=models.DecimalField(),
)
)
@ -1629,11 +1637,15 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
)
queryset = queryset.annotate(
available_variant_stock=ExpressionWrapper(
available_variant_stock=Greatest(
ExpressionWrapper(
F('variant_stock_total')
- F('variant_bo_allocations')
- F('variant_so_allocations'),
output_field=FloatField(),
),
0,
output_field=FloatField(),
)
)

View File

@ -3,7 +3,7 @@
from decimal import Decimal
from django.db.models import DecimalField, ExpressionWrapper, F, Q
from django.db.models.functions import Coalesce
from django.db.models.functions import Coalesce, Greatest
from sql_util.utils import SubquerySum
@ -24,11 +24,15 @@ def annotate_on_order_quantity():
return Coalesce(
SubquerySum(
Greatest(
ExpressionWrapper(
F('purchase_order_line_items__quantity')
- F('purchase_order_line_items__received'),
output_field=DecimalField(),
),
0,
output_field=DecimalField(),
),
filter=order_filter,
),
Decimal(0),

View File

@ -14,7 +14,7 @@ from django.db.models import (
Value,
When,
)
from django.db.models.functions import Coalesce
from django.db.models.functions import Coalesce, Greatest
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
@ -1205,11 +1205,15 @@ class SalesOrderLineItemSerializer(
)
queryset = queryset.annotate(
available_stock=ExpressionWrapper(
available_stock=Greatest(
ExpressionWrapper(
F('total_stock')
- F('allocated_to_sales_orders')
- F('allocated_to_build_orders'),
output_field=models.DecimalField(),
),
0,
output_field=models.DecimalField(),
)
)
@ -1232,11 +1236,15 @@ class SalesOrderLineItemSerializer(
)
queryset = queryset.annotate(
available_variant_stock=ExpressionWrapper(
available_variant_stock=Greatest(
ExpressionWrapper(
F('variant_stock_total')
- F('variant_bo_allocations')
- F('variant_so_allocations'),
output_field=models.DecimalField(),
),
0,
output_field=models.DecimalField(),
)
)

View File

@ -29,7 +29,7 @@ from django.db.models import (
Value,
When,
)
from django.db.models.functions import Coalesce
from django.db.models.functions import Cast, Coalesce, Greatest
from sql_util.utils import SubquerySum
@ -73,14 +73,20 @@ def annotate_scheduled_to_build_quantity(reference: str = ''):
return Coalesce(
SubquerySum(
Greatest(
ExpressionWrapper(
F(f'{reference}builds__quantity') - F(f'{reference}builds__completed'),
output_field=DecimalField(),
Cast(F(f'{reference}builds__quantity'), output_field=IntegerField())
- Cast(
F(f'{reference}builds__completed'), output_field=IntegerField()
),
output_field=IntegerField(),
),
0,
),
filter=building_filter,
),
Decimal(0),
output_field=DecimalField(),
0,
output_field=IntegerField(),
)
@ -100,7 +106,8 @@ def annotate_on_order_quantity(reference: str = ''):
order__status__in=PurchaseOrderStatusGroups.OPEN, quantity__gt=F('received')
)
return Coalesce(
return Greatest(
Coalesce(
SubquerySum(
ExpressionWrapper(
F(f'{reference}supplier_parts__purchase_order_line_items__quantity')
@ -111,7 +118,8 @@ def annotate_on_order_quantity(reference: str = ''):
),
Decimal(0),
output_field=DecimalField(),
) - Coalesce(
)
- Coalesce(
SubquerySum(
ExpressionWrapper(
F(f'{reference}supplier_parts__purchase_order_line_items__received')
@ -122,6 +130,9 @@ def annotate_on_order_quantity(reference: str = ''):
),
Decimal(0),
output_field=DecimalField(),
),
Decimal(0),
output_field=DecimalField(),
)

View File

@ -18,7 +18,7 @@ from django.core.exceptions import ValidationError
from django.core.validators import MinLengthValidator, MinValueValidator
from django.db import models, transaction
from django.db.models import ExpressionWrapper, F, Q, QuerySet, Sum, UniqueConstraint
from django.db.models.functions import Coalesce
from django.db.models.functions import Coalesce, Greatest
from django.db.models.signals import post_delete, post_save
from django.db.utils import IntegrityError
from django.dispatch import receiver
@ -1530,9 +1530,13 @@ class Part(
# Calculate the 'available stock' based on previous annotations
queryset = queryset.annotate(
available_stock=ExpressionWrapper(
available_stock=Greatest(
ExpressionWrapper(
F('total_stock') - F('so_allocations') - F('bo_allocations'),
output_field=models.DecimalField(),
),
0,
output_field=models.DecimalField(),
)
)
@ -1549,11 +1553,15 @@ class Part(
)
queryset = queryset.annotate(
substitute_stock=ExpressionWrapper(
substitute_stock=Greatest(
ExpressionWrapper(
F('sub_total_stock')
- F('sub_so_allocations')
- F('sub_bo_allocations'),
output_field=models.DecimalField(),
),
0,
output_field=models.DecimalField(),
)
)
@ -1573,11 +1581,15 @@ class Part(
)
queryset = queryset.annotate(
variant_stock=ExpressionWrapper(
variant_stock=Greatest(
ExpressionWrapper(
F('var_total_stock')
- F('var_bo_allocations')
- F('var_so_allocations'),
output_field=models.DecimalField(),
),
0,
output_field=models.DecimalField(),
)
)

View File

@ -9,7 +9,7 @@ from django.core.files.base import ContentFile
from django.core.validators import MinValueValidator
from django.db import IntegrityError, models, transaction
from django.db.models import ExpressionWrapper, F, FloatField, Q
from django.db.models.functions import Coalesce
from django.db.models.functions import Coalesce, Greatest
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
@ -843,11 +843,15 @@ class PartSerializer(
# Annotate with the total 'available stock' quantity
# This is the current stock, minus any allocations
queryset = queryset.annotate(
unallocated_stock=ExpressionWrapper(
unallocated_stock=Greatest(
ExpressionWrapper(
F('total_in_stock')
- F('allocated_to_sales_orders')
- F('allocated_to_build_orders'),
output_field=models.DecimalField(),
),
Decimal(0),
output_field=models.DecimalField(),
)
)
@ -1226,7 +1230,7 @@ class PartRequirementsSerializer(InvenTree.serializers.InvenTreeModelSerializer)
read_only=True, label=_('In Production'), source='quantity_in_production'
)
scheduled_to_build = serializers.FloatField(
scheduled_to_build = serializers.IntegerField(
read_only=True, label=_('Scheduled to Build'), source='quantity_being_built'
)
@ -1866,11 +1870,15 @@ class BomItemSerializer(
# Calculate 'available_stock' based on previously annotated fields
queryset = queryset.annotate(
available_stock=ExpressionWrapper(
available_stock=Greatest(
ExpressionWrapper(
F('total_stock')
- F('allocated_to_sales_orders')
- F('allocated_to_build_orders'),
output_field=models.DecimalField(),
),
Decimal(0),
output_field=models.DecimalField(),
)
)
@ -1896,11 +1904,15 @@ class BomItemSerializer(
# Calculate 'available_substitute_stock' field
queryset = queryset.annotate(
available_substitute_stock=ExpressionWrapper(
available_substitute_stock=Greatest(
ExpressionWrapper(
F('substitute_stock')
- F('substitute_build_allocations')
- F('substitute_sales_allocations'),
output_field=models.DecimalField(),
),
Decimal(0),
output_field=models.DecimalField(),
)
)
@ -1920,11 +1932,15 @@ class BomItemSerializer(
)
queryset = queryset.annotate(
available_variant_stock=ExpressionWrapper(
available_variant_stock=Greatest(
ExpressionWrapper(
F('variant_stock_total')
- F('variant_bo_allocations')
- F('variant_so_allocations'),
output_field=FloatField(),
),
0,
output_field=FloatField(),
)
)

View File

@ -1999,7 +1999,7 @@ class PartPricingDetailTests(InvenTreeAPITestCase):
class PartAPIAggregationTest(InvenTreeAPITestCase):
"""Tests to ensure that the various aggregation annotations are working correctly..."""
"""Tests to ensure that the various aggregation annotations are working correctly."""
fixtures = [
'category',
@ -2303,6 +2303,70 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
# The annotated quantity must also match the part.on_order quantity
self.assertEqual(on_order, p.on_order)
def test_building(self):
"""Test the 'building' quantity annotations."""
# Create a new "buildable" part
part = Part.objects.create(
name='Buildable Part',
description='A part which can be built',
category=PartCategory.objects.get(pk=1),
assembly=True,
)
url = reverse('api-part-detail', kwargs={'pk': part.pk})
# Initially, no quantity in production
data = self.get(url).data
self.assertEqual(data['building'], 0)
self.assertEqual(data['scheduled_to_build'], 0)
# Create some builds for this part
builds = []
for idx in range(3):
builds.append(
build.models.Build.objects.create(
part=part,
quantity=10 * (idx + 1),
title=f'Build {idx + 1}',
reference=f'BO-{idx + 999}',
)
)
data = self.get(url).data
# There should now be 60 "scheduled", but nothing currently "building"
self.assertEqual(data['building'], 0)
self.assertEqual(data['scheduled_to_build'], 60)
# Update the "completed" count for the first build
# Even though this build will be "negative" it should not affect the other builds
# The "scheduled_to_build" count should reduce by 10, not 9999
builds[0].completed = 9999
builds[0].save()
data = self.get(url).data
self.assertEqual(data['building'], 0)
self.assertEqual(data['scheduled_to_build'], 50)
# Create some "in production" items against the third build
for idx in range(10):
StockItem.objects.create(
part=part, build=builds[2], quantity=(1 + idx), is_building=True
)
# Let's also update the "completeed" count
builds[1].completed = 5
builds[1].save()
builds[2].completed = 13
builds[2].save()
data = self.get(url).data
self.assertEqual(data['building'], 55)
self.assertEqual(data['scheduled_to_build'], 32)
class BomItemTest(InvenTreeAPITestCase):
"""Unit tests for the BomItem API."""