[IMP] account: Allow batching vendor bills and refunds for same partner

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/odoo#69578

Task: 2475598
Related: odoo/enterprise#19902
Signed-off-by: William André (wan) <wan@odoo.com>
This commit is contained in:
jbw
2021-07-27 09:06:18 +00:00
parent 131239f7c4
commit cef3f87321
2 changed files with 199 additions and 19 deletions
@@ -79,6 +79,16 @@ class TestAccountPaymentRegister(AccountTestInvoicingCommon):
})
(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
@@ -373,6 +383,123 @@ class TestAccountPaymentRegister(AccountTestInvoicingCommon):
},
])
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.
@@ -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):
@@ -128,9 +129,7 @@ class AccountPaymentRegister(models.TransientModel):
'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():
@@ -158,8 +157,10 @@ class AccountPaymentRegister(models.TransientModel):
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()
@@ -171,17 +172,69 @@ class AccountPaymentRegister(models.TransientModel):
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
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())
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,
})
return res
@api.model
def _get_wizard_values_from_batch(self, batch_result):
@@ -190,27 +243,27 @@ class AccountPaymentRegister(models.TransientModel):
: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
# -------------------------------------------------------------------------
@@ -436,7 +489,7 @@ class AccountPaymentRegister(models.TransientModel):
'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
}