Coverage for jacc/settle.py : 82%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from decimal import Decimal
2from django.conf import settings
3from django.core.exceptions import ValidationError
4from django.db import transaction
5from django.utils.timezone import now
6from django.utils.translation import gettext as _
7from jacc.models import AccountEntry, Invoice, EntryType, Account, INVOICE_CREDIT_NOTE, INVOICE_DEFAULT
10@transaction.atomic
11def settle_invoice(receivables_account: Account, settlement: AccountEntry, invoice: Invoice, cls, **kwargs) -> list:
12 """
13 Finds unpaid items in the invoice and generates entries to receivables account.
14 Settlement is matched to invoice items based on entry types payback order.
15 Generated payment entries have 'parent' field pointing to settlement, so that
16 if settlement is (ever) deleted the payment entries will get deleted as well.
17 In case of overpayment method generates entry to receivables account without matching invoice settled_item
18 (only matching settled_invoice).
19 :param receivables_account: Account which receives settled entries of the invoice
20 :param settlement: Settlement to target to unpaid invoice items
21 :param invoice: Invoice to be settled
22 :param cls: Class for generated account entries, e.g. AccountEntry
23 :param kwargs: Extra attributes for created for generated account entries
24 :return: list (generated receivables account entries)
25 """
26 assert isinstance(invoice, Invoice)
27 if not invoice:
28 raise ValidationError('Cannot target settlement {} without settled invoice'.format(settlement))
29 if not receivables_account:
30 raise ValidationError('Receivables account missing. Invoice with no rows?')
31 if settlement.amount is None: # nothing to do
32 return list()
33 if settlement.amount < Decimal(0) and invoice.type != INVOICE_CREDIT_NOTE:
34 raise ValidationError('Cannot target negative settlement {} to invoice {}'.format(settlement, invoice))
35 if settlement.amount > Decimal(0) and invoice.type == INVOICE_CREDIT_NOTE:
36 raise ValidationError('Cannot target positive settlement {} to credit note {}'.format(settlement, invoice))
37 if settlement.type is None or not settlement.type.is_settlement:
38 raise ValidationError('Cannot settle account entry {} which is not settlement'.format(settlement))
40 new_payments = []
41 remaining = Decimal(settlement.amount)
42 timestamp = kwargs.pop('timestamp', settlement.timestamp)
43 assert isinstance(invoice, Invoice)
44 for item, bal in invoice.get_unpaid_items(receivables_account):
45 if invoice.type == INVOICE_DEFAULT:
46 if bal > Decimal(0):
47 amt = min(remaining, bal)
48 ae = cls.objects.create(account=receivables_account, amount=-amt, type=item.type, settled_item=item, settled_invoice=invoice,
49 timestamp=timestamp, description=settlement.description, parent=settlement, **kwargs)
50 new_payments.append(ae)
51 remaining -= amt
52 if remaining <= Decimal(0):
53 break
54 elif invoice.type == INVOICE_CREDIT_NOTE:
55 if bal < Decimal(0):
56 amt = max(remaining, bal)
57 ae = cls.objects.create(account=receivables_account, amount=-amt, type=item.type, settled_item=item, settled_invoice=invoice,
58 timestamp=timestamp, description=settlement.description, parent=settlement, **kwargs)
59 new_payments.append(ae)
60 remaining -= amt
61 if remaining >= Decimal(0):
62 break
63 else:
64 raise Exception('jacc.settle.settle_assigned_invoice() unimplemented for invoice type {}'.format(invoice.type))
66 invoice.update_cached_fields()
67 return new_payments
70@transaction.atomic
71def settle_assigned_invoice(receivables_account: Account, settlement: AccountEntry, cls, **kwargs) -> list:
72 """
73 Finds unpaid items in the invoice and generates entries to receivables account.
74 Settlement is matched to invoice items based on entry types payback order.
75 Generated payment entries have 'parent' field pointing to settlement, so that
76 if settlement is (ever) deleted the payment entries will get deleted as well.
77 In case of overpayment method generates entry to receivables account without matching invoice settled_item
78 (only matching settled_invoice).
79 :param receivables_account: Account which receives settled entries of the invoice
80 :param settlement: Settlement to target to unpaid invoice items
81 :param cls: Class for generated account entries, e.g. AccountEntry
82 :param kwargs: Extra attributes for created for generated account entries
83 :return: list (generated receivables account entries)
84 """
85 if settlement.settled_invoice is None:
86 raise ValidationError('Cannot target settlement {} without settled invoice'.format(settlement))
87 return settle_invoice(receivables_account, settlement, settlement.settled_invoice, cls, **kwargs)
90@transaction.atomic
91def settle_credit_note(credit_note: Invoice, debit_note: Invoice, cls, account: Account, **kwargs) -> list:
92 """
93 Settles credit note. Records settling account entries for both original invoice and the credit note
94 (negative entries for the credit note).
95 Default timestamp for account entries is 'created' time of the credit note, can be overriden by kwargs.
96 :param credit_note: Credit note to settle
97 :param debit_note: Invoice to settle
98 :param cls: AccountEntry (derived) class to use for new entries
99 :param account: Settlement account
100 :param kwargs: Variable arguments to cls() instance creation
101 :return: list of new payments
102 """
103 assert isinstance(credit_note, Invoice)
104 assert credit_note.type == INVOICE_CREDIT_NOTE
105 assert debit_note
106 assert debit_note.type == INVOICE_DEFAULT
108 credit = -credit_note.get_unpaid_amount()
109 balance = debit_note.get_unpaid_amount()
111 amt = min(balance, credit)
112 amount = kwargs.pop('amount', None)
113 if amount is not None:
114 if amount > amt:
115 raise ValidationError(_('Cannot settle credit note amount which is larger than remaining unpaid balance'))
116 amt = amount
118 entry_type = kwargs.pop('entry_type', None)
119 if entry_type is None:
120 if not hasattr(settings, 'E_CREDIT_NOTE_RECONCILIATION'):
121 raise Exception('settle_credit_note() requires settings.E_CREDIT_NOTE_RECONCILIATION (account entry type code) '
122 'or entry_type to be pass in kwargs')
123 entry_type = EntryType.objects.get(code=settings.E_CREDIT_NOTE_RECONCILIATION)
124 description = kwargs.pop('description', _('credit.note.reconciliation'))
126 pmts = []
127 if amt > Decimal(0):
128 timestamp = kwargs.pop('timestamp', credit_note.created or now())
129 # record entry to debit note settlement account
130 pmt1 = cls.objects.create(account=account, amount=amt, type=entry_type, settled_invoice=debit_note,
131 description=description + ' #{}'.format(credit_note.number), timestamp=timestamp, **kwargs)
132 pmts.append(pmt1)
133 # record entry to credit note settlement account
134 pmt2 = cls.objects.create(account=account, parent=pmt1, amount=-amt, type=entry_type, settled_invoice=credit_note,
135 description=description + ' #{}'.format(debit_note.number), timestamp=timestamp, **kwargs)
136 pmts.append(pmt2)
138 return pmts