Skip to content

Commit

Permalink
[IMP] account: Allow batching vendor bills and refunds for same partner
Browse files Browse the repository at this point in the history
Improve usability of payments. Allow selecting multiple Vendor Bills AND Refunds
and pay them all together in one single payment.

E.g. When running server action “Register Payment” for :
 - one 100€ Vendor Bill
 - one 50€ Vendor Bill
- one 80€ Refund
Previous result = one 150€ outgoing payment + one 80€ incoming payment
New result = one 70€ outgoing payment

closes odoo#69578

Task: 2475598
Related: odoo/enterprise#19902
Signed-off-by: William André (wan) <[email protected]>
  • Loading branch information
jbw-odoo committed Jul 27, 2021
1 parent 131239f commit cef3f87
Show file tree
Hide file tree
Showing 2 changed files with 199 additions and 19 deletions.
127 changes: 127 additions & 0 deletions addons/account/tests/test_account_payment_register.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,16 @@ def setUpClass(cls, chart_template_ref=None):
})
(cls.in_invoice_1 + cls.in_invoice_2 + cls.in_invoice_3).action_post()

# Credit note
cls.in_refund_1 = cls.env['account.move'].create({
'move_type': 'in_refund',
'date': '2017-01-01',
'invoice_date': '2017-01-01',
'partner_id': cls.partner_a.id,
'invoice_line_ids': [(0, 0, {'product_id': cls.product_a.id, 'price_unit': 1600.0})],
})
cls.in_refund_1.action_post()

def test_register_payment_single_batch_grouped_keep_open_lower_amount(self):
''' Pay 800.0 with 'open' as payment difference handling on two customer invoices (1000 + 2000). '''
active_ids = (self.out_invoice_1 + self.out_invoice_2).ids
Expand Down Expand Up @@ -373,6 +383,123 @@ def test_register_payment_single_batch_not_grouped(self):
},
])

def test_register_payment_single_batch_grouped_with_credit_note(self):
''' Pay 1400.0 on two vendor bills (1000.0 + 2000.0) and one credit note (1600.0). '''
active_ids = (self.in_invoice_1 + self.in_invoice_2 + self.in_refund_1).ids
payments = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=active_ids).create({
'group_payment': True,
})._create_payments()
self.assertRecordValues(payments, [
{
'ref': 'BILL/2017/01/0001 BILL/2017/01/0002 RBILL/2017/01/0001',
'payment_method_line_id': self.outbound_payment_method_line.id,
},
])
self.assertRecordValues(payments[0].line_ids.sorted('balance'), [
# Liquidity line:
{
'debit': 0.0,
'credit': 1400.0,
'currency_id': self.company_data['currency'].id,
'amount_currency': -1400.0,
'reconciled': False,
},
# Payable line:
{
'debit': 1400.0,
'credit': 0.0,
'currency_id': self.company_data['currency'].id,
'amount_currency': 1400.0,
'reconciled': True,
},
])

