From 82dfe561eefd057459b22f01ca1f84505eb798b6 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 5 Jul 2025 11:16:42 +1000 Subject: [PATCH] [feature] Disable pricing task (#9730) * Add setting to control pricing auto-update * Auto pricing updates depend on global setting * Tweak menu layout * Fix typo * Skip pricing task * Tweak serializer * Updated docs * Update logic around automatic pricing updates * Logic fix * Remove daily holdoff --- docs/docs/part/pricing.md | 30 +++++++++++++++---- docs/docs/settings/global.md | 1 + .../InvenTree/common/setting/system.py | 10 ++++++- src/backend/InvenTree/part/models.py | 14 +++++++-- src/backend/InvenTree/part/serializers.py | 9 ++++-- src/backend/InvenTree/part/tasks.py | 9 ++++-- .../AdminCenter/CurrencyManagementPanel.tsx | 6 +++- .../pages/Index/Settings/SystemSettings.tsx | 1 + 8 files changed, 65 insertions(+), 15 deletions(-) diff --git a/docs/docs/part/pricing.md b/docs/docs/part/pricing.md index fb6e9b3723..1f19c82958 100644 --- a/docs/docs/part/pricing.md +++ b/docs/docs/part/pricing.md @@ -16,7 +16,7 @@ Pricing information can be determined from multiple sources: | Purchase Cost | Historical cost information for parts purchased | [Purchase Order](../purchasing/purchase_order.md) | | BOM Price | Total price for an assembly (total price of all component items) | [Part](../part/index.md) | -#### Override Pricing +### Override Pricing In addition to caching pricing data as documented in the above table, manual pricing overrides can be specified for a particular part. Both the *minimum price* and *maximum price* can be specified manually, independent of the calculated values. If an manual price is specified for a part, it overrides any calculated value. @@ -120,10 +120,28 @@ For this reason, all information displayed in the [pricing overview](#pricing-ov Pricing data is cached in the [default currency](../concepts/pricing.md/#default-currency), which ensures that pricing can be compared across multiple parts in a consistent format. -#### Data Updates +## Pricing Updates -The pricing data caching is intended to occur *automatically*, and generally be up-to-date without user interaction. Pricing data is re-calculated and cached by the [background worker](../settings/tasks.md) in the following ways: +If automated pricing updates are enabled, the pricing data updated *automatically*, and will generally be up-to-date without user interaction. Pricing data is re-calculated and cached by the [background worker](../settings/tasks.md) in the following ways: -- **Automatically** - If the underlying pricing data changes, part pricing is scheduled to be updated -- **Periodically** - A daily task ensures that any outdated or missing pricing is kept updated -- **Manually** - The user can manually recalculate pricing for a given part in the [pricing overview](#pricing-overview) display +### Response to Data Changes + +When underlying pricing data changes, the cached pricing data is automatically updated. This includes (but is not limited to): + +- Stock items being received against a [Purchase Order](../purchasing/purchase_order.md) with provided pricing +- Stock items being sold against a [Sales Order](../sales/sales_order.md) with provided pricing +- [Bills of Material](../manufacturing//bom.md) being created or modified, which may change the pricing of an assembly + +### Periodic Updates + +A periodic task runs in the background to ensure that any outdated or missing pricing data is kept up-to-date. This task runs at a scheduled regular interval, as controlled via the {{ globalsetting("PRICING_UPDATE_DAYS", short=True) }} setting. The default value is 30 days, meaning that pricing data is updated at least once every 30 days. Setting this value to zero disables periodic updates. + +### Manual Updates + +Additionally, the user can manually recalculate pricing for a given part. This can be done from the [pricing overview](#pricing-overview) display, by pressing the "Recalculate" button. + +### Disable Automatic Updates + +Automatic pricing updates are enabled by default. If desired, this functionality can be disabled (via the {{ globalsetting("PRICING_AUTO_UPDATE", short=True) }} setting). If this is done, then pricing data will not be automatically updated, and the user must manually recalculate pricing data as required. + +Disabling automatic pricing updates may be prudent in systems where pricing data changes frequently, but the user wants to control when the pricing data is updated. In this case, the user can manually recalculate pricing data as required. diff --git a/docs/docs/settings/global.md b/docs/docs/settings/global.md index ce9c28133e..668d989332 100644 --- a/docs/docs/settings/global.md +++ b/docs/docs/settings/global.md @@ -108,6 +108,7 @@ Configuration of pricing data and currency support: {{ globalsetting("CURRENCY_UPDATE_INTERVAL") }} {{ globalsetting("PRICING_DECIMAL_PLACES_MIN") }} {{ globalsetting("PRICING_DECIMAL_PLACES") }} +{{ globalsetting("PRICING_AUTO_UPDATE") }} {{ globalsetting("PRICING_UPDATE_DAYS") }} #### Part Pricing diff --git a/src/backend/InvenTree/common/setting/system.py b/src/backend/InvenTree/common/setting/system.py index fa297c96e7..9a933fb117 100644 --- a/src/backend/InvenTree/common/setting/system.py +++ b/src/backend/InvenTree/common/setting/system.py @@ -588,12 +588,20 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = { 'default': False, 'validator': bool, }, + 'PRICING_AUTO_UPDATE': { + 'name': _('Auto Update Pricing'), + 'description': _( + 'Automatically update part pricing when internal data changes' + ), + 'default': True, + 'validator': bool, + }, 'PRICING_UPDATE_DAYS': { 'name': _('Pricing Rebuild Interval'), 'description': _('Number of days before part pricing is automatically updated'), 'units': _('days'), 'default': 30, - 'validator': [int, MinValueValidator(10)], + 'validator': [int, MinValueValidator(0)], }, 'PART_INTERNAL_PRICE': { 'name': _('Internal Prices'), diff --git a/src/backend/InvenTree/part/models.py b/src/backend/InvenTree/part/models.py index 8b7e0b9988..9b3501167c 100644 --- a/src/backend/InvenTree/part/models.py +++ b/src/backend/InvenTree/part/models.py @@ -2033,7 +2033,7 @@ class Part( return pricing - def schedule_pricing_update(self, create: bool = False): + def schedule_pricing_update(self, create: bool = False, force: bool = False): """Helper function to schedule a pricing update. Importantly, catches any errors which may occur during deletion of related objects, @@ -2043,7 +2043,13 @@ class Part( Arguments: create: Whether or not a new PartPricing object should be created if it does not already exist + force: If True, force the pricing to be updated even auto pricing is disabled """ + if not force and not get_global_setting( + 'PRICING_AUTO_UPDATE', backup_value=True + ): + return + try: self.refresh_from_db() except Part.DoesNotExist: @@ -2705,7 +2711,11 @@ class PartPricing(common.models.MetaMixin): return result def schedule_for_update(self, counter: int = 0): - """Schedule this pricing to be updated.""" + """Schedule this pricing to be updated. + + Arguments: + counter: Recursion counter (used to prevent infinite recursion) + """ import InvenTree.ready # If importing data, skip pricing update diff --git a/src/backend/InvenTree/part/serializers.py b/src/backend/InvenTree/part/serializers.py index 9eeebe1fe2..049256cc8f 100644 --- a/src/backend/InvenTree/part/serializers.py +++ b/src/backend/InvenTree/part/serializers.py @@ -1593,9 +1593,12 @@ class PartPricingSerializer(InvenTree.serializers.InvenTreeModelSerializer): """Called when the serializer is saved.""" super().save() - # Update part pricing - pricing = self.instance - pricing.update_pricing() + data = self.validated_data + + if data.get('update', False): + # Update part pricing + pricing = self.instance + pricing.update_pricing() class PartRelationSerializer(InvenTree.serializers.InvenTreeModelSerializer): diff --git a/src/backend/InvenTree/part/tasks.py b/src/backend/InvenTree/part/tasks.py index b4d2891d4a..15349e92e1 100644 --- a/src/backend/InvenTree/part/tasks.py +++ b/src/backend/InvenTree/part/tasks.py @@ -251,6 +251,13 @@ def check_missing_pricing(limit=250): Arguments: limit: Maximum number of parts to process at once """ + # Find any parts which have 'old' pricing information + days = int(get_global_setting('PRICING_UPDATE_DAYS', 30)) + + if days <= 0: + # Task does not run if the interval is zero + return + # Find parts for which pricing information has never been updated results = part_models.PartPricing.objects.filter(updated=None)[:limit] @@ -260,8 +267,6 @@ def check_missing_pricing(limit=250): for pp in results: pp.schedule_for_update() - # Find any parts which have 'old' pricing information - days = int(get_global_setting('PRICING_UPDATE_DAYS', 30)) stale_date = datetime.now().date() - timedelta(days=days) results = part_models.PartPricing.objects.filter(updated__lte=stale_date)[:limit] diff --git a/src/frontend/src/pages/Index/Settings/AdminCenter/CurrencyManagementPanel.tsx b/src/frontend/src/pages/Index/Settings/AdminCenter/CurrencyManagementPanel.tsx index 02d8c2b8c2..c2c47d9787 100644 --- a/src/frontend/src/pages/Index/Settings/AdminCenter/CurrencyManagementPanel.tsx +++ b/src/frontend/src/pages/Index/Settings/AdminCenter/CurrencyManagementPanel.tsx @@ -104,7 +104,11 @@ export default function CurrencyManagementPanel() { ); diff --git a/src/frontend/src/pages/Index/Settings/SystemSettings.tsx b/src/frontend/src/pages/Index/Settings/SystemSettings.tsx index 6fb4c85961..9dba178e2b 100644 --- a/src/frontend/src/pages/Index/Settings/SystemSettings.tsx +++ b/src/frontend/src/pages/Index/Settings/SystemSettings.tsx @@ -138,6 +138,7 @@ export default function SystemSettings() { 'PART_BOM_USE_INTERNAL_PRICE', 'PRICING_DECIMAL_PLACES_MIN', 'PRICING_DECIMAL_PLACES', + 'PRICING_AUTO_UPDATE', 'PRICING_UPDATE_DAYS' ]} />