示例#1
0
class Calibration(TimeStampedModel):
    id = SmallUUIDField(default=uuid_default(),
                        primary_key=True,
                        db_index=True,
                        editable=False,
                        verbose_name='ID')
    calibrator = models.ForeignKey('calibrations.Calibrator',
                                   related_name='calibrations',
                                   on_delete=models.CASCADE)

    start_date = models.DateTimeField()
    end_date = models.DateTimeField(db_index=True)

    r2 = models.FloatField()
    formula = models.CharField(max_length=255,
                               blank=True,
                               default='',
                               validators=[validate_formula])

    def __str__(self):
        return self.formula

    @property
    def days(self):
        return (self.end_date - self.start_date).days
示例#2
0
class Calibration(TimeStampedModel):
    COUNTIES = Choices(*County.names)
    MONITOR_TYPES = lazy(lambda: Choices(*Monitor.subclasses()), list)()

    id = SmallUUIDField(
        default=uuid_default(),
        primary_key=True,
        db_index=True,
        editable=False,
        verbose_name='ID'
    )

    monitor_type = models.CharField(max_length=20, choices=MONITOR_TYPES)
    county = models.CharField(max_length=20, choices=COUNTIES)
    pm25_formula = models.CharField(max_length=255, blank=True,
        default='', validators=[validate_formula])

    class Meta:
        indexes = [
            models.Index(fields=['monitor_type', 'county'])
        ]

        unique_together = [
            ('monitor_type', 'county')
        ]

    def __str__(self):
        return f'{self.monitor_type} – {self.county}'
示例#3
0
class Subscription(TimeStampedModel):
    LEVELS = LEVELS

    id = SmallUUIDField(default=uuid_default(),
                        primary_key=True,
                        db_index=True,
                        editable=False,
                        verbose_name='ID')

    user = models.ForeignKey(
        'accounts.User',
        related_name='subscriptions',
        on_delete=models.CASCADE,
    )

    monitor = models.ForeignKey(
        'monitors.Monitor',
        related_name='subscriptions',
        on_delete=models.CASCADE,
    )

    level = models.CharField(max_length=25, choices=LEVELS)

    class Meta:
        constraints = [
            models.UniqueConstraint(fields=['user', 'monitor'],
                                    name='user_subscriptions')
        ]

        ordering = ['monitor__name']

    def __str__(self):
        return f'{self.user_id} : {self.monitor_id} @ {self.level}'
示例#4
0
class StatementImport(models.Model):
    """ Records an import of a bank statement

    Attributes:

        uuid (SmallUUID): UUID for statement import. Use to prevent leaking of IDs (if desired).
        timestamp (datetime): The datetime when the object was created.
        bank_account (Account): The account the import is for (should normally point to an asset
            account which represents your bank account)

    """

    uuid = SmallUUIDField(default=uuid_default(), editable=False)
    timestamp = models.DateTimeField(default=timezone.now)
    # TODO: Add constraint to ensure destination account expects statements (copy 0007)
    bank_account = models.ForeignKey(Account,
                                     related_name="imports",
                                     on_delete=models.CASCADE)
    source = models.CharField(
        max_length=20,
        help_text="A value uniquely identifying where this data came from. "
        'Examples: "csv", "teller.io".',
    )
    extra = JSONField(
        default=json_default,
        help_text="Any extra data relating to the import, probably specific "
        "to the data source.",
    )

    objects = StatementImportManager()

    def natural_key(self):
        return (self.uuid, )
示例#5
0
class User(AbstractEmailUser):
    uuid = SmallUUIDField(editable=False, default=uuid_default())

    first_name = models.CharField(max_length=31, null=True, blank=True)
    last_name = models.CharField(max_length=31, null=True, blank=True)

    objects = CaseInsensitiveEmailUserManager()

    current_campaign = models.ForeignKey(
        'accounts.Campaign',
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='current_users',
    )

    campaigns = models.ManyToManyField('accounts.Campaign',
                                       blank=True,
                                       through='accounts.CampaignRole')

    class Meta:
        ordering = ['last_name', 'first_name']

    def __str__(self):
        return f'{self.name} <{self.email}>'

    @property
    def name(self):
        return f'{self.first_name} {self.last_name}'
示例#6
0
class UUIDModel(models.Model):
    uuid = SmallUUIDField(default=uuid_default(),
                          editable=False,
                          primary_key=True)

    class Meta(object):
        abstract = True
示例#7
0
class User(AbstractUser, TimeStampedModel):
    CITY_CHOICES = (
        (1, 'بندرعباس'),
        (2, 'کرمان'),
        (3, 'یزد'),
    )

    city = models.PositiveIntegerField(null=True,
                                       blank=True,
                                       default=1,
                                       verbose_name='شهر',
                                       choices=CITY_CHOICES)
    mobile = models.CharField(unique=True,
                              max_length=14,
                              null=True,
                              blank=True,
                              verbose_name='موبایل')
    mobile_verified = models.BooleanField(default=False,
                                          verbose_name='موبایل تأیید شده')
    email_verified = models.BooleanField(
        default=False, verbose_name='پست الکترونیکی تأیید شده')
    invitations_count = models.PositiveSmallIntegerField(
        null=True, blank=True, verbose_name='تعداد دعوت‌ها', default=0)
    used_invite_code = models.BooleanField(
        default=False, verbose_name='از کد دعوت کاربر دیگر استفاده کرده‌است')
    invite_id = SmallUUIDField(default=uuid_default(),
                               null=True,
                               blank=True,
                               verbose_name='شناسه یکتا')

    MOBILE_FIELD = 'mobile'

    description = models.TextField(null=True,
                                   blank=True,
                                   verbose_name='توضیحات')
