2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-10-23 09:27:39 +00:00

Default stock currency (#10641)

* Fix for useStockFields

- Use default currency

* Ensure default currency is observed

* Specify field default

* Improve import (for ty)

* Update migration files

- Point currency fields to the correct default method

* Unit tests

- Ensure stock item gets correct default currency

* Cleaner generation of default currency value

- Return empty string during migratoins

* Update existing migrations

* Reduce noise

* Ignore "no-matching-overload" rule for ty

* Tweak money_kwargs
This commit is contained in:
Oliver
2025-10-21 13:43:24 +11:00
committed by GitHub
parent 6cd733a83a
commit f8fd9f5f07
17 changed files with 65 additions and 29 deletions

View File

@@ -113,7 +113,7 @@ invalid-argument-type="ignore" # 49
possibly-unbound-attribute="ignore" # 25 # https://github.com/astral-sh/ty/issues/164 possibly-unbound-attribute="ignore" # 25 # https://github.com/astral-sh/ty/issues/164
unknown-argument="ignore" # 3 # need to wait for betterdjango field stubs unknown-argument="ignore" # 3 # need to wait for betterdjango field stubs
invalid-assignment="ignore" # 17 # need to wait for betterdjango field stubs invalid-assignment="ignore" # 17 # need to wait for betterdjango field stubs
no-matching-overload="ignore" # 3 # need to wait for betterdjango field stubs
[tool.coverage.run] [tool.coverage.run]
source = ["src/backend/InvenTree", "InvenTree"] source = ["src/backend/InvenTree", "InvenTree"]

View File

@@ -15,6 +15,8 @@ from rest_framework.fields import URLField as RestURLField
from rest_framework.fields import empty from rest_framework.fields import empty
import InvenTree.helpers import InvenTree.helpers
import InvenTree.ready
from common.currency import currency_code_default
from common.settings import get_global_setting from common.settings import get_global_setting
from .validators import AllowedURLValidator, allowable_url_schemes from .validators import AllowedURLValidator, allowable_url_schemes
@@ -59,7 +61,7 @@ class InvenTreeURLField(models.URLField):
def money_kwargs(**kwargs): def money_kwargs(**kwargs):
"""Returns the database settings for MoneyFields.""" """Returns the database settings for MoneyFields."""
from common.currency import currency_code_default, currency_code_mappings from common.currency import currency_code_mappings
# Default values (if not specified) # Default values (if not specified)
if 'max_digits' not in kwargs: if 'max_digits' not in kwargs:
@@ -71,8 +73,14 @@ def money_kwargs(**kwargs):
if 'currency_choices' not in kwargs: if 'currency_choices' not in kwargs:
kwargs['currency_choices'] = currency_code_mappings() kwargs['currency_choices'] = currency_code_mappings()
if 'default_currency' not in kwargs: if InvenTree.ready.isRunningMigrations():
kwargs['default_currency'] = currency_code_default() # During migrations, avoid setting a default currency
# This prevents issues related to early evaluation of the default currency value
kwargs['default_currency'] = ''
else:
# Override default currency with a callable function
# This ensures that the default currency is always up-to-date
kwargs['default_currency'] = currency_code_default
return kwargs return kwargs

View File

@@ -9,7 +9,7 @@ from django.db.models import F, Q
from django.urls import include, path from django.urls import include, path
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_filters import rest_framework as rest_filters import django_filters.rest_framework.filters as rest_filters
from django_filters.rest_framework.filterset import FilterSet from django_filters.rest_framework.filterset import FilterSet
from drf_spectacular.utils import extend_schema, extend_schema_field from drf_spectacular.utils import extend_schema, extend_schema_field
from rest_framework import serializers, status from rest_framework import serializers, status

View File

@@ -11,6 +11,7 @@ import structlog
from moneyed import CURRENCIES from moneyed import CURRENCIES
import InvenTree.helpers import InvenTree.helpers
import InvenTree.ready
logger = structlog.get_logger('inventree') logger = structlog.get_logger('inventree')
@@ -19,6 +20,9 @@ def currency_code_default(create: bool = True):
"""Returns the default currency code (or USD if not specified).""" """Returns the default currency code (or USD if not specified)."""
from common.settings import get_global_setting from common.settings import get_global_setting
if InvenTree.ready.isRunningMigrations():
return '' # pragma: no cover
try: try:
code = get_global_setting( code = get_global_setting(
'INVENTREE_DEFAULT_CURRENCY', create=create, cache=True 'INVENTREE_DEFAULT_CURRENCY', create=create, cache=True

View File

@@ -3,7 +3,6 @@
from django.db import migrations, connection from django.db import migrations, connection
import djmoney.models.fields import djmoney.models.fields
import common.currency import common.currency
import common.settings
class Migration(migrations.Migration): class Migration(migrations.Migration):
@@ -17,11 +16,11 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='supplierpricebreak', model_name='supplierpricebreak',
name='price', name='price',
field=djmoney.models.fields.MoneyField(decimal_places=4, default_currency=common.currency.currency_code_default(), help_text='Unit price at specified quantity', max_digits=19, null=True, verbose_name='Price'), field=djmoney.models.fields.MoneyField(decimal_places=4, default_currency='', help_text='Unit price at specified quantity', max_digits=19, null=True, verbose_name='Price'),
), ),
migrations.AddField( migrations.AddField(
model_name='supplierpricebreak', model_name='supplierpricebreak',
name='price_currency', name='price_currency',
field=djmoney.models.fields.CurrencyField(choices=common.currency.currency_code_mappings(), default=common.currency.currency_code_default(), editable=False, max_length=3), field=djmoney.models.fields.CurrencyField(choices=common.currency.currency_code_mappings(), default='', editable=False, max_length=3),
), ),
] ]

