models.py 6.3 KB

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