示例#8
0
class Option(TimeStampedModel):
    uuid = SmallUUIDField(default=uuid_default())
    name = models.CharField(max_length=200)

    class Meta:
        ordering = ['name']

    def get_fitness_for_user(self, user):
        return self.get_fitness([user])

    def get_fitness(self, users=None):
        if users is None:
            users = get_user_model().objects.all()

        fitnesses = [c.get_fitness(self, users) for c in Criterion.objects.all()]
        # Some criterion may not have been scored, in which case ignore them
        fitnesses = list(filter(lambda f: f is not None, fitnesses))
        if fitnesses:
            return sum(fitnesses)
        else:
            return None

    def get_completed_users(self):
        total_criteria = Criterion.objects.count()
        res = Score.objects.filter(option=self).\
            values('user').\
            annotate(total_scores=Count('user')).\
            order_by('total_scores')

        user_ids = [r['user'] for r in res if r['total_scores'] == total_criteria]
        return get_user_model().objects.filter(pk__in=user_ids)
示例#9
0
class StorageItem(PartnerModel, UUIDModel, TimestampModel):
    token = SmallUUIDField(default=uuid_default())
    app = EnumField(enums.FileType)
    file = models.FileField(storage=HighValueDownloadStorage())
    email = models.EmailField(blank=True, null=True)
    expires = models.DateTimeField(default=storage_expire_date_time)
    first_download = models.DateTimeField(blank=True, null=True)

    class Meta:
        ordering = ["-created_at"]

    def get_absolute_url(self):
        return self.file.url

    def refresh_token(self):
        self.token = smalluuid.SmallUUID()
        self.expires = storage_expire_date_time()
        self.save(update_fields=["token", "expires"])
        return self.token

    def validate_token(self, token):
        return token == str(self.token)

    @property
    def download_url(self):
        path = reverse("storage:download", kwargs={"pk": self.pk})
        return f"{settings.PRIMARY_ORIGIN}{path}?token={self.token}"

    @property
    def reset_url(self):
        return settings.FILE_TOKEN_RESET_URL.format(item_id=self.pk)
示例#10
0
class Alert(TimeStampedModel):
    LEVELS = LEVELS

    id = SmallUUIDField(default=uuid_default(),
                        primary_key=True,
                        db_index=True,
                        editable=False,
                        verbose_name='ID')

    start_time = models.DateTimeField()
    end_time = models.DateTimeField(blank=True, null=True)
    monitor = models.ForeignKey('monitors.Monitor',
                                related_name='alerts',
                                on_delete=models.CASCADE)
    pm25_average = models.DecimalField(max_digits=8,
                                       decimal_places=2,
                                       null=True)
    level = models.CharField(max_length=25, choices=LEVELS)

    class Meta:
        ordering = ['-start_time']

    def __str__(self):
        return f'{self.monitor_id} : {self.get_level_display()}'

    def get_average(self, hours=1):
        end_time = timezone.now()
        start_time = end_time - timedelta(hours=hours)
        return (self.monitor.entries.filter(
            timestamp__range=(start_time, end_time)).aggregate(
                average=Avg('pm25'))['average'])

    def send_notifications(self):
        # If SMS alerts are disbaled, do nothing.
        if not settings.SEND_SMS_ALERTS:
            return

        values = [level[0] for level in LEVELS]
        notification_levels = values[:values.index(self.level) + 1]

        message = '\n'.join([
            f'Air Quality Alert in {self.monitor.county} County for {self.monitor.name} ({self.monitor.device})',
            '',
            f'{self.get_level_display()}: {GUIDANCE[self.level]}',
            '',
            f'https://sjvair.com{self.monitor.get_absolute_url()}',
        ])

        queryset = (Subscription.objects.filter(
            monitor_id=self.monitor.pk).select_related('user'))

        # hazardous means everyone gets it. Evey other
        # level gets filtered.
        if self.level != LEVELS.hazardous:
            queryset = queryset.filter(level=self.level)

        for subscription in queryset:
            subscription.user.send_sms(message)
示例#11
0
class Transaction(models.Model):
    """ Represents a transaction

    A transaction is a movement of funds between two accounts. Each transaction
    will have two or more legs, each leg specifies an account and an amount.

    .. note:

        When working with Hordak Transaction objects you will typically need to do so
        within a database transaction. This is because the database has integrity checks in
        place to ensure the validity of the transaction (i.e. money in = money out).

    See Also:

        :meth:`Account.transfer_to()` is a useful shortcut to avoid having to create transactions manually.

    Examples:

        You can manually create a transaction as follows::

            from django.db import transaction as db_transaction
            from hordak.models import Transaction, Leg

            with db_transaction.atomic():
                transaction = Transaction.objects.create()
                Leg.objects.create(transaction=transaction, account=my_account1, amount=Money(100, 'EUR'))
                Leg.objects.create(transaction=transaction, account=my_account2, amount=Money(-100, 'EUR'))

    Attributes:

        uuid (SmallUUID): UUID for transaction. Use to prevent leaking of IDs (if desired).
        timestamp (datetime): The datetime when the object was created.
        date (date): The date when the transaction actually occurred, as this may be different to
            :attr:`timestamp`.
        description (str): Optional user-provided description

    """

    uuid = SmallUUIDField(default=uuid_default(), editable=False)
    timestamp = models.DateTimeField(
        default=timezone.now,
        help_text="The creation date of this transaction object")
    date = models.DateField(
        default=timezone.now,
        help_text="The date on which this transaction occurred")
    description = models.TextField(default="", blank=True)

    objects = TransactionManager()

    class Meta:
        get_latest_by = "date"

    def balance(self):
        return self.legs.sum_to_balance()

    def natural_key(self):
        return (self.uuid, )
示例#12
0
class UUIDModel(models.Model):
    """Abstract model that adds a UUID Field"""
    uuid = SmallUUIDField(default=uuid_default(),
                          editable=False,
                          db_index=True)

    class Meta(object):
        """Meta info for TimeStamp abstract model"""
        abstract = True
