Skip to content

Commit d4749d1

Browse files
[FIX] account_reconcile_oca: When doing manual reconciliations, allow to set the amount to reconcile in the invoice currency.
Before this fix, when attempting to reconcile a statement line in EUR with an invoice in USD, upon indicating the amount to reconcile in the currency invoice the results would be incorrect. This fix also includes an improvement on the message shown in the bank statement line on the effect of the reconciliation to the invoice.
1 parent 77a78f0 commit d4749d1

File tree

4 files changed

+217
-26
lines changed

4 files changed

+217
-26
lines changed

account_reconcile_oca/models/account_bank_statement_line.py

Lines changed: 94 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from odoo.exceptions import UserError
1111
from odoo.fields import first
1212
from odoo.tools import LazyTranslate, float_compare, float_is_zero
13+
from odoo.tools.misc import formatLang
1314

1415
_lt = LazyTranslate(__name__, default_lang="en_US")
1516

@@ -84,6 +85,11 @@ class AccountBankStatementLine(models.Model):
8485
prefetch=False,
8586
currency_field="manual_in_currency_id",
8687
)
88+
changed_manual_amount_in_currency = fields.Boolean(
89+
store=False,
90+
default=False,
91+
prefetch=False,
92+
)
8793
manual_exchange_counterpart = fields.Boolean(
8894
store=False,
8995
)
@@ -106,8 +112,16 @@ class AccountBankStatementLine(models.Model):
106112
manual_currency_id = fields.Many2one(
107113
"res.currency", readonly=True, store=False, prefetch=False
108114
)
109-
manual_original_amount = fields.Monetary(
110-
default=False, store=False, prefetch=False, readonly=True
115+
manual_original_amount_in_currency = fields.Monetary(
116+
default=False,
117+
store=False,
118+
prefetch=False,
119+
readonly=True,
120+
currency_field="manual_in_currency_id",
121+
)
122+
manual_amount_message = fields.Char(compute="_compute_manual_amount_message")
123+
show_full_reconcile_button = fields.Boolean(
124+
compute="_compute_manual_amount_message"
111125
)
112126
manual_move_type = fields.Selection(
113127
lambda r: r.env["account.move"]._fields["move_type"].selection,
@@ -124,6 +138,34 @@ class AccountBankStatementLine(models.Model):
124138
aggregate_id = fields.Integer(compute="_compute_reconcile_aggregate")
125139
aggregate_name = fields.Char(compute="_compute_reconcile_aggregate")
126140

141+
@api.depends(
142+
"manual_original_amount_in_currency",
143+
"manual_currency_id",
144+
"manual_amount_in_currency",
145+
"manual_move_id",
146+
)
147+
def _compute_manual_amount_message(self):
148+
for rec in self:
149+
rec.show_full_reconcile_button = True
150+
rec.manual_amount_message = ""
151+
if not rec.manual_currency_id:
152+
continue
153+
resulting_amt = (
154+
rec.manual_original_amount_in_currency - rec.manual_amount_in_currency
155+
)
156+
if rec.manual_currency_id.is_zero(resulting_amt):
157+
msg = _("will be fully paid.")
158+
rec.show_full_reconcile_button = False
159+
else:
160+
msg = _("will be reduced by %s") % (
161+
formatLang(
162+
self.env,
163+
rec.manual_amount_in_currency,
164+
currency_obj=rec.manual_currency_id,
165+
)
166+
)
167+
rec.manual_amount_message = msg
168+
127169
@api.model
128170
def _reconcile_aggregate_map(self):
129171
lang = self.env["res.lang"]._lang_get(self.env.user.lang)
@@ -381,7 +423,7 @@ def _get_manual_delete_vals(self):
381423
"manual_move_id": False,
382424
"manual_move_type": False,
383425
"manual_kind": False,
384-
"manual_original_amount": False,
426+
"manual_original_amount_in_currency": False,
385427
"manual_currency_id": False,
386428
"analytic_distribution": False,
387429
}
@@ -406,7 +448,9 @@ def _process_manual_reconcile_from_line(self, line):
406448
self.manual_move_id = self.manual_line_id.move_id
407449
self.manual_move_type = self.manual_line_id.move_id.move_type
408450
self.manual_kind = line["kind"]
409-
self.manual_original_amount = line.get("original_amount", 0.0)
451+
self.manual_original_amount_in_currency = line.get(
452+
"original_amount_currency", 0.0
453+
)
410454

411455
@api.onchange("manual_reference", "manual_delete")
412456
def _onchange_manual_reconcile_reference(self):
@@ -437,6 +481,39 @@ def _onchange_manual_reconcile_reference(self):
437481
)
438482
self.can_reconcile = self.reconcile_data_info.get("can_reconcile", False)
439483