def test_register_payment_multiple_batch_grouped_with_credit_note(self):
''' Do not batch payments if multiple partner_bank_id '''
test_bank = self.env['res.bank'].create({'name': 'test'})
bank1 = self.env['res.partner.bank'].create({
'acc_number': 'BE43798822936101',
'partner_id': self.partner_a.id,
'bank_id': test_bank.id,
})
bank2 = self.env['res.partner.bank'].create({
'acc_number': 'BE85812541345906',
'partner_id': self.partner_a.id,
'bank_id': test_bank.id,
})
self.in_invoice_1.partner_bank_id = bank1
self.in_invoice_2.partner_bank_id = bank2
active_ids = (self.in_invoice_1 + self.in_invoice_2 + self.in_refund_1).ids
payments = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=active_ids).create({
'group_payment': True,
})._create_payments()
self.assertRecordValues(payments, [
{
'ref': 'BILL/2017/01/0001',
'payment_method_line_id': self.outbound_payment_method_line.id,
},
{
'ref': 'BILL/2017/01/0002',
'payment_method_line_id': self.outbound_payment_method_line.id,
},
{
'ref': 'RBILL/2017/01/0001',
'payment_method_line_id': self.outbound_payment_method_line.id,
},
])
self.assertRecordValues(payments[0].line_ids.sorted('balance') + payments[1].line_ids.sorted('balance') + payments[2].line_ids.sorted('balance'), [
# Liquidity line:
{
'debit': 0.0,
'credit': 1000.0,
'currency_id': self.company_data['currency'].id,
'amount_currency': -1000.0,
'reconciled': False,
},
# Payable line:
{
'debit': 1000.0,
'credit': 0.0,
'currency_id': self.company_data['currency'].id,
'amount_currency': 1000.0,
'reconciled': True,
},
# Liquidity line:
{
'debit': 0.0,
'credit': 2000.0,
'currency_id': self.company_data['currency'].id,
'amount_currency': -2000.0,
'reconciled': False,
},
# Payable line:
{
'debit': 2000.0,
'credit': 0.0,
'currency_id': self.company_data['currency'].id,
'amount_currency': 2000.0,
'reconciled': True,
},
# Receivable line:
{
'debit': 0.0,
'credit': 1600.0,
'currency_id': self.company_data['currency'].id,
'amount_currency': -1600.0,
'reconciled': True,
},
# Liquidity line:
{
'debit': 1600.0,
'credit': 0.0,
'currency_id': self.company_data['currency'].id,
'amount_currency': 1600.0,
'reconciled': False,
},
])
self.in_invoice_1.partner_bank_id = None
self.in_invoice_2.partner_bank_id = None

def test_register_payment_multi_batches_grouped(self):
''' Choose to pay multiple batches, one with two customer invoices (1000 + 2000)
and one with a vendor bill of 600, by grouping payments.
Expand Down
91 changes: 72 additions & 19 deletions addons/account/wizard/account_payment_register.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from odoo import models, fields, api, _
from odoo.exceptions import UserError
from odoo.tools import float_compare, frozendict


class AccountPaymentRegister(models.TransientModel):
Expand Down Expand Up @@ -128,9 +129,7 @@ def _get_line_batch_key(self, line):
'partner_id': line.partner_id.id,
'account_id': line.account_id.id,
'currency_id': (line.currency_id or line.company_currency_id).id,
'partner_bank_id': (line.move_id.partner_bank_id or line.partner_id.commercial_partner_id.bank_ids[:1]).id,
'partner_type': 'customer' if line.account_internal_type == 'receivable' else 'supplier',
'payment_type': 'inbound' if line.balance > 0.0 else 'outbound',
}

if line.move_id.partner_bank_id and line.move_id.is_inbound():
Expand Down Expand Up @@ -158,8 +157,10 @@ def _search_journal(self, company_id, currrency_id, partner_bank_id=None):

def _get_batches(self):
''' Group the account.move.line linked to the wizard together.
Lines are groupes if they share 'partner_id','account_id','currency_id' & 'partner_type' and if
0 or 1 partner_bank_id can be determined for the group.
:return: A list of batches, each one containing:
* key_values: The key as a dictionary used to group the journal items together.
* payment_values: A dictionary of payment values.
* moves: An account.move recordset.
'''
self.ensure_one()
Expand All @@ -171,17 +172,69 @@ def _get_batches(self):
if not lines:
raise UserError(_("You can't open the register payment wizard without at least one receivable/payable line."))

# Lines are grouped first on 'partner_id','account_id','currency_id' & 'partner_type' and
# then on 'partner_bank_id' & 'payment_type'. 'batches' data structure :
# {common_pay_val_key:{
# part_bank_pay_type_key : lines,
# part_bank_pay_type_key : lines,
# }}
batches = {}

for line in lines:
batch_key = self._get_line_batch_key(line)
common_pay_val_key = frozendict(batch_key)
part_bank_pay_type_key = frozendict(
payment_type='inbound' if line.balance > 0.0 else 'outbound',
partner_bank_id=line.move_id.partner_bank_id.id,
)
batches.setdefault(common_pay_val_key, {})
batches[common_pay_val_key].setdefault(part_bank_pay_type_key, self.env['account.move.line'])
batches[common_pay_val_key][part_bank_pay_type_key] += line

res = []

for common_pay_val_key, common_params_group in batches.items():
# Group all lines with same 'partner_id','account_id','currency_id' & 'partner_type' in one batch if:
# - the resulting batch payment is outbound and there is max 1 partner_bank_id for all outbound lines
# or
# - the resulting batch payment is inbound and there is max 1 partner_bank_id for all inbound lines
all_batch_aml = self.env['account.move.line'].browse(
id
for lines in common_params_group.values()
for id in lines.ids
)
total = sum(all_batch_aml.mapped('balance'))
if float_compare(total, 0.0, precision_digits=line.move_id.currency_id.decimal_places) > 0.0:
if_grouped_payment_type = 'inbound'
else:
if_grouped_payment_type = 'outbound'
partner_bank_id_candidates = {
key['partner_bank_id']
for key in common_params_group
if key['partner_bank_id'] and key['payment_type'] == if_grouped_payment_type
}
if len(partner_bank_id_candidates) < 2:
# condition is met, grouping in 1 batch
partner_bank_id = partner_bank_id_candidates and partner_bank_id_candidates.pop() or None
payment_values = dict(common_pay_val_key)
payment_values['partner_bank_id'] = partner_bank_id
payment_values['payment_type'] = if_grouped_payment_type
res.append({
'payment_values': payment_values,
'lines': all_batch_aml,
})
else:
# no grouping
for part_bank_pay_type_key, lines in common_params_group.items():
payment_values = dict(common_pay_val_key)
payment_values['payment_type'] = part_bank_pay_type_key['payment_type']
payment_values['partner_bank_id'] = part_bank_pay_type_key['partner_bank_id']
res.append({
'payment_values': payment_values,
'lines': lines,
})

serialized_key = '-'.join(str(v) for v in batch_key.values())
batches.setdefault(serialized_key, {
'key_values': batch_key,
'lines': self.env['account.move.line'],
})
batches[serialized_key]['lines'] += line
return list(batches.values())
return res

@api.model
def _get_wizard_values_from_batch(self, batch_result):
Expand All @@ -190,27 +243,27 @@ def _get_wizard_values_from_batch(self, batch_result):
:param batch_result: A batch returned by '_get_batches'.
:return: A dictionary containing valid fields
'''
key_values = batch_result['key_values']
payment_values = batch_result['payment_values']
lines = batch_result['lines']
company = lines[0].company_id

