models.py 6.1 KB

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