示例#1
0
    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
示例#2
0
def test_fix_amount_for_overlap(amount, overlap, remainder, expected):
    assert fix_amount_for_overlap(amount, overlap, remainder) == expected
示例#3
0
    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
示例#4
0
文件: rent.py 项目: igordavydsson/mvj
    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
示例#5
0
文件: invoice.py 项目: hkotkanen/mvj
    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