示例#13
0
class Category(TimeStampedModel):
    uuid = SmallUUIDField(default=uuid_default())
    name = models.CharField(max_length=200)
    order_num = models.IntegerField()

    class Meta:
        verbose_name_plural = 'categories'
        ordering = ['order_num']

    def __str__(self):
        return self.name or 'Unnamed Category'

    def save(self, **kwargs):
        if self.order_num is None:
            self.order_num = Category.objects.count()
        super(Category, self).save(**kwargs)

    def get_average_weight(self, *a, **kw):
        res = Weight.objects.filter(criterion__category=self, **kw).aggregate(Avg('value'))
        return res['value__avg']

    def get_total_weight(self, *a, **kw):
        res = Weight.objects.filter(criterion__category=self, **kw).aggregate(Sum('value'))
        return res['value__sum']

    def get_average_score(self, option, *a, **kw):
        res = Score.objects.filter(criterion__category=self, option=option, **kw).aggregate(Avg('value'))
        return res['value__avg']

    def get_total_fitness(self, option):
        total = None
        for criterion in self.criteria.all():
            fitness = criterion.get_fitness(option)
            if fitness is not None:
                if total is None:
                    total = 0
                total += fitness
        return total

    def get_total_fitness_for_user(self, option, user):
        total = None
        for criterion in self.criteria.all():
            fitness = criterion.get_fitness_for_user(option, user)
            if fitness is not None:
                if total is None:
                    total = 0
                total += fitness
        return total

    def get_normalised_fitness(self, option, *a, **kw):
        total_fitness = self.get_total_fitness(option, *a, **kw)
        total_weight = self.get_total_weight(*a, **kw)
        if total_fitness is None or total_weight is None:
            return None
        else:
            return total_fitness / total_weight
示例#14
0
class UserOwnedModel(models.Model):
    class Meta:
        abstract = True

    user = models.ForeignKey(settings.AUTH_USER_MODEL,
                             on_delete=models.CASCADE,
                             related_name="%(app_label)s_%(class)s_owned_set",
                             editable=False)
    uuid = SmallUUIDField(editable=False, default=uuid_default())
    objects = UserModelManager()
示例#15
0
class Weight(TimeStampedModel):
    uuid = SmallUUIDField(default=uuid_default())
    user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='weights')
    criterion = models.ForeignKey('criterion', related_name='weights')
    value = models.IntegerField()

    class Meta:
        unique_together = (
            ('user', 'criterion'),
        )
示例#16
0
class CampaignOwnedModel(models.Model):
    class Meta:
        abstract = True

    campaign = models.ForeignKey(
        'accounts.Campaign',
        on_delete=models.CASCADE,
        related_name="%(app_label)s_%(class)s_owned_set",
        editable=False)
    uuid = SmallUUIDField(editable=False, default=uuid_default())
    objects = CampaignModelManager()
示例#17
0
class Housemate(models.Model):
    uuid = SmallUUIDField(default=uuid_default(), editable=False)
    account = models.OneToOneField(Account,
                                   related_name='housemate',
                                   unique=True)
    user = models.OneToOneField(settings.AUTH_USER_MODEL,
                                on_delete=models.SET_NULL,
                                related_name='housemate',
                                blank=True,
                                null=True,
                                unique=True)
示例#18
0
class RecurringCostSplit(models.Model):
    """Represents how a recurring cost should be split between accounts (i.e. housemates)"""
    uuid = SmallUUIDField(default=uuid_default(), editable=False)
    recurring_cost = models.ForeignKey(RecurringCost, related_name='splits')
    from_account = models.ForeignKey('hordak.Account')
    portion = models.DecimalField(max_digits=13, decimal_places=2, default=1)

    objects = models.Manager.from_queryset(RecurringCostSplitQuerySet)()

    class Meta:
        base_manager_name = 'objects'
        unique_together = (('recurring_cost', 'from_account'), )
示例#19
0
class Campaign(models.Model):
    uuid = SmallUUIDField(editable=False, default=uuid_default())
    name = models.CharField(max_length=63)
    creation_date = models.DateField(auto_now_add=True)

    active_encounter = models.OneToOneField('plot.Encounter',
                                            on_delete=models.SET_NULL,
                                            null=True,
                                            blank=True,
                                            related_name='campaign_active_in')

    def __str__(self):
        return self.name
示例#20
0
class User(AbstractBaseUser, PermissionsMixin, models.Model):
    id = SmallUUIDField(default=uuid_default(),
                        primary_key=True,
                        db_index=True,
                        editable=False,
                        verbose_name='ID')
    full_name = models.CharField(max_length=100)
    email = models.EmailField(unique=True, db_index=True)

    # Normally provided by auth.AbstractUser, but we're not using that here.
    date_joined = models.DateTimeField(_('date joined'),
                                       default=timezone.now,
                                       editable=False)
    is_active = models.BooleanField(
        _('active'),
        default=True,
        help_text=_('Designates whether this user should be treated as '
                    'active. Unselect this instead of deleting accounts.'))
    is_staff = models.BooleanField(
        _('staff status'),
        default=False,
        help_text=_(
            'Designates whether the user can log into this admin site.')
    )  # Required for Django Admin, for tenant staff/admin see role

    EMAIL_FIELD = 'email'
    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['full_name']

    objects = managers.UserManager()

    class Meta:
        ordering = ('-date_joined', )

    def get_name(self):
        name = HumanName(self.full_name)
        name.capitalize()
        return name

    def set_name(self, value):
        self.full_name = value
        del self.name

    name = cached_property(get_name)
    name.setter = set_name

    def get_short_name(self):
        return self.name.first

    def get_full_name(self):
        return self.name
