Browse Source

Add the notion of unit and capacity to resources

Jocelyn Delande 9 years ago
parent
commit
be5e513cab

+ 34 - 0
costs/migrations/0007_auto_20151209_0027.py

@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('costs', '0006_service_document'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='cost',
+            name='capacity_unit',
+            field=models.CharField(blank=True, max_length='10', choices=[('w', 'Watt'), ('mbps', 'Mbps'), ('u', 'U'), ('ipv4', 'IPv4')]),
+        ),
+        migrations.AddField(
+            model_name='cost',
+            name='total_capacity',
+            field=models.FloatField(default=1),
+        ),
+        migrations.AddField(
+            model_name='good',
+            name='capacity_unit',
+            field=models.CharField(blank=True, max_length='10', choices=[('w', 'Watt'), ('mbps', 'Mbps'), ('u', 'U'), ('ipv4', 'IPv4')]),
+        ),
+        migrations.AddField(
+            model_name='good',
+            name='total_capacity',
+            field=models.FloatField(default=1),
+        ),
+    ]

+ 75 - 9
costs/models.py

@@ -3,8 +3,6 @@ from django.core.exceptions import ValidationError
 from django.core.urlresolvers import reverse
 from django.core.urlresolvers import reverse
 from django.db import models
 from django.db import models
 
 
-from .validators import less_than_one
-
 
 
 class Document(models.Model):
 class Document(models.Model):
     """ A document is a scenario or a record from facts, on 1 month.
     """ A document is a scenario or a record from facts, on 1 month.
@@ -62,7 +60,64 @@ class AbstractItem(models.Model):
         abstract = True
         abstract = True
 
 
 
 
-class Cost(AbstractItem):
+class AbstractResource(AbstractItem):
+    UNIT_AMP = 'a'
+    UNIT_MBPS = 'mbps'
+    UNIT_U = 'u'
+    UNIT_IPV4 = 'ipv4'
+    UNIT_ETHERNET_PORT = 'eth'
+
+    capacity_unit = models.CharField(
+        max_length=10,
+        choices=(
+            (UNIT_AMP, 'A'),
+            (UNIT_MBPS, 'Mbps'),
+            (UNIT_U, 'U'),
+            (UNIT_IPV4, 'IPv4'),
+            (UNIT_ETHERNET_PORT, 'ports'),
+        ),
+        blank=True,
+    )
+    total_capacity = models.FloatField(default=1)
+
+    class Meta:
+        abstract = True
+
+    def get_use_class(self):
+        raise NotImplemented
+
+    def used(self, except_by=None):
+        """ Return the used fraction of an item
+
+        :type: Service
+        :param except_by: exclude this service from the math
+        :rtype: float
+        """
+        sharing_costs = self.get_use_class().objects.filter(resource=self)
+        if except_by:
+            sharing_costs = sharing_costs.exclude(service=except_by)
+
+        existing_uses_sum = sum(
+            sharing_costs.values_list('share', flat=True))
+
+        return existing_uses_sum
+
+    def used_fraction(self, *args, **kwargs):
+        return self.used(*args, **kwargs)/self.total_capacity
+
+    def unused(self):
+        return self.total_capacity-self.used()
+
+    def __str__(self):
+        if self.capacity_unit == '':
+            return self.name
+        else:
+            return '{} {:.0f} {}'.format(
+                self.name, self.total_capacity,
+                self.get_capacity_unit_display())
+
+
+class Cost(AbstractResource):
     """ A monthtly cost we have to pay
     """ A monthtly cost we have to pay
     """
     """
     price = models.FloatField(help_text="Coût mensuel")
     price = models.FloatField(help_text="Coût mensuel")
@@ -74,7 +129,7 @@ class Cost(AbstractItem):
         verbose_name = 'Coût'
         verbose_name = 'Coût'
 
 
 
 
-class Good(AbstractItem):
+class Good(AbstractResource):
     """ A good, which replacement is provisioned
     """ A good, which replacement is provisioned
     """
     """
     price = models.FloatField()
     price = models.FloatField()
@@ -92,7 +147,7 @@ class Good(AbstractItem):
 
 
 
 
 class AbstractUse(models.Model):
 class AbstractUse(models.Model):
-    share = models.FloatField(validators=[less_than_one])
+    share = models.FloatField()
     service = models.ForeignKey('Service')
     service = models.ForeignKey('Service')
 
 
     class Meta:
     class Meta:
@@ -100,7 +155,8 @@ class AbstractUse(models.Model):
 
 
     def clean(self):
     def clean(self):
         if hasattr(self, 'resource'):
         if hasattr(self, 'resource'):
