Coverage for jacc/settle.py: 82%

# Coverage for jacc/settle.py : 82%

## 79 statements 65 run 14 missing 0 excluded

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

1 from decimal import Decimal

2 from django.conf import settings

3 from django.core.exceptions import ValidationError

4 from django.db import transaction

5 from django.utils.timezone import now

6 from django.utils.translation import gettext as _

7 from jacc.models import AccountEntry, Invoice, EntryType, Account, INVOICE_CREDIT_NOTE, INVOICE_DEFAULT

8

9

10@transaction.atomic

11 def 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))

39

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))

65

66 invoice.update_cached_fields()

67 return new_payments

68

69

70@transaction.atomic

71 def 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)

88

89

90@transaction.atomic

91 def 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

107

108 credit = -credit_note.get_unpaid_amount()

109 balance = debit_note.get_unpaid_amount()

110

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

117

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'))

125

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)

137

138 return pmts