View File

@@ -2,7 +2,6 @@
import InvenTree.validators import InvenTree.validators
import common.currency import common.currency
import common.settings
from django.db import migrations, models from django.db import migrations, models

View File

@@ -3,7 +3,6 @@
from django.db import migrations from django.db import migrations
import djmoney.models.fields import djmoney.models.fields
import common.currency import common.currency
import common.settings
class Migration(migrations.Migration): class Migration(migrations.Migration):
@@ -17,11 +16,11 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='purchaseorderlineitem', model_name='purchaseorderlineitem',
name='purchase_price', name='purchase_price',
field=djmoney.models.fields.MoneyField(decimal_places=4, default_currency=common.currency.currency_code_default(), help_text='Unit purchase price', max_digits=19, null=True, verbose_name='Purchase Price'), field=djmoney.models.fields.MoneyField(decimal_places=4, default_currency='', help_text='Unit purchase price', max_digits=19, null=True, verbose_name='Purchase Price'),
), ),
migrations.AddField( migrations.AddField(
model_name='purchaseorderlineitem', model_name='purchaseorderlineitem',
name='purchase_price_currency', name='purchase_price_currency',
field=djmoney.models.fields.CurrencyField(choices=common.currency.currency_code_mappings(), default=common.currency.currency_code_default(), editable=False, max_length=3), field=djmoney.models.fields.CurrencyField(choices=common.currency.currency_code_mappings(), default='', editable=False, max_length=3),
), ),
] ]

View File

@@ -2,8 +2,6 @@
from django.db import migrations from django.db import migrations
import djmoney.models.fields import djmoney.models.fields
import common.currency
import common.settings
class Migration(migrations.Migration): class Migration(migrations.Migration):
@@ -16,6 +14,6 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='purchaseorderlineitem', model_name='purchaseorderlineitem',
name='purchase_price', name='purchase_price',
field=djmoney.models.fields.MoneyField(blank=True, decimal_places=4, default_currency=common.currency.currency_code_default(), help_text='Unit purchase price', max_digits=19, null=True, verbose_name='Purchase Price'), field=djmoney.models.fields.MoneyField(blank=True, decimal_places=4, default_currency='', help_text='Unit purchase price', max_digits=19, null=True, verbose_name='Purchase Price'),
), ),
] ]

View File

