models.py 7.0 KB

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