484+
@api.onchange("manual_amount_in_currency")
485+
def _onchange_manual_amount_in_currency(self):
486+
if (
487+
self.manual_line_id.exists()
488+
and self.manual_line_id
489+
and self.manual_kind != "liquidity"
490+
):
491+
self.manual_amount = self.manual_in_currency_id._convert(
492+
self.manual_amount_in_currency,
493+
self.company_id.currency_id,
494+
self.company_id,
495+
self.manual_line_id.date,
496+
)
497+
self.changed_manual_amount_in_currency = True
498+
self._onchange_manual_reconcile_vals()
499+
500+
@api.onchange("manual_amount")
501+
def _onchange_manual_amount(self):
502+
if (
503+
self.manual_line_id.exists()
504+
and self.manual_line_id
505+
and self.manual_kind != "liquidity"
506+
and not self.changed_manual_amount_in_currency
507+
):
508+
self.manual_amount_in_currency = self.company_id.currency_id._convert(
509+
self.manual_amount,
510+
self.manual_in_currency_id,
511+
self.company_id,
512+
self.manual_line_id.date,
513+
)
514+
self.changed_manual_amount_in_currency = False
515+
self._onchange_manual_reconcile_vals()
516+
440517
def _get_manual_reconcile_vals(self):
441518
vals = {
442519
"name": self.manual_name,
@@ -459,16 +536,19 @@ def _get_manual_reconcile_vals(self):
459536
}
460537
liquidity_lines, _suspense_lines, _other_lines = self._seek_for_lines()
461538
if self.manual_line_id and self.manual_line_id.id not in liquidity_lines.ids:
462-
vals.update(
463-
{
464-
"currency_amount": self.manual_currency_id._convert(
465-
self.manual_amount,
466-
self.manual_in_currency_id,
467-
self.company_id,
468-
self.manual_line_id.date,
469-
),
470-
}
471-
)
539+
if self.manual_in_currency_id and self.manual_amount_in_currency:
540+
vals.update({"currency_amount": self.manual_amount_in_currency})
541+
else:
542+
vals.update(
543+
{
544+
"currency_amount": self.manual_currency_id._convert(
545+
self.manual_amount,
546+
self.manual_in_currency_id,
547+
self.company_id,
548+
self.manual_line_id.date,
549+
),
550+
}
551+
)
472552
return vals
473553

