Files
account-reconcile/account_reconciliation_widget/models/account_bank_statement.py

514 lines
22 KiB
Python

from odoo import _, fields, models
from odoo.exceptions import UserError
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"
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"]
company_currency = self.journal_id.company_id.currency_id
statement_currency = self.journal_id.currency_id or company_currency
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
if suspense_moves_mode:
if any(not line.journal_entry_ids for line in self):
raise UserError(
_(
"Some selected statement line were not already "
"reconciled with an account move."
)
)
else:
if any(line.journal_entry_ids for line in self):
raise UserError(
_(
"A selected statement line was already reconciled with "
"an account move."
)
)
# Fully reconciled moves are just linked to the bank statement
total = self.amount
currency = self.currency_id or statement_currency
for aml_rec in payment_aml_rec:
balance = (
aml_rec.amount_currency if aml_rec.currency_id else aml_rec.balance
)
aml_currency = aml_rec.currency_id or aml_rec.company_currency_id
total -= aml_currency._convert(
balance, currency, aml_rec.company_id, aml_rec.date
)
aml_rec.with_context(check_move_validity=False).write(
{"statement_line_id": self.id}
)
counterpart_moves = counterpart_moves | aml_rec.move_id
if (
aml_rec.journal_id.post_at == "bank_rec"
and aml_rec.payment_id
and aml_rec.move_id.state == "draft"
):
# In case the journal is set to only post payments when
# performing bank reconciliation, we modify its date and post
# it.
aml_rec.move_id.date = self.date
aml_rec.payment_id.payment_date = self.date
aml_rec.move_id.post()
# We check the paid status of the invoices reconciled with this
# payment
for invoice in aml_rec.payment_id.reconciled_invoice_ids:
self._check_invoice_state(invoice)
# 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:
# Create the move
self.sequence = self.statement_id.line_ids.ids.index(self.id) + 1
move_vals = self._prepare_reconciliation_move(self.statement_id.name)
if suspense_moves_mode:
self.button_cancel_reconciliation()
move = (
self.env["account.move"]
.with_context(default_journal_id=move_vals["journal_id"])
.create(move_vals)
)
counterpart_moves = counterpart_moves | move
# Create The payment
payment = self.env["account.payment"]
partner_id = (
self.partner_id
or (aml_dict.get("move_line") and aml_dict["move_line"].partner_id)
or self.env["res.partner"]
)
if abs(total) > 0.00001:
payment_vals = self._prepare_payment_vals(total)
if not payment_vals["partner_id"]:
payment_vals["partner_id"] = partner_id.id
if payment_vals["partner_id"] and len(account_types) == 1:
payment_vals["partner_type"] = (
"customer"
if account_types == receivable_account_type
else "supplier"
)
payment = payment.create(payment_vals)
# 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"] = move.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)
# Create write-offs
for aml_dict in new_aml_dicts:
aml_dict["payment_id"] = payment and payment.id or False
aml_obj.with_context(check_move_validity=False).create(aml_dict)
# Create counterpart move lines and reconcile them
for aml_dict in counterpart_aml_dicts:
if (
aml_dict["move_line"].payment_id
and 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["payment_id"] = payment and payment.id or False
counterpart_move_line = aml_dict.pop("move_line")
new_aml = aml_obj.with_context(check_move_validity=False).create(
aml_dict
)
(new_aml | counterpart_move_line).reconcile()
self._check_invoice_state(counterpart_move_line.move_id)
# Balance the move
st_line_amount = -sum([x.balance for x in move.line_ids])
aml_dict = self._prepare_reconciliation_move_line(move, st_line_amount)
aml_dict["payment_id"] = payment and payment.id or False
aml_obj.with_context(check_move_validity=False).create(aml_dict)
# Needs to be called manually as lines were created 1 by 1
move.update_lines_tax_exigibility()
move.post()
# record the move name on the statement line to be able to retrieve
# it in case of unreconciliation
self.write({"move_name": move.name})
payment and payment.write({"payment_reference": move.name})
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.bank_account_id:
# Search bank account without partner to handle the case the
# res.partner.bank already exists but is set on a different partner.
self.bank_account_id = self._find_or_create_bank_account()
counterpart_moves._check_balanced()
return counterpart_moves
def _prepare_reconciliation_move(self, move_ref):
"""Prepare the dict of values to create the move from a statement line.
This method may be overridden to adapt domain logic through model
inheritance (make sure to call super() to establish a clean extension
chain).
:param char move_ref: will be used as the reference of the generated
account move
:return: dict of value to create() the account.move
"""
ref = move_ref or ""
if self.ref:
ref = move_ref + " - " + self.ref if move_ref else self.ref
data = {
"type": "entry",
"journal_id": self.statement_id.journal_id.id,
"currency_id": self.statement_id.currency_id.id,
"date": self.statement_id.accounting_date or self.date,
"partner_id": self.partner_id.id,
"ref": ref,
}
if self.move_name:
data.update(name=self.move_name)
return data
def _prepare_reconciliation_move_line(self, move, amount):
"""Prepare the dict of values to balance the move.
:param recordset move: the account.move to link the move line
:param dict move: a dict of vals of a account.move which will be created
later
:param float amount: the amount of transaction that wasn't already
reconciled
"""
company_currency = self.journal_id.company_id.currency_id
statement_currency = self.journal_id.currency_id or company_currency
st_line_currency = self.currency_id or statement_currency
amount_currency = False
st_line_currency_rate = (
self.currency_id and (self.amount_currency / self.amount) or False
)
if isinstance(move, dict):
amount_sum = sum(x[2].get("amount_currency", 0) for x in move["line_ids"])
else:
amount_sum = sum(x.amount_currency for x in move.line_ids)
# We have several use case here to compare the currency and amount
# currency of counterpart line to balance the move:
if (
st_line_currency != company_currency
and st_line_currency == statement_currency
):
# company in currency A, statement in currency B and transaction in
# currency B
# counterpart line must have currency B and correct amount is
# inverse of already existing lines
amount_currency = -amount_sum
elif (
st_line_currency != company_currency
and statement_currency == company_currency
):
# company in currency A, statement in currency A and transaction in
# currency B
# counterpart line must have currency B and correct amount is
# inverse of already existing lines
amount_currency = -amount_sum
elif (
st_line_currency != company_currency
and st_line_currency != statement_currency
):
# company in currency A, statement in currency B and transaction in
# currency C
# counterpart line must have currency B and use rate between B and
# C to compute correct amount
amount_currency = -amount_sum / st_line_currency_rate
elif (
st_line_currency == company_currency
and statement_currency != company_currency
):
# company in currency A, statement in currency B and transaction in
# currency A
# counterpart line must have currency B and amount is computed using
# the rate between A and B
amount_currency = amount / st_line_currency_rate
# last case is company in currency A, statement in currency A and
# transaction in currency A
# and in this case counterpart line does not need any second currency
# nor amount_currency
# Check if default_debit or default_credit account are properly configured
account_id = (
amount >= 0
and self.statement_id.journal_id.default_credit_account_id.id
or self.statement_id.journal_id.default_debit_account_id.id
)
if not account_id:
raise UserError(
_(
"No default debit and credit account defined on journal %s "
"(ids: %s)."
% (
self.statement_id.journal_id.name,
self.statement_id.journal_id.ids,
)
)
)
aml_dict = {
"name": self.name,
"partner_id": self.partner_id and self.partner_id.id or False,
"account_id": account_id,
"credit": amount < 0 and -amount or 0.0,
"debit": amount > 0 and amount or 0.0,
"statement_line_id": self.id,
"currency_id": statement_currency != company_currency
and statement_currency.id
or (st_line_currency != company_currency and st_line_currency.id or False),
"amount_currency": amount_currency,
}
if isinstance(move, self.env["account.move"].__class__):
aml_dict["move_id"] = move.id
return aml_dict
def _get_communication(self, payment_method_id):
return self.name or ""
def _prepare_payment_vals(self, total):
"""Prepare the dict of values to create the payment from a statement
line. This method may be overridden for update dict
through model inheritance (make sure to call super() to establish a
clean extension chain).
:param float total: will be used as the amount of the generated payment
:return: dict of value to create() the account.payment
"""
self.ensure_one()
partner_type = False
if self.partner_id:
if total < 0:
partner_type = "supplier"
else:
partner_type = "customer"
if not partner_type and self.env.context.get("default_partner_type"):
partner_type = self.env.context["default_partner_type"]
currency = self.journal_id.currency_id or self.company_id.currency_id
payment_methods = (
(total > 0)
and self.journal_id.inbound_payment_method_ids
or self.journal_id.outbound_payment_method_ids
)
return {
"payment_method_id": payment_methods and payment_methods[0].id or False,
"payment_type": total > 0 and "inbound" or "outbound",
"partner_id": self.partner_id.id,
"partner_type": partner_type,
"journal_id": self.statement_id.journal_id.id,
"payment_date": self.date,
"state": "reconciled",
"currency_id": currency.id,
"amount": abs(total),
"communication": self._get_communication(
payment_methods[0] if payment_methods else False
),
"name": self.statement_id.name or _("Bank Statement %s") % self.date,
}
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.currency_id or statement_currency
st_line_currency_rate = (
self.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.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.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()