models.py 10 KB

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