diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 1e993e5068..6c9d4a9b55 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -83,6 +83,7 @@ INSTALLED_APPS = [ 'django.contrib.staticfiles', # InvenTree apps + 'common.apps.CommonConfig', 'part.apps.PartConfig', 'stock.apps.StockConfig', 'company.apps.CompanyConfig', diff --git a/InvenTree/common/__init__.py b/InvenTree/common/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/InvenTree/common/admin.py b/InvenTree/common/admin.py new file mode 100644 index 0000000000..1628f811a7 --- /dev/null +++ b/InvenTree/common/admin.py @@ -0,0 +1,10 @@ +from django.contrib import admin + +from .models import Currency + + +class CurrencyAdmin(admin.ModelAdmin): + list_display = ('symbol', 'suffix', 'description', 'value', 'base') + + +admin.site.register(Currency, CurrencyAdmin) diff --git a/InvenTree/common/apps.py b/InvenTree/common/apps.py new file mode 100644 index 0000000000..5f2f078473 --- /dev/null +++ b/InvenTree/common/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class CommonConfig(AppConfig): + name = 'common' diff --git a/InvenTree/common/fixtures/currency.yaml b/InvenTree/common/fixtures/currency.yaml new file mode 100644 index 0000000000..639b0751df --- /dev/null +++ b/InvenTree/common/fixtures/currency.yaml @@ -0,0 +1,16 @@ +# Test fixtures for Currency objects + +- model: common.currency + fields: + symbol: '$' + suffix: 'AUD' + description: 'Australian Dollars' + base: True + +- model: common.currency + fields: + symbol: '$' + suffix: 'USD' + description: 'US Dollars' + base: False + value: 1.4 \ No newline at end of file diff --git a/InvenTree/common/migrations/0001_initial.py b/InvenTree/common/migrations/0001_initial.py new file mode 100644 index 0000000000..a3a357b86f --- /dev/null +++ b/InvenTree/common/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# Generated by Django 2.2.4 on 2019-09-02 23:02 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Currency', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('symbol', models.CharField(help_text='Currency Symbol e.g. $', max_length=10)), + ('suffix', models.CharField(help_text='Currency Suffix e.g. AUD', max_length=10, unique=True)), + ('description', models.CharField(help_text='Currency Description', max_length=100)), + ('value', models.DecimalField(decimal_places=5, help_text='Currency Value', max_digits=10, validators=[django.core.validators.MinValueValidator(1e-05), django.core.validators.MaxValueValidator(100000)])), + ('base', models.BooleanField(default=False, help_text='Use this currency as the base currency')), + ], + ), + ] diff --git a/InvenTree/common/migrations/0002_auto_20190902_2304.py b/InvenTree/common/migrations/0002_auto_20190902_2304.py new file mode 100644 index 0000000000..a8daf16139 --- /dev/null +++ b/InvenTree/common/migrations/0002_auto_20190902_2304.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.4 on 2019-09-02 23:04 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='currency', + options={'verbose_name_plural': 'Currencies'}, + ), + ] diff --git a/InvenTree/common/migrations/0003_auto_20190902_2310.py b/InvenTree/common/migrations/0003_auto_20190902_2310.py new file mode 100644 index 0000000000..7bfdefa8c4 --- /dev/null +++ b/InvenTree/common/migrations/0003_auto_20190902_2310.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.4 on 2019-09-02 23:10 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0002_auto_20190902_2304'), + ] + + operations = [ + migrations.AlterField( + model_name='currency', + name='value', + field=models.DecimalField(decimal_places=5, default=1.0, help_text='Currency Value', max_digits=10, validators=[django.core.validators.MinValueValidator(1e-05), django.core.validators.MaxValueValidator(100000)]), + ), + ] diff --git a/InvenTree/common/migrations/__init__.py b/InvenTree/common/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py new file mode 100644 index 0000000000..de3b9bab2c --- /dev/null +++ b/InvenTree/common/models.py @@ -0,0 +1,79 @@ +""" +Common database model definitions. +These models are 'generic' and do not fit a particular business logic object. +""" + +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models +from django.utils.translation import ugettext as _ +from django.core.validators import MinValueValidator, MaxValueValidator + + +class Currency(models.Model): + """ + A Currency object represents a particular unit of currency. + Each Currency has a scaling factor which relates it to the base currency. + There must be one (and only one) currency which is selected as the base currency, + and each other currency is calculated relative to it. + + Attributes: + symbol: Currency symbol e.g. $ + suffix: Currency suffix e.g. AUD + description: Long-form description e.g. "Australian Dollars" + value: The value of this currency compared to the base currency. + base: True if this currency is the base currency + + """ + + symbol = models.CharField(max_length=10, blank=False, unique=False, help_text=_('Currency Symbol e.g. $')) + + suffix = models.CharField(max_length=10, blank=False, unique=True, help_text=_('Currency Suffix e.g. AUD')) + + description = models.CharField(max_length=100, blank=False, help_text=_('Currency Description')) + + value = models.DecimalField(default=1.0, max_digits=10, decimal_places=5, validators=[MinValueValidator(0.00001), MaxValueValidator(100000)], help_text=_('Currency Value')) + + base = models.BooleanField(default=False, help_text=_('Use this currency as the base currency')) + + class Meta: + verbose_name_plural = 'Currencies' + + def __str__(self): + """ Format string for currency representation """ + s = "{sym} {suf} - {desc}".format( + sym=self.symbol, + suf=self.suffix, + desc=self.description + ) + + if self.base: + s += " (Base)" + + else: + s += " = {v}".format(v=self.value) + + return s + + def save(self, *args, **kwargs): + """ Validate the model before saving + + - Ensure that there is only one base currency! + """ + + # If this currency is set as the base currency, ensure no others are + if self.base: + for cur in Currency.objects.filter(base=True).exclude(pk=self.pk): + cur.base = False + cur.save() + + # If there are no currencies set as the base currency, set this as base + if not Currency.objects.filter(base=True).exists(): + self.base = True + + # If this is the base currency, ensure value is set to unity + if self.base: + self.value = 1.0 + + super().save(*args, **kwargs) diff --git a/InvenTree/common/tests.py b/InvenTree/common/tests.py new file mode 100644 index 0000000000..5c6171a65c --- /dev/null +++ b/InvenTree/common/tests.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.test import TestCase + +from .models import Currency + + +class CurrencyTest(TestCase): + """ Tests for Currency model """ + + fixtures = [ + 'currency', + ] + + def test_currency(self): + # Simple test for now (improve this later!) + + self.assertEqual(Currency.objects.count(), 2) diff --git a/InvenTree/common/views.py b/InvenTree/common/views.py new file mode 100644 index 0000000000..60f00ef0ef --- /dev/null +++ b/InvenTree/common/views.py @@ -0,0 +1 @@ +# Create your views here. diff --git a/InvenTree/company/forms.py b/InvenTree/company/forms.py index 76b1c1342c..3fec44cf63 100644 --- a/InvenTree/company/forms.py +++ b/InvenTree/company/forms.py @@ -70,5 +70,6 @@ class EditPriceBreakForm(HelperForm): fields = [ 'part', 'quantity', - 'cost' + 'cost', + 'currency', ] diff --git a/InvenTree/company/migrations/0006_supplierpricebreak_currency.py b/InvenTree/company/migrations/0006_supplierpricebreak_currency.py new file mode 100644 index 0000000000..94f533cf66 --- /dev/null +++ b/InvenTree/company/migrations/0006_supplierpricebreak_currency.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.4 on 2019-09-02 23:34 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0003_auto_20190902_2310'), + ('company', '0005_auto_20190525_2356'), + ] + + operations = [ + migrations.AddField( + model_name='supplierpricebreak', + name='currency', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='common.Currency'), + ), + ] diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index 9d09557f17..f62b2f27a2 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -8,6 +8,7 @@ from __future__ import unicode_literals import os import math +from decimal import Decimal from django.core.validators import MinValueValidator from django.db import models @@ -19,6 +20,7 @@ from django.conf import settings from django.contrib.staticfiles.templatetags.staticfiles import static from InvenTree.status_codes import OrderStatus +from common.models import Currency def rename_company_image(instance, filename): @@ -310,7 +312,8 @@ class SupplierPart(models.Model): # If this price-break quantity is the largest so far, use it! if pb.quantity > pb_quantity: pb_quantity = pb.quantity - pb_cost = pb.cost + # Convert everything to base currency + pb_cost = pb.converted_cost if pb_found: cost = pb_cost * quantity @@ -369,6 +372,7 @@ class SupplierPriceBreak(models.Model): part: Link to a SupplierPart object that this price break applies to quantity: Quantity required for price break cost: Cost at specified quantity + currency: Reference to the currency of this pricebreak (leave empty for base currency) """ part = models.ForeignKey(SupplierPart, on_delete=models.CASCADE, related_name='pricebreaks') @@ -377,6 +381,19 @@ class SupplierPriceBreak(models.Model): cost = models.DecimalField(max_digits=10, decimal_places=5, validators=[MinValueValidator(0)]) + currency = models.ForeignKey(Currency, blank=True, null=True, on_delete=models.SET_NULL) + + @property + def converted_cost(self): + """ Return the cost of this price break, converted to the base currency """ + + scaler = Decimal(1.0) + + if self.currency: + scaler = self.currency.value + + return self.cost * scaler + class Meta: unique_together = ("part", "quantity") diff --git a/InvenTree/company/templates/company/partdetail.html b/InvenTree/company/templates/company/partdetail.html index d945ec31c4..00e1d03b63 100644 --- a/InvenTree/company/templates/company/partdetail.html +++ b/InvenTree/company/templates/company/partdetail.html @@ -88,7 +88,10 @@ InvenTree | {{ company.name }} - Parts {% for pb in part.price_breaks.all %} {{ pb.quantity }} - {{ pb.cost }} + + {% if pb.currency %}{{ pb.currency.symbol }}{% endif %} + {{ pb.cost }} + {% if pb.currency %}{{ pb.currency.suffix }}{% endif %}
diff --git a/Makefile b/Makefile index c898af195d..0b3379a724 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,7 @@ clean: # Perform database migrations (after schema changes are made) migrate: + python3 InvenTree/manage.py makemigrations common python3 InvenTree/manage.py makemigrations company python3 InvenTree/manage.py makemigrations part python3 InvenTree/manage.py makemigrations stock @@ -40,12 +41,12 @@ style: # Run unit tests test: python3 InvenTree/manage.py check - python3 InvenTree/manage.py test build company part stock order + python3 InvenTree/manage.py test build common company order part stock # Run code coverage coverage: python3 InvenTree/manage.py check - coverage run InvenTree/manage.py test build company part stock order InvenTree + coverage run InvenTree/manage.py test build common company order part stock InvenTree coverage html # Install packages required to generate code docs