Hide keyboard shortcuts

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 

8 

9 

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

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 

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) 

88 

89 

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 

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