diff --git a/deployment_options/docker-compose/Caddyfile b/deployment_options/docker-compose/Caddyfile index d5265b1..ad94900 100644 --- a/deployment_options/docker-compose/Caddyfile +++ b/deployment_options/docker-compose/Caddyfile @@ -1,9 +1,14 @@ :8080 -reverse_proxy mventory:9000 -file_server /static/* { - root assets +handle_path /static/* { + root * /opt/app/assets + file_server +} + +handle { + reverse_proxy mventory:9000 } log { - format json + format json } + diff --git a/deployment_options/docker-compose/docker-compose.yml b/deployment_options/docker-compose/docker-compose.yml index 3bf62ae..d03f876 100644 --- a/deployment_options/docker-compose/docker-compose.yml +++ b/deployment_options/docker-compose/docker-compose.yml @@ -43,6 +43,7 @@ services: - mventory volumes: - $PWD/deployment_options/docker-compose/Caddyfile:/etc/caddy/Caddyfile + - $PWD/:/opt/app ports: - "8180:8080" diff --git a/inventory/admin.py b/inventory/admin.py index 206aae6..66fd5c5 100644 --- a/inventory/admin.py +++ b/inventory/admin.py @@ -6,7 +6,9 @@ Room, StorageUnit, StorageBin, - Component) + Component, + Supplier, + ComponentSupplier) admin.site.register(ComponentMeasurementUnit) admin.site.register(Building) @@ -14,3 +16,5 @@ admin.site.register(StorageUnit) admin.site.register(StorageBin) admin.site.register(Component) +admin.site.register(Supplier) +admin.site.register(ComponentSupplier) diff --git a/inventory/migrations/0006_auto_20220305_1803.py b/inventory/migrations/0006_auto_20220305_1803.py new file mode 100644 index 0000000..11d5796 --- /dev/null +++ b/inventory/migrations/0006_auto_20220305_1803.py @@ -0,0 +1,45 @@ +# Generated by Django 3.2.5 on 2022-03-05 18:03 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0005_auto_20210623_0537'), + ] + + operations = [ + migrations.CreateModel( + name='ComponentSupplier', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('cost', models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True)), + ('markup_percentage', models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True)), + ('price', models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True)), + ('component', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='components', to='inventory.component')), + ], + ), + migrations.CreateModel( + name='Supplier', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('description', models.CharField(blank=True, max_length=200, null=True)), + ('website', models.URLField(blank=True, null=True)), + ('contact_email', models.EmailField(blank=True, max_length=254, null=True)), + ('components_available', models.ManyToManyField(through='inventory.ComponentSupplier', to='inventory.Component')), + ], + ), + migrations.AddField( + model_name='componentsupplier', + name='supplier', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='suppliers', to='inventory.supplier'), + ), + migrations.AddField( + model_name='component', + name='supplier_options', + field=models.ManyToManyField(through='inventory.ComponentSupplier', to='inventory.Supplier'), + ), + ] diff --git a/inventory/migrations/0007_componentsupplier_currency.py b/inventory/migrations/0007_componentsupplier_currency.py new file mode 100644 index 0000000..0231b9f --- /dev/null +++ b/inventory/migrations/0007_componentsupplier_currency.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.5 on 2022-03-05 19:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0006_auto_20220305_1803'), + ] + + operations = [ + migrations.AddField( + model_name='componentsupplier', + name='currency', + field=models.CharField(choices=[('GBP', 'British Pound'), ('EUR', 'Euro'), ('CHF', 'Swiss Franc'), ('USD', 'US Dollar')], default='GBP', max_length=3), + ), + ] diff --git a/inventory/migrations/0008_auto_20220305_2316.py b/inventory/migrations/0008_auto_20220305_2316.py new file mode 100644 index 0000000..9c9b0c0 --- /dev/null +++ b/inventory/migrations/0008_auto_20220305_2316.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.5 on 2022-03-05 23:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0007_componentsupplier_currency'), + ] + + operations = [ + migrations.RenameField( + model_name='componentsupplier', + old_name='cost', + new_name='bought_at', + ), + migrations.RenameField( + model_name='componentsupplier', + old_name='price', + new_name='members_price', + ), + migrations.AddField( + model_name='componentsupplier', + name='buying_discount_from_rrp', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True), + ), + migrations.AddField( + model_name='componentsupplier', + name='rrp', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True), + ), + ] diff --git a/inventory/models.py b/inventory/models.py index 4ab70f9..e108c83 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -10,6 +10,16 @@ class ComponentMeasurementUnit(ExportModelOperationsMixin('ComponentMeasurementU def __str__(self): return self.unit_name +class Supplier(ExportModelOperationsMixin('Supplier'),models.Model): + name = models.CharField(max_length=100) + description = models.CharField(max_length=200,null=True, blank=True) + website = models.URLField(null=True,blank=True) + contact_email = models.EmailField(null=True,blank=True) + components_available = models.ManyToManyField('Component', through='ComponentSupplier') + + def __str__(self): + return self.name + class Building(ExportModelOperationsMixin('Building'), models.Model): name = models.CharField(max_length=200) @@ -57,6 +67,37 @@ class Component(ExportModelOperationsMixin('Component'), models.Model): storage_bin = models.ManyToManyField(StorageBin) measurement_unit = models.ForeignKey(ComponentMeasurementUnit, on_delete=models.CASCADE) qty = models.IntegerField(default=0, validators=[MinValueValidator(0)]) + supplier_options = models.ManyToManyField('Supplier', through='ComponentSupplier') + def __str__(self): return self.name + + +class ComponentSupplier(ExportModelOperationsMixin('ComponentSupplier'),models.Model): + BRITISH_POUND = 'GBP' + EURO = 'EUR' + SWISS_FRANC = 'CHF' + US_DOLLAR = 'USD' + + CURRENCIES = [ + (BRITISH_POUND, 'British Pound'), + (EURO, 'Euro'), + (SWISS_FRANC, 'Swiss Franc'), + (US_DOLLAR, 'US Dollar') + ] + + component = models.ForeignKey('Component',models.SET_NULL,related_name='components',null=True,blank=True) + supplier = models.ForeignKey('Supplier',models.SET_NULL,related_name='suppliers',null=True,blank=True) + bought_at = models.DecimalField(decimal_places=2,max_digits=7,null=True,blank=True) + rrp = models.DecimalField(decimal_places=2,max_digits=7,null=True,blank=True) + buying_discount_from_rrp = models.DecimalField(decimal_places=2,max_digits=7,null=True,blank=True) + markup_percentage = models.DecimalField(decimal_places=2,max_digits=7,null=True,blank=True) + members_price = models.DecimalField(decimal_places=2,max_digits=7,null=True,blank=True) + currency = models.CharField(max_length = 3, + choices = CURRENCIES, + default = BRITISH_POUND) + + + def __str__(self): + return f"{self.component} from {self.supplier}: {self.price}{self.currency}" diff --git a/inventory/serializers.py b/inventory/serializers.py index f385951..a153cdd 100644 --- a/inventory/serializers.py +++ b/inventory/serializers.py @@ -1,6 +1,6 @@ import os from .utils import OctopartClient -from .models import Building, Room, StorageUnit, StorageBin, Component, ComponentMeasurementUnit +from .models import Building, Room, StorageUnit, StorageBin, Component, ComponentMeasurementUnit, ComponentSupplier, Supplier from rest_framework import serializers @@ -25,8 +25,14 @@ class Meta: model = StorageBin fields = ['url', 'name', 'short_code', 'unit_row','unit_column', 'storage_unit'] +class SupplierSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Supplier + fields = ['url', 'name', 'description'] + class ComponentSerializer(serializers.HyperlinkedModelSerializer): octopart_data = serializers.SerializerMethodField() + component_prices = serializers.SerializerMethodField() def get_octopart_data(self, obj): op_data = {} @@ -44,9 +50,28 @@ def get_octopart_data(self, obj): op_data["datasheet_url"] = None return op_data + def get_component_prices(self, obj): + prices = [] + + for supplier in obj.supplier_options.all(): + suppliers = serializers.HyperlinkedRelatedField( + view_name='suppliers', + lookup_field='supplier', + read_only=True + ) + details = {} + details['supplier'] = supplier.name + cs_details = ComponentSupplier.objects.get(supplier=supplier, component=obj) + details['price'] = cs_details.members_price + details['currency'] = cs_details.currency + details['included_donation_amount'] = cs_details.members_price - cs_details.bought_at + prices.append(details) + + return prices + class Meta: model = Component - fields = ['url', 'name', 'sku', 'mpn', 'upc', 'octopart_data', 'storage_bin','measurement_unit', 'qty'] + fields = ['url', 'name', 'sku', 'mpn', 'upc', 'octopart_data', 'storage_bin','measurement_unit', 'qty', 'component_prices'] depth = 4 class ComponentMeasurementUnitSerializer(serializers.HyperlinkedModelSerializer): diff --git a/inventory/views.py b/inventory/views.py index cb0efbf..b066249 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -1,7 +1,7 @@ from django.shortcuts import render # Create your views here. -from .models import Building, Room, StorageUnit, StorageBin, Component, ComponentMeasurementUnit +from .models import Building, Room, StorageUnit, StorageBin, Component, ComponentMeasurementUnit, Supplier from rest_framework import viewsets, permissions, filters from inventory.serializers import ( BuildingSerializer, @@ -9,7 +9,8 @@ StorageUnitSerializer, StorageBinSerializer, ComponentSerializer, - ComponentMeasurementUnitSerializer + ComponentMeasurementUnitSerializer, + SupplierSerializer ) @@ -80,3 +81,11 @@ class ComponentMeasurementUnitViewSet(viewsets.ModelViewSet): queryset = ComponentMeasurementUnit.objects.all() serializer_class = ComponentMeasurementUnitSerializer permission_classes = [permissions.IsAuthenticatedOrReadOnly] + +class SupplierViewSet(viewsets.ModelViewSet): + """ + API endpoint that allows suppliers to be viewed or edited. + """ + queryset = Supplier.objects.all() + serializer_class = SupplierSerializer + permission_classes = [permissions.IsAuthenticatedOrReadOnly] diff --git a/mventory/urls.py b/mventory/urls.py index ae2a03c..60c0bab 100644 --- a/mventory/urls.py +++ b/mventory/urls.py @@ -27,6 +27,7 @@ router.register(r'storage_bins', views.StorageBinViewSet) router.register(r'components', views.ComponentViewSet) router.register(r'component_measurements', views.ComponentMeasurementUnitViewSet) +router.register(r'suppliers', views.SupplierViewSet) urlpatterns = [ path('', views.index), diff --git a/scripts/prod_deploy.sh b/scripts/prod_deploy.sh index 7b09d66..3a8eb77 100755 --- a/scripts/prod_deploy.sh +++ b/scripts/prod_deploy.sh @@ -8,6 +8,4 @@ python manage.py wait_for_db python manage.py collectstatic --noinput python manage.py migrate -#unset MVENTORY_DEBUG - uwsgi --ini uwsgi.ini