fix: trigger pricing recalculation when SupplierPart pack_quantity changes (#12421)

* fix: trigger pricing recalculation when SupplierPart is saved or deleted

When a SupplierPart's pack_quantity is updated after price breaks have
already been created, the Part's pricing (and BOM cost rollups for any
assemblies using that part) was not recalculated. This is because there
was no post_save or post_delete signal handler for the SupplierPart model
to trigger schedule_pricing_update on the linked Part.

Added post_save and post_delete signal handlers for SupplierPart that
mirror the existing SupplierPriceBreak signal handlers. The pricing
cascade (via update_assemblies) ensures BOM line costs in parent
assemblies are also updated.

Fixes #12285

* style: fix ruff format issues

* style: apply ruff format with --preview flag (matching project config)

* fix: resolve PartPricing.DoesNotExist in pack_quantity test

The test captured self.part.pricing before any PartPricing row existed,
yielding an unsaved instance; the later refresh_from_db() then raised
DoesNotExist. Re-fetch self.part.pricing after the price break creates
the row, matching the pattern in test_supplier_part_pricing.

---------

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
Co-authored-by: Aman Jain <jainamn@amazon.com>
This commit is contained in:
amanjain57-gif
2026-08-21 08:44:09 +10:00
committed by GitHub
co-authored by Oliver Aman Jain
parent a48ba58ee9
commit be9faff766
2 changed files with 63 additions and 0 deletions
+30
View File
@@ -1039,6 +1039,36 @@ class SupplierPriceBreak(common.models.PriceBreak):
)
@receiver(post_save, sender=SupplierPart, dispatch_uid='post_save_supplier_part')
def after_save_supplier_part(sender, instance, created, **kwargs):
"""Callback function when a SupplierPart is created or updated.
Triggers a pricing update for the linked Part, so that changes to
pack_quantity are reflected in Part pricing and BOM cost rollups.
"""
if (
InvenTree.ready.canAppAccessDatabase(allow_test=settings.TESTING_PRICING)
and not InvenTree.ready.isImportingData()
and instance.part
):
instance.part.schedule_pricing_update(create=True)
@receiver(post_delete, sender=SupplierPart, dispatch_uid='post_delete_supplier_part')
def after_delete_supplier_part(sender, instance, **kwargs):
"""Callback function when a SupplierPart is deleted.
Triggers a pricing update for the linked Part, so that removal of a
supplier part is reflected in Part pricing and BOM cost rollups.
"""
if (
InvenTree.ready.canAppAccessDatabase(allow_test=settings.TESTING_PRICING)
and not InvenTree.ready.isImportingData()
and instance.part
):
instance.part.schedule_pricing_update(create=False)
@receiver(
post_save, sender=SupplierPriceBreak, dispatch_uid='post_save_supplier_price_break'
)
@@ -175,6 +175,39 @@ class PartPricingTests(InvenTreeTestCase):
self.assertIsNone(pricing.supplier_price_min)
self.assertIsNone(pricing.supplier_price_max)
@override_settings(TESTING_PRICING=True)
def test_supplier_part_pack_quantity_update(self):
"""Test that changing pack_quantity on a SupplierPart triggers pricing recalculation."""
supplier = company.models.Company.objects.create(
name='Pack Test Supplier', is_supplier=True
)
sp = company.models.SupplierPart.objects.create(
supplier=supplier, part=self.part, SKU='PACK_TEST', pack_quantity='1'
)
company.models.SupplierPriceBreak.objects.create(
part=sp, quantity=1, price=50, price_currency='USD'
)
# Re-fetch pricing (the price break creation triggers the PartPricing row)
pricing = self.part.pricing
pricing.refresh_from_db()
# Price per unit should be $50 / 1 = $50
self.assertEqual(pricing.supplier_price_min, Money(50, 'USD'))
# Now update pack_quantity to 100 (i.e. 100 units per pack)
sp.pack_quantity = '100'
sp.save()
pricing = self.part.pricing
pricing.refresh_from_db()
# Price per unit should now be $50 / 100 = $0.50
self.assertEqual(pricing.supplier_price_min, Money('0.5', 'USD'))
self.assertEqual(pricing.supplier_price_max, Money('0.5', 'USD'))
@override_settings(TESTING_PRICING=True)
def test_internal_pricing(self):
"""Tests for internal price breaks."""