示例#21
0
class Invite(UUIDModel, TimestampModel):
    email = models.EmailField()
    token = SmallUUIDField(default=uuid_default(), editable=False)
    user = models.ForeignKey(User,
                             null=True,
                             on_delete=models.SET_NULL,
                             editable=False)
    expires = models.DateTimeField(default=expire_date_time)
    consumed_at = models.DateTimeField(null=True, editable=False)
    clients = models.ManyToManyField("multi_tenant.Client",
                                     through="multi_tenant.InviteAssociation")
    primary_client = models.ForeignKey(
        "multi_tenant.Client",
        related_name="invited_client",
        on_delete=models.PROTECT,
    )

    objects = models.Manager()
    actives = ActiveInvitesManager()

    class Meta(object):
        verbose_name = _("Invite")
        verbose_name_plural = _("Invites")
        ordering = ["created_at"]

    def __str__(self):
        return f"{self.email} {self.token}"

    @property
    def expired(self):
        return self.expires <= now()

    @property
    def full_url(self):
        path = reverse("accounts:consume_invite", kwargs={"slug": self.token})
        return f"{settings.PRIMARY_ORIGIN}{path}"

    def consume_invite(self, user):
        new_associations = []
        new_associations.append(
            Association(client=self.primary_client, user=user))
        for client in self.clients.exclude(pk=self.primary_client.pk):
            new_associations.append(Association(client=client, user=user))
        Association.objects.bulk_create(new_associations)

        Invite.objects.filter(email__iexact=user.email).update(
            user=user, consumed_at=now())

    def get_absolute_url(self):
        return reverse("accounts:consume_invite", kwargs={"slug": self.token})
示例#22
0
class Post(models.Model):
    title = models.CharField(max_length=200, blank=False)
    text = models.TextField()
    uuid = SmallUUIDField(default=uuid_default())
    pub_date = models.DateTimeField('date published', default=timezone.now)
    user = models.ForeignKey(User,
                             on_delete=models.CASCADE,
                             blank=True,
                             null=True)

    @classmethod
    def create_post(self, title, text):
        posti = self(title=title, text=text)
        return posti
示例#23
0
class StatementImport(models.Model):
    """ Records an import of a bank statement

    Attributes:

        uuid (SmallUUID): UUID for statement import. Use to prevent leaking of IDs (if desired).
        timestamp (datetime): The datetime when the object was created.
        bank_account (Account): The account the import is for (should normally point to an asset
            account which represents your bank account)

    """
    uuid = SmallUUIDField(default=uuid_default(), editable=False)
    timestamp = models.DateTimeField(default=timezone.now)
    # TODO: Add constraint to ensure destination account expects statements (copy 0007)
    bank_account = models.ForeignKey(Account, related_name='imports')

    objects = StatementImportManager()

    def natural_key(self):
        return (self.uuid, )
