models.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  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 unit_share(self):
  115. if self.service.subscriptions_count == 0:
  116. return 0
  117. else:
  118. return self.share/self.service.subscriptions_count
  119. return
  120. def unit_real_share(self):
  121. if self.service.subscriptions_count == 0:
  122. return 0
  123. else:
  124. return self.real_share()/self.service.subscriptions_count
  125. def value_share(self):
  126. return (
  127. self.resource.price
  128. * self.real_share()
  129. / self.resource.total_capacity
  130. )
  131. def unit_value_share(self):
  132. if self.service.subscriptions_count == 0:
  133. return 0
  134. else:
  135. return self.value_share()/self.service.subscriptions_count
  136. class CostUse(AbstractUse):
  137. resource = models.ForeignKey(Cost)
  138. def cost_share(self):
  139. return (
  140. self.real_share() / self.resource.total_capacity
  141. * self.resource.price
  142. )
  143. def unit_cost_share(self):
  144. subscriptions_count = self.service.subscriptions_count
  145. if subscriptions_count == 0:
  146. return 0
  147. else:
  148. return self.cost_share()/self.service.subscriptions_count
  149. class GoodUse(AbstractUse):
  150. resource = models.ForeignKey(Good)
  151. def monthly_provision_share(self):
  152. return (
  153. self.real_share()
  154. * self.resource.monthly_provision()
  155. / self.resource.total_capacity)
  156. def unit_monthly_provision_share(self):
  157. subscriptions_count = self.service.subscriptions_count
  158. monthly_share = self.monthly_provision_share()
  159. if subscriptions_count == 0:
  160. return 0
  161. else:
  162. return monthly_share/subscriptions_count
  163. class Service(AbstractItem):
  164. """ A service we sell
  165. (considered monthly)
  166. """
  167. costs = models.ManyToManyField(
  168. Cost,
  169. through=CostUse,
  170. related_name='using_services')
  171. goods = models.ManyToManyField(
  172. Good,
  173. through=GoodUse,
  174. related_name='using_services')
  175. # services = models.ManyToMany('Service') #TODO
  176. subscriptions_count = models.PositiveIntegerField(default=0)
  177. def get_absolute_url(self):
  178. return reverse('detail-service', kwargs={'pk': self.pk})