mirror of
https://github.com/OCA/account-reconcile.git
synced 2025-01-20 12:27:39 +02:00
When having a statement line in a foreign currency, each resulting journal item computes the debit/credit amount from the foreign currency rate, and then rounding the result to company currency digits. There's a chance in this process that the journal entry final balance is not 0 summing the rounded balances in company currency. For fixing this, there can be several strategies, like creating an extra journal item for the difference, but I have opted for removing the difference in the latest counterpart aml, so no extra noise and no need of account decision algorithm for this extra move. As currency amounts are correct and are the ones used in reconciliation, there won't be any problem adjusting this amount. TT45568
365 lines
16 KiB
Python
365 lines
16 KiB
Python
from odoo import _, fields, models
|
|
from odoo.exceptions import UserError
|
|
from odoo.tools import float_is_zero
|
|
|
|
|
|
class AccountBankStatement(models.Model):
|
|
|
|
_inherit = "account.bank.statement"
|
|
|
|
accounting_date = fields.Date(
|
|
string="Accounting Date",
|
|
help="If set, the accounting entries created during the bank statement "
|
|
"reconciliation process will be created at this date.\n"
|
|
"This is useful if the accounting period in which the entries should "
|
|
"normally be booked is already closed.",
|
|
states={"open": [("readonly", False)]},
|
|
readonly=True,
|
|
)
|
|
|
|
def action_bank_reconcile_bank_statements(self):
|
|
self.ensure_one()
|
|
bank_stmt_lines = self.mapped("line_ids")
|
|
return {
|
|
"type": "ir.actions.client",
|
|
"tag": "bank_statement_reconciliation_view",
|
|
"context": {
|
|
"statement_line_ids": bank_stmt_lines.ids,
|
|
"company_ids": self.mapped("company_id").ids,
|
|
},
|
|
}
|
|
|
|
|
|
class AccountBankStatementLine(models.Model):
|
|
|
|
_inherit = "account.bank.statement.line"
|
|
|
|
# FIXME: is this necessary now?
|
|
move_name = fields.Char(
|
|
string="Journal Entry Name",
|
|
readonly=True,
|
|
default=False,
|
|
copy=False,
|
|
help="Technical field holding the number given to the journal entry, "
|
|
"automatically set when the statement line is reconciled then "
|
|
"stored to set the same number again if the line is cancelled, "
|
|
"set to draft and re-processed again.",
|
|
)
|
|
|
|
def process_reconciliation(
|
|
self, counterpart_aml_dicts=None, payment_aml_rec=None, new_aml_dicts=None
|
|
):
|
|
"""Match statement lines with existing payments (eg. checks) and/or
|
|
payables/receivables (eg. invoices and credit notes) and/or new move
|
|
lines (eg. write-offs).
|
|
If any new journal item needs to be created (via new_aml_dicts or
|
|
counterpart_aml_dicts), a new journal entry will be created and will
|
|
contain those items, as well as a journal item for the bank statement
|
|
line.
|
|
Finally, mark the statement line as reconciled by putting the matched
|
|
moves ids in the column journal_entry_ids.
|
|
|
|
:param self: browse collection of records that are supposed to have no
|
|
accounting entries already linked.
|
|
:param (list of dicts) counterpart_aml_dicts: move lines to create to
|
|
reconcile with existing payables/receivables.
|
|
The expected keys are :
|
|
- 'name'
|
|
- 'debit'
|
|
- 'credit'
|
|
- 'move_line'
|
|
# The move line to reconcile (partially if specified
|
|
# debit/credit is lower than move line's credit/debit)
|
|
|
|
:param (list of recordsets) payment_aml_rec: recordset move lines
|
|
representing existing payments (which are already fully reconciled)
|
|
|
|
:param (list of dicts) new_aml_dicts: move lines to create. The expected
|
|
keys are :
|
|
- 'name'
|
|
- 'debit'
|
|
- 'credit'
|
|
- 'account_id'
|
|
- (optional) 'tax_ids'
|
|
- (optional) Other account.move.line fields like analytic_account_id
|
|
or analytics_id
|
|
- (optional) 'reconcile_model_id'
|
|
|
|
:returns: The journal entries with which the transaction was matched.
|
|
If there was at least an entry in counterpart_aml_dicts or
|
|
new_aml_dicts, this list contains the move created by the
|
|
reconciliation, containing entries for the statement.line (1), the
|
|
counterpart move lines (0..*) and the new move lines (0..*).
|
|
"""
|
|
payable_account_type = self.env.ref("account.data_account_type_payable")
|
|
receivable_account_type = self.env.ref("account.data_account_type_receivable")
|
|
suspense_moves_mode = self._context.get("suspense_moves_mode")
|
|
counterpart_aml_dicts = counterpart_aml_dicts or []
|
|
payment_aml_rec = payment_aml_rec or self.env["account.move.line"]
|
|
new_aml_dicts = new_aml_dicts or []
|
|
|
|
aml_obj = self.env["account.move.line"]
|
|
counterpart_moves = self.env["account.move"]
|
|
|
|
# Check and prepare received data
|
|
if any(rec.statement_id for rec in payment_aml_rec):
|
|
raise UserError(_("A selected move line was already reconciled."))
|
|
for aml_dict in counterpart_aml_dicts:
|
|
if aml_dict["move_line"].reconciled and not suspense_moves_mode:
|
|
raise UserError(_("A selected move line was already reconciled."))
|
|
if isinstance(aml_dict["move_line"], int):
|
|
aml_dict["move_line"] = aml_obj.browse(aml_dict["move_line"])
|
|
|
|
account_types = self.env["account.account.type"]
|
|
for aml_dict in counterpart_aml_dicts + new_aml_dicts:
|
|
if aml_dict.get("tax_ids") and isinstance(aml_dict["tax_ids"][0], int):
|
|
# Transform the value in the format required for One2many and
|
|
# Many2many fields
|
|
aml_dict["tax_ids"] = [(4, id, None) for id in aml_dict["tax_ids"]]
|
|
|
|
user_type_id = (
|
|
self.env["account.account"]
|
|
.browse(aml_dict.get("account_id"))
|
|
.user_type_id
|
|
)
|
|
if (
|
|
user_type_id in [payable_account_type, receivable_account_type]
|
|
and user_type_id not in account_types
|
|
):
|
|
account_types |= user_type_id
|
|
# Fully reconciled moves are just linked to the bank statement (blue lines),
|
|
# but the generated move on statement post should be removed and link the
|
|
# payment one for not having double entry
|
|
# TODO: To mix already done payments with new ones. Not sure if possible.
|
|
old_move = self.move_id.with_context(force_delete=True)
|
|
for aml_rec in payment_aml_rec:
|
|
aml_rec.with_context(check_move_validity=False).write(
|
|
{"statement_line_id": self.id}
|
|
)
|
|
# This overwrites the value on each loop, so only one will be linked, but
|
|
# there's no better solution in this case
|
|
self.move_id = aml_rec.move_id.id
|
|
counterpart_moves |= aml_rec.move_id
|
|
if payment_aml_rec:
|
|
old_move.button_draft()
|
|
old_move.unlink()
|
|
# Create move line(s). Either matching an existing journal entry
|
|
# (eg. invoice), in which case we reconcile the existing and the new
|
|
# move lines together, or being a write-off.
|
|
if counterpart_aml_dicts or new_aml_dicts:
|
|
counterpart_moves = self._create_counterpart_and_new_aml(
|
|
counterpart_moves, counterpart_aml_dicts, new_aml_dicts
|
|
)
|
|
|
|
elif self.move_name:
|
|
raise UserError(
|
|
_(
|
|
"Operation not allowed. Since your statement line already "
|
|
"received a number (%s), you cannot reconcile it entirely "
|
|
"with existing journal entries otherwise it would make a "
|
|
"gap in the numbering. You should book an entry and make a "
|
|
"regular revert of it in case you want to cancel it."
|
|
)
|
|
% (self.move_name)
|
|
)
|
|
|
|
# create the res.partner.bank if needed
|
|
if self.account_number and self.partner_id and not self.partner_bank_id:
|
|
# Search bank account without partner to handle the case the
|
|
# res.partner.bank already exists but is set on a different partner.
|
|
self.partner_bank_id = self._find_or_create_bank_account()
|
|
|
|
counterpart_moves._check_balanced()
|
|
return counterpart_moves
|
|
|
|
def _create_counterpart_and_new_aml(
|
|
self, counterpart_moves, counterpart_aml_dicts, new_aml_dicts
|
|
):
|
|
|
|
aml_obj = self.env["account.move.line"]
|
|
|
|
# Delete previous move_lines
|
|
self.move_id.line_ids.with_context(force_delete=True).unlink()
|
|
|
|
# Create liquidity line
|
|
liquidity_aml_dict = self._prepare_liquidity_move_line_vals()
|
|
aml_obj.with_context(check_move_validity=False).create(liquidity_aml_dict)
|
|
|
|
self.sequence = self.statement_id.line_ids.ids.index(self.id) + 1
|
|
counterpart_moves = counterpart_moves | self.move_id
|
|
|
|
# Complete dicts to create both counterpart move lines and write-offs
|
|
to_create = counterpart_aml_dicts + new_aml_dicts
|
|
date = self.date or fields.Date.today()
|
|
for aml_dict in to_create:
|
|
aml_dict["move_id"] = self.move_id.id
|
|
aml_dict["partner_id"] = self.partner_id.id
|
|
aml_dict["statement_line_id"] = self.id
|
|
self._prepare_move_line_for_currency(aml_dict, date)
|
|
# Adjust latest counterpart move debit/credit if currency amount is balanced
|
|
# but company amount is not
|
|
if self.currency_id != self.company_currency_id:
|
|
all_amls = to_create + [liquidity_aml_dict]
|
|
balance_currency = sum([x["amount_currency"] for x in all_amls])
|
|
balance = sum([x["debit"] - x["credit"] for x in all_amls])
|
|
if float_is_zero(
|
|
balance_currency, precision_rounding=self.currency_id.rounding
|
|
) and not float_is_zero(
|
|
balance, precision_rounding=self.company_currency_id.rounding
|
|
):
|
|
aml_dict = to_create[-1]
|
|
if aml_dict["debit"]:
|
|
aml_dict["debit"] = self.company_currency_id.round(
|
|
aml_dict["debit"] - balance
|
|
)
|
|
else:
|
|
aml_dict["credit"] = self.company_currency_id.round(
|
|
aml_dict["credit"] + balance
|
|
)
|
|
# Create write-offs
|
|
wo_aml = self.env["account.move.line"]
|
|
for aml_dict in new_aml_dicts:
|
|
wo_aml |= aml_obj.with_context(check_move_validity=False).create(aml_dict)
|
|
analytic_wo_aml = wo_aml.filtered(
|
|
lambda l: l.analytic_account_id or l.analytic_tag_ids
|
|
)
|
|
|
|
# Create counterpart move lines and reconcile them
|
|
aml_to_reconcile = []
|
|
for aml_dict in counterpart_aml_dicts:
|
|
if not aml_dict["move_line"].statement_line_id:
|
|
aml_dict["move_line"].write({"statement_line_id": self.id})
|
|
if aml_dict["move_line"].partner_id.id:
|
|
aml_dict["partner_id"] = aml_dict["move_line"].partner_id.id
|
|
aml_dict["account_id"] = aml_dict["move_line"].account_id.id
|
|
aml_dict["analytic_account_id"] = (
|
|
aml_dict["move_line"].analytic_account_id.id or False
|
|
)
|
|
|
|
counterpart_move_line = aml_dict.pop("move_line")
|
|
new_aml = aml_obj.with_context(check_move_validity=False).create(aml_dict)
|
|
|
|
aml_to_reconcile.append((new_aml, counterpart_move_line))
|
|
|
|
# Post to allow reconcile
|
|
if self.move_id.state != "posted":
|
|
self.move_id.with_context(
|
|
skip_account_move_synchronization=True
|
|
).action_post()
|
|
elif analytic_wo_aml:
|
|
# if already posted the analytic entry has to be created
|
|
analytic_wo_aml.create_analytic_lines()
|
|
|
|
# Reconcile new lines with counterpart
|
|
for new_aml, counterpart_move_line in aml_to_reconcile:
|
|
(new_aml | counterpart_move_line).reconcile()
|
|
|
|
self._check_invoice_state(counterpart_move_line.move_id)
|
|
|
|
# Needs to be called manually as lines were created 1 by 1
|
|
self.move_id.update_lines_tax_exigibility()
|
|
# record the move name on the statement line to be able to retrieve
|
|
# it in case of unreconciliation
|
|
self.write({"move_name": self.move_id.name})
|
|
|
|
return counterpart_moves
|
|
|
|
def _prepare_move_line_for_currency(self, aml_dict, date):
|
|
self.ensure_one()
|
|
company_currency = self.journal_id.company_id.currency_id
|
|
statement_currency = self.journal_id.currency_id or company_currency
|
|
st_line_currency = self.foreign_currency_id or statement_currency
|
|
st_line_currency_rate = (
|
|
self.foreign_currency_id and (self.amount_currency / self.amount) or False
|
|
)
|
|
company = self.company_id
|
|
|
|
if st_line_currency.id != company_currency.id:
|
|
aml_dict["amount_currency"] = aml_dict["debit"] - aml_dict["credit"]
|
|
aml_dict["currency_id"] = st_line_currency.id
|
|
if (
|
|
self.foreign_currency_id
|
|
and statement_currency.id == company_currency.id
|
|
and st_line_currency_rate
|
|
):
|
|
# Statement is in company currency but the transaction is in
|
|
# foreign currency
|
|
aml_dict["debit"] = company_currency.round(
|
|
aml_dict["debit"] / st_line_currency_rate
|
|
)
|
|
aml_dict["credit"] = company_currency.round(
|
|
aml_dict["credit"] / st_line_currency_rate
|
|
)
|
|
elif self.foreign_currency_id and st_line_currency_rate:
|
|
# Statement is in foreign currency and the transaction is in
|
|
# another one
|
|
aml_dict["debit"] = statement_currency._convert(
|
|
aml_dict["debit"] / st_line_currency_rate,
|
|
company_currency,
|
|
company,
|
|
date,
|
|
)
|
|
aml_dict["credit"] = statement_currency._convert(
|
|
aml_dict["credit"] / st_line_currency_rate,
|
|
company_currency,
|
|
company,
|
|
date,
|
|
)
|
|
else:
|
|
# Statement is in foreign currency and no extra currency is
|
|
# given for the transaction
|
|
aml_dict["debit"] = st_line_currency._convert(
|
|
aml_dict["debit"], company_currency, company, date
|
|
)
|
|
aml_dict["credit"] = st_line_currency._convert(
|
|
aml_dict["credit"], company_currency, company, date
|
|
)
|
|
elif statement_currency.id != company_currency.id:
|
|
# Statement is in foreign currency but the transaction is in company
|
|
# currency
|
|
prorata_factor = (
|
|
aml_dict["debit"] - aml_dict["credit"]
|
|
) / self.amount_currency
|
|
aml_dict["amount_currency"] = prorata_factor * self.amount
|
|
aml_dict["currency_id"] = statement_currency.id
|
|
|
|
def _check_invoice_state(self, invoice):
|
|
if invoice.is_invoice(include_receipts=True):
|
|
invoice._compute_amount()
|
|
|
|
def button_undo_reconciliation(self):
|
|
"""Handle the case when the reconciliation was done against a direct payment
|
|
with the bank account as counterpart. This may be the case for payments made
|
|
in previous versions of Odoo.
|
|
"""
|
|
handled = self.env[self._name]
|
|
for record in self:
|
|
if record.move_id.payment_id:
|
|
# The reconciliation was done against a blue line (existing move)
|
|
# We remove the link on the current existing move, preserving it,
|
|
# and recreate a new move as if the statement line was new
|
|
record.move_id.line_ids.statement_line_id = False
|
|
statement = record.statement_id
|
|
journal = statement.journal_id
|
|
line_vals_list = record._prepare_move_line_default_vals()
|
|
new_move = self.env["account.move"].create(
|
|
{
|
|
"move_type": "entry",
|
|
"statement_line_id": record.id,
|
|
"ref": statement.name,
|
|
"date": record.date,
|
|
"journal_id": journal.id,
|
|
"partner_id": record.partner_id.id,
|
|
"currency_id": (
|
|
journal.currency_id or journal.company_id.currency_id
|
|
).id,
|
|
"line_ids": [(0, 0, line_vals) for line_vals in line_vals_list],
|
|
}
|
|
)
|
|
new_move.action_post()
|
|
record.move_id = new_move.id
|
|
handled += record
|
|
return super(
|
|
AccountBankStatementLine, self - handled
|
|
).button_undo_reconciliation()
|