示例#24
0
class Account(MPTTModel):
    """ Represents an account

    An account may have a parent, and may have zero or more children. Only root
    accounts can have a type, all child accounts are assumed to have the same
    type as their parent.

    An account's balance is calculated as the sum of all of the transaction Leg's
    referencing the account.

    Attributes:

        uuid (SmallUUID): UUID for account. Use to prevent leaking of IDs (if desired).
        name (str): Name of the account. Required.
        parent (Account|None): Parent account, nonen if root account
        code (str): Account code. Must combine with account codes of parent
            accounts to get fully qualified account code.
        type (str): Type of account as defined by :attr:`Account.TYPES`. Can only be set on
            root accounts. Child accounts are assumed to have the same time as their parent.
        TYPES (Choices): Available account types. Uses ``Choices`` from ``django-model-utils``. Types can be
            accessed in the form ``Account.TYPES.asset``, ``Account.TYPES.expense``, etc.
        is_bank_account (bool): Is this a bank account. This implies we can import bank statements into
            it and that it only supports a single currency.


    """

    TYPES = Choices(
        ("AS", "asset", "Asset"),  # Eg. Cash in bank
        ("LI", "liability",
         "Liability"),  # Eg. Loans, bills paid after the fact (in arrears)
        ("IN", "income", "Income"),  # Eg. Sales, housemate contributions
        ("EX", "expense", "Expense"),  # Eg. Office supplies, paying bills
        ("EQ", "equity", "Equity"),  # Eg. Money from shares
        ("TR", "trading",
         "Currency Trading"),  # Used to represent currency conversions
    )
    uuid = SmallUUIDField(default=uuid_default(), editable=False)
    name = models.CharField(max_length=50)
    parent = TreeForeignKey(
        "self",
        null=True,
        blank=True,
        related_name="children",
        db_index=True,
        on_delete=models.CASCADE,
    )
    code = models.CharField(max_length=3, null=True, blank=True)
    full_code = models.CharField(max_length=100,
                                 db_index=True,
                                 unique=True,
                                 null=True,
                                 blank=True)
    # TODO: Implement this child_code_width field, as it is probably a good idea
    # child_code_width = models.PositiveSmallIntegerField(default=1)
    type = models.CharField(max_length=2, choices=TYPES, blank=True)
    is_bank_account = models.BooleanField(
        default=False,
        blank=True,
        help_text="Is this a bank account. This implies we can import bank "
        "statements into it and that it only supports a single currency",
    )
    currencies = ArrayField(models.CharField(max_length=3), db_index=True)

    objects = AccountManager.from_queryset(AccountQuerySet)()

    class MPTTMeta:
        order_insertion_by = ["code"]

    class Meta:
        unique_together = (("parent", "code"), )

    def __init__(self, *args, **kwargs):
        super(Account, self).__init__(*args, **kwargs)
        self._initial_code = self.code

    def save(self, *args, **kwargs):
        is_creating = not bool(self.pk)
        if is_creating:
            update_fields = None
        else:
            # See issues #19 & #31. It seems that on Django 1.2, django-mptt's left/right
            # tree fields get overwritten on save. The solution here is to exclude them from
            # being modified upon saving by using the save methods' update_fields argument.
            update_fields = [
                "uuid",
                "name",
                "parent",
                "code",
                "type",
                "is_bank_account",
                "currencies",
            ]
        super(Account, self).save(*args, update_fields=update_fields, **kwargs)

        do_refresh = False

        # If we've just created a non-root node then we're going to need to load
        # the type back from the DB (as it is set by trigger)
        if is_creating and not self.is_root_node():
            do_refresh = True

        # If we've just create this account or if the code has changed then we're
        # going to need to reload from the DB (full_code is set by trigger)
        if is_creating or self._initial_code != self.code:
            do_refresh = True

        if do_refresh:
            self.refresh_from_db()

    @classmethod
    def validate_accounting_equation(cls):
        """Check that all accounts sum to 0"""
        balances = [
            account.balance(raw=True)
            for account in Account.objects.root_nodes()
        ]
        if sum(balances, Balance()) != 0:
            raise exceptions.AccountingEquationViolationError(
                "Account balances do not sum to zero. They sum to {}".format(
                    sum(balances)))

    def __str__(self):
        name = self.name or "Unnamed Account"
        if self.is_leaf_node():
            try:
                balance = self.balance()
            except ValueError:
                if self.full_code:
                    return "{} {}".format(self.full_code, name)
                else:
                    return name
            else:
                if self.full_code:
                    return "{} {} [{}]".format(self.full_code, name, balance)
                else:
                    return "{} [{}]".format(name, balance)

        else:
            return name

    def natural_key(self):
        return (self.uuid, )

    @property
    def sign(self):
        """
        Returns 1 if a credit should increase the value of the
        account, or -1 if a credit should decrease the value of the
        account.

        This is based on the account type as is standard accounting practice.
        The signs can be derrived from the following expanded form of the
        accounting equation:

            Assets = Liabilities + Equity + (Income - Expenses)

        Which can be rearranged as:

            0 = Liabilities + Equity + Income - Expenses - Assets

        Further details here: https://en.wikipedia.org/wiki/Debits_and_credits

        """
        return -1 if self.type in (Account.TYPES.asset,
                                   Account.TYPES.expense) else 1

    def balance(self, as_of=None, raw=False, leg_query=None, **kwargs):
        """Get the balance for this account, including child accounts

        Args:
            as_of (Date): Only include transactions on or before this date
            raw (bool): If true the returned balance should not have its sign
                        adjusted for display purposes.
            kwargs (dict): Will be used to filter the transaction legs

        Returns:
            Balance

        See Also:
            :meth:`simple_balance()`
        """
        balances = [
            account.simple_balance(as_of=as_of,
                                   raw=raw,
                                   leg_query=leg_query,
                                   **kwargs)
            for account in self.get_descendants(include_self=True)
        ]
        return sum(balances, Balance())

    def simple_balance(self, as_of=None, raw=False, leg_query=None, **kwargs):
        """Get the balance for this account, ignoring all child accounts

        Args:
            as_of (Date): Only include transactions on or before this date
            raw (bool): If true the returned balance should not have its sign
                        adjusted for display purposes.
            leg_query (models.Q): Django Q-expression, will be used to filter the transaction legs.
                                  allows for more complex filtering than that provided by **kwargs.
            kwargs (dict): Will be used to filter the transaction legs

        Returns:
            Balance
        """
        legs = self.legs
        if as_of:
            legs = legs.filter(transaction__date__lte=as_of)

        if leg_query or kwargs:
            leg_query = leg_query or models.Q()
            legs = legs.filter(leg_query, **kwargs)

        return legs.sum_to_balance() * (1 if raw else
                                        self.sign) + self._zero_balance()

    def _zero_balance(self):
        """Get a balance for this account with all currencies set to zero"""
        return Balance([Money("0", currency) for currency in self.currencies])

    @db_transaction.atomic()
    def transfer_to(self, to_account, amount, **transaction_kwargs):
        """Create a transaction which transfers amount to to_account

        This is a shortcut utility method which simplifies the process of
        transferring between accounts.

        This method attempts to perform the transaction in an intuitive manner.
        For example:

          * Transferring income -> income will result in the former decreasing and the latter increasing
          * Transferring asset (i.e. bank) -> income will result in the balance of both increasing
          * Transferring asset -> asset will result in the former decreasing and the latter increasing

        .. note::

            Transfers in any direction between ``{asset | expense} <-> {income | liability | equity}``
            will always result in both balances increasing. This may change in future if it is
            found to be unhelpful.

            Transfers to trading accounts will always behave as normal.

        Args:

            to_account (Account): The destination account.
            amount (Money): The amount to be transferred.
            transaction_kwargs: Passed through to transaction creation. Useful for setting the
                transaction `description` field.
        """
        if not isinstance(amount, Money):
            raise TypeError("amount must be of type Money")

        if to_account.sign == 1 and to_account.type != self.TYPES.trading:
            # Transferring from two positive-signed accounts implies that
            # the caller wants to reduce the first account and increase the second
            # (which is opposite to the implicit behaviour)
            direction = -1
        elif self.type == self.TYPES.liability and to_account.type == self.TYPES.expense:
            # Transfers from liability -> asset accounts should reduce both.
            # For example, moving money from Rent Payable (liability) to your Rent (expense) account
            # should use the funds you've built up in the liability account to pay off the expense account.
            direction = -1
        else:
            direction = 1

        transaction = Transaction.objects.create(**transaction_kwargs)
        Leg.objects.create(transaction=transaction,
                           account=self,
                           amount=+amount * direction)
        Leg.objects.create(transaction=transaction,
                           account=to_account,
                           amount=-amount * direction)
        return transaction
