mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
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:
parent
dabd95db85
commit
6090ddfdf3
@ -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
|
||||
|
36
InvenTree/part/migrations/0119_auto_20231120_0457.py
Normal file
36
InvenTree/part/migrations/0119_auto_20231120_0457.py
Normal 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),
|
||||
),
|
||||
]
|
@ -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'),
|
||||
|
@ -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,15 +1118,41 @@ 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)):
|
||||
super().save()
|
||||
|
||||
# Update part pricing
|
||||
pricing = self.instance
|
||||
pricing.update_pricing()
|
||||
|
@ -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>
|
||||
|
@ -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'), {
|
||||
|
@ -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) |
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user