models.py 10 KB

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