mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'master' of https://github.com/inventree/InvenTree into one-pricing-view
This commit is contained in:
commit
f8e2d53ad4
@ -4,7 +4,6 @@ import logging
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.core.exceptions import AppRegistryNotReady
|
||||
from django.conf import settings
|
||||
|
||||
from InvenTree.ready import isInTestMode, canAppAccessDatabase
|
||||
import InvenTree.tasks
|
||||
@ -66,10 +65,11 @@ class InvenTreeConfig(AppConfig):
|
||||
from djmoney.contrib.exchange.models import ExchangeBackend
|
||||
from datetime import datetime, timedelta
|
||||
from InvenTree.tasks import update_exchange_rates
|
||||
from common.settings import currency_code_default
|
||||
except AppRegistryNotReady:
|
||||
pass
|
||||
|
||||
base_currency = settings.BASE_CURRENCY
|
||||
base_currency = currency_code_default()
|
||||
|
||||
update = False
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
from django.conf import settings as inventree_settings
|
||||
from common.settings import currency_code_default, currency_codes
|
||||
|
||||
from djmoney.contrib.exchange.backends.base import SimpleExchangeBackend
|
||||
|
||||
@ -22,8 +22,8 @@ class InvenTreeExchange(SimpleExchangeBackend):
|
||||
return {
|
||||
}
|
||||
|
||||
def update_rates(self, base_currency=inventree_settings.BASE_CURRENCY):
|
||||
def update_rates(self, base_currency=currency_code_default()):
|
||||
|
||||
symbols = ','.join(inventree_settings.CURRENCIES)
|
||||
symbols = ','.join(currency_codes())
|
||||
|
||||
super().update_rates(base=base_currency, symbols=symbols)
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
import sys
|
||||
|
||||
from .validators import allowable_url_schemes
|
||||
|
||||
@ -13,8 +14,11 @@ from django.core import validators
|
||||
from django import forms
|
||||
|
||||
from decimal import Decimal
|
||||
from djmoney.models.fields import MoneyField as ModelMoneyField
|
||||
from djmoney.forms.fields import MoneyField
|
||||
|
||||
import InvenTree.helpers
|
||||
import common.settings
|
||||
|
||||
|
||||
class InvenTreeURLFormField(FormURLField):
|
||||
@ -34,6 +38,42 @@ class InvenTreeURLField(models.URLField):
|
||||
})
|
||||
|
||||
|
||||
def money_kwargs():
|
||||
""" returns the database settings for MoneyFields """
|
||||
kwargs = {}
|
||||
kwargs['currency_choices'] = common.settings.currency_code_mappings()
|
||||
kwargs['default_currency'] = common.settings.currency_code_default
|
||||
return kwargs
|
||||
|
||||
|
||||
class InvenTreeModelMoneyField(ModelMoneyField):
|
||||
""" custom MoneyField for clean migrations while using dynamic currency settings """
|
||||
def __init__(self, **kwargs):
|
||||
# detect if creating migration
|
||||
if 'makemigrations' in sys.argv:
|
||||
# remove currency information for a clean migration
|
||||
kwargs['default_currency'] = ''
|
||||
kwargs['currency_choices'] = []
|
||||
else:
|
||||
# set defaults
|
||||
kwargs.update(money_kwargs())
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
""" override form class to use own function """
|
||||
kwargs['form_class'] = InvenTreeMoneyField
|
||||
return super().formfield(**kwargs)
|
||||
|
||||
|
||||
class InvenTreeMoneyField(MoneyField):
|
||||
""" custom MoneyField for clean migrations while using dynamic currency settings """
|
||||
def __init__(self, *args, **kwargs):
|
||||
# override initial values with the real info from database
|
||||
kwargs.update(money_kwargs())
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class DatePickerFormField(forms.DateField):
|
||||
"""
|
||||
Custom date-picker field
|
||||
|
@ -21,6 +21,9 @@ import InvenTree.version
|
||||
|
||||
from common.models import InvenTreeSetting
|
||||
from .settings import MEDIA_URL, STATIC_URL
|
||||
from common.settings import currency_code_default
|
||||
|
||||
from djmoney.money import Money
|
||||
|
||||
|
||||
def getSetting(key, backup_value=None):
|
||||
@ -247,6 +250,22 @@ def decimal2string(d):
|
||||
return s.rstrip("0").rstrip(".")
|
||||
|
||||
|
||||
def decimal2money(d, currency=None):
|
||||
"""
|
||||
Format a Decimal number as Money
|
||||
|
||||
Args:
|
||||
d: A python Decimal object
|
||||
currency: Currency of the input amount, defaults to default currency in settings
|
||||
|
||||
Returns:
|
||||
A Money object from the input(s)
|
||||
"""
|
||||
if not currency:
|
||||
currency = currency_code_default()
|
||||
return Money(d, currency)
|
||||
|
||||
|
||||
def WrapWithQuotes(text, quote='"'):
|
||||
""" Wrap the supplied text with quotes
|
||||
|
||||
|
@ -7,8 +7,6 @@ from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
@ -46,16 +44,13 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
|
||||
|
||||
def __init__(self, instance=None, data=empty, **kwargs):
|
||||
|
||||
self.instance = instance
|
||||
# self.instance = instance
|
||||
|
||||
# If instance is None, we are creating a new instance
|
||||
if instance is None:
|
||||
if instance is None and data is not empty:
|
||||
|
||||
if data is empty:
|
||||
data = OrderedDict()
|
||||
else:
|
||||
# Required to side-step immutability of a QueryDict
|
||||
data = data.copy()
|
||||
# Required to side-step immutability of a QueryDict
|
||||
data = data.copy()
|
||||
|
||||
# Add missing fields which have default values
|
||||
ModelClass = self.Meta.model
|
||||
@ -64,6 +59,11 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
|
||||
|
||||
for field_name, field in fields.fields.items():
|
||||
|
||||
"""
|
||||
Update the field IF (and ONLY IF):
|
||||
- The field has a specified default value
|
||||
- The field does not already have a value set
|
||||
"""
|
||||
if field.has_default() and field_name not in data:
|
||||
|
||||
value = field.default
|
||||
@ -85,7 +85,7 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
|
||||
Use the 'default' values specified by the django model definition
|
||||
"""
|
||||
|
||||
initials = super().get_initial()
|
||||
initials = super().get_initial().copy()
|
||||
|
||||
# Are we creating a new instance?
|
||||
if self.instance is None:
|
||||
@ -111,7 +111,8 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
|
||||
return initials
|
||||
|
||||
def run_validation(self, data=empty):
|
||||
""" Perform serializer validation.
|
||||
"""
|
||||
Perform serializer validation.
|
||||
In addition to running validators on the serializer fields,
|
||||
this class ensures that the underlying model is also validated.
|
||||
"""
|
||||
|
@ -522,10 +522,6 @@ for currency in CURRENCIES:
|
||||
print(f"Currency code '{currency}' is not supported")
|
||||
sys.exit(1)
|
||||
|
||||
BASE_CURRENCY = get_setting(
|
||||
'INVENTREE_BASE_CURRENCY',
|
||||
CONFIG.get('base_currency', 'USD')
|
||||
)
|
||||
|
||||
# Custom currency exchange backend
|
||||
EXCHANGE_BACKEND = 'InvenTree.exchange.InvenTreeExchange'
|
||||
|
@ -170,7 +170,7 @@ def update_exchange_rates():
|
||||
try:
|
||||
from InvenTree.exchange import InvenTreeExchange
|
||||
from djmoney.contrib.exchange.models import ExchangeBackend, Rate
|
||||
from django.conf import settings
|
||||
from common.settings import currency_code_default, currency_codes
|
||||
except AppRegistryNotReady:
|
||||
# Apps not yet loaded!
|
||||
logger.info("Could not perform 'update_exchange_rates' - App registry not ready")
|
||||
@ -192,14 +192,14 @@ def update_exchange_rates():
|
||||
backend = InvenTreeExchange()
|
||||
print(f"Updating exchange rates from {backend.url}")
|
||||
|
||||
base = settings.BASE_CURRENCY
|
||||
base = currency_code_default()
|
||||
|
||||
print(f"Using base currency '{base}'")
|
||||
|
||||
backend.update_rates(base_currency=base)
|
||||
|
||||
# Remove any exchange rates which are not in the provided currencies
|
||||
Rate.objects.filter(backend="InvenTreeExchange").exclude(currency__in=settings.CURRENCIES).delete()
|
||||
Rate.objects.filter(backend="InvenTreeExchange").exclude(currency__in=currency_codes()).delete()
|
||||
|
||||
|
||||
def send_email(subject, body, recipients, from_email=None):
|
||||
|
@ -2,6 +2,11 @@
|
||||
|
||||
from rest_framework import status
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Group
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
from InvenTree.api_tester import InvenTreeAPITestCase
|
||||
@ -11,6 +16,87 @@ from users.models import RuleSet
|
||||
from base64 import b64encode
|
||||
|
||||
|
||||
class HTMLAPITests(TestCase):
|
||||
"""
|
||||
Test that we can access the REST API endpoints via the HTML interface.
|
||||
|
||||
History: Discovered on 2021-06-28 a bug in InvenTreeModelSerializer,
|
||||
which raised an AssertionError when using the HTML API interface,
|
||||
while the regular JSON interface continued to work as expected.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
# Create a user
|
||||
user = get_user_model()
|
||||
|
||||
self.user = user.objects.create_user(
|
||||
username='username',
|
||||
email='user@email.com',
|
||||
password='password'
|
||||
)
|
||||
|
||||
# Put the user into a group with the correct permissions
|
||||
group = Group.objects.create(name='mygroup')
|
||||
self.user.groups.add(group)
|
||||
|
||||
# Give the group *all* the permissions!
|
||||
for rule in group.rule_sets.all():
|
||||
rule.can_view = True
|
||||
rule.can_change = True
|
||||
rule.can_add = True
|
||||
rule.can_delete = True
|
||||
|
||||
rule.save()
|
||||
|
||||
self.client.login(username='username', password='password')
|
||||
|
||||
def test_part_api(self):
|
||||
url = reverse('api-part-list')
|
||||
|
||||
# Check JSON response
|
||||
response = self.client.get(url, HTTP_ACCEPT='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Check HTTP response
|
||||
response = self.client.get(url, HTTP_ACCEPT='text/html')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_build_api(self):
|
||||
url = reverse('api-build-list')
|
||||
|
||||
# Check JSON response
|
||||
response = self.client.get(url, HTTP_ACCEPT='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Check HTTP response
|
||||
response = self.client.get(url, HTTP_ACCEPT='text/html')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_stock_api(self):
|
||||
url = reverse('api-stock-list')
|
||||
|
||||
# Check JSON response
|
||||
response = self.client.get(url, HTTP_ACCEPT='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Check HTTP response
|
||||
response = self.client.get(url, HTTP_ACCEPT='text/html')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_company_list(self):
|
||||
url = reverse('api-company-list')
|
||||
|
||||
# Check JSON response
|
||||
response = self.client.get(url, HTTP_ACCEPT='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Check HTTP response
|
||||
response = self.client.get(url, HTTP_ACCEPT='text/html')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class APITests(InvenTreeAPITestCase):
|
||||
""" Tests for the InvenTree API """
|
||||
|
||||
|
@ -5,8 +5,6 @@ from django.test import TestCase
|
||||
import django.core.exceptions as django_exceptions
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from djmoney.money import Money
|
||||
from djmoney.contrib.exchange.models import Rate, convert_money
|
||||
from djmoney.contrib.exchange.exceptions import MissingRate
|
||||
@ -22,6 +20,7 @@ from decimal import Decimal
|
||||
import InvenTree.tasks
|
||||
|
||||
from stock.models import StockLocation
|
||||
from common.settings import currency_codes
|
||||
|
||||
|
||||
class ValidatorTest(TestCase):
|
||||
@ -337,13 +336,11 @@ class CurrencyTests(TestCase):
|
||||
with self.assertRaises(MissingRate):
|
||||
convert_money(Money(100, 'AUD'), 'USD')
|
||||
|
||||
currencies = settings.CURRENCIES
|
||||
|
||||
InvenTree.tasks.update_exchange_rates()
|
||||
|
||||
rates = Rate.objects.all()
|
||||
|
||||
self.assertEqual(rates.count(), len(currencies))
|
||||
self.assertEqual(rates.count(), len(currency_codes()))
|
||||
|
||||
# Now that we have some exchange rate information, we can perform conversions
|
||||
|
||||
|
@ -12,7 +12,6 @@ from django.utils.translation import gettext_lazy as _
|
||||
from django.template.loader import render_to_string
|
||||
from django.http import HttpResponse, JsonResponse, HttpResponseRedirect
|
||||
from django.urls import reverse_lazy
|
||||
from django.conf import settings
|
||||
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
|
||||
@ -21,6 +20,7 @@ from django.views.generic import ListView, DetailView, CreateView, FormView, Del
|
||||
from django.views.generic.base import RedirectView, TemplateView
|
||||
|
||||
from djmoney.contrib.exchange.models import ExchangeBackend, Rate
|
||||
from common.settings import currency_code_default, currency_codes
|
||||
|
||||
from part.models import Part, PartCategory
|
||||
from stock.models import StockLocation, StockItem
|
||||
@ -820,8 +820,8 @@ class CurrencySettingsView(TemplateView):
|
||||
ctx = super().get_context_data(**kwargs).copy()
|
||||
|
||||
ctx['settings'] = InvenTreeSetting.objects.all().order_by('key')
|
||||
ctx["base_currency"] = settings.BASE_CURRENCY
|
||||
ctx["currencies"] = settings.CURRENCIES
|
||||
ctx["base_currency"] = currency_code_default()
|
||||
ctx["currencies"] = currency_codes
|
||||
|
||||
ctx["rates"] = Rate.objects.filter(backend="InvenTreeExchange")
|
||||
|
||||
|
23
InvenTree/common/migrations/0010_migrate_currency_setting.py
Normal file
23
InvenTree/common/migrations/0010_migrate_currency_setting.py
Normal file
@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.2.4 on 2021-07-01 15:39
|
||||
|
||||
from django.db import migrations
|
||||
from common.models import InvenTreeSetting
|
||||
from InvenTree.settings import get_setting, CONFIG
|
||||
|
||||
def set_default_currency(apps, schema_editor):
|
||||
""" migrate the currency setting from config.yml to db """
|
||||
# get value from settings-file
|
||||
base_currency = get_setting('INVENTREE_BASE_CURRENCY', CONFIG.get('base_currency', 'USD'))
|
||||
# write to database
|
||||
InvenTreeSetting.set_setting('INVENTREE_DEFAULT_CURRENCY', base_currency, None, create=True)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('common', '0009_delete_currency'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(set_default_currency),
|
||||
]
|
@ -14,11 +14,11 @@ from django.db import models, transaction
|
||||
from django.db.utils import IntegrityError, OperationalError
|
||||
from django.conf import settings
|
||||
|
||||
from djmoney.models.fields import MoneyField
|
||||
from djmoney.settings import CURRENCY_CHOICES
|
||||
from djmoney.contrib.exchange.models import convert_money
|
||||
from djmoney.contrib.exchange.exceptions import MissingRate
|
||||
|
||||
from common.settings import currency_code_default
|
||||
import common.settings
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.validators import MinValueValidator, URLValidator
|
||||
@ -81,6 +81,13 @@ class InvenTreeSetting(models.Model):
|
||||
'default': '',
|
||||
},
|
||||
|
||||
'INVENTREE_DEFAULT_CURRENCY': {
|
||||
'name': _('Default Currency'),
|
||||
'description': _('Default currency'),
|
||||
'default': 'USD',
|
||||
'choices': CURRENCY_CHOICES,
|
||||
},
|
||||
|
||||
'INVENTREE_DOWNLOAD_FROM_URL': {
|
||||
'name': _('Download from URL'),
|
||||
'description': _('Allow download of remote images and files from external URL'),
|
||||
@ -219,6 +226,13 @@ class InvenTreeSetting(models.Model):
|
||||
'validator': bool,
|
||||
},
|
||||
|
||||
'PART_SHOW_RELATED': {
|
||||
'name': _('Show related parts'),
|
||||
'description': _('Display related parts for a part'),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
|
||||
'PART_INTERNAL_PRICE': {
|
||||
'name': _('Internal Prices'),
|
||||
'description': _('Enable internal prices for parts'),
|
||||
@ -728,10 +742,9 @@ class PriceBreak(models.Model):
|
||||
help_text=_('Price break quantity'),
|
||||
)
|
||||
|
||||
price = MoneyField(
|
||||
price = InvenTree.fields.InvenTreeModelMoneyField(
|
||||
max_digits=19,
|
||||
decimal_places=4,
|
||||
default_currency=currency_code_default(),
|
||||
null=True,
|
||||
verbose_name=_('Price'),
|
||||
help_text=_('Unit price at specified quantity'),
|
||||
@ -784,7 +797,7 @@ def get_price(instance, quantity, moq=True, multiples=True, currency=None, break
|
||||
|
||||
if currency is None:
|
||||
# Default currency selection
|
||||
currency = currency_code_default()
|
||||
currency = common.settings.currency_code_default()
|
||||
|
||||
pb_min = None
|
||||
for pb in price_breaks:
|
||||
|
@ -6,9 +6,9 @@ User-configurable settings for the common app
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from moneyed import CURRENCIES
|
||||
from django.conf import settings
|
||||
|
||||
import common.models
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def currency_code_default():
|
||||
@ -16,7 +16,7 @@ def currency_code_default():
|
||||
Returns the default currency code (or USD if not specified)
|
||||
"""
|
||||
|
||||
code = settings.BASE_CURRENCY
|
||||
code = common.models.InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY')
|
||||
|
||||
if code not in CURRENCIES:
|
||||
code = 'USD'
|
||||
@ -24,6 +24,20 @@ def currency_code_default():
|
||||
return code
|
||||
|
||||
|
||||
def currency_code_mappings():
|
||||
"""
|
||||
Returns the current currency choices
|
||||
"""
|
||||
return [(a, a) for a in settings.CURRENCIES]
|
||||
|
||||
|
||||
def currency_codes():
|
||||
"""
|
||||
Returns the current currency codes
|
||||
"""
|
||||
return [a for a in settings.CURRENCIES]
|
||||
|
||||
|
||||
def stock_expiry_enabled():
|
||||
"""
|
||||
Returns True if the stock expiry feature is enabled
|
||||
|
@ -6,13 +6,12 @@ Django Forms for interacting with Company app
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from InvenTree.forms import HelperForm
|
||||
from InvenTree.fields import RoundingDecimalFormField
|
||||
from InvenTree.fields import InvenTreeMoneyField, RoundingDecimalFormField
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
import django.forms
|
||||
|
||||
import djmoney.settings
|
||||
from djmoney.forms.fields import MoneyField
|
||||
|
||||
from common.settings import currency_code_default
|
||||
|
||||
@ -129,9 +128,8 @@ class EditSupplierPartForm(HelperForm):
|
||||
'note': 'fa-pencil-alt',
|
||||
}
|
||||
|
||||
single_pricing = MoneyField(
|
||||
single_pricing = InvenTreeMoneyField(
|
||||
label=_('Single Price'),
|
||||
default_currency=currency_code_default(),
|
||||
help_text=_('Single quantity price'),
|
||||
decimal_places=4,
|
||||
max_digits=19,
|
||||
|
25
InvenTree/company/migrations/0039_auto_20210701_0509.py
Normal file
25
InvenTree/company/migrations/0039_auto_20210701_0509.py
Normal file
@ -0,0 +1,25 @@
|
||||
# Generated by Django 3.2.4 on 2021-07-01 05:09
|
||||
|
||||
import InvenTree.fields
|
||||
from django.db import migrations
|
||||
import djmoney.models.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('company', '0038_manufacturerpartparameter'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='supplierpricebreak',
|
||||
name='price',
|
||||
field=InvenTree.fields.InvenTreeModelMoneyField(currency_choices=[], decimal_places=4, default_currency='', help_text='Unit price at specified quantity', max_digits=19, null=True, verbose_name='Price'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='supplierpricebreak',
|
||||
name='price_currency',
|
||||
field=djmoney.models.fields.CurrencyField(choices=[], default='', editable=False, max_length=3),
|
||||
),
|
||||
]
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -10,15 +10,12 @@ from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from mptt.fields import TreeNodeChoiceField
|
||||
|
||||
from djmoney.forms.fields import MoneyField
|
||||
|
||||
from InvenTree.forms import HelperForm
|
||||
from InvenTree.fields import RoundingDecimalFormField
|
||||
from InvenTree.fields import InvenTreeMoneyField, RoundingDecimalFormField
|
||||
from InvenTree.fields import DatePickerFormField
|
||||
|
||||
from InvenTree.helpers import clean_decimal
|
||||
|
||||
from common.models import InvenTreeSetting
|
||||
from common.forms import MatchItemForm
|
||||
|
||||
import part.models
|
||||
@ -321,9 +318,8 @@ class OrderMatchItemForm(MatchItemForm):
|
||||
)
|
||||
# set price field
|
||||
elif 'price' in col_guess.lower():
|
||||
return MoneyField(
|
||||
return InvenTreeMoneyField(
|
||||
label=_(col_guess),
|
||||
default_currency=InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY'),
|
||||
decimal_places=5,
|
||||
max_digits=19,
|
||||
required=False,
|
||||
|
35
InvenTree/order/migrations/0047_auto_20210701_0509.py
Normal file
35
InvenTree/order/migrations/0047_auto_20210701_0509.py
Normal file
@ -0,0 +1,35 @@
|
||||
# Generated by Django 3.2.4 on 2021-07-01 05:09
|
||||
|
||||
import InvenTree.fields
|
||||
from django.db import migrations
|
||||
import djmoney.models.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('order', '0046_purchaseorderlineitem_destination'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='purchaseorderlineitem',
|
||||
name='purchase_price',
|
||||
field=InvenTree.fields.InvenTreeModelMoneyField(blank=True, currency_choices=[], decimal_places=4, default_currency='', help_text='Unit purchase price', max_digits=19, null=True, verbose_name='Purchase Price'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='purchaseorderlineitem',
|
||||
name='purchase_price_currency',
|
||||
field=djmoney.models.fields.CurrencyField(choices=[], default='', editable=False, max_length=3),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='salesorderlineitem',
|
||||
name='sale_price',
|
||||
field=InvenTree.fields.InvenTreeModelMoneyField(blank=True, currency_choices=[], decimal_places=4, default_currency='', help_text='Unit sale price', max_digits=19, null=True, verbose_name='Sale Price'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='salesorderlineitem',
|
||||
name='sale_price_currency',
|
||||
field=djmoney.models.fields.CurrencyField(choices=[], default='', editable=False, max_length=3),
|
||||
),
|
||||
]
|
@ -17,19 +17,15 @@ from django.contrib.auth.models import User
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.settings import currency_code_default
|
||||
|
||||
from markdownx.models import MarkdownxField
|
||||
from mptt.models import TreeForeignKey
|
||||
|
||||
from djmoney.models.fields import MoneyField
|
||||
|
||||
from users import models as UserModels
|
||||
from part import models as PartModels
|
||||
from stock import models as stock_models
|
||||
from company.models import Company, SupplierPart
|
||||
|
||||
from InvenTree.fields import RoundingDecimalField
|
||||
from InvenTree.fields import InvenTreeModelMoneyField, RoundingDecimalField
|
||||
from InvenTree.helpers import decimal2string, increment, getSetting
|
||||
from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockStatus, StockHistoryCode
|
||||
from InvenTree.models import InvenTreeAttachment
|
||||
@ -664,10 +660,9 @@ class PurchaseOrderLineItem(OrderLineItem):
|
||||
|
||||
received = models.DecimalField(decimal_places=5, max_digits=15, default=0, verbose_name=_('Received'), help_text=_('Number of items received'))
|
||||
|
||||
purchase_price = MoneyField(
|
||||
purchase_price = InvenTreeModelMoneyField(
|
||||
max_digits=19,
|
||||
decimal_places=4,
|
||||
default_currency=currency_code_default(),
|
||||
null=True, blank=True,
|
||||
verbose_name=_('Purchase Price'),
|
||||
help_text=_('Unit purchase price'),
|
||||
@ -716,10 +711,9 @@ class SalesOrderLineItem(OrderLineItem):
|
||||
|
||||
part = models.ForeignKey('part.Part', on_delete=models.SET_NULL, related_name='sales_order_line_items', null=True, verbose_name=_('Part'), help_text=_('Part'), limit_choices_to={'salable': True})
|
||||
|
||||
sale_price = MoneyField(
|
||||
sale_price = InvenTreeModelMoneyField(
|
||||
max_digits=19,
|
||||
decimal_places=4,
|
||||
default_currency=currency_code_default(),
|
||||
null=True, blank=True,
|
||||
verbose_name=_('Sale Price'),
|
||||
help_text=_('Unit sale price'),
|
||||
|
35
InvenTree/part/migrations/0069_auto_20210701_0509.py
Normal file
35
InvenTree/part/migrations/0069_auto_20210701_0509.py
Normal file
@ -0,0 +1,35 @@
|
||||
# Generated by Django 3.2.4 on 2021-07-01 05:09
|
||||
|
||||
import InvenTree.fields
|
||||
from django.db import migrations
|
||||
import djmoney.models.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('part', '0068_part_unique_part'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='partinternalpricebreak',
|
||||
name='price',
|
||||
field=InvenTree.fields.InvenTreeModelMoneyField(currency_choices=[], decimal_places=4, default_currency='', help_text='Unit price at specified quantity', max_digits=19, null=True, verbose_name='Price'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='partinternalpricebreak',
|
||||
name='price_currency',
|
||||
field=djmoney.models.fields.CurrencyField(choices=[], default='', editable=False, max_length=3),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='partsellpricebreak',
|
||||
name='price',
|
||||
field=InvenTree.fields.InvenTreeModelMoneyField(currency_choices=[], decimal_places=4, default_currency='', help_text='Unit price at specified quantity', max_digits=19, null=True, verbose_name='Price'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='partsellpricebreak',
|
||||
name='price_currency',
|
||||
field=djmoney.models.fields.CurrencyField(choices=[], default='', editable=False, max_length=3),
|
||||
),
|
||||
]
|
@ -39,7 +39,7 @@ from InvenTree import helpers
|
||||
from InvenTree import validators
|
||||
from InvenTree.models import InvenTreeTree, InvenTreeAttachment
|
||||
from InvenTree.fields import InvenTreeURLField
|
||||
from InvenTree.helpers import decimal2string, normalize
|
||||
from InvenTree.helpers import decimal2string, normalize, decimal2money
|
||||
|
||||
from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus, SalesOrderStatus
|
||||
|
||||
@ -2414,7 +2414,7 @@ class BomItem(models.Model):
|
||||
return "{n} x {child} to make {parent}".format(
|
||||
parent=self.part.full_name,
|
||||
child=self.sub_part.full_name,
|
||||
n=helpers.decimal2string(self.quantity))
|
||||
n=decimal2string(self.quantity))
|
||||
|
||||
def available_stock(self):
|
||||
"""
|
||||
@ -2498,12 +2498,12 @@ class BomItem(models.Model):
|
||||
return required
|
||||
|
||||
@property
|
||||
def price_range(self):
|
||||
def price_range(self, internal=False):
|
||||
""" Return the price-range for this BOM item. """
|
||||
|
||||
# get internal price setting
|
||||
use_internal = common.models.InvenTreeSetting.get_setting('PART_BOM_USE_INTERNAL_PRICE', False)
|
||||
prange = self.sub_part.get_price_range(self.quantity, intenal=use_internal)
|
||||
prange = self.sub_part.get_price_range(self.quantity, internal=use_internal and internal)
|
||||
|
||||
if prange is None:
|
||||
return prange
|
||||
@ -2511,11 +2511,11 @@ class BomItem(models.Model):
|
||||
pmin, pmax = prange
|
||||
|
||||
if pmin == pmax:
|
||||
return decimal2string(pmin)
|
||||
return decimal2money(pmin)
|
||||
|
||||
# Convert to better string representation
|
||||
pmin = decimal2string(pmin)
|
||||
pmax = decimal2string(pmax)
|
||||
pmin = decimal2money(pmin)
|
||||
pmax = decimal2money(pmax)
|
||||
|
||||
return "{pmin} to {pmax}".format(pmin=pmin, pmax=pmax)
|
||||
|
||||
|
@ -377,7 +377,7 @@ class PartStarSerializer(InvenTreeModelSerializer):
|
||||
class BomItemSerializer(InvenTreeModelSerializer):
|
||||
""" Serializer for BomItem object """
|
||||
|
||||
# price_range = serializers.CharField(read_only=True)
|
||||
price_range = serializers.CharField(read_only=True)
|
||||
|
||||
quantity = serializers.FloatField()
|
||||
|
||||
@ -492,7 +492,7 @@ class BomItemSerializer(InvenTreeModelSerializer):
|
||||
'reference',
|
||||
'sub_part',
|
||||
'sub_part_detail',
|
||||
# 'price_range',
|
||||
'price_range',
|
||||
'validated',
|
||||
]
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% settings_value "PART_INTERNAL_PRICE" as show_internal_price %}
|
||||
{% settings_value 'PART_SHOW_RELATED' as show_related %}
|
||||
|
||||
<ul class='list-group'>
|
||||
<li class='list-group-item'>
|
||||
@ -112,12 +113,14 @@
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if show_related %}
|
||||
<li class='list-group-item {% if tab == "related" %}active{% endif %}' title='{% trans "Related Parts" %}'>
|
||||
<a href='{% url "part-related" part.id %}'>
|
||||
<span class='menu-tab-icon fas fa-random sidebar-icon'></span>
|
||||
{% trans "Related Parts" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class='list-group-item {% if tab == "attachments" %}active{% endif %}' title='{% trans "Attachments" %}'>
|
||||
<a href='{% url "part-attachments" part.id %}'>
|
||||
<span class='menu-tab-icon fas fa-paperclip sidebar-icon'></span>
|
||||
|
@ -2982,7 +2982,7 @@ class PartSalePriceBreakCreate(AjaxCreateView):
|
||||
|
||||
initials['part'] = self.get_part()
|
||||
|
||||
default_currency = settings.BASE_CURRENCY
|
||||
default_currency = inventree_settings.currency_code_default()
|
||||
currency = CURRENCIES.get(default_currency, None)
|
||||
|
||||
if currency is not None:
|
||||
|
25
InvenTree/stock/migrations/0065_auto_20210701_0509.py
Normal file
25
InvenTree/stock/migrations/0065_auto_20210701_0509.py
Normal file
@ -0,0 +1,25 @@
|
||||
# Generated by Django 3.2.4 on 2021-07-01 05:09
|
||||
|
||||
import InvenTree.fields
|
||||
from django.db import migrations
|
||||
import djmoney.models.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('stock', '0064_auto_20210621_1724'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='stockitem',
|
||||
name='purchase_price',
|
||||
field=InvenTree.fields.InvenTreeModelMoneyField(blank=True, currency_choices=[], 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.AlterField(
|
||||
model_name='stockitem',
|
||||
name='purchase_price_currency',
|
||||
field=djmoney.models.fields.CurrencyField(choices=[], default='', editable=False, max_length=3),
|
||||
),
|
||||
]
|
@ -20,14 +20,10 @@ from django.contrib.auth.models import User
|
||||
from django.db.models.signals import pre_delete
|
||||
from django.dispatch import receiver
|
||||
|
||||
from common.settings import currency_code_default
|
||||
|
||||
from markdownx.models import MarkdownxField
|
||||
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
|
||||
from djmoney.models.fields import MoneyField
|
||||
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from datetime import datetime, timedelta
|
||||
from InvenTree import helpers
|
||||
@ -38,7 +34,7 @@ import label.models
|
||||
|
||||
from InvenTree.status_codes import StockStatus, StockHistoryCode
|
||||
from InvenTree.models import InvenTreeTree, InvenTreeAttachment
|
||||
from InvenTree.fields import InvenTreeURLField
|
||||
from InvenTree.fields import InvenTreeModelMoneyField, InvenTreeURLField
|
||||
|
||||
from users.models import Owner
|
||||
|
||||
@ -533,10 +529,9 @@ class StockItem(MPTTModel):
|
||||
help_text=_('Stock Item Notes')
|
||||
)
|
||||
|
||||
purchase_price = MoneyField(
|
||||
purchase_price = InvenTreeModelMoneyField(
|
||||
max_digits=19,
|
||||
decimal_places=4,
|
||||
default_currency=currency_code_default(),
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('Purchase Price'),
|
||||
|
@ -12,6 +12,13 @@
|
||||
|
||||
{% block settings %}
|
||||
|
||||
<table class='table table-striped table-condensed'>
|
||||
{% include "InvenTree/settings/header.html" %}
|
||||
<tbody>
|
||||
{% include "InvenTree/settings/setting.html" with key="INVENTREE_DEFAULT_CURRENCY" icon="fa-globe" %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table class='table table-striped table-condensed'>
|
||||
<tbody>
|
||||
<tr>
|
||||
|
@ -21,6 +21,7 @@
|
||||
{% include "InvenTree/settings/setting.html" with key="PART_ALLOW_EDIT_IPN" %}
|
||||
{% include "InvenTree/settings/setting.html" with key="PART_SHOW_QUANTITY_IN_FORMS" icon="fa-hashtag" %}
|
||||
{% include "InvenTree/settings/setting.html" with key="PART_SHOW_PRICE_IN_FORMS" icon="fa-dollar-sign" %}
|
||||
{% include "InvenTree/settings/setting.html" with key="PART_SHOW_RELATED" icon="fa-random" %}
|
||||
{% include "InvenTree/settings/setting.html" with key="PART_RECENT_COUNT" icon="fa-clock" %}
|
||||
<tr><td colspan='5 '></td></tr>
|
||||
{% include "InvenTree/settings/setting.html" with key="PART_TEMPLATE" icon="fa-clone" %}
|
||||
|
@ -259,26 +259,19 @@ function loadBomTable(table, options) {
|
||||
sortable: true,
|
||||
});
|
||||
|
||||
/*
|
||||
|
||||
// TODO - Re-introduce the pricing column at a later stage,
|
||||
// once the pricing has been "fixed"
|
||||
// O.W. 2020-11-24
|
||||
|
||||
cols.push(
|
||||
{
|
||||
field: 'price_range',
|
||||
title: '{% trans "Price" %}',
|
||||
title: '{% trans "Buy Price" %}',
|
||||
sortable: true,
|
||||
formatter: function(value, row, index, field) {
|
||||
if (value) {
|
||||
return value;
|
||||
} else {
|
||||
return "<span class='warning-msg'>{% trans "No pricing available" %}</span>";
|
||||
return "<span class='warning-msg'>{% trans 'No pricing available' %}</span>";
|
||||
}
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
cols.push({
|
||||
field: 'optional',
|
||||
|
@ -231,6 +231,7 @@ function loadBuildOrderAllocationTable(table, options={}) {
|
||||
{
|
||||
field: 'quantity',
|
||||
title: '{% trans "Quantity" %}',
|
||||
sortable: true,
|
||||
}
|
||||
]
|
||||
});
|
||||
|
@ -391,6 +391,7 @@ function loadSalesOrderAllocationTable(table, options={}) {
|
||||
{
|
||||
field: 'quantity',
|
||||
title: '{% trans "Quantity" %}',
|
||||
sortable: true,
|
||||
}
|
||||
]
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM python:alpine as base
|
||||
FROM alpine:3.13 as base
|
||||
|
||||
# GitHub source
|
||||
ARG repository="https://github.com/inventree/InvenTree.git"
|
||||
@ -57,7 +57,7 @@ RUN apk add --no-cache cairo cairo-dev pango pango-dev
|
||||
RUN apk add --no-cache fontconfig ttf-droid ttf-liberation ttf-dejavu ttf-opensans ttf-ubuntu-font-family font-croscore font-noto
|
||||
|
||||
# Python
|
||||
RUN apk add --no-cache python3 python3-dev
|
||||
RUN apk add --no-cache python3 python3-dev py3-pip
|
||||
|
||||
# SQLite support
|
||||
RUN apk add --no-cache sqlite
|
||||
|
@ -32,7 +32,7 @@ echo "Starting InvenTree server..."
|
||||
|
||||
# Wait for the database to be ready
|
||||
cd ${INVENTREE_HOME}/InvenTree
|
||||
python manage.py wait_for_db
|
||||
python3 manage.py wait_for_db
|
||||
|
||||
sleep 10
|
||||
|
||||
@ -40,10 +40,10 @@ echo "Running InvenTree database migrations..."
|
||||
|
||||
# We assume at this stage that the database is up and running
|
||||
# Ensure that the database schema are up to date
|
||||
python manage.py check || exit 1
|
||||
python manage.py migrate --noinput || exit 1
|
||||
python manage.py migrate --run-syncdb || exit 1
|
||||
python manage.py clearsessions || exit 1
|
||||
python3 manage.py check || exit 1
|
||||
python3 manage.py migrate --noinput || exit 1
|
||||
python3 manage.py migrate --run-syncdb || exit 1
|
||||
python3 manage.py clearsessions || exit 1
|
||||
|
||||
# Launch a development server
|
||||
python manage.py runserver ${INVENTREE_WEB_ADDR}:${INVENTREE_WEB_PORT}
|
||||
python3 manage.py runserver ${INVENTREE_WEB_ADDR}:${INVENTREE_WEB_PORT}
|
||||
|
@ -11,9 +11,9 @@ sleep 5
|
||||
|
||||
# Wait for the database to be ready
|
||||
cd InvenTree
|
||||
python manage.py wait_for_db
|
||||
python3 manage.py wait_for_db
|
||||
|
||||
sleep 10
|
||||
|
||||
# Now we can launch the background worker process
|
||||
python manage.py qcluster
|
||||
python3 manage.py qcluster
|
||||
|
@ -23,7 +23,7 @@ echo "Starting InvenTree server..."
|
||||
|
||||
# Wait for the database to be ready
|
||||
cd $INVENTREE_MNG_DIR
|
||||
python manage.py wait_for_db
|
||||
python3 manage.py wait_for_db
|
||||
|
||||
sleep 10
|
||||
|
||||
@ -31,12 +31,12 @@ echo "Running InvenTree database migrations and collecting static files..."
|
||||
|
||||
# We assume at this stage that the database is up and running
|
||||
# Ensure that the database schema are up to date
|
||||
python manage.py check || exit 1
|
||||
python manage.py migrate --noinput || exit 1
|
||||
python manage.py migrate --run-syncdb || exit 1
|
||||
python manage.py prerender || exit 1
|
||||
python manage.py collectstatic --noinput || exit 1
|
||||
python manage.py clearsessions || exit 1
|
||||
python3 manage.py check || exit 1
|
||||
python3 manage.py migrate --noinput || exit 1
|
||||
python3 manage.py migrate --run-syncdb || exit 1
|
||||
python3 manage.py prerender || exit 1
|
||||
python3 manage.py collectstatic --noinput || exit 1
|
||||
python3 manage.py clearsessions || exit 1
|
||||
|
||||
# Now we can launch the server
|
||||
gunicorn -c $INVENTREE_HOME/gunicorn.conf.py InvenTree.wsgi -b 0.0.0.0:$INVENTREE_WEB_PORT
|
@ -6,9 +6,9 @@ sleep 5
|
||||
|
||||
# Wait for the database to be ready
|
||||
cd $INVENTREE_MNG_DIR
|
||||
python manage.py wait_for_db
|
||||
python3 manage.py wait_for_db
|
||||
|
||||
sleep 10
|
||||
|
||||
# Now we can launch the background worker process
|
||||
python manage.py qcluster
|
||||
python3 manage.py qcluster
|
Loading…
x
Reference in New Issue
Block a user