def create(self, validated_data): today = timezone.now().date() lease = validated_data.get('lease') billing_period_start_date = validated_data.get( 'billing_period_start_date', today) billing_period_end_date = validated_data.get('billing_period_end_date', today) billing_period = (billing_period_start_date, billing_period_end_date) total_amount = sum( [row.get('amount') for row in validated_data.get('rows', [])]) # TODO: Handle possible exception shares = lease.get_tenant_shares_for_period(billing_period_start_date, billing_period_end_date) invoice = None invoiceset = None if len(shares.items()) > 1: invoiceset = InvoiceSet.objects.create( lease=lease, billing_period_start_date=billing_period_start_date, billing_period_end_date=billing_period_end_date) # TODO: check for periods without 1/1 shares for contact, share in shares.items(): invoice_row_data = [] billable_amount = Decimal(0) for tenant, overlaps in share.items(): for row in validated_data.get('rows', []): overlap_amount = Decimal(0) for overlap in overlaps: overlap_amount += fix_amount_for_overlap( row.get('amount', Decimal(0)), overlap, subtract_ranges_from_ranges([billing_period], [overlap])) share_amount = Decimal( overlap_amount * Decimal(tenant.share_numerator / tenant.share_denominator)).quantize( Decimal('.01'), rounding=ROUND_HALF_UP) billable_amount += share_amount invoice_row_data.append({ 'tenant': tenant, 'receivable_type': row.get('receivable_type'), 'billing_period_start_date': overlap[0], 'billing_period_end_date': overlap[1], 'amount': share_amount, }) invoice = Invoice.objects.create( type=InvoiceType.CHARGE, lease=lease, recipient=contact, due_date=validated_data.get('due_date'), invoicing_date=today, state=InvoiceState.OPEN, billing_period_start_date=billing_period_start_date, billing_period_end_date=billing_period_end_date, total_amount=total_amount, billed_amount=billable_amount, outstanding_amount=billable_amount, invoiceset=invoiceset, notes=validated_data.get('notes', ''), ) for invoice_row_datum in invoice_row_data: invoice_row_datum['invoice'] = invoice InvoiceRow.objects.create(**invoice_row_datum) if invoiceset: return invoiceset else: return invoice
def test_fix_amount_for_overlap(amount, overlap, remainder, expected): assert fix_amount_for_overlap(amount, overlap, remainder) == expected
def calculate_invoices(self, period_rents): from leasing.models import ReceivableType # TODO: Make configurable receivable_type_rent = ReceivableType.objects.get(pk=1) # rents = self.determine_payable_rents_and_periods(self.start_date, self.end_date) invoice_data = [] for billing_period, period_rent in period_rents.items(): billing_period_invoices = [] rent_amount = period_rent['amount'] shares = self.get_tenant_shares_for_period(*billing_period) for contact, share in shares.items(): billable_amount = Decimal(0) contact_ranges = [] invoice_row_data = [] for tenant, overlaps in share.items(): overlap_amount = Decimal(0) for overlap in overlaps: overlap_amount += fix_amount_for_overlap( rent_amount, overlap, subtract_ranges_from_ranges([billing_period], [overlap])) share_amount = Decimal( overlap_amount * Decimal(tenant.share_numerator / tenant.share_denominator)).quantize( Decimal('.01'), rounding=ROUND_HALF_UP) billable_amount += share_amount contact_ranges.append(overlap) invoice_row_data.append({ 'tenant': tenant, 'receivable_type': receivable_type_rent, 'billing_period_start_date': overlap[0], 'billing_period_end_date': overlap[1], 'amount': share_amount, }) combined_contact_ranges = combine_ranges(contact_ranges) total_contact_period_amount = Decimal(0) for combined_contact_range in combined_contact_ranges: total_contact_period_amount += fix_amount_for_overlap( rent_amount, combined_contact_range, subtract_ranges_from_ranges([billing_period], [combined_contact_range])) total_contact_period_amount = Decimal( total_contact_period_amount).quantize( Decimal('.01'), rounding=ROUND_HALF_UP) invoice_datum = { 'type': InvoiceType.CHARGE, 'lease': self, 'recipient': contact, 'due_date': period_rent['due_date'], 'billing_period_start_date': billing_period[0], 'billing_period_end_date': billing_period[1], 'total_amount': total_contact_period_amount, 'billed_amount': billable_amount, 'rows': invoice_row_data, 'explanations': period_rent['explanations'], 'state': InvoiceState.OPEN, } billing_period_invoices.append(invoice_datum) invoice_data.append(billing_period_invoices) return invoice_data
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
def create(self, validated_data): # noqa: C901 TODO today = timezone.now().date() lease = validated_data.get('lease') billing_period_start_date = validated_data.get('billing_period_start_date', today) billing_period_end_date = validated_data.get('billing_period_end_date', today) billing_period = (billing_period_start_date, billing_period_end_date) total_amount = sum([row.get('amount') for row in validated_data.get('rows', [])]) # TODO: Handle possible exception shares = lease.get_tenant_shares_for_period(billing_period_start_date, billing_period_end_date) invoice = None invoiceset = None if len(shares.items()) > 1: invoiceset = InvoiceSet.objects.create(lease=lease, billing_period_start_date=billing_period_start_date, billing_period_end_date=billing_period_end_date) invoice_data = [] # TODO: check for periods without 1/1 shares for contact, share in shares.items(): invoice_rows_by_index = defaultdict(list) for tenant, overlaps in share.items(): for row_index, row in enumerate(validated_data.get('rows', [])): overlap_amount = Decimal(0) for overlap in overlaps: overlap_amount += fix_amount_for_overlap( row.get('amount', Decimal(0)), overlap, subtract_ranges_from_ranges([billing_period], [overlap])) # Notice! Custom charge uses tenant share, not rent share share_amount = Decimal( overlap_amount * Decimal(tenant.share_numerator / tenant.share_denominator) ).quantize(Decimal('.01'), rounding=ROUND_HALF_UP) invoice_rows_by_index[row_index].append({ 'tenant': tenant, 'receivable_type': row.get('receivable_type'), 'billing_period_start_date': overlap[0], 'billing_period_end_date': overlap[1], 'amount': share_amount, }) invoice_data.append({ 'type': InvoiceType.CHARGE, 'lease': lease, 'recipient': contact, 'due_date': validated_data.get('due_date'), 'invoicing_date': today, 'state': InvoiceState.OPEN, 'billing_period_start_date': billing_period_start_date, 'billing_period_end_date': billing_period_end_date, 'total_amount': total_amount, 'invoiceset': invoiceset, 'notes': validated_data.get('notes', ''), 'rows': invoice_rows_by_index, }) # Check that the total row amount is correct or add the missing # amount to a random invoice if not for input_row_index, input_row in enumerate(validated_data.get('rows', [])): row_sum = Decimal(0) all_rows = [] for invoice_datum in invoice_data: for row_data in invoice_datum['rows'][input_row_index]: row_sum += row_data['amount'] all_rows.append(row_data) difference = input_row['amount'] - row_sum if difference: random_row = choice(all_rows) random_row['amount'] += difference # Flatten rows, update totals and save the invoices for invoice_datum in invoice_data: invoice_datum['rows'] = [row for rows in invoice_datum['rows'].values() for row in rows] rows_sum = sum([row['amount'] for row in invoice_datum['rows']]) invoice_datum['billed_amount'] = rows_sum invoice_datum['outstanding_amount'] = rows_sum invoice_row_data = invoice_datum.pop('rows') invoice = Invoice.objects.create(**invoice_datum) for invoice_row_datum in invoice_row_data: invoice_row_datum['invoice'] = invoice InvoiceRow.objects.create(**invoice_row_datum) if invoiceset: return invoiceset else: return invoice