diff --git a/src/backend/InvenTree/part/migrations/0148_partrequirements.py b/src/backend/InvenTree/part/migrations/0148_partrequirements.py new file mode 100644 index 000000000000..93b115de60e5 --- /dev/null +++ b/src/backend/InvenTree/part/migrations/0148_partrequirements.py @@ -0,0 +1,119 @@ +# Generated by Django 5.2.13 on 2026-04-11 06:56 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("part", "0147_remove_part_default_supplier"), + ] + + operations = [ + migrations.CreateModel( + name="PartRequirements", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "updated", + models.DateTimeField( + auto_now=True, + help_text="Timestamp of last update", + null=True, + verbose_name="Updated", + ), + ), + ( + "in_stock", + models.DecimalField( + decimal_places=6, + default=0, + help_text="Total quantity of items in stock for this part", + max_digits=19, + verbose_name="In Stock", + ), + ), + ( + "on_order", + models.DecimalField( + decimal_places=6, + default=0, + help_text="Total quantity of items on order for this part", + max_digits=19, + verbose_name="On Order", + ), + ), + ( + "building", + models.DecimalField( + decimal_places=6, + default=0, + help_text="Total quantity of items currently being built for this part", + max_digits=19, + verbose_name="Building", + ), + ), + ( + "build_order_requirements", + models.DecimalField( + decimal_places=6, + default=0, + help_text="Total quantity of this part required for all open build orders", + max_digits=19, + verbose_name="Build Order Requirements", + ), + ), + ( + "sales_order_requirements", + models.DecimalField( + decimal_places=6, + default=0, + help_text="Total quantity of this part required for all open sales orders", + max_digits=19, + verbose_name="Sales Order Requirements", + ), + ), + ( + "build_order_allocations", + models.DecimalField( + decimal_places=6, + default=0, + help_text="Total quantity of this part allocated to open build orders", + max_digits=19, + verbose_name="Build Order Allocations", + ), + ), + ( + "sales_order_allocations", + models.DecimalField( + decimal_places=6, + default=0, + help_text="Total quantity of this part allocated to open sales orders", + max_digits=19, + verbose_name="Sales Order Allocations", + ), + ), + ( + "part", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="requirements_data", + to="part.part", + verbose_name="part", + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/src/backend/InvenTree/part/models.py b/src/backend/InvenTree/part/models.py index 152df37b45fb..e43de2116840 100644 --- a/src/backend/InvenTree/part/models.py +++ b/src/backend/InvenTree/part/models.py @@ -2703,6 +2703,94 @@ def after_save_part(sender, instance: Part, created, **kwargs): ) +class PartRequirements(common.models.MetaMixin): + """Model for caching the various "requirements" associated with a Part. + + This information is cached because it is expensive to calculate "on the fly", + and does not change very often (compared to the number of times it is accessed). + + The following attributes are cached against each part: + + - in_stock: The total quantity of items in stock for this part + - on_order: The total quantity of items on order for this part + - building: The total quantity of items currently being built for this part + - build_order_requirements: The total quantity of items required for all open build orders + - sales_order_requirements: The total quantity of items required for all open sales orders + - build_order_allocations: The total quantity of items allocated to open build orders + - sales_order_allocations: The total quantity of items allocated to open sales orders + + Note that each of these fields includes the quantity for any active variants of this part, if they exist. + """ + + part = models.OneToOneField( + Part, + on_delete=models.CASCADE, + related_name='requirements_data', + verbose_name=_('part'), + ) + + in_stock = models.DecimalField( + max_digits=19, + decimal_places=6, + default=0, + verbose_name=_('In Stock'), + help_text=_('Total quantity of items in stock for this part'), + ) + + on_order = models.DecimalField( + max_digits=19, + decimal_places=6, + default=0, + verbose_name=_('On Order'), + help_text=_('Total quantity of items on order for this part'), + ) + + building = models.DecimalField( + max_digits=19, + decimal_places=6, + default=0, + verbose_name=_('Building'), + help_text=_('Total quantity of items currently being built for this part'), + ) + + build_order_requirements = models.DecimalField( + max_digits=19, + decimal_places=6, + default=0, + verbose_name=_('Build Order Requirements'), + help_text=_('Total quantity of this part required for all open build orders'), + ) + + sales_order_requirements = models.DecimalField( + max_digits=19, + decimal_places=6, + default=0, + verbose_name=_('Sales Order Requirements'), + help_text=_('Total quantity of this part required for all open sales orders'), + ) + + build_order_allocations = models.DecimalField( + max_digits=19, + decimal_places=6, + default=0, + verbose_name=_('Build Order Allocations'), + help_text=_('Total quantity of this part allocated to open build orders'), + ) + + sales_order_allocations = models.DecimalField( + max_digits=19, + decimal_places=6, + default=0, + verbose_name=_('Sales Order Allocations'), + help_text=_('Total quantity of this part allocated to open sales orders'), + ) + + @property + def is_valid(self): + """Return True if the cached requirements data is valid.""" + return self.updated is not None + + class PartPricing(common.models.MetaMixin): """Model for caching min/max pricing information for a particular Part.