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
unknown-argument="ignore" # 3 # 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]
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
import InvenTree.helpers
import InvenTree.ready
from common.currency import currency_code_default
from common.settings import get_global_setting
from .validators import AllowedURLValidator, allowable_url_schemes
@@ -59,7 +61,7 @@ class InvenTreeURLField(models.URLField):
def money_kwargs(**kwargs):
"""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)
if 'max_digits' not in kwargs:
@@ -71,8 +73,14 @@ def money_kwargs(**kwargs):
if 'currency_choices' not in kwargs:
kwargs['currency_choices'] = currency_code_mappings()
if 'default_currency' not in kwargs:
kwargs['default_currency'] = currency_code_default()
if InvenTree.ready.isRunningMigrations():
# 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

View File

@@ -9,7 +9,7 @@ from django.db.models import F, Q
from django.urls import include, path
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 drf_spectacular.utils import extend_schema, extend_schema_field
from rest_framework import serializers, status

View File

@@ -11,6 +11,7 @@ import structlog
from moneyed import CURRENCIES
import InvenTree.helpers
import InvenTree.ready
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)."""
from common.settings import get_global_setting
if InvenTree.ready.isRunningMigrations():
return '' # pragma: no cover
try:
code = get_global_setting(
'INVENTREE_DEFAULT_CURRENCY', create=create, cache=True

View File

@@ -3,7 +3,6 @@
from django.db import migrations, connection
import djmoney.models.fields
import common.currency
import common.settings
class Migration(migrations.Migration):
@@ -17,11 +16,11 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='supplierpricebreak',
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(
model_name='supplierpricebreak',
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 common.currency
import common.settings
from django.db import migrations, models

View File

@@ -3,7 +3,6 @@
from django.db import migrations
import djmoney.models.fields
import common.currency
import common.settings
class Migration(migrations.Migration):
@@ -17,11 +16,11 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='purchaseorderlineitem',
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(
model_name='purchaseorderlineitem',
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
import djmoney.models.fields
import common.currency
import common.settings
class Migration(migrations.Migration):
@@ -16,6 +14,6 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='purchaseorderlineitem',
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
import common.currency
import common.settings
import djmoney.models.fields
@@ -16,11 +15,11 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='salesorderlineitem',
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(
model_name='salesorderlineitem',
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(
model_name='partsellpricebreak',
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(
model_name='partsellpricebreak',
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 django.core.validators
import common.currency
import common.settings
from django.db import migrations, models
import django.db.models.deletion
import djmoney.models.fields
@@ -21,8 +20,8 @@ class Migration(migrations.Migration):
fields=[
('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')),
('price_currency', djmoney.models.fields.CurrencyField(choices=common.currency.currency_code_mappings(), default=common.currency.currency_code_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_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='', 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')),
],
options={

View File

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

View File

@@ -16,11 +16,11 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='stockitem',
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(
model_name='stockitem',
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
import djmoney.models.fields
import common.currency
class Migration(migrations.Migration):
@@ -15,6 +14,6 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='stockitem',
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.test import override_settings
from djmoney.money import Money
from build.models import Build
from common.models import InvenTreeSetting
from company.models import Company
@@ -803,6 +805,27 @@ class StockTest(StockTestBase):
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):
"""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.generic.base import RedirectView
import django_filters.rest_framework.filters as rest_filters
import structlog
from django_filters import rest_framework as rest_filters
from django_filters.rest_framework.filterset import FilterSet
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
from rest_framework import exceptions

View File

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