Example #1
0
    def get_amount_for_date_range(self,
                                  date_range_start,
                                  date_range_end,
                                  explain=False):  # noqa: C901 TODO
        assert date_range_start <= date_range_end, 'date_range_start cannot be after date_range_end.'

        explanation = Explanation()

        range_filtering = Q(
            Q(Q(end_date=None) | Q(end_date__gte=date_range_start))
            & Q(Q(start_date=None) | Q(start_date__lte=date_range_end)))

        fixed_initial_year_rents = self.fixed_initial_year_rents.filter(
            range_filtering)
        contract_rents = self.contract_rents.filter(range_filtering)
        rent_adjustments = self.rent_adjustments.filter(range_filtering)

        total = Decimal('0.00')
        fixed_applied = False
        remaining_ranges = []

        # TODO: seasonal spanning multiple years
        if self.is_seasonal():
            seasonal_period_start = datetime.date(
                year=date_range_start.year,
                month=self.seasonal_start_month,
                day=self.seasonal_start_day)
            seasonal_period_end = datetime.date(year=date_range_start.year,
                                                month=self.seasonal_end_month,
                                                day=self.seasonal_end_day)

            if date_range_start < seasonal_period_start and date_range_start < seasonal_period_end:
                date_range_start = seasonal_period_start

            if date_range_end > seasonal_period_end and date_range_end > seasonal_period_start:
                date_range_end = seasonal_period_end
        else:
            if ((self.start_date and date_range_start < self.start_date) and
                (not self.end_date or date_range_start < self.end_date)):
                date_range_start = self.start_date

            if ((self.end_date and date_range_end > self.end_date) and
                (self.start_date and date_range_end > self.start_date)):
                date_range_end = self.end_date

        for fixed_initial_year_rent in fixed_initial_year_rents:
            (fixed_overlap,
             fixed_remainders) = get_range_overlap_and_remainder(
                 date_range_start, date_range_end,
                 *fixed_initial_year_rent.date_range)

            if not fixed_overlap:
                continue

            if fixed_remainders:
                remaining_ranges.extend(fixed_remainders)

            fixed_applied = True

            fixed_amount = fixed_initial_year_rent.get_amount_for_date_range(
                *fixed_overlap)

            fixed_explanation_item = explanation.add(
                subject=fixed_initial_year_rent,
                date_ranges=[fixed_overlap],
                amount=fixed_amount)

            for rent_adjustment in rent_adjustments:
                if fixed_initial_year_rent.intended_use and \
                        rent_adjustment.intended_use != fixed_initial_year_rent.intended_use:
                    continue

                (adjustment_overlap,
                 adjustment_remainders) = get_range_overlap_and_remainder(
                     fixed_overlap[0], fixed_overlap[1],
                     *rent_adjustment.date_range)

                if not adjustment_overlap:
                    continue

                tmp_amount = fix_amount_for_overlap(fixed_amount,
                                                    adjustment_overlap,
                                                    adjustment_remainders)
                adjustment_amount = rent_adjustment.get_amount_for_date_range(
                    tmp_amount, *adjustment_overlap)
                fixed_amount += adjustment_amount

                explanation.add(subject=rent_adjustment,
                                date_ranges=[adjustment_overlap],
                                amount=adjustment_amount,
                                related_item=fixed_explanation_item)

            total += fixed_amount

        if fixed_applied:
            if not remaining_ranges:
                if explain:
                    explanation.add(subject=self,
                                    date_ranges=[(date_range_start,
                                                  date_range_end)],
                                    amount=total)

                    return total, explanation
                else:
                    return total
            else:
                date_ranges = remaining_ranges
        else:
            date_ranges = [(date_range_start, date_range_end)]

        # We may need to calculate multiple separate ranges if the rent
        # type is index or manual because the index number could be different
        # in different years.
        if self.type in [RentType.INDEX, RentType.MANUAL]:
            date_ranges = self.split_ranges_by_cycle(date_ranges)

        for (range_start, range_end) in date_ranges:
            if self.type == RentType.ONE_TIME:
                total += self.amount
                continue

            for contract_rent in contract_rents:
                (contract_overlap,
                 _remainder) = get_range_overlap_and_remainder(
                     range_start, range_end, *contract_rent.date_range)

                if not contract_overlap:
                    continue

                if self.type == RentType.FIXED:
                    contract_amount = contract_rent.get_amount_for_date_range(
                        *contract_overlap)
                    contract_rent_explanation_item = explanation.add(
                        subject=contract_rent,
                        date_ranges=[contract_overlap],
                        amount=contract_amount)
                elif self.type == RentType.MANUAL:
                    contract_amount = contract_rent.get_amount_for_date_range(
                        *contract_overlap)
                    explanation.add(subject=contract_rent,
                                    date_ranges=[contract_overlap],
                                    amount=contract_amount)

                    manual_ratio = self.manual_ratio

                    if self.cycle == RentCycle.APRIL_TO_MARCH and is_date_on_first_quarter(
                            contract_overlap[0]):
                        manual_ratio = self.manual_ratio_previous

                    contract_amount *= manual_ratio

                    contract_rent_explanation_item = explanation.add(
                        subject={
                            "subject_type":
                            "ratio",
                            "description":
                            _("Manual ratio {ratio}").format(
                                ratio=manual_ratio),
                        },
                        date_ranges=[contract_overlap],
                        amount=contract_amount)
                elif self.type == RentType.INDEX:
                    original_rent_amount = contract_rent.get_base_amount_for_date_range(
                        *contract_overlap)

                    index = self.get_index_for_date(contract_overlap[0])

                    index_calculation = IndexCalculation(
                        amount=original_rent_amount,
                        index=index,
                        index_type=self.index_type,
                        precision=self.index_rounding,
                        x_value=self.x_value,
                        y_value=self.y_value)

                    contract_amount = index_calculation.calculate()

                    contract_rent_explanation_item = explanation.add(
                        subject=contract_rent,
                        date_ranges=[contract_overlap],
                        amount=original_rent_amount)

                    index_explanation_item = explanation.add(
                        subject=index,
                        date_ranges=[contract_overlap],
                        amount=contract_amount,
                        related_item=contract_rent_explanation_item)

                    for item in index_calculation.explanation_items:
                        explanation.add_item(
                            item, related_item=index_explanation_item)

                elif self.type == RentType.FREE:
                    continue
                else:
                    raise NotImplementedError(
                        'RentType {} not implemented'.format(self.type))

                for rent_adjustment in rent_adjustments:
                    if rent_adjustment.intended_use != contract_rent.intended_use:
                        continue

                    (adjustment_overlap,
                     adjustment_remainders) = get_range_overlap_and_remainder(
                         contract_overlap[0], contract_overlap[1],
                         *rent_adjustment.date_range)

                    if not adjustment_overlap:
                        continue

                    tmp_amount = fix_amount_for_overlap(
                        contract_amount, adjustment_overlap,
                        adjustment_remainders)
                    adjustment_amount = rent_adjustment.get_amount_for_date_range(
                        tmp_amount, *adjustment_overlap)
                    contract_amount += adjustment_amount

                    explanation.add(
                        subject=rent_adjustment,
                        date_ranges=[adjustment_overlap],
                        amount=adjustment_amount,
                        related_item=contract_rent_explanation_item)

                total += max(Decimal(0), contract_amount)

        explanation.add(subject=self,
                        date_ranges=[(date_range_start, date_range_end)],
                        amount=total)

        if explain:
            return total, explanation
        else:
            return total