@@ -2,7 +2,6 @@
from django.db import migrations from django.db import migrations
import common.currency import common.currency
import common.settings
import djmoney.models.fields import djmoney.models.fields
@@ -16,11 +15,11 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='salesorderlineitem', model_name='salesorderlineitem',
name='sale_price', name='sale_price',
field=djmoney.models.fields.MoneyField(blank=True, decimal_places=4, default_currency=common.currency.currency_code_default(), help_text='Unit sale price', max_digits=19, null=True, verbose_name='Sale Price'), field=djmoney.models.fields.MoneyField(blank=True, decimal_places=4, default_currency='', help_text='Unit sale price', max_digits=19, null=True, verbose_name='Sale Price'),
), ),
migrations.AddField( migrations.AddField(
model_name='salesorderlineitem', model_name='salesorderlineitem',
name='sale_price_currency', name='sale_price_currency',
field=djmoney.models.fields.CurrencyField(choices=common.currency.currency_code_mappings(), default=common.currency.currency_code_default(), editable=False, max_length=3), field=djmoney.models.fields.CurrencyField(choices=common.currency.currency_code_mappings(), default='', editable=False, max_length=3),
), ),
] ]

View File

@@ -16,11 +16,11 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='partsellpricebreak', model_name='partsellpricebreak',
name='price', name='price',
field=djmoney.models.fields.MoneyField(decimal_places=4, default_currency=common.currency.currency_code_default(), help_text='Unit price at specified quantity', max_digits=19, null=True, verbose_name='Price'), field=djmoney.models.fields.MoneyField(decimal_places=4, default_currency='', help_text='Unit price at specified quantity', max_digits=19, null=True, verbose_name='Price'),
), ),
migrations.AddField( migrations.AddField(
model_name='partsellpricebreak', model_name='partsellpricebreak',
name='price_currency', name='price_currency',
field=djmoney.models.fields.CurrencyField(choices=common.currency.currency_code_mappings(), default=common.currency.currency_code_default(), editable=False, max_length=3), field=djmoney.models.fields.CurrencyField(choices=common.currency.currency_code_mappings(), default='', editable=False, max_length=3),
), ),
] ]

View File

@@ -3,7 +3,6 @@
import InvenTree.fields import InvenTree.fields
import django.core.validators import django.core.validators
import common.currency import common.currency
import common.settings
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
import djmoney.models.fields import djmoney.models.fields
@@ -21,8 +20,8 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', InvenTree.fields.RoundingDecimalField(decimal_places=5, default=1, help_text='Price break quantity', max_digits=15, validators=[django.core.validators.MinValueValidator(1)], verbose_name='Quantity')), ('quantity', InvenTree.fields.RoundingDecimalField(decimal_places=5, default=1, help_text='Price break quantity', max_digits=15, validators=[django.core.validators.MinValueValidator(1)], verbose_name='Quantity')),
('price_currency', djmoney.models.fields.CurrencyField(choices=common.currency.currency_code_mappings(), default=common.currency.currency_code_default(), editable=False, max_length=3)), ('price_currency', djmoney.models.fields.CurrencyField(choices=common.currency.currency_code_mappings(), default='', editable=False, max_length=3)),
('price', djmoney.models.fields.MoneyField(decimal_places=4, default_currency=common.currency.currency_code_default(), help_text='Unit price at specified quantity', max_digits=19, null=True, verbose_name='Price')), ('price', djmoney.models.fields.MoneyField(decimal_places=4, default_currency='', help_text='Unit price at specified quantity', max_digits=19, null=True, verbose_name='Price')),
('part', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='internalpricebreaks', to='part.part', verbose_name='Part')), ('part', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='internalpricebreaks', to='part.part', verbose_name='Part')),
], ],
options={ options={

View File

@@ -8,7 +8,7 @@ import djmoney.models.validators
import InvenTree.fields import InvenTree.fields
import common.currency import common.currency
import common.settings
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@@ -16,11 +16,11 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='stockitem', model_name='stockitem',
name='purchase_price', name='purchase_price',
field=djmoney.models.fields.MoneyField(decimal_places=4, default_currency=common.currency.currency_code_default(), help_text='Single unit purchase price at time of purchase', max_digits=19, null=True, verbose_name='Purchase Price'), field=djmoney.models.fields.MoneyField(decimal_places=4, default_currency='', help_text='Single unit purchase price at time of purchase', max_digits=19, null=True, verbose_name='Purchase Price'),
), ),
migrations.AddField( migrations.AddField(
model_name='stockitem', model_name='stockitem',
name='purchase_price_currency', name='purchase_price_currency',
field=djmoney.models.fields.CurrencyField(choices=common.currency.all_currency_codes(), default=common.currency.currency_code_default(), editable=False, max_length=3), field=djmoney.models.fields.CurrencyField(choices=common.currency.all_currency_codes(), default='', editable=False, max_length=3),
), ),
] ]

