[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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user