-            if (self.resource.used(except_by=self.service) + self.share) > 1:
+            usage = self.resource.used(except_by=self.service) + self.share
+            if usage > self.resource.total_capacity:
                 raise ValidationError(
                 raise ValidationError(
                     "Cannot use more than 100% of {})".format(self.resource))
                     "Cannot use more than 100% of {})".format(self.resource))
 
 
@@ -116,7 +172,11 @@ class AbstractUse(models.Model):
         )
         )
 
 
     def value_share(self):
     def value_share(self):
-        return self.resource.price*self.real_share()
+        return (
+            self.resource.price
+            * self.real_share()
+            / self.resource.total_capacity
+        )
 
 
     def unit_value_share(self):
     def unit_value_share(self):
         if self.service.subscriptions_count == 0:
         if self.service.subscriptions_count == 0:
@@ -129,7 +189,10 @@ class CostUse(AbstractUse):
     resource = models.ForeignKey(Cost)
     resource = models.ForeignKey(Cost)
 
 
     def cost_share(self):
     def cost_share(self):
-        return self.real_share()*self.resource.price
+        return (
+            self.real_share() / self.resource.total_capacity
+            * self.resource.price
+        )
 
 
     def unit_cost_share(self):
     def unit_cost_share(self):
         subscriptions_count = self.service.subscriptions_count
         subscriptions_count = self.service.subscriptions_count
@@ -143,7 +206,10 @@ class GoodUse(AbstractUse):
     resource = models.ForeignKey(Good)
     resource = models.ForeignKey(Good)
 
 
     def monthly_provision_share(self):
     def monthly_provision_share(self):
-        return self.real_share()*self.resource.monthly_provision()
+        return (
+            self.real_share()
+            * self.resource.monthly_provision()
+            / self.resource.total_capacity)
 
 
     def unit_monthly_provision_share(self):
     def unit_monthly_provision_share(self):
         subscriptions_count = self.service.subscriptions_count
         subscriptions_count = self.service.subscriptions_count

+ 2 - 2
costs/templates/costs/document_detail.html

@@ -33,7 +33,7 @@
           {{ cost.name }}
           {{ cost.name }}
         </a>
         </a>
       </td>
       </td>
-      <td>{{ cost.used|percent }}</td>
+      <td>{{ cost.used_fraction|percent }}</td>
       <td>{{ cost.price|price }}</td>
       <td>{{ cost.price|price }}</td>
     </tr>
     </tr>
 {% empty %}
 {% empty %}
@@ -59,7 +59,7 @@
       </a>
       </a>
     </td>
     </td>
     <td>
     <td>
-      {{ good.used|percent }}
+      {{ good.used_fraction|percent }}
     </td>
     </td>
     <td>
     <td>
       {{good.price|price }}
       {{good.price|price }}

+ 75 - 17
costs/tests/test_models.py

@@ -14,23 +14,45 @@ class AbstractUseTests(TestCase):
         self.mailbox_service = Service.objects.create(
         self.mailbox_service = Service.objects.create(
             name='Mailbox', document=self.doc)
             name='Mailbox', document=self.doc)
 
 
-        self.datacenter_cost = Cost.objects.create(
-            name='Datacenter', price=100, document=self.doc)
+        self.electricity_cost = Cost.objects.create(
+            name='electricity',
+            price=10,
+            document=self.doc,
+            capacity_unit='A',
+            total_capacity=4,
+        )
 
 
     def test_can_add_service_share(self):
     def test_can_add_service_share(self):
         use = CostUse(
         use = CostUse(
             service=self.hosting_service,
             service=self.hosting_service,
-            resource=self.datacenter_cost,
+            resource=self.electricity_cost,
             share=0.4)
             share=0.4)
 
 
         use.full_clean()
         use.full_clean()
         use.save()
         use.save()
 
 
+    def test_can_add_service_share_with_custom_unity(self):
+        use = CostUse(
+            service=self.hosting_service,
+            resource=self.electricity_cost,
+            share=2)  # means 2 Amps
+
+        self.hosting_service.subscriptions_count = 2
+        self.hosting_service.save()
+
+        use.full_clean()
+        use.save()
+
+        self.assertEqual(use.share, 2.0)
+        self.assertEqual(use.real_share(), 4.0)
+        self.assertEqual(use.cost_share(), 10)
+        self.assertEqual(use.unit_cost_share(), 5)
+
     def test_cannot_add_excess_share_one(self):
     def test_cannot_add_excess_share_one(self):
         use = CostUse(
         use = CostUse(
             service=self.hosting_service,
             service=self.hosting_service,
-            resource=self.datacenter_cost,
-            share=1.1)
+            resource=self.electricity_cost,
+            share=40.1)
 
 
         with self.assertRaises(ValidationError):
         with self.assertRaises(ValidationError):
             use.full_clean()
             use.full_clean()
