models.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. from django.conf import settings
  2. from django.core.exceptions import ValidationError
  3. from django.core.urlresolvers import reverse
  4. from django.db import models
  5. class Document(models.Model):
  6. """ A document is a scenario or a record from facts, on 1 month.
  7. """
  8. TYPE_FACT = 'fact'
  9. TYPE_PLAN = 'plan'
  10. name = models.CharField(max_length=130)
  11. comment = models.TextField(blank=True)
  12. date = models.DateField(auto_now_add=True)
  13. type = models.CharField(max_length=10, choices=(
  14. (TYPE_FACT, 'relevé'),
  15. (TYPE_PLAN, 'scénario/estimation'),
  16. ))
  17. def __str__(self):
  18. return '{} {:%b %Y}'.format(self.name, self.date)
  19. def get_absolute_url(self):
  20. return reverse('detail-document', kwargs={'pk': self.pk})
  21. class AbstractItem(models.Model):
  22. name = models.CharField(max_length=130)
  23. description = models.TextField(blank=True)
  24. document = models.ForeignKey(Document)
  25. def __str__(self):
  26. return self.name
  27. class Meta:
  28. abstract = True
  29. class AbstractResource(AbstractItem):
  30. UNIT_AMP = 'a'
  31. UNIT_MBPS = 'mbps'
  32. UNIT_U = 'u'
  33. UNIT_IPV4 = 'ipv4'
  34. UNIT_ETHERNET_PORT = 'eth'
  35. capacity_unit = models.CharField(
  36. max_length=10,
  37. choices=(
  38. (UNIT_AMP, 'A'),
  39. (UNIT_MBPS, 'Mbps'),
  40. (UNIT_U, 'U'),
  41. (UNIT_IPV4, 'IPv4'),
  42. (UNIT_ETHERNET_PORT, 'ports'),
  43. ),
  44. blank=True,
  45. )
  46. total_capacity = models.FloatField(default=1)
  47. class Meta:
  48. abstract = True
  49. def get_use_class(self):
  50. raise NotImplemented
  51. def used(self, except_by=None):
  52. """ Return the used fraction of an item
  53. :type: Service
  54. :param except_by: exclude this service from the math
  55. :rtype: float
  56. """
  57. sharing_costs = self.get_use_class().objects.filter(resource=self)
  58. if except_by:
  59. sharing_costs = sharing_costs.exclude(service=except_by)
  60. existing_uses_sum = sum(
  61. sharing_costs.values_list('share', flat=True))
  62. return existing_uses_sum
  63. def used_fraction(self, *args, **kwargs):
  64. return self.used(*args, **kwargs)/self.total_capacity
  65. def unused(self):
  66. return self.total_capacity-self.used()
  67. def __str__(self):
  68. if self.capacity_unit == '':
  69. return self.name
  70. else:
  71. return '{} {:.0f} {}'.format(
  72. self.name, self.total_capacity,
  73. self.get_capacity_unit_display())
  74. class Cost(AbstractResource):
  75. """ A monthtly cost we have to pay
  76. """
  77. price = models.FloatField(help_text="Coût mensuel")
  78. def get_use_class(self):
  79. return CostUse
  80. class Meta:
  81. verbose_name = 'Coût'
  82. class Good(AbstractResource):
  83. """ A good, which replacement is provisioned
  84. """
  85. price = models.FloatField()
  86. provisioning_duration = models.DurationField(
  87. choices=settings.PROVISIONING_DURATIONS)
  88. def get_use_class(self):
  89. return GoodUse
  90. def monthly_provision(self):
  91. return self.price/self.provisioning_duration.days*(365.25/12)
  92. class Meta:
  93. verbose_name = 'Bien'
  94. class AbstractUse(models.Model):
  95. share = models.FloatField()
  96. service = models.ForeignKey('Service')
  97. class Meta:
  98. abstract = True
  99. def clean(self):
  100. if hasattr(self, 'resource'):
  101. usage = self.resource.used(except_by=self.service) + self.share
  102. if usage > self.resource.total_capacity:
  103. raise ValidationError(
  104. "Cannot use more than 100% of {})".format(self.resource))
  105. def real_share(self):
  106. """The share, + wasted space share
  107. Taking into account that the unused space is
  108. wasted and has to be divided among actual users
  109. """
  110. return (
  111. self.share +
  112. (self.share/self.resource.used())*self.resource.unused()
  113. )
  114. def value_share(self):
  115. return (
  116. self.resource.price
  117. * self.real_share()
  118. / self.resource.total_capacity
  119. )
  120. def unit_value_share(self):
  121. if self.service.subscriptions_count == 0:
  122. return 0
  123. else:
  124. return self.value_share()/self.service.subscriptions_count
  125. class CostUse(AbstractUse):
  126. resource = models.ForeignKey(Cost)
  127. def cost_share(self):
  128. return (
  129. self.real_share() / self.resource.total_capacity
  130. * self.resource.price
  131. )
  132. def unit_cost_share(self):
  133. subscriptions_count = self.service.subscriptions_count
  134. if subscriptions_count == 0:
  135. return 0
  136. else:
  137. return self.cost_share()/self.service.subscriptions_count
  138. class GoodUse(AbstractUse):
  139. resource = models.ForeignKey(Good)
  140. def monthly_provision_share(self):
  141. return (
  142. self.real_share()
  143. * self.resource.monthly_provision()
  144. / self.resource.total_capacity)
  145. def unit_monthly_provision_share(self):
  146. subscriptions_count = self.service.subscriptions_count
  147. monthly_share = self.monthly_provision_share()
  148. if subscriptions_count == 0:
  149. return 0
  150. else:
  151. return monthly_share/subscriptions_count
  152. class Service(AbstractItem):
  153. """ A service we sell
  154. (considered monthly)
  155. """
  156. costs = models.ManyToManyField(
  157. Cost,
  158. through=CostUse,
  159. related_name='using_services')
  160. goods = models.ManyToManyField(
  161. Good,
  162. through=GoodUse,
  163. related_name='using_services')
  164. # services = models.ManyToMany('Service') #TODO
  165. subscriptions_count = models.PositiveIntegerField(default=0)
  166. def get_absolute_url(self):
  167. return reverse('detail-service', kwargs={'pk': self.pk})