示例#25
0
class StatementLine(models.Model):
    """ Records an single imported bank statement line

    A StatementLine is purely a utility to aid in the creation of transactions
    (in the process known as reconciliation). StatementLines have no impact on
    account balances.

    However, the :meth:`StatementLine.create_transaction()` method can be used to create
    a transaction based on the information in the StatementLine.

    Attributes:

        uuid (SmallUUID): UUID for statement line. Use to prevent leaking of IDs (if desired).
        timestamp (datetime): The datetime when the object was created.
        date (date): The date given by the statement line
        statement_import (StatementImport): The import to which the line belongs
        amount (Decimal): The amount for the statement line, positive or nagative.
        description (str): Any description/memo information provided
        transaction (Transaction): Optionally, the transaction created for this statement line. This normally
            occurs during reconciliation. See also :meth:`StatementLine.create_transaction()`.
    """

    uuid = SmallUUIDField(default=uuid_default(), editable=False)
    timestamp = models.DateTimeField(default=timezone.now)
    date = models.DateField()
    statement_import = models.ForeignKey(StatementImport,
                                         related_name="lines",
                                         on_delete=models.CASCADE)
    amount = models.DecimalField(max_digits=MAX_DIGITS,
                                 decimal_places=DECIMAL_PLACES)
    description = models.TextField(default="", blank=True)
    type = models.CharField(max_length=50, default="")
    # TODO: Add constraint to ensure transaction amount = statement line amount
    # TODO: Add constraint to ensure one statement line per transaction
    transaction = models.ForeignKey(
        Transaction,
        default=None,
        blank=True,
        null=True,
        help_text="Reconcile this statement line to this transaction",
        on_delete=models.SET_NULL,
    )
    source_data = JSONField(
        default=json_default,
        help_text="Original data received from the data source.")

    objects = StatementLineManager()

    def natural_key(self):
        return (self.uuid, )

    @property
    def is_reconciled(self):
        """Has this statement line been reconciled?

        Determined as ``True`` if :attr:`transaction` has been set.

        Returns:
            bool: ``True`` if reconciled, ``False`` if not.
        """
        return bool(self.transaction)

    @db_transaction.atomic()
    def create_transaction(self, to_account):
        """Create a transaction for this statement amount and account, into to_account

        This will also set this StatementLine's ``transaction`` attribute to the newly
        created transaction.

        Args:
            to_account (Account): The account the transaction is into / out of.

        Returns:
            Transaction: The newly created (and committed) transaction.

        """
        from_account = self.statement_import.bank_account

        transaction = Transaction.objects.create()
        Leg.objects.create(transaction=transaction,
                           account=from_account,
                           amount=+(self.amount * -1))
        Leg.objects.create(transaction=transaction,
                           account=to_account,
                           amount=-(self.amount * -1))

        transaction.date = self.date
        transaction.save()

        self.transaction = transaction
        self.save()
        return transaction
示例#26
0
class Leg(models.Model):
    """ The leg of a transaction

    Represents a single amount either into or out of a transaction. All legs for a transaction
    must sum to zero, all legs must be of the same currency.

    Attributes:

        uuid (SmallUUID): UUID for transaction leg. Use to prevent leaking of IDs (if desired).
        transaction (Transaction): Transaction to which the Leg belongs.
        account (Account): Account the leg is transferring to/from.
        amount (Money): The amount being transferred
        description (str): Optional user-provided description
        type (str): :attr:`hordak.models.DEBIT` or :attr:`hordak.models.CREDIT`.

    """

    uuid = SmallUUIDField(default=uuid_default(), editable=False)
    transaction = models.ForeignKey(Transaction,
                                    related_name="legs",
                                    on_delete=models.CASCADE)
    account = models.ForeignKey(Account,
                                related_name="legs",
                                on_delete=models.CASCADE)
    amount = MoneyField(
        max_digits=MAX_DIGITS,
        decimal_places=DECIMAL_PLACES,
        help_text="Record debits as positive, credits as negative",
        default_currency=defaults.INTERNAL_CURRENCY,
    )
    description = models.TextField(default="", blank=True)

    objects = LegManager.from_queryset(LegQuerySet)()

    def save(self, *args, **kwargs):
        if self.amount.amount == 0:
            raise exceptions.ZeroAmountError()
        return super(Leg, self).save(*args, **kwargs)

    def natural_key(self):
        return (self.uuid, )

    @property
    def type(self):
        if self.amount.amount < 0:
            return DEBIT
        elif self.amount.amount > 0:
            return CREDIT
        else:
            # This should have been caught earlier by the database integrity check.
            # If you are seeing this then something is wrong with your DB checks.
            raise exceptions.ZeroAmountError()

    def is_debit(self):
        return self.type == DEBIT

    def is_credit(self):
        return self.type == CREDIT

    def account_balance_after(self):
        """Get the balance of the account associated with this leg following the transaction"""
        # TODO: Consider moving to annotation, particularly once we can count on Django 1.11's subquery support
        transaction_date = self.transaction.date
        return self.account.balance(leg_query=(
            models.Q(transaction__date__lt=transaction_date)
            | (models.Q(transaction__date=transaction_date)
               & models.Q(transaction_id__lte=self.transaction_id))))

    def account_balance_before(self):
        """Get the balance of the account associated with this leg before the transaction"""
        # TODO: Consider moving to annotation, particularly once we can count on Django 1.11's subquery support
        transaction_date = self.transaction.date
        return self.account.balance(
            leg_query=(models.Q(transaction__date__lt=transaction_date)
                       | (models.Q(transaction__date=transaction_date)
                          & models.Q(transaction_id__lt=self.transaction_id))))
