def get_index_for_date(self, the_date): if self.cycle == RentCycle.APRIL_TO_MARCH and is_date_on_first_quarter( the_date): return Index.objects.get_latest_for_year(the_date.year - 1) return Index.objects.get_latest_for_date(the_date)
def test_is_date_on_first_quarter(the_day, expected): if inspect.isclass(expected) and issubclass(expected, Exception): with pytest.raises(expected): is_date_on_first_quarter(the_day) else: assert is_date_on_first_quarter(the_day) == expected
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