View File

@@ -2,7 +2,6 @@
from django.db import migrations from django.db import migrations
import djmoney.models.fields import djmoney.models.fields
import common.currency
class Migration(migrations.Migration): class Migration(migrations.Migration):
@@ -15,6 +14,6 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='stockitem', model_name='stockitem',
name='purchase_price', name='purchase_price',
field=djmoney.models.fields.MoneyField(blank=True, decimal_places=4, default_currency=common.currency.currency_code_default(), help_text='Single unit purchase price at time of purchase', max_digits=19, null=True, verbose_name='Purchase Price'), field=djmoney.models.fields.MoneyField(blank=True, decimal_places=4, default_currency='', help_text='Single unit purchase price at time of purchase', max_digits=19, null=True, verbose_name='Purchase Price'),
), ),
] ]

View File

@@ -6,6 +6,8 @@ from django.core.exceptions import ValidationError
from django.db.models import Sum from django.db.models import Sum
from django.test import override_settings from django.test import override_settings
from djmoney.money import Money
from build.models import Build from build.models import Build
from common.models import InvenTreeSetting from common.models import InvenTreeSetting
from company.models import Company from company.models import Company
@@ -803,6 +805,27 @@ class StockTest(StockTestBase):
self.assertTrue(check_func()) self.assertTrue(check_func())
def test_purchase_price(self):
"""Test purchase price field."""
from common.currency import currency_code_default
from common.settings import set_global_setting
part = Part.objects.filter(virtual=False).first()
for currency in ['AUD', 'USD', 'JPY']:
set_global_setting('INVENTREE_DEFAULT_CURRENCY', currency)
self.assertEqual(currency_code_default(), currency)
# Create stock item, do not specify currency - should get default
item = StockItem.objects.create(part=part, quantity=10)
self.assertEqual(item.purchase_price_currency, currency)
# Create stock item, specify currency
item = StockItem.objects.create(
part=part, quantity=10, purchase_price=Money(5, 'GBP')
)
self.assertEqual(item.purchase_price_currency, 'GBP')
class StockBarcodeTest(StockTestBase): class StockBarcodeTest(StockTestBase):
"""Run barcode tests for the stock app.""" """Run barcode tests for the stock app."""

View File

@@ -12,8 +12,8 @@ from django.urls import include, path
from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.generic.base import RedirectView from django.views.generic.base import RedirectView
import django_filters.rest_framework.filters as rest_filters
import structlog import structlog
from django_filters import rest_framework as rest_filters
from django_filters.rest_framework.filterset import FilterSet from django_filters.rest_framework.filterset import FilterSet
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
from rest_framework import exceptions from rest_framework import exceptions

View File

@@ -124,10 +124,18 @@ export function useStockFields({
} }
}, [pricing, quantity]); }, [pricing, quantity]);
// Set the supplier part if provided
useEffect(() => { useEffect(() => {
if (supplierPartId && !supplierPart) setSupplierPart(supplierPartId); if (supplierPartId && !supplierPart) setSupplierPart(supplierPartId);
}, [partInstance, supplierPart, supplierPartId]); }, [partInstance, supplierPart, supplierPartId]);
// Set default currency from global settings
useEffect(() => {
setPurchasePriceCurrency(
globalSettings.getSetting('INVENTREE_DEFAULT_CURRENCY')
);
}, [globalSettings]);
return useMemo(() => { return useMemo(() => {
const fields: ApiFormFieldSet = { const fields: ApiFormFieldSet = {
part: { part: {
@@ -248,6 +256,7 @@ export function useStockFields({
}, },
purchase_price_currency: { purchase_price_currency: {
icon: <IconCoins />, icon: <IconCoins />,
default: globalSettings.getSetting('INVENTREE_DEFAULT_CURRENCY'),
value: purchasePriceCurrency, value: purchasePriceCurrency,
onValueChange: (value) => { onValueChange: (value) => {
setPurchasePriceCurrency(value); setPurchasePriceCurrency(value);