class TransactionCsvImport(models.Model):
    STATES = Choices(
        ('pending', 'Pending'),
        ('uploaded', 'Uploaded, ready to import'),
        ('done', 'Import complete'),
    )

    uuid = SmallUUIDField(default=uuid_default(), editable=False)
    timestamp = models.DateTimeField(default=timezone.now, editable=False)
    has_headings = models.BooleanField(
        default=True, verbose_name='First line of file contains headings')
    file = models.FileField(upload_to='transaction_imports',
                            verbose_name='CSV file to import')
    state = models.CharField(max_length=20, choices=STATES, default='pending')
    date_format = models.CharField(choices=DATE_FORMATS,
                                   max_length=50,
                                   default='%d-%m-%Y',
                                   null=False)
    hordak_import = models.ForeignKey('hordak.StatementImport',
                                      on_delete=models.CASCADE)

    def _get_csv_reader(self):
        # TODO: Refactor to support multiple readers (xls, quickbooks, etc)
        csv_buffer = StringIO(self.file.read().decode())
        return csv.reader(csv_buffer)

    def create_columns(self):
        """For each column in file create a TransactionCsvImportColumn"""
        reader = self._get_csv_reader()
        headings = six.next(reader)
        try:
            examples = six.next(reader)
        except StopIteration:
            examples = []

        found_fields = set()
        for i, value in enumerate(headings):
            if i >= 20:
                break

            infer_field = self.has_headings and value not in found_fields

            to_field = {
                'date': 'date',
                'amount': 'amount',
                'description': 'description',
                'memo': 'description',
                'notes': 'description',
            }.get(value.lower(), '') if infer_field else ''

            if to_field:
                found_fields.add(to_field)

            TransactionCsvImportColumn.objects.update_or_create(
                transaction_import=self,
                column_number=i + 1,
                column_heading=value if self.has_headings else '',
                to_field=to_field,
                example=examples[i].strip() if examples else '',
            )

    def get_dataset(self):
        reader = self._get_csv_reader()
        if self.has_headings:
            six.next(reader)

        data = list(reader)
        headers = [
            column.to_field or 'col_%s' % column.column_number
            for column in self.columns.all()
        ]
        return Dataset(*data, headers=headers)
示例#28
0
class User(TimestampModel, OrganizationMixin, auth_models.AbstractBaseUser,
           auth_models.PermissionsMixin):
    """Kenedy application user"""
    username = SmallUUIDField('Username',
                              max_length=150,
                              unique=True,
                              default=uuid_default(),
                              editable=False)

    is_staff = models.BooleanField(_('Staff Status'), default=False)
    is_active = models.BooleanField(_('Active'), default=True, db_index=True)

    email = models.EmailField(_('Email Address'), editable=False)
    first_name = models.CharField(_('First Name'), max_length=100, blank=True)
    last_name = models.CharField(_('Last Name'), max_length=200, blank=True)

    locations = models.ManyToManyField('location.Location',
                                       through='location.UserLocation',
                                       related_name='userlocations_set')
    location = models.ForeignKey('location.Location')

    unsubscribed = models.BooleanField(default=False)

    # The actual username field will be email, but we can't add a unique
    # constraint on email, while we can on username
    USERNAME_FIELD = 'username'
    REQUIRED_FIELDS = ['email', 'organization']

    objects = UserManager()

    class Meta(object):
        """Meta options for the model"""
        unique_together = ('email', 'organization')
        indexes = [
            models.Index(fields=['organization', 'is_active', 'unsubscribed'],
                         name="org_active_unsub_idx"),
            models.Index(fields=['organization', 'email'],
                         name="org_email_idx")
        ]
        ordering = ['pk']

    def __unicode__(self):
        """Unicode representation of the user"""
        if self.first_name and self.last_name:
            return self.first_name + u' ' + self.last_name
        if self.first_name:
            return self.first_name
        return self.email

    def get_short_name(self):
        """Short name representation of user"""
        return self.first_name

    @property
    def to_address(self):
        """Address to be used in the 'To' field in emails"""
        return u'"{0}" <{1}>'.format(
            unicode(Header(unicode(self), 'utf-8')).replace(u'"', u"'"),
            self.email)

    @cached_property
    def state(self):
        """State the user is in"""
        return self.location.state

    def manage_url(self, email):
        """The URL a user can visit to manage their profile and unsubscribe"""
        return 'https://{domain}{path}?email={email_uuid}'.format(
            domain=self.organization.primary_domain.hostname,
            path=reverse('accounts:self_update_user',
                         kwargs={'slug': self.username}),
            email_uuid=email.uuid)

    def unsubscribe_url(self, email):
        """URL the user can visit to unsubscribe"""
        # pylint: disable=line-too-long
        return 'https://{domain}{path}?email={email_uuid}&user={user_uuid}'.format(
            domain=self.organization.primary_domain.hostname,
            path=reverse('unsubscribe:unsubscribe'),
            email_uuid=email.uuid,
            user_uuid=self.username)
示例#29
0
class Invitation(models.Model):
    """Stores information during the process of both parties
    approving a link between a User and an Account."""

    EXPIRE_TIMEDELTA = datetime.timedelta(days=7)

    uuid = SmallUUIDField(editable=False, default=uuid_default())
    creation_date = models.DateField(auto_now_add=True)
    completion_date = models.DateField(editable=False, null=True, blank=True)
    campaign = models.ForeignKey('accounts.Campaign', on_delete=models.CASCADE)
    joiner = models.ForeignKey(settings.AUTH_USER_MODEL,
                               on_delete=models.CASCADE,
                               null=True,
                               blank=True)
    approver = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        related_name='invites_approved',
    )
    joiner_external_identifier = models.TextField(null=True, blank=True)

    def __str__(self):
        return f"{self.joiner_external_identifier} -> {self.campaign!s}"

    class Expired(Exception):
        pass

    class ApproverNotAuthorized(Exception):
        pass

    def _validate(self, approving_user, accepting_user):
        assert approving_user, accepting_user
        if not approving_user.campaigns.filter(
                campaignrole__is_gm=True).filter(id=self.campaign_id):
            raise self.ApproverNotAuthorized
        if datetime.date.today() > self.creation_date + self.EXPIRE_TIMEDELTA:
            raise self.Expired

    def accept(self, accepting_user=None):
        self._validate(self.approver, accepting_user or self.joiner)
        self._complete(self.approver, accepting_user or self.joiner)

    def reject(self):
        # reject intentionally gives no information to the inviter that the invitee
        #   has seen the message; it will just hang out in inviter's queue until it expires
        self.joiner = None
        self.save()

    def approve(self, approving_user):
        self._validate(approving_user, self.joiner)
        self._complete(approving_user, self.joiner)

    def _complete(self, approving_user, accepting_user):
        CampaignRole.objects.create(user=accepting_user,
                                    campaign=self.campaign)
        self.approver = approving_user
        self.joiner = accepting_user
        self.completion_date = datetime.date.today()
        self.save()

    @classmethod
    def get_current_invites(cls):
        earliest_allowed_creation_date = datetime.date.today(
        ) - datetime.timedelta(days=7)
        return cls.objects.filter(
            creation_date__gt=earliest_allowed_creation_date).filter(
                completion_date__isnull=True)
