import datetime from itertools import chain from django.conf import settings from django.core.exceptions import ValidationError from django.core.urlresolvers import reverse from django.db import models, transaction import markdown class Document(models.Model): """ A document is a scenario or a record from facts, on 1 month. """ TYPE_FACT = 'fact' TYPE_PLAN = 'plan' name = models.CharField('Nom', max_length=130) comment = models.TextField( 'commentaire', blank=True, help_text="Texte brut ou markdown") comment_html = models.TextField(blank=True, editable=False) date = models.DateField(default=datetime.datetime.now) type = models.CharField(max_length=10, choices=( (TYPE_FACT, 'rapports de transparence'), (TYPE_PLAN, 'scénario/estimation'), )) def __str__(self): return self.name def save(self, *args, **kwargs): self.comment_html = markdown.markdown(self.comment) super().save(*args, **kwargs) def get_absolute_url(self): return reverse('detail-document', kwargs={'pk': self.pk}) def copy(self): """ Deep copy and saving of a document All related resources are copied. """ with transaction.atomic(): new_doc = Document.objects.get(pk=self.pk) new_doc.pk = None new_doc.name = 'COPIE DE {}'.format(new_doc.name) new_doc.save() new_services, new_costs, new_goods = {}, {}, {} to_copy = ( (self.service_set, new_services), (self.good_set, new_goods), (self.cost_set, new_costs), ) for qs, index in to_copy: for item in qs.all(): old_pk = item.pk item.pk = None item.document = new_doc item.save() index[old_pk] = item to_reproduce = ( (ServiceUse, new_services), (GoodUse, new_goods), (CostUse, new_costs), ) for Klass, index in to_reproduce: src_qs = Klass.objects.filter(service__document=self) for use in src_qs.all(): use.pk = None use.service = new_services[use.service.pk] use.resource = index[use.resource.pk] use.save() return new_doc class AbstractItem(models.Model): name = models.CharField('Nom', max_length=130) description = models.TextField('description', blank=True) description_html = models.TextField(blank=True) document = models.ForeignKey(Document) def __str__(self): return self.name def save(self, *args, **kwargs): self.description_html = markdown.markdown(self.description) super().save(*args, **kwargs) class Meta: abstract = True class AbstractResource(AbstractItem): UNIT_AMP = 'a' UNIT_MBPS = 'mbps' UNIT_U = 'u' UNIT_IPV4 = 'ipv4' UNIT_ETHERNET_PORT = 'eth' UNIT_SERVICE = 'services' capacity_unit = models.CharField( 'unité', max_length=10, choices=( (UNIT_AMP, 'A'), (UNIT_MBPS, 'Mbps'), (UNIT_U, 'U'), (UNIT_IPV4, 'IPv4'), (UNIT_ETHERNET_PORT, 'ports'), (UNIT_SERVICE, 'abonnement'), ), blank=True, help_text="unité de capacité (si applicable)", ) total_capacity = models.FloatField( 'Capacité totale', default=1, help_text="Laisser à 1 si non divisible") class Meta: abstract = True def get_use_class(self): raise NotImplemented def used(self, except_by=None): """ Return the used fraction of an item :type: Service :param except_by: exclude this service from the math :rtype: float """ sharing_costs = self.get_use_class().objects.filter(resource=self) if except_by: sharing_costs = sharing_costs.exclude(service=except_by) existing_uses_sum = sum( sharing_costs.values_list('share', flat=True)) return existing_uses_sum def used_fraction(self, *args, **kwargs): return self.used(*args, **kwargs)/self.total_capacity def unused(self): return self.total_capacity-self.used() def __str__(self): if self.capacity_unit == '': return self.name else: return '{} {:.0f} {}'.format( self.name, self.total_capacity, self.get_capacity_unit_display()) class Cost(AbstractResource): """ A monthtly cost we have to pay """ price = models.FloatField("Coût mensuel") def get_use_class(self): return CostUse class Meta: verbose_name = 'Coût mensuel' class Good(AbstractResource): """ A good, which replacement is provisioned """ price = models.FloatField("Prix d'achat") provisioning_duration = models.DurationField( "Durée d'amortissement", choices=settings.PROVISIONING_DURATIONS) def get_use_class(self): return GoodUse def monthly_provision(self): return self.price/self.provisioning_duration.days*(365.25/12) class Meta: verbose_name = 'Bien' class AbstractUse(models.Model): share = models.FloatField() service = models.ForeignKey('Service') class Meta: abstract = True def __str__(self): return str(self.resource) def clean(self): if hasattr(self, 'resource'): usage = self.resource.used(except_by=self.service) + self.share if usage > self.resource.total_capacity: raise ValidationError( "Cannot use more than 100% of {})".format(self.resource)) def real_share(self): """The share, + wasted space share Taking into account that the unused space is wasted and has to be divided among actual users """ return ( self.share + (self.share/self.resource.used())*self.resource.unused() ) def unit_share(self): if self.service.subscriptions_count == 0: return 0 else: return self.share/self.service.subscriptions_count return def unit_real_share(self): if self.service.subscriptions_count == 0: return 0 else: return self.real_share()/self.service.subscriptions_count def value_share(self): return ( self.resource.price * self.real_share() / self.resource.total_capacity ) def unit_value_share(self): if self.service.subscriptions_count == 0: return 0 else: return self.value_share()/self.service.subscriptions_count class CostUse(AbstractUse): resource = models.ForeignKey(Cost) class Meta: verbose_name = 'Coût mensuel associé' verbose_name_plural = 'Coûts mensuels associés' def cost_share(self): return ( self.real_share() / self.resource.total_capacity * self.resource.price ) def unit_cost_share(self): subscriptions_count = self.service.subscriptions_count if subscriptions_count == 0: return 0 else: return self.cost_share()/self.service.subscriptions_count class GoodUse(AbstractUse): resource = models.ForeignKey(Good) class Meta: verbose_name = "Matériel ou frais d'accès utilisé" verbose_name_plural = "Matériels et frais d'accès utilisés" def monthly_provision_share(self): return ( self.real_share() * self.resource.monthly_provision() / self.resource.total_capacity) def unit_monthly_provision_share(self): subscriptions_count = self.service.subscriptions_count monthly_share = self.monthly_provision_share() if subscriptions_count == 0: return 0 else: return monthly_share/subscriptions_count class Service(AbstractResource): """ A service we sell (considered monthly) """ costs = models.ManyToManyField( Cost, through=CostUse, related_name='using_services', verbose_name='coûts mensuels associés') goods = models.ManyToManyField( Good, through=GoodUse, related_name='using_services', verbose_name="matériels et frais d'accès utilisés") subscriptions_count = models.PositiveIntegerField( "Nombre d'abonnements", default=0) reusable = models.BooleanField( default=False, help_text="Peut-être utilisé par d'autres services") @property def price(self): return self.get_prices()['total_recurring_price'] def save(self, *args, **kwargs): if self.reusable: self.capacity_unit = self.UNIT_SERVICE self.total_capacity = self.subscriptions_count return super().save(*args, **kwargs) def get_absolute_url(self): return reverse('detail-service', kwargs={'pk': self.pk}) def get_use_class(self): return ServiceUse def get_prices(self): costs_uses = CostUse.objects.filter(service=self) goods_uses = GoodUse.objects.filter(service=self) services_uses = ServiceUse.objects.filter(service=self) total_recurring_price = sum(chain( (i.monthly_provision_share() for i in goods_uses), (i.cost_share() for i in costs_uses), (i.cost_share() for i in services_uses) )) total_goods_value_share = sum( (i.value_share() for i in chain(goods_uses, services_uses))) if self.subscriptions_count == 0: unit_recurring_price = 0 unit_goods_value_share = 0 else: unit_recurring_price = \ total_recurring_price/self.subscriptions_count unit_goods_value_share = \ total_goods_value_share/self.subscriptions_count unit_staggered_goods_share = \ unit_goods_value_share/settings.SETUP_COST_STAGGERING_MONTHS return { 'total_recurring_price': total_recurring_price, 'total_goods_value_share': total_goods_value_share, 'unit_goods_value_share': unit_goods_value_share, 'unit_recurring_price': unit_recurring_price, 'unit_staggered_goods_share': unit_staggered_goods_share, 'unit_consolidated_cost': ( unit_staggered_goods_share + unit_recurring_price), } def validate_reusable_service(v): if not Service.objects.get(pk=v).reusable: raise ValidationError('{} is not a reusable service'.format(v)) class ServiceUse(AbstractUse): class Meta: verbose_name = 'service utilisé' verbose_name_plural = 'services utilisés' resource = models.ForeignKey( Service, related_name='dependent_services', limit_choices_to={'reusable': True}, validators=[validate_reusable_service]) def cost_share(self): return ( self.share / self.resource.total_capacity * self.resource.price ) def unit_cost_share(self): subscriptions_count = self.service.subscriptions_count if subscriptions_count == 0: return 0 else: return self.cost_share()/self.service.subscriptions_count def value_share(self): return self.share*self.resource.get_prices()['unit_goods_value_share'] def clean(self): """ Checks for cycles in service using services """ start_resource = self.resource def crawl(service): for use in service.dependent_services.all(): if use.service == start_resource: raise ValidationError( 'Cycle detected in services using services') else: crawl(use.service) crawl(self.service)