Example #2
0
def test_get_overlap(s1, e1, s2, e2, expected):
    assert get_range_overlap_and_remainder(s1, e1, s2, e2) == expected
Example #3
0
    def get_tenant_shares_for_period(
            self, billing_period_start_date,
            billing_period_end_date):  # noqa C901 TODO
        tenant_range_filter = Q(
            Q(
                Q(tenantcontact__end_date=None)
                | Q(tenantcontact__end_date__gte=billing_period_start_date))
            & Q(
                Q(tenantcontact__start_date=None)
                | Q(tenantcontact__start_date__lte=billing_period_end_date))
            & Q(tenantcontact__deleted__isnull=True))

        shares = {}
        for tenant in self.tenants.filter(tenant_range_filter).distinct():
            tenant_tenantcontacts = tenant.get_tenant_tenantcontacts(
                billing_period_start_date, billing_period_end_date)
            billing_tenantcontacts = tenant.get_billing_tenantcontacts(
                billing_period_start_date, billing_period_end_date)

            if not tenant_tenantcontacts or not billing_tenantcontacts:
                raise Exception(
                    'No suitable contacts in the period {} - {}'.format(
                        billing_period_start_date, billing_period_end_date))

            (tenant_overlap,
             tenant_remainders) = get_range_overlap_and_remainder(
                 billing_period_start_date, billing_period_end_date,
                 *tenant_tenantcontacts[0].date_range)

            if not tenant_overlap:
                raise Exception('No overlap with this billing period. Error?')

            for billing_tenantcontact in billing_tenantcontacts:
                (billing_overlap,
                 billing_remainders) = get_range_overlap_and_remainder(
                     tenant_overlap[0], tenant_overlap[1],
                     *billing_tenantcontact.date_range)

                if not billing_overlap:
                    continue

                if billing_tenantcontact.contact not in shares:
                    shares[billing_tenantcontact.contact] = {}

                if tenant not in shares[billing_tenantcontact.contact]:
                    shares[billing_tenantcontact.contact][tenant] = []

                shares[billing_tenantcontact.contact][tenant].append(
                    billing_overlap)

            ranges_for_billing_contacts = []
            for billing_contact, tenant_overlaps in shares.items():
                if tenant in tenant_overlaps:
                    ranges_for_billing_contacts.extend(tenant_overlaps[tenant])

            leftover_ranges = subtract_ranges_from_ranges(
                [tenant_overlap], ranges_for_billing_contacts)

            if leftover_ranges:
                # TODO: Which tenantcontact to use when multiple tenantcontacts
                if tenant_tenantcontacts[0].contact not in shares:
                    shares[tenant_tenantcontacts[0].contact] = {
                        tenant: [],
                    }
                shares[tenant_tenantcontacts[0].contact][tenant].extend(
                    leftover_ranges)

        return shares