Part pricing override (#5956)

* Add override fields for part pricing

* Allow pricing override values to be specified via the API

* Fix serializer

* Update pricing docs

* Add UI elements for manually overriding pricing data

* Increment API version
This commit is contained in:
Oliver 2023-11-21 14:53:45 +11:00 committed by GitHub
parent dabd95db85
commit 6090ddfdf3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 167 additions and 8 deletions

View File

@ -2,11 +2,14 @@
# InvenTree API version
INVENTREE_API_VERSION = 152
INVENTREE_API_VERSION = 153
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v153 -> 2023-11-21 : https://github.com/inventree/InvenTree/pull/5956
- Adds override_min and override_max fields to part pricing API
v152 -> 2023-11-20 : https://github.com/inventree/InvenTree/pull/5949
- Adds barcode support for manufacturerpart model
- Adds API endpoint for adding parts to purchase order using barcode scan

View File

@ -0,0 +1,36 @@
# Generated by Django 3.2.23 on 2023-11-20 04:57
import InvenTree.fields
from django.db import migrations
import djmoney.models.fields
import djmoney.models.validators
class Migration(migrations.Migration):
dependencies = [
('part', '0118_auto_20231024_1844'),
]
operations = [
migrations.AddField(
model_name='partpricing',
name='override_max',
field=InvenTree.fields.InvenTreeModelMoneyField(blank=True, currency_choices=[], decimal_places=6, default_currency='', help_text='Override maximum cost', max_digits=19, null=True, validators=[djmoney.models.validators.MinMoneyValidator(0)], verbose_name='Maximum Cost'),
),
migrations.AddField(
model_name='partpricing',
name='override_max_currency',
field=djmoney.models.fields.CurrencyField(choices=[], default='', editable=False, max_length=3, null=True),
),
migrations.AddField(
model_name='partpricing',
name='override_min',
field=InvenTree.fields.InvenTreeModelMoneyField(blank=True, currency_choices=[], decimal_places=6, default_currency='', help_text='Override minimum cost', max_digits=19, null=True, validators=[djmoney.models.validators.MinMoneyValidator(0)], verbose_name='Minimum Cost'),
),
migrations.AddField(
model_name='partpricing',
name='override_min_currency',
field=djmoney.models.fields.CurrencyField(choices=[], default='', editable=False, max_length=3, null=True),
),
]

View File

@ -2369,6 +2369,7 @@ class PartPricing(common.models.MetaMixin):
def update_pricing(self, counter: int = 0, cascade: bool = True):
"""Recalculate all cost data for the referenced Part instance"""
# If importing data, skip pricing update
if InvenTree.ready.isImportingData():
return
@ -2698,6 +2699,7 @@ class PartPricing(common.models.MetaMixin):
Here we simply take the minimum / maximum values of the other calculated fields.
"""
overall_min = None
overall_max = None
@ -2758,7 +2760,14 @@ class PartPricing(common.models.MetaMixin):
if self.internal_cost_max is not None:
overall_max = self.internal_cost_max
if self.override_min is not None:
overall_min = self.convert(self.override_min)
self.overall_min = overall_min
if self.override_max is not None:
overall_max = self.convert(self.override_max)
self.overall_max = overall_max
def update_sale_cost(self, save=True):
@ -2897,6 +2906,18 @@ class PartPricing(common.models.MetaMixin):
help_text=_('Calculated maximum cost of variant parts'),
)
override_min = InvenTree.fields.InvenTreeModelMoneyField(
null=True, blank=True,
verbose_name=_('Minimum Cost'),
help_text=_('Override minimum cost'),
)
override_max = InvenTree.fields.InvenTreeModelMoneyField(
null=True, blank=True,
verbose_name=_('Maximum Cost'),
help_text=_('Override maximum cost'),
)
overall_min = InvenTree.fields.InvenTreeModelMoneyField(
null=True, blank=True,
verbose_name=_('Minimum Cost'),

View File

@ -5,6 +5,7 @@ import io
import logging
from decimal import Decimal
from django.core.exceptions import ValidationError
from django.core.files.base import ContentFile
from django.core.validators import MinValueValidator
from django.db import IntegrityError, models, transaction
@ -13,11 +14,14 @@ from django.db.models.functions import Coalesce
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from djmoney.contrib.exchange.exceptions import MissingRate
from djmoney.contrib.exchange.models import convert_money
from rest_framework import serializers
from sql_util.utils import SubqueryCount, SubquerySum
from taggit.serializers import TagListSerializerField
import common.models
import common.settings
import company.models
import InvenTree.helpers
import InvenTree.serializers
@ -1042,6 +1046,10 @@ class PartPricingSerializer(InvenTree.serializers.InvenTreeModelSerializer):
'supplier_price_max',
'variant_cost_min',
'variant_cost_max',
'override_min',
'override_min_currency',
'override_max',
'override_max_currency',
'overall_min',
'overall_max',
'sale_price_min',
@ -1073,6 +1081,30 @@ class PartPricingSerializer(InvenTree.serializers.InvenTreeModelSerializer):
variant_cost_min = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True)
variant_cost_max = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True)
override_min = InvenTree.serializers.InvenTreeMoneySerializer(
label=_('Minimum Price'),
help_text=_('Override calculated value for minimum price'),
allow_null=True, read_only=False, required=False,
)
override_min_currency = serializers.ChoiceField(
label=_('Minimum price currency'),
read_only=False, required=False,
choices=common.settings.currency_code_mappings(),
)
override_max = InvenTree.serializers.InvenTreeMoneySerializer(
label=_('Maximum Price'),
help_text=_('Override calculated value for maximum price'),
allow_null=True, read_only=False, required=False,
)
override_max_currency = serializers.ChoiceField(
label=_('Maximum price currency'),
read_only=False, required=False,
choices=common.settings.currency_code_mappings(),
)
overall_min = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True)
overall_max = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True)
@ -1086,18 +1118,44 @@ class PartPricingSerializer(InvenTree.serializers.InvenTreeModelSerializer):
write_only=True,
label=_('Update'),
help_text=_('Update pricing for this part'),
default=False,
required=False,
default=False, required=False, allow_null=True,
)
def validate(self, data):
"""Validate supplied pricing data"""
super().validate(data)
# Check that override_min is not greater than override_max
override_min = data.get('override_min', None)
override_max = data.get('override_max', None)
default_currency = common.settings.currency_code_default()
if override_min is not None and override_max is not None:
try:
override_min = convert_money(override_min, default_currency)
override_max = convert_money(override_max, default_currency)
except MissingRate:
raise ValidationError(_(f'Could not convert from provided currencies to {default_currency}'))
if override_min > override_max:
raise ValidationError({
'override_min': _('Minimum price must not be greater than maximum price'),
'override_max': _('Maximum price must not be less than minimum price')
})
return data
def save(self):
"""Called when the serializer is saved"""
data = self.validated_data
if InvenTree.helpers.str2bool(data.get('update', False)):
# Update part pricing
pricing = self.instance
pricing.update_pricing()
super().save()
# Update part pricing
pricing = self.instance
pricing.update_pricing()
class PartRelationSerializer(InvenTree.serializers.InvenTreeModelSerializer):

View File

@ -14,6 +14,9 @@
<button type='button' class='btn btn-success' id='part-pricing-refresh' title='{% trans "Refresh Part Pricing" %}'>
<span class='fas fa-redo-alt'></span> {% trans "Refresh" %}
</button>
<button type='button' class='btn btn-success' id='part-pricing-edit' title='{% trans "Override Part Pricing" %}'>
<span class='fas fa-edit'></span> {% trans "Edit" %}
</button>
</div>
</div>
</div>
@ -97,6 +100,14 @@
<td>{% render_currency pricing.variant_cost_max %}</td>
</tr>
{% endif %}
{% if pricing.override_min or pricing.override_max %}
<tr>
<td><a href='#pricing-overrides'><span class='fas fa-exclamation-circle'></span></a></td>
<th>{% trans "Pricing Overrides" %}</th>
<td>{% render_currency pricing.override_min currency=pricing.override_min_currency %}</td>
<td>{% render_currency pricing.override_max currency=pricing.override_max_currency %}</td>
</tr>
{% endif %}
<tr>
<td></td>
<th>{% trans "Overall Pricing" %}</th>

View File

@ -19,6 +19,25 @@ $('#part-pricing-refresh').click(function() {
);
});
$('#part-pricing-edit').click(function() {
constructForm('{% url "api-part-pricing" part.pk %}', {
title: '{% trans "Update Pricing" %}',
fields: {
override_min: {},
override_min_currency: {},
override_max: {},
override_max_currency: {},
update: {
hidden: true,
value: true,
}
},
onSuccess: function(response) {
location.reload();
}
});
});
// Internal Pricebreaks
{% if show_internal_price and roles.sales_order.view %}
initPriceBreakSet($('#internal-price-break-table'), {

View File

@ -28,6 +28,17 @@ Pricing information can be determined from multiple sources:
| Supplier Price | The price to theoretically purchase a part from a given supplier (with price-breaks) | [Supplier](../order/company.md#suppliers) |
| Purchase Cost | Historical cost information for parts purchased | [Purchase Order](../order/purchase_order.md) |
| BOM Price | Total price for an assembly (total price of all component items) | [Part](../part/part.md) |
#### 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.
### Sale Pricing
Additionally, the following information is stored for each part, in relation to sale pricing:
| Pricing Source | Description | Linked to |
| --- | --- | --- |
| Sale Price | How much a salable item is sold for (with price-breaks) | [Part](../part/part.md) |
| Sale Cost | How much an item was sold for | [Sales Order](../order/sales_order.md) |