models.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  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. UNIT_SERVICE = 'services'
  38. capacity_unit = models.CharField(
  39. max_length=10,
  40. choices=(
  41. (UNIT_AMP, 'A'),
  42. (UNIT_MBPS, 'Mbps'),
  43. (UNIT_U, 'U'),
  44. (UNIT_IPV4, 'IPv4'),
  45. (UNIT_ETHERNET_PORT, 'ports'),
  46. (UNIT_SERVICE, 'abonnement'),
  47. ),
  48. blank=True,
  49. )
  50. total_capacity = models.FloatField(default=1)
  51. class Meta:
  52. abstract = True
  53. def get_use_class(self):
  54. raise NotImplemented
  55. def used(self, except_by=None):
  56. """ Return the used fraction of an item
  57. :type: Service
  58. :param except_by: exclude this service from the math
  59. :rtype: float
  60. """
  61. sharing_costs = self.get_use_class().objects.filter(resource=self)
  62. if except_by:
  63. sharing_costs = sharing_costs.exclude(service=except_by)
  64. existing_uses_sum = sum(
  65. sharing_costs.values_list('share', flat=True))
  66. return existing_uses_sum
  67. def used_fraction(self, *args, **kwargs):
  68. return self.used(*args, **kwargs)/self.total_capacity
  69. def unused(self):
  70. return self.total_capacity-self.used()
  71. def __str__(self):
  72. if self.capacity_unit == '':
  73. return self.name
  74. else:
  75. return '{} {:.0f} {}'.format(
  76. self.name, self.total_capacity,
  77. self.get_capacity_unit_display())
  78. class Cost(AbstractResource):
  79. """ A monthtly cost we have to pay
  80. """
  81. price = models.FloatField(help_text="Coût mensuel")
  82. def get_use_class(self):
  83. return CostUse
  84. class Meta:
  85. verbose_name = 'Coût'
  86. class Good(AbstractResource):
  87. """ A good, which replacement is provisioned
  88. """
  89. price = models.FloatField()
  90. provisioning_duration = models.DurationField(
  91. choices=settings.PROVISIONING_DURATIONS)
  92. def get_use_class(self):
  93. return GoodUse
  94. def monthly_provision(self):
  95. return self.price/self.provisioning_duration.days*(365.25/12)
  96. class Meta:
  97. verbose_name = 'Bien'
  98. class AbstractUse(models.Model):
  99. share = models.FloatField()
  100. service = models.ForeignKey('Service')
  101. class Meta:
  102. abstract = True
  103. def clean(self):
  104. if hasattr(self, 'resource'):
  105. usage = self.resource.used(except_by=self.service) + self.share
  106. if usage > self.resource.total_capacity:
  107. raise ValidationError(
  108. "Cannot use more than 100% of {})".format(self.resource))
  109. def real_share(self):
  110. """The share, + wasted space share
  111. Taking into account that the unused space is
  112. wasted and has to be divided among actual users
  113. """
  114. return (
  115. self.share +
  116. (self.share/self.resource.used())*self.resource.unused()
  117. )
  118. def unit_share(self):
  119. if self.service.subscriptions_count == 0:
  120. return 0
  121. else:
  122. return self.share/self.service.subscriptions_count
  123. return
  124. def unit_real_share(self):
  125. if self.service.subscriptions_count == 0:
  126. return 0
  127. else:
  128. return self.real_share()/self.service.subscriptions_count
  129. def value_share(self):
  130. return (
  131. self.resource.price
  132. * self.real_share()
  133. / self.resource.total_capacity
  134. )
  135. def unit_value_share(self):
  136. if self.service.subscriptions_count == 0:
  137. return 0
  138. else:
  139. return self.value_share()/self.service.subscriptions_count
  140. class CostUse(AbstractUse):
  141. resource = models.ForeignKey(Cost)
  142. def cost_share(self):
  143. return (
  144. self.real_share() / self.resource.total_capacity
  145. * self.resource.price
  146. )
  147. def unit_cost_share(self):
  148. subscriptions_count = self.service.subscriptions_count
  149. if subscriptions_count == 0:
  150. return 0
  151. else:
  152. return self.cost_share()/self.service.subscriptions_count
  153. class GoodUse(AbstractUse):
  154. resource = models.ForeignKey(Good)
  155. def monthly_provision_share(self):
  156. return (
  157. self.real_share()
  158. * self.resource.monthly_provision()
  159. / self.resource.total_capacity)
  160. def unit_monthly_provision_share(self):
  161. subscriptions_count = self.service.subscriptions_count
  162. monthly_share = self.monthly_provision_share()
  163. if subscriptions_count == 0:
  164. return 0
  165. else:
  166. return monthly_share/subscriptions_count
  167. class Service(AbstractResource):
  168. """ A service we sell
  169. (considered monthly)
  170. """
  171. costs = models.ManyToManyField(
  172. Cost,
  173. through=CostUse,
  174. related_name='using_services')
  175. goods = models.ManyToManyField(
  176. Good,
  177. through=GoodUse,
  178. related_name='using_services')
  179. subscriptions_count = models.PositiveIntegerField(default=0)
  180. reusable = models.BooleanField(
  181. default=False,
  182. help_text="Peut-être utilisé par d'autres services")
  183. @property
  184. def price(self):
  185. return self.get_prices()['total_recurring_price']
  186. def save(self, *args, **kwargs):
  187. if self.reusable:
  188. self.capacity_unit = self.UNIT_SERVICE
  189. self.total_capacity = self.subscriptions_count
  190. return super().save(*args, **kwargs)
  191. def get_absolute_url(self):
  192. return reverse('detail-service', kwargs={'pk': self.pk})
  193. def get_use_class(self):
  194. return ServiceUse
  195. def get_prices(self):
  196. costs_uses = CostUse.objects.filter(service=self)
  197. goods_uses = GoodUse.objects.filter(service=self)
  198. services_uses = ServiceUse.objects.filter(service=self)
  199. total_recurring_price = sum(chain(
  200. (i.monthly_provision_share() for i in goods_uses),
  201. (i.cost_share() for i in costs_uses),
  202. (i.cost_share() for i in services_uses)
  203. ))
  204. total_goods_value_share = sum(i.value_share() for i in goods_uses)
  205. if self.subscriptions_count == 0:
  206. unit_recurring_price = 0
  207. unit_goods_value_share = 0
  208. else:
  209. unit_recurring_price = total_recurring_price/self.subscriptions_count
  210. unit_goods_value_share = \
  211. total_goods_value_share/self.subscriptions_count
  212. return {
  213. 'total_recurring_price': total_recurring_price,
  214. 'unit_recurring_price': unit_recurring_price,
  215. 'total_goods_value_share': total_goods_value_share,
  216. 'unit_goods_value_share': unit_goods_value_share,
  217. }
  218. def validate_reusable_service(v):
  219. if not Service.objects.get(pk=v).reusable:
  220. raise ValidationError('{} is not a reusable service'.format(v))
  221. class ServiceUse(AbstractUse):
  222. resource = models.ForeignKey(
  223. Service, related_name='dependent_services',
  224. limit_choices_to={'reusable': True},
  225. validators=[validate_reusable_service])
  226. def cost_share(self):
  227. return (
  228. self.share / self.resource.total_capacity
  229. * self.resource.price
  230. )
  231. def unit_cost_share(self):
  232. subscriptions_count = self.service.subscriptions_count
  233. if subscriptions_count == 0:
  234. return 0
  235. else:
  236. return self.cost_share()/self.service.subscriptions_count
  237. def clean(self):
  238. """ Checks for cycles in service using services
  239. """
  240. start_resource = self.resource
  241. def crawl(service):
  242. for use in service.dependent_services.all():
  243. if use.service == start_resource:
  244. raise ValidationError(
  245. 'Cycle detected in services using services')
  246. else:
  247. crawl(use.service)
  248. crawl(self.service)