@@ -39,7 +61,7 @@ class AbstractUseTests(TestCase):
     def test_add_several_service_share(self):
     def test_add_several_service_share(self):
         u1 = CostUse(
         u1 = CostUse(
             service=self.hosting_service,
             service=self.hosting_service,
-            resource=self.datacenter_cost,
+            resource=self.electricity_cost,
             share=0.4)
             share=0.4)
 
 
         u1.full_clean()
         u1.full_clean()
@@ -47,7 +69,7 @@ class AbstractUseTests(TestCase):
 
 
         u2 = CostUse(
         u2 = CostUse(
             service=self.mailbox_service,
             service=self.mailbox_service,
-            resource=self.datacenter_cost,
+            resource=self.electricity_cost,
             share=0.6)
             share=0.6)
 
 
         u2.full_clean()
         u2.full_clean()
@@ -56,16 +78,18 @@ class AbstractUseTests(TestCase):
     def test_add_several_service_share_excessive_sum(self):
     def test_add_several_service_share_excessive_sum(self):
         u1 = CostUse(
         u1 = CostUse(
             service=self.hosting_service,
             service=self.hosting_service,
-            resource=self.datacenter_cost,
-            share=0.5)
+            resource=self.electricity_cost,
+            share=3)
 
 
         u1.full_clean()
         u1.full_clean()
         u1.save()
         u1.save()
 
 
         u2 = CostUse(
         u2 = CostUse(
             service=self.mailbox_service,
             service=self.mailbox_service,
-            resource=self.datacenter_cost,
-            share=0.6)
+            resource=self.electricity_cost,
+            share=1.1)
+
+        # Would be 4.1 out of 4 amp...
 
 
         with self.assertRaises(ValidationError):
         with self.assertRaises(ValidationError):
             u2.full_clean()
             u2.full_clean()
@@ -74,7 +98,7 @@ class AbstractUseTests(TestCase):
     def test_modify_service_share_no_error(self):
     def test_modify_service_share_no_error(self):
         u1 = CostUse(
         u1 = CostUse(
             service=self.hosting_service,
             service=self.hosting_service,
-            resource=self.datacenter_cost,
+            resource=self.electricity_cost,
             share=1)
             share=1)
 
 
         u1.full_clean()
         u1.full_clean()
@@ -85,12 +109,46 @@ class AbstractUseTests(TestCase):
     def test_real_shares(self):
     def test_real_shares(self):
         u1 = CostUse.objects.create(
         u1 = CostUse.objects.create(
             service=self.hosting_service,
             service=self.hosting_service,
-            resource=self.datacenter_cost,
-            share=0.4)
+            resource=self.electricity_cost,
+            share=1.6)
         u2 = CostUse.objects.create(
         u2 = CostUse.objects.create(
             service=self.hosting_service,
             service=self.hosting_service,
-            resource=self.datacenter_cost,
-            share=0.2)
+            resource=self.electricity_cost,
+            share=0.6)
 
 
-        self.assertEqual(u1.real_share() + u2.real_share(), 1)
+        self.assertEqual(u1.real_share() + u2.real_share(), 4)
         self.assertEqual(u1.share/u2.share, u1.real_share()/u2.real_share())
         self.assertEqual(u1.share/u2.share, u1.real_share()/u2.real_share())
+
+    def test_unit_value_share(self):
+        self.mailbox_service.subscriptions_count = 2
+        self.mailbox_service.share = 0.5
+        self.hosting_service.save()
+
+        self.hosting_service.subscriptions_count = 1
+        self.hosting_service.share = 0.5
+        self.hosting_service.save()
+
+        mailbox_use = CostUse.objects.create(
+            service=self.mailbox_service,
+            resource=self.electricity_cost,
+            share=2)
+
+        hosting_use = CostUse.objects.create(
+            service=self.hosting_service,
+            resource=self.electricity_cost,
+            share=2)
+
+        self.assertEqual(mailbox_use.value_share(), 5)
+        self.assertEqual(mailbox_use.unit_value_share(), 2.5)
+
+        self.assertEqual(hosting_use.value_share(), 5)
+        self.assertEqual(hosting_use.unit_value_share(), 5)
+
+    def test_used(self):
+        CostUse.objects.create(
+            service=self.mailbox_service,
+            resource=self.electricity_cost,
+            share=0.5)
+
+        self.assertEqual(self.electricity_cost.used(), 0.5)
+        self.assertEqual(self.electricity_cost.unused(), 3.5)