models.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  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. from .validators import less_than_one
  6. class Document(models.Model):
  7. """ A document is a scenario or a record from facts, on 1 month.
  8. """
  9. TYPE_FACT = 'fact'
  10. TYPE_PLAN = 'plan'
  11. name = models.CharField(max_length=130)
  12. comment = models.TextField(blank=True)
  13. date = models.DateField(auto_now_add=True)
  14. type = models.CharField(max_length=10, choices=(
  15. (TYPE_FACT, 'relevé'),
  16. (TYPE_PLAN, 'scénario/estimation'),
  17. ))
  18. def __str__(self):
  19. return '{} {:%b %Y}'.format(self.name, self.date)
  20. class AbstractItem(models.Model):
  21. name = models.CharField(max_length=130)
  22. description = models.TextField(blank=True)
  23. def __str__(self):
  24. return self.name
  25. def get_use_class(self):
  26. raise NotImplemented
  27. def used(self, except_by=None):
  28. """ Return the used fraction of an item
  29. :type: Service
  30. :param except_by: exclude this service from the math
  31. :rtype: float
  32. """
  33. sharing_costs = self.get_use_class().objects.filter(resource=self)
  34. if except_by:
  35. sharing_costs = sharing_costs.exclude(service=except_by)
  36. existing_uses_sum = sum(
  37. sharing_costs.values_list('share', flat=True))
  38. return existing_uses_sum
  39. def unused(self):
  40. return 1-self.used()
  41. class Meta:
  42. abstract = True
  43. class AbstractCostingItem(AbstractItem):
  44. """ A costing item, linked to a document
  45. """
  46. document = models.ForeignKey(Document)
  47. class Meta:
  48. abstract = True
  49. class Cost(AbstractCostingItem):
  50. """ A monthtly cost we have to pay
  51. """
  52. price = models.FloatField(help_text="Coût mensuel")
  53. def get_use_class(self):
  54. return CostUse
  55. class Meta:
  56. verbose_name = 'Coût'
  57. class Good(AbstractCostingItem):
  58. """ A good, which replacement is provisioned
  59. """
  60. price = models.FloatField()
  61. provisioning_duration = models.DurationField(
  62. choices=settings.PROVISIONING_DURATIONS)
  63. def get_use_class(self):
  64. return GoodUse
  65. def monthly_provision(self):
  66. return self.price/self.provisioning_duration.days*(365.25/12)
  67. class Meta:
  68. verbose_name = 'Bien'
  69. class AbstractUse(models.Model):
  70. share = models.FloatField(validators=[less_than_one])
  71. service = models.ForeignKey('Service')
  72. class Meta:
  73. abstract = True
  74. def clean(self):
  75. if hasattr(self, 'resource'):
  76. if (self.resource.used(except_by=self.service) + self.share) > 1:
  77. raise ValidationError(
  78. "Cannot use more than 100% of {})".format(self.resource))
  79. def real_share(self):
  80. """The share, + wasted space share
  81. Taking into account that the unused space is
  82. wasted and has to be divided among actual users
  83. """
  84. return (
  85. self.share +
  86. (self.share/self.resource.used())*self.resource.unused()
  87. )
  88. def value_share(self):
  89. return self.resource.price*self.real_share()
  90. def unit_value_share(self):
  91. if self.service.subscriptions_count == 0:
  92. return 0
  93. else:
  94. return self.value_share()/self.service.subscriptions_count
  95. class CostUse(AbstractUse):
  96. resource = models.ForeignKey(Cost)
  97. def cost_share(self):
  98. return self.real_share()*self.resource.price
  99. def unit_cost_share(self):
  100. subscriptions_count = self.service.subscriptions_count
  101. if subscriptions_count == 0:
  102. return 0
  103. else:
  104. return self.cost_share()/self.service.subscriptions_count
  105. class GoodUse(AbstractUse):
  106. resource = models.ForeignKey(Good)
  107. def monthly_provision_share(self):
  108. return self.real_share()*self.resource.monthly_provision()
  109. def unit_monthly_provision_share(self):
  110. subscriptions_count = self.service.subscriptions_count
  111. monthly_share = self.monthly_provision_share()
  112. if subscriptions_count == 0:
  113. return 0
  114. else:
  115. return monthly_share/subscriptions_count
  116. class Service(AbstractItem):
  117. """ A service we sell
  118. (considered monthly)
  119. """
  120. costs = models.ManyToManyField(
  121. Cost,
  122. through=CostUse,
  123. related_name='using_services')
  124. goods = models.ManyToManyField(
  125. Good,
  126. through=GoodUse,
  127. related_name='using_services')
  128. # services = models.ManyToMany('Service') #TODO
  129. subscriptions_count = models.PositiveIntegerField(default=0)
  130. def get_absolute_url(self):
  131. return reverse('detail-service', kwargs={'pk': self.pk})
  132. def document(self):
  133. if self.costs.exists():
  134. return self.costs.first().document
  135. elif self.goods.exists():
  136. return self.costs.first().document
  137. else:
  138. return None