source_amount = abs(sum(lines.mapped('amount_residual')))
if key_values['currency_id'] == company.currency_id.id:
if payment_values['currency_id'] == company.currency_id.id:
source_amount_currency = source_amount
else:
source_amount_currency = abs(sum(lines.mapped('amount_residual_currency')))

res = {
'company_id': company.id,
'partner_id': key_values['partner_id'],
'partner_type': key_values['partner_type'],
'payment_type': key_values['payment_type'],
'source_currency_id': key_values['currency_id'],
'partner_id': payment_values['partner_id'],
'partner_type': payment_values['partner_type'],
'payment_type': payment_values['payment_type'],
'source_currency_id': payment_values['currency_id'],
'source_amount': source_amount,
'source_amount_currency': source_amount_currency,
}
if key_values.get('journal_id'):
res['journal_id'] = key_values['journal_id']
if payment_values.get('journal_id'):
res['journal_id'] = payment_values['journal_id']
return res

# -------------------------------------------------------------------------
Expand Down Expand Up @@ -436,7 +489,7 @@ def _create_payment_vals_from_batch(self, batch_result):
'journal_id': self.journal_id.id,
'currency_id': batch_values['source_currency_id'],
'partner_id': batch_values['partner_id'],
'partner_bank_id': batch_result['key_values']['partner_bank_id'],
'partner_bank_id': batch_result['payment_values']['partner_bank_id'],
'payment_method_line_id': self.payment_method_line_id.id,
'destination_account_id': batch_result['lines'][0].account_id.id
}
Expand Down

0 comments on commit cef3f87

Please sign in to comment.