示例#30
0
class Calibrator(TimeStampedModel):
    id = SmallUUIDField(default=uuid_default(),
                        primary_key=True,
                        db_index=True,
                        editable=False,
                        verbose_name='ID')

    reference = models.ForeignKey('monitors.Monitor',
                                  related_name='reference_calibrator',
                                  on_delete=models.CASCADE)

    # TODO: What if this has gone inactive?
    colocated = models.ForeignKey('monitors.Monitor',
                                  related_name='colocated_calibrator',
                                  on_delete=models.CASCADE)

    is_active = models.BooleanField(default=False)

    calibration = models.OneToOneField('calibrations.Calibration',
                                       blank=True,
                                       null=True,
                                       related_name='calibrator_current',
                                       on_delete=models.SET_NULL)

    def get_distance(self):
        return geopy_distance(
            (self.reference.position.y, self.reference.position.x),
            (self.colocated.position.y, self.colocated.position.x),
        )

    def calibrate(self, end_date=None):
        # assert self.reference.is_active
        # assert self.colocated.is_active

        if end_date is None:
            end_date = timezone.now().date()

        # Set of coefficients to test and the
        # formulas for their calibrations
        formulas = [
            (['particles_03-10', 'particles_10-25',
              'humidity'], lambda reg: ' + '.join([
                  f"((particles_03um - particles_10um) * {reg.coef_[0]})",
                  f"((particles_10um - particles_25um) * {reg.coef_[1]})",
                  f"(humidity * {reg.coef_[2]})"
                  f"{reg.intercept_}",
              ])),
            (['particles_10-25', 'particles_25-05',
              'humidity'], lambda reg: ' + '.join([
                  f"((particles_10um - particles_25um) * {reg.coef_[0]})",
                  f"((particles_25um - particles_05um) * {reg.coef_[1]})",
                  f"(humidity * {reg.coef_[2]})",
                  f"{reg.intercept_}",
              ]))
        ]

        results = list(
            filter(bool, [
                self.generate_calibration(
                    coefs=coefs,
                    formula=formula,
                    end_date=end_date,
                    days=days,
                ) for (coefs, formula
                       ), days in itertools.product(formulas, [7, 14, 21, 28])
            ]))

        print(
            f'{self.reference.name} / {self.colocated.name} ({self.get_distance().meters} m)'
        )
        pprint(results)
        print('\n')

        if results:
            # Sort by R2 (highest last)...
            results.sort(key=lambda i: i['r2'])

            # ...and save the calibration
            self.calibrations.create(**results[-1])
            self.calibration = self.calibrations.order_by('end_date').first()

    def generate_calibration(self, coefs, formula, end_date, days):
        start_date = end_date - timedelta(days=days)

        # Load the reference entries into a DataFrame, sampled hourly.
        ref_qs = (self.reference.entries.filter(
            timestamp__date__range=(start_date, end_date)).annotate(
                ref_pm25=F('pm25')).values('timestamp', 'ref_pm25'))

        if not ref_qs.exists():
            print(
                f'Calibrator {self.pk}: no ref_qs ({start_date} - {end_date})')
            return

        ref_df = pd.DataFrame(ref_qs).set_index('timestamp')
        ref_df = pd.to_numeric(ref_df.ref_pm25)
        # ref_df = ref_df.ref_pm25.astype('float')
        ref_df = ref_df.resample('H').mean()

        # Load the colocated entries into a DataFrame
        col_qs = (self.colocated.entries.filter(
            timestamp__date__range=(start_date, end_date),
            sensor=self.colocated.default_sensor,
        ).annotate(col_pm25=F('pm25')).values('timestamp', 'col_pm25',
                                              'humidity', 'particles_03um',
                                              'particles_05um',
                                              'particles_10um',
                                              'particles_25um'))

        if not col_qs.exists():
            print(
                f'Calibrator {self.pk}: no col_qs ({start_date} - {end_date})')
            return

        col_df = pd.DataFrame(col_qs).set_index('timestamp')

        # Convert columns to floats
        # cols = df.columns[df.dtypes.eq('object')]
        col_df[col_df.columns] = col_df[col_df.columns].apply(pd.to_numeric,
                                                              errors='coerce')

        # particle count calculations and hourly sample
        col_df['particles_03-10'] = col_df['particles_03um'] - col_df[
            'particles_10um']
        col_df['particles_10-25'] = col_df['particles_10um'] - col_df[
            'particles_25um']
        col_df['particles_25-05'] = col_df['particles_25um'] - col_df[
            'particles_05um']
        col_df = col_df.resample('H').mean()

        # Merge the dataframes
        merged = pd.concat([ref_df, col_df], axis=1, join="inner")
        merged = merged.dropna()

        if not len(merged):
            print(
                f'Calibrator {self.pk}: no merged ({start_date} - {end_date})')
            return

        # Linear Regression time!
        endog = merged['ref_pm25']
        exog = merged[coefs]

        try:
            reg = LinearRegression()
            reg.fit(exog, endog)
        except ValueError as err:
            import code
            code.interact(local=locals())

        return {
            'start_date': start_date,
            'end_date': end_date,
            'r2': reg.score(exog, endog),
            'formula': formula(reg),
        }