474554
@api.onchange(

account_reconcile_oca/models/account_reconcile_abstract.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,11 @@ def _get_reconcile_line(
4949
date = self.date if "date" in self._fields else line.date
5050
original_amount = amount = net_amount = line.debit - line.credit
5151
line_currency = line.currency_id
52+
original_amount_currency = line.amount_currency
5253
if is_counterpart:
53-
currency_amount = -line.amount_residual_currency or line.amount_residual
54+
currency_amount = original_amount_currency = (
55+
-line.amount_residual_currency or line.amount_residual
56+
)
5457
amount = -line.amount_residual
5558
currency = line.currency_id or line.company_id.currency_id
5659
original_amount = net_amount = -line.amount_residual
@@ -87,6 +90,7 @@ def _get_reconcile_line(
8790
currency_amount = line.amount_currency
8891
else:
8992
currency_amount = self.amount_currency or self.amount
93+
original_amount_currency = self.amount_currency
9094
line_currency = self._get_reconcile_currency()
9195
vals = {
9296
"move_id": move and line.move_id.id,
@@ -108,6 +112,7 @@ def _get_reconcile_line(
108112
"currency_amount": currency_amount,
109113
"analytic_distribution": line.analytic_distribution,
110114
"kind": kind,
115+
"original_amount_currency": original_amount_currency,
111116
}
112117
if from_unreconcile:
113118
vals.update(

account_reconcile_oca/tests/test_bank_account_reconcile.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,105 @@ def test_reconcile_invoice_reconcile_full(self):
265265
f.manual_reference = f"account.move.line;{receivable1.id}"
266266
self.assertEqual(-100, f.manual_amount)
267267

268+
def test_reconcile_invoice_reconcile_with_currency(self):
269+
"""
270+
We want to test the reconcile widget for bank statements on invoices
271+
with a different currency. We want to indicate manually in the bank
272+
statement line the amount that we want to reconcile, expressed in
273+
the invoice currency.
274+
"""
275+
new_rate = 1.6299
276+
self.env["res.currency.rate"].create(
277+
{
278+
"name": time.strftime("%Y-07-01"),
279+
"rate": new_rate,
280+
"currency_id": self.currency_usd_id,
281+
"company_id": self.bank_journal_euro.company_id.id,
282+
}
283+
)
284+
inv1 = self.create_invoice(currency_id=self.currency_usd_id, invoice_amount=100)
285+
bank_stmt = self.acc_bank_stmt_model.create(
286+
{
287+
"journal_id": self.bank_journal_euro.id,
288+
"date": time.strftime("%Y-07-15"),
289+
"name": "test",
290+
}
291+
)
292+
bank_stmt_line = self.acc_bank_stmt_line_model.create(
293+
{
294+
"name": "testLine",
295+
"journal_id": self.bank_journal_euro.id,
296+
"statement_id": bank_stmt.id,
297+
"amount": 50,
298+
"date": time.strftime("%Y-07-15"),
299+
}
300+
)
301+
receivable1 = inv1.line_ids.filtered(
302+
lambda line: line.account_id.account_type == "asset_receivable"
303+
)
304+
with Form(
305+
bank_stmt_line,
306+
view="account_reconcile_oca.bank_statement_line_form_reconcile_view",
307+
) as f:
308+
self.assertFalse(f.can_reconcile)
309+
f.add_account_move_line_id = receivable1
310+
self.assertFalse(f.add_account_move_line_id)
311+
self.assertTrue(f.can_reconcile)
312+
f.manual_reference = f"account.move.line;{receivable1.id}"
313+
f.manual_amount_in_currency = -100
314+
bank_stmt_line.reconcile_bank_line()
315+
self.assertEqual(inv1.amount_residual_signed, 0)
316+
self.assertEqual(inv1.payment_state, "paid")
317+
318+
def test_reconcile_invoice_reconcile_full_with_currency(self):
319+
"""
320+
We want to test the reconcile widget for bank statements on invoices
321+
with a different currency. We want to press the button on the statement
322+
line to force the full reconciliation.
323+
"""
324+
new_rate = 1.6299
325+
self.env["res.currency.rate"].create(
326+
{
327+
"name": time.strftime("%Y-07-01"),
328+
"rate": new_rate,
329+
"currency_id": self.currency_usd_id,
330+
"company_id": self.bank_journal_euro.company_id.id,
331+
}
332+
)
333+
inv1 = self.create_invoice(currency_id=self.currency_usd_id, invoice_amount=100)
334+
bank_stmt = self.acc_bank_stmt_model.create(
335+
{
336+
"journal_id": self.bank_journal_euro.id,
337+
"date": time.strftime("%Y-07-15"),
338+
"name": "test",
339+
}
340+
)
341+
bank_stmt_line = self.acc_bank_stmt_line_model.create(
342+
{
343+
"name": "testLine",
344+
"journal_id": self.bank_journal_euro.id,
345+
"statement_id": bank_stmt.id,
346+
"amount": 50,
347+
"date": time.strftime("%Y-07-15"),
348+
}
349+
)
350+
receivable1 = inv1.line_ids.filtered(
351+
lambda line: line.account_id.account_type == "asset_receivable"
352+
)
353+
with Form(
354+
bank_stmt_line,
355+
view="account_reconcile_oca.bank_statement_line_form_reconcile_view",
356+
) as f:
357+
self.assertFalse(f.can_reconcile)
358+
f.add_account_move_line_id = receivable1
359+
self.assertFalse(f.add_account_move_line_id)
360+
self.assertTrue(f.can_reconcile)
361+
f.manual_reference = f"account.move.line;{receivable1.id}"
362+
bank_stmt_line.button_manual_reference_full_paid()
363+
bank_stmt_line.reconcile_bank_line()
364+
self.assertEqual(inv1.amount_residual_signed, 0)
365+
self.assertEqual(inv1.payment_state, "paid")
366+
268367
def test_reconcile_invoice_unreconcile(self):
269368
"""
270369
We want to test the reconcile widget for bank statements on invoices.

account_reconcile_oca/views/account_bank_statement_line.xml

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -295,34 +295,41 @@
295295
modifiers="{'readonly': ['|', '|', ('manual_exchange_counterpart', '=', True), ('manual_reference', '=', False), ('is_reconciled', '=', True)]}"
296296
/>
297297
<field name="manual_currency_id" invisible="1" />
298-
<field name="manual_original_amount" invisible="1" />
298+
<field
299+
name="manual_original_amount_in_currency"
300+
invisible="1"
301+
/>
299302
<field name="manual_move_type" invisible="1" />
303+
<field
304+
name="show_full_reconcile_button"
305+
invisible="1"
306+
/>
300307
<label
301308
for="manual_move_id"
302309
string=""
303-
insible="manual_move_type not in ['in_invoice', 'in_refund', 'out_invoice', 'out_refund'] or manual_original_amount == 0"
310+
insible="manual_move_type not in ['in_invoice', 'in_refund', 'out_invoice', 'out_refund'] or manual_original_amount_in_currency == 0"
304311
/>
312+
305313
<div
306-
insible="manual_move_type not in ['in_invoice', 'in_refund', 'out_invoice', 'out_refund'] or manual_original_amount == 0"
314+
invisible="manual_move_type not in ['in_invoice', 'in_refund', 'out_invoice', 'out_refund'] or not manual_original_amount_in_currency == 0"
307315
>
308316
Invoice <field
309317
class="oe_inline"
310318
name="manual_move_id"
311319
/>
312320
with an open amount <field
313321
class="oe_inline"
314-
name="manual_original_amount"
315-
/> will be reduced by <field
316-
class="oe_inline"
317-
name="manual_amount"
318-
readonly="1"
319-
/>.
320-
<br />
321-
You might want to set the invoice as <button
322+
name="manual_original_amount_in_currency"
323+
/>
324+
<field name="manual_amount_message" />
325+
<div invisible="not show_full_reconcile_button">
326+
<br />
327+
You might want to set the invoice as <button
322328
name="button_manual_reference_full_paid"
323329
type="object"
324330
method_args="[1]"
325331
>fully paid</button>.
332+
</div>
326333
</div>
327334
</group>
328335
</group>

0 commit comments

Comments
 (0)