[ADD] account_reconciliation_widget: Base module code extracted from Odoo 13.0

This commit is contained in:
Odoo
2020-12-14 02:57:39 +01:00
committed by Francisco Ivan Anton Prieto
parent 8708c16fc4
commit 267f6653d0
11 changed files with 11893 additions and 0 deletions

View File

@@ -0,0 +1,513 @@
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()

View File

@@ -0,0 +1,22 @@
from odoo import models
class AccountJournal(models.Model):
_inherit = "account.journal"
def action_open_reconcile(self):
# Open reconciliation view for bank statements belonging to this journal
bank_stmt = (
self.env["account.bank.statement"]
.search([("journal_id", "in", self.ids)])
.mapped("line_ids")
)
return {
"type": "ir.actions.client",
"tag": "bank_statement_reconciliation_view",
"context": {
"statement_line_ids": bank_stmt.ids,
"company_ids": self.mapped("company_id").ids,
},
}

View File

@@ -0,0 +1,127 @@
from odoo import _, fields, models
from odoo.exceptions import UserError
class AccountMoveLine(models.Model):
_inherit = "account.move.line"
def _create_writeoff(self, writeoff_vals):
"""Create a writeoff move per journal for the account.move.lines in
self. If debit/credit is not specified in vals, the writeoff amount
will be computed as the sum of amount_residual of the given recordset.
:param writeoff_vals: list of dicts containing values suitable for
account_move_line.create(). The data in vals will be processed to
create bot writeoff account.move.line and their enclosing
account.move.
"""
def compute_writeoff_counterpart_vals(values):
line_values = values.copy()
line_values["debit"], line_values["credit"] = (
line_values["credit"],
line_values["debit"],
)
if "amount_currency" in values:
line_values["amount_currency"] = -line_values["amount_currency"]
return line_values
# Group writeoff_vals by journals
writeoff_dict = {}
for val in writeoff_vals:
journal_id = val.get("journal_id", False)
if not writeoff_dict.get(journal_id, False):
writeoff_dict[journal_id] = [val]
else:
writeoff_dict[journal_id].append(val)
partner_id = (
self.env["res.partner"]._find_accounting_partner(self[0].partner_id).id
)
company_currency = self[0].account_id.company_id.currency_id
writeoff_currency = self[0].account_id.currency_id or company_currency
line_to_reconcile = self.env["account.move.line"]
# Iterate and create one writeoff by journal
writeoff_moves = self.env["account.move"]
for journal_id, lines in writeoff_dict.items():
total = 0
total_currency = 0
writeoff_lines = []
date = fields.Date.today()
for vals in lines:
# Check and complete vals
if "account_id" not in vals or "journal_id" not in vals:
raise UserError(
_(
"It is mandatory to specify an account and a "
"journal to create a write-off."
)
)
if ("debit" in vals) ^ ("credit" in vals):
raise UserError(_("Either pass both debit and credit or none."))
if "date" not in vals:
vals["date"] = self._context.get("date_p") or fields.Date.today()
vals["date"] = fields.Date.to_date(vals["date"])
if vals["date"] and vals["date"] < date:
date = vals["date"]
if "name" not in vals:
vals["name"] = self._context.get("comment") or _("Write-Off")
if "analytic_account_id" not in vals:
vals["analytic_account_id"] = self.env.context.get(
"analytic_id", False
)
# compute the writeoff amount if not given
if "credit" not in vals and "debit" not in vals:
amount = sum([r.amount_residual for r in self])
vals["credit"] = amount > 0 and amount or 0.0
vals["debit"] = amount < 0 and abs(amount) or 0.0
vals["partner_id"] = partner_id
total += vals["debit"] - vals["credit"]
if (
"amount_currency" not in vals
and writeoff_currency != company_currency
):
vals["currency_id"] = writeoff_currency.id
sign = 1 if vals["debit"] > 0 else -1
vals["amount_currency"] = sign * abs(
sum([r.amount_residual_currency for r in self])
)
total_currency += vals["amount_currency"]
writeoff_lines.append(compute_writeoff_counterpart_vals(vals))
# Create balance line
writeoff_lines.append(
{
"name": _("Write-Off"),
"debit": total > 0 and total or 0.0,
"credit": total < 0 and -total or 0.0,
"amount_currency": total_currency,
"currency_id": total_currency and writeoff_currency.id or False,
"journal_id": journal_id,
"account_id": self[0].account_id.id,
"partner_id": partner_id,
}
)
# Create the move
writeoff_move = self.env["account.move"].create(
{
"journal_id": journal_id,
"date": date,
"state": "draft",
"line_ids": [(0, 0, line) for line in writeoff_lines],
}
)
writeoff_moves += writeoff_move
line_to_reconcile += writeoff_move.line_ids.filtered(
lambda r: r.account_id == self[0].account_id
).sorted(key="id")[-1:]
# post all the writeoff moves at once
if writeoff_moves:
writeoff_moves.post()
# Return the writeoff move.line which is to be reconciled
return line_to_reconcile

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,14 @@
from odoo import fields, models
class ResCompany(models.Model):
_inherit = "res.company"
account_bank_reconciliation_start = fields.Date(
string="Bank Reconciliation Threshold",
help="The bank reconciliation widget won't ask to reconcile payments "
"older than this date.\n"
"This is useful if you install accounting after having used invoicing "
"for some time and don't want to reconcile all the past payments with "
"bank statements.",
)

View File

@@ -0,0 +1,547 @@
odoo.define("account.ReconciliationClientAction", function (require) {
"use strict";
var AbstractAction = require("web.AbstractAction");
var ReconciliationModel = require("account.ReconciliationModel");
var ReconciliationRenderer = require("account.ReconciliationRenderer");
var core = require("web.core");
var QWeb = core.qweb;
/**
* Widget used as action for 'account.bank.statement' reconciliation
*/
var StatementAction = AbstractAction.extend({
hasControlPanel: true,
withSearchBar: true,
loadControlPanel: true,
title: core._t("Bank Reconciliation"),
contentTemplate: "reconciliation",
custom_events: {
change_mode: "_onAction",
change_filter: "_onAction",
change_offset: "_onAction",
change_partner: "_onAction",
add_proposition: "_onAction",
remove_proposition: "_onAction",
update_proposition: "_onAction",
create_proposition: "_onAction",
getPartialAmount: "_onActionPartialAmount",
quick_create_proposition: "_onAction",
partial_reconcile: "_onAction",
validate: "_onValidate",
close_statement: "_onCloseStatement",
load_more: "_onLoadMore",
reload: "reload",
search: "_onSearch",
navigation_move: "_onNavigationMove",
},
config: _.extend({}, AbstractAction.prototype.config, {
// Used to instantiate the model
Model: ReconciliationModel.StatementModel,
// Used to instantiate the action interface
ActionRenderer: ReconciliationRenderer.StatementRenderer,
// Used to instantiate each widget line
LineRenderer: ReconciliationRenderer.LineRenderer,
// Used context params
params: ["statement_line_ids"],
// Number of statements/partners/accounts to display
defaultDisplayQty: 10,
// Number of moves lines displayed in 'match' mode
limitMoveLines: 15,
}),
_onNavigationMove: function (ev) {
var non_reconciled_keys = _.keys(
_.pick(this.model.lines, function (value, key, object) {
return !value.reconciled;
})
);
var currentIndex = _.indexOf(non_reconciled_keys, ev.data.handle);
var widget = false;
switch (ev.data.direction) {
case "up":
ev.stopPropagation();
widget = this._getWidget(non_reconciled_keys[currentIndex - 1]);
break;
case "down":
ev.stopPropagation();
widget = this._getWidget(non_reconciled_keys[currentIndex + 1]);
break;
case "validate":
ev.stopPropagation();
widget = this._getWidget(non_reconciled_keys[currentIndex]);
widget.$("caption .o_buttons button:visible").click();
break;
}
if (widget) widget.$el.focus();
},
/**
* @override
* @param {Object} params
* @param {Object} params.context
*
*/
init: function (parent, params) {
this._super.apply(this, arguments);
this.action_manager = parent;
this.params = params;
this.controlPanelParams.modelName = "account.bank.statement.line";
this.model = new this.config.Model(this, {
modelName: "account.reconciliation.widget",
defaultDisplayQty:
(params.params && params.params.defaultDisplayQty) ||
this.config.defaultDisplayQty,
limitMoveLines:
(params.params && params.params.limitMoveLines) ||
this.config.limitMoveLines,
});
this.widgets = [];
// Adding values from the context is necessary to put this information in the url via the action manager so that
// you can retrieve it if the person shares his url or presses f5
_.each(params.params, function (value, name) {
params.context[name] =
name.indexOf("_ids") !== -1
? _.map(String(value).split(","), parseFloat)
: value;
});
params.params = {};
_.each(this.config.params, function (name) {
if (params.context[name]) {
params.params[name] = params.context[name];
}
});
},
/**
* Instantiate the action renderer
*
* @override
*/
willStart: function () {
var self = this;
var def = this.model.load(this.params.context).then(this._super.bind(this));
return def.then(function () {
if (!self.model.context || !self.model.context.active_id) {
self.model.context = {
active_id: self.params.context.active_id,
active_model: self.params.context.active_model,
};
}
var journal_id = self.params.context.journal_id;
if (
self.model.context.active_id &&
self.model.context.active_model === "account.journal"
) {
journal_id = journal_id || self.model.context.active_id;
}
if (journal_id) {
var promise = self._rpc({
model: "account.journal",
method: "read",
args: [journal_id, ["display_name"]],
});
} else {
var promise = Promise.resolve();
}
return promise.then(function (result) {
var title =
result && result[0]
? result[0].display_name
: self.params.display_name || "";
self._setTitle(title);
self.renderer = new self.config.ActionRenderer(self, self.model, {
bank_statement_line_id: self.model.bank_statement_line_id,
valuenow: self.model.valuenow,
valuemax: self.model.valuemax,
defaultDisplayQty: self.model.defaultDisplayQty,
title: title,
});
});
});
},
reload: function () {
// On reload destroy all rendered line widget, reload data and then rerender widget
var self = this;
self.$(".o_reconciliation_lines").addClass("d-none"); // Prevent the browser from recomputing css after each destroy for HUGE perf improvement on a lot of lines
_.each(this.widgets, function (widget) {
widget.destroy();
});
this.widgets = [];
self.$(".o_reconciliation_lines").removeClass("d-none");
return this.model.reload().then(function () {
return self._renderLinesOrRainbow();
});
},
_renderLinesOrRainbow: function () {
var self = this;
return self._renderLines().then(function () {
var initialState = self.renderer._initialState;
var valuenow = self.model.statement
? self.model.statement.value_min
: initialState.valuenow;
var valuemax = self.model.statement
? self.model.statement.value_max
: initialState.valuemax;
// No more lines to reconcile, trigger the rainbowman.
if (valuenow === valuemax) {
initialState.valuenow = valuenow;
initialState.context = self.model.getContext();
self.renderer.showRainbowMan(initialState);
self.remove_cp();
} else {
// Create a notification if some lines have been reconciled automatically.
if (initialState.valuenow > 0)
self.renderer._renderNotifications(
self.model.statement.notifications
);
self._openFirstLine();
self.renderer.$('[data-toggle="tooltip"]').tooltip();
self.do_show();
}
});
},
/**
* Append the renderer and instantiate the line renderers
*
* @override
*/
start: function () {
var self = this;
var args = arguments;
var sup = this._super;
return this.renderer.prependTo(self.$(".o_form_sheet")).then(function () {
return self._renderLinesOrRainbow().then(function () {
self.do_show();
return sup.apply(self, args);
});
});
},
/**
* Update the control panel and breadcrumbs
*
* @override
*/
do_show: function () {
this._super.apply(this, arguments);
if (this.action_manager) {
this.$pager = $(
QWeb.render("reconciliation.control.pager", {widget: this.renderer})
);
this.updateControlPanel({
clear: true,
cp_content: {
$pager: this.$pager,
},
});
this.renderer.$progress = this.$pager;
$(this.renderer.$progress)
.parent()
.css("width", "100%")
.css("padding-left", "0");
}
},
remove_cp: function () {
this.updateControlPanel({
clear: true,
});
},
// --------------------------------------------------------------------------
// Private
// --------------------------------------------------------------------------
/**
* @private
* @param {String} handle
* @returns {Widget} widget line
*/
_getWidget: function (handle) {
return _.find(this.widgets, function (widget) {
return widget.handle === handle;
});
},
/**
*
*/
_loadMore: function (qty) {
var self = this;
return this.model.loadMore(qty).then(function () {
return self._renderLines();
});
},
/**
* Sitch to 'match' the first available line
*
* @private
*/
_openFirstLine: function (previous_handle) {
var self = this;
previous_handle = previous_handle || "rline0";
var handle = _.compact(
_.map(this.model.lines, function (line, handle) {
return line.reconciled ||
parseInt(handle.substr(5)) < parseInt(previous_handle.substr(5))
? null
: handle;
})
)[0];
if (handle) {
var line = this.model.getLine(handle);
this.model
.changeMode(handle, "default")
.then(function () {
self._getWidget(handle).update(line);
})
.guardedCatch(function () {
self._getWidget(handle).update(line);
})
.then(function () {
self._getWidget(handle).$el.focus();
});
}
return handle;
},
_forceUpdate: function () {
var self = this;
_.each(this.model.lines, function (handle) {
var widget = self._getWidget(handle.handle);
if (widget && handle.need_update) {
widget.update(handle);
widget.need_update = false;
}
});
},
/**
* Render line widget and append to view
*
* @private
*/
_renderLines: function () {
var self = this;
var linesToDisplay = this.model.getStatementLines();
var linePromises = [];
_.each(linesToDisplay, function (line, handle) {
var widget = new self.config.LineRenderer(self, self.model, line);
widget.handle = handle;
self.widgets.push(widget);
linePromises.push(widget.appendTo(self.$(".o_reconciliation_lines")));
});
if (this.model.hasMoreLines() === false) {
this.renderer.hideLoadMoreButton(true);
} else {
this.renderer.hideLoadMoreButton(false);
}
return Promise.all(linePromises);
},
// --------------------------------------------------------------------------
// Handlers
// --------------------------------------------------------------------------
/**
* dispatch on the camelcased event name to model method then update the
* line renderer with the new state. If the mode was switched from 'inactive'
* to 'create' or 'match_rp' or 'match_other', the other lines switch to
* 'inactive' mode
*
* @private
* @param {OdooEvent} event
*/
_onAction: function (event) {
var self = this;
var handle = event.target.handle;
var current_line = this.model.getLine(handle);
this.model[_.str.camelize(event.name)](handle, event.data.data).then(
function () {
var widget = self._getWidget(handle);
if (widget) {
widget.update(current_line);
}
if (current_line.mode !== "inactive") {
_.each(self.model.lines, function (line, _handle) {
if (line.mode !== "inactive" && _handle !== handle) {
self.model.changeMode(_handle, "inactive");
var widget = self._getWidget(_handle);
if (widget) {
widget.update(line);
}
}
});
}
}
);
},
/**
* @private
* @param {OdooEvent} ev
*/
_onSearch: function (ev) {
var self = this;
ev.stopPropagation();
this.model.domain = ev.data.domain;
this.model.display_context = "search";
self.reload().then(function () {
self.renderer._updateProgressBar({
valuenow: self.model.valuenow,
valuemax: self.model.valuemax,
});
});
},
_onActionPartialAmount: function (event) {
var self = this;
var handle = event.target.handle;
var line = this.model.getLine(handle);
var amount = this.model.getPartialReconcileAmount(handle, event.data);
self._getWidget(handle).updatePartialAmount(event.data.data, amount);
},
/**
* Call 'closeStatement' model method
*
* @private
* @param {OdooEvent} event
*/
_onCloseStatement: function (event) {
var self = this;
return this.model.closeStatement().then(function (result) {
self.do_action({
name: "Bank Statements",
res_model: "account.bank.statement.line",
res_id: result,
views: [[false, "form"]],
type: "ir.actions.act_window",
view_mode: "form",
});
$(".o_reward").remove();
});
},
/**
* Load more statement and render them
*
* @param {OdooEvent} event
*/
_onLoadMore: function (event) {
return this._loadMore(this.model.defaultDisplayQty);
},
/**
* Call 'validate' model method then destroy the
* validated lines and update the action renderer with the new status bar
* values and notifications then open the first available line
*
* @private
* @param {OdooEvent} event
*/
_onValidate: function (event) {
var self = this;
var handle = event.target.handle;
this.model.validate(handle).then(function (result) {
self.renderer.update({
valuenow: self.model.valuenow,
valuemax: self.model.valuemax,
title: self.title,
time: Date.now() - self.time,
notifications: result.notifications,
context: self.model.getContext(),
});
self._forceUpdate();
_.each(result.handles, function (handle) {
var widget = self._getWidget(handle);
if (widget) {
widget.destroy();
var index = _.findIndex(self.widgets, function (widget) {
return widget.handle === handle;
});
self.widgets.splice(index, 1);
}
});
// Get number of widget and if less than constant and if there are more to laod, load until constant
if (
self.widgets.length < self.model.defaultDisplayQty &&
self.model.valuemax - self.model.valuenow >=
self.model.defaultDisplayQty
) {
var toLoad = self.model.defaultDisplayQty - self.widgets.length;
self._loadMore(toLoad);
}
self._openFirstLine(handle);
});
},
});
/**
* Widget used as action for 'account.move.line' and 'res.partner' for the
* manual reconciliation and mark data as reconciliate
*/
var ManualAction = StatementAction.extend({
title: core._t("Journal Items to Reconcile"),
withSearchBar: false,
config: _.extend({}, StatementAction.prototype.config, {
Model: ReconciliationModel.ManualModel,
ActionRenderer: ReconciliationRenderer.ManualRenderer,
LineRenderer: ReconciliationRenderer.ManualLineRenderer,
params: ["company_ids", "mode", "partner_ids", "account_ids"],
defaultDisplayQty: 30,
limitMoveLines: 15,
}),
// --------------------------------------------------------------------------
// Handlers
// --------------------------------------------------------------------------
/**
* call 'validate' model method then destroy the
* reconcilied lines, update the not reconcilied and update the action
* renderer with the new status bar values and notifications then open the
* first available line
*
* @private
* @param {OdooEvent} event
*/
_onValidate: function (event) {
var self = this;
var handle = event.target.handle;
var method = "validate";
this.model[method](handle).then(function (result) {
_.each(result.reconciled, function (handle) {
self._getWidget(handle).destroy();
});
_.each(result.updated, function (handle) {
self._getWidget(handle).update(self.model.getLine(handle));
});
self.renderer.update({
valuenow: _.compact(_.invoke(self.widgets, "isDestroyed")).length,
valuemax: self.widgets.length,
title: self.title,
time: Date.now() - self.time,
});
if (
!_.any(result.updated, function (handle) {
return self.model.getLine(handle).mode !== "inactive";
})
) {
self._openFirstLine(handle);
}
});
},
});
core.action_registry.add("bank_statement_reconciliation_view", StatementAction);
core.action_registry.add("manual_reconciliation_view", ManualAction);
return {
StatementAction: StatementAction,
ManualAction: ManualAction,
};
});

View File

@@ -0,0 +1,382 @@
.progress-reconciliation {
.progress-bar {
font-size: 1.08333333rem;
height: 14px;
background-color: $o-enterprise-color;
span {
display: contents;
}
}
}
.o_reconciliation {
.o_filter_input_wrapper {
position: relative;
width: 150px;
margin: 0.5rem !important;
.searchIcon {
position: absolute;
right: 10px;
}
.o_filter_input {
border: none;
border-bottom: 1px black solid;
}
}
.import_to_suspense {
margin: 0.5rem !important;
}
.notification_area {
clear: both;
}
.o_view_noreconciliation {
max-width: none;
padding: 0 10%;
color: $o-main-color-muted;
font-size: 125%;
}
.accounting_view {
width: 100%;
.cell_left {
border-right: 1px solid #333;
padding-right: 5px;
}
.edit_amount {
margin-left: 20px;
color: #bbb;
}
.cell:hover .edit_amount {
color: #00a09d;
}
.strike_amount {
text-decoration: line-through;
}
tbody tr:hover .cell_account_code::before {
content: "\f068";
font-family: FontAwesome;
position: relative;
margin-left: -17px;
left: -4px;
line-height: 0;
padding: 3px 2px 5px 5px;
}
}
.o_multi_currency {
margin-right: 5px;
&.o_multi_currency_color_0 {
color: #dd6666;
}
&.o_multi_currency_color_1 {
color: #aaaaaa;
}
&.o_multi_currency_color_2 {
color: #66dd66;
}
&.o_multi_currency_color_3 {
color: #6666dd;
}
&.o_multi_currency_color_4 {
color: #dddd66;
}
&.o_multi_currency_color_5 {
color: #dd66dd;
}
&.o_multi_currency_color_6 {
color: #66dddd;
}
&.o_multi_currency_color_7 {
color: #aaa333;
}
}
.o_reconciliation_line {
margin-bottom: 30px;
table {
width: 100%;
vertical-align: top;
}
tbody tr {
cursor: pointer;
}
tr.already_reconciled {
color: $o-account-info-color;
}
tr.invalid {
text-decoration: line-through;
}
td {
padding: 1px 2px;
}
thead td {
border-top: $o-account-light-border;
padding-top: 4px;
padding-bottom: 5px;
background-color: $o-account-initial-line-background;
}
tfoot td {
color: #bbb;
}
/* columns */
.cell_action {
width: 15px;
color: gray("700");
background: #fff;
border: 0;
text-align: center;
.fa-add-remove:before {
content: "";
}
}
tr:hover .cell_action .fa-add-remove:before {
content: "\f068";
}
.is_tax .cell_action .fa-add-remove:before {
position: relative;
top: -18px;
}
.cell_account_code {
width: 80px;
padding-left: 5px;
}
.cell_due_date {
width: 100px;
}
.cell_label {
width: auto;
}
.cell_left {
padding-right: 5px;
}
.cell_right,
.cell_left {
text-align: right;
width: 120px;
}
.cell_info_popover {
text-align: right;
width: 15px;
color: #ccc;
&:empty {
padding: 0;
width: 0;
}
}
table.accounting_view {
.cell_right,
.cell_left,
.cell_label,
.cell_due_date,
.cell_account_code,
.cell_info_popover {
box-shadow: 0 1px 0 #eaeaea;
}
}
/* info popover */
.popover {
max-width: none;
}
table.details {
vertical-align: top;
td:first-child {
vertical-align: top;
padding-right: 10px;
font-weight: bold;
}
}
tr.one_line_info {
td {
padding-top: 10px;
text-align: center;
color: $o-account-info-color;
}
}
/* Icons */
.toggle_match,
.toggle_create {
transform: rotate(0deg);
transition: transform 300ms ease 0s;
}
.visible_toggle,
&[data-mode="match"] .toggle_match,
&[data-mode="create"] .toggle_create {
visibility: visible !important;
transform: rotate(90deg);
}
.toggle_create {
font-size: 10px;
}
/* Match view & Create view */
> .o_notebook {
display: none;
> .o_notebook_headers {
margin-right: 0;
margin-left: 0;
}
}
> .o_notebook > .tab-content > div {
border: 1px solid #ddd;
border-top: 0;
}
> .o_notebook .match table tr:hover {
background-color: #eee;
}
&:not([data-mode="inactive"]) > .o_notebook {
display: block;
}
&:not(:focus-within) .o_web_accesskey_overlay {
display: none;
}
&:focus caption .o_buttons button {
outline: none;
box-shadow: 4px 4px 4px 0px $o-enterprise-color;
}
&:focus {
outline: none;
box-shadow: 0 0 0 0;
}
}
.o_reconcile_models .btn-primary {
margin: 0 2px 3px 0;
}
/* Match view */
.match {
.cell_action .fa-add-remove:before {
content: "";
}
tr:hover .cell_action .fa-add-remove:before {
content: "\f067";
}
.match_controls {
padding: 5px 0 5px
($o-account-action-col-width + $o-account-main-table-borders-padding);
.filter {
width: 240px;
display: inline-block;
}
.fa-chevron-left,
.fa-chevron-right {
display: inline-block;
cursor: pointer;
}
.fa-chevron-left {
margin-right: 10px;
}
.fa-chevron-left.disabled,
.fa-chevron-right.disabled {
color: #ddd;
cursor: default;
}
}
.show_more {
display: inline-block;
margin-left: (
$o-account-action-col-width + $o-account-main-table-borders-padding
);
margin-top: 5px;
}
}
/* Create view */
.create {
> div > div.quick_add > .o_reconcile_models {
max-width: 100%;
max-height: 70px;
flex-wrap: wrap;
overflow: auto;
& > * {
flex-grow: 0;
}
}
.quick_add {
margin-bottom: 7px;
padding: 0 8px;
}
.o_group table.o_group_col_6 {
width: 49%;
margin: 0;
vertical-align: top;
}
.o_group table.o_group_col_6:first-child {
margin-left: 8px;
}
.btn {
padding-top: 0;
padding-bottom: 0;
}
.add_line_container {
text-align: center;
clear: both;
color: $o-enterprise-primary-color;
cursor: pointer;
}
}
.o_notebook .tab-content > .tab-pane {
padding: 5px 0;
}
}
/*Manual Reconciliation*/
.o_manual_statement {
.accounting_view {
td[colspan="3"] span:first-child {
width: 100%;
display: inline-block;
}
td[colspan="2"] {
border-bottom: 1px solid #333;
text-align: center;
width: 240px;
}
.do_partial_reconcile_true {
display: none;
}
}
}
// This is rtl language specific fix
// It will flip the fa-fa play icon in left direction
.o_rtl {
.o_reconciliation {
.o_reconciliation_line {
.toggle_match,
.toggle_create {
transform: rotate(180deg);
transition: transform 300ms;
}
.visible_toggle,
&[data-mode="match"] .toggle_match,
&[data-mode="create"] .toggle_create {
transform: rotate(270deg);
}
}
}
}

View File

@@ -0,0 +1,636 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<div t-name="reconciliation" class="o_reconciliation">
<div class="o_form_view">
<div class="o_form_sheet_bg">
<div class="o_form_sheet" />
</div>
</div>
</div>
<t t-name="reconciliation.control.pager">
<div class="progress progress-reconciliation">
<div
aria-valuemin="0"
t-att-aria-valuenow="widget._initialState.valuenow"
t-att-aria-valuemax="widget._initialState.valuemax"
class="progress-bar"
role="progressbar"
style="width: 0%;"
><span class="valuenow"><t
t-esc="widget._initialState.valuenow"
/></span> / <span class="valuemax"><t
t-esc="widget._initialState.valuemax"
/></span></div>
</div>
</t>
<t t-name="reconciliation.statement">
<div t-if="widget._initialState.valuemax">
<div class="notification_area" />
<div class="o_reconciliation_lines" />
<div
t-if="widget._initialState.valuemax &gt; widget._initialState.defaultDisplayQty"
>
<button class="btn btn-secondary js_load_more">Load more</button>
</div>
</div>
<div t-else="" class="o_view_noreconciliation">
<p>Nothing to do!</p>
<p
>This page displays all the bank transactions that are to be reconciled and provides with a neat interface to do so.</p>
</div>
</t>
<t t-name="reconciliation.manual.statement" t-extend="reconciliation.statement">
<t t-jquery="div:first" t-operation="attributes">
<attribute name="class" value="o_manual_statement" />
</t>
<t t-jquery=".o_view_noreconciliation p" t-operation="replace" />
<t t-jquery=".o_filter_input_wrapper" t-operation="replace" />
<t t-jquery=".o_view_noreconciliation" t-operation="append">
<p><b>Good Job!</b> There is nothing to reconcile.</p>
<p
>All invoices and payments have been matched, your accounts' balances are clean.</p>
<p>
From now on, you may want to:
<ul>
<li>Check that you have no bank statement lines to <a
href="#"
rel="do_action"
data-tag="bank_statement_reconciliation_view"
>reconcile</a></li>
<li>Verify <a
href="#"
rel="do_action"
data-action_name="Unpaid Customer Invoices"
data-model="account.move"
data-domain="[('type', 'in', ('out_invoice', 'out_refund'))]"
data-context="{'search_default_unpaid': 1}"
>unpaid invoices</a> and follow-up customers</li>
<li>Pay your <a
href="#"
rel="do_action"
data-action_name="Unpaid Vendor Bills"
data-model="account.move"
data-domain="[('type', 'in', ('in_invoice', 'in_refund'))]"
data-context="{'search_default_unpaid': 1}"
>vendor bills</a></li>
<li>Check all <a
href="#"
rel="do_action"
data-action_name="Unreconciled Entries"
data-model="account.move.line"
data-context="{'search_default_unreconciled': 1}"
>unreconciled entries</a></li>
</ul>
</p>
</t>
</t>
<div t-name="reconciliation.done" class="done_message">
<h2>Congrats, you're all done!</h2>
<p>You reconciled <strong><t t-esc="number" /></strong> transactions in <strong><t
t-esc="duration"
/></strong>.
<t t-if="number > 1">
<br />That's on average <t
t-esc="timePerTransaction"
/> seconds per transaction.
</t>
</p>
<t t-if="context &amp;&amp; context.active_model">
<p
t-if="context['active_model'] === 'account.journal' || context['active_model'] === 'account.bank.statement' || context['active_model'] === 'account.bank.statement.import'"
class="actions_buttons"
>
<t t-if="context.journal_id">
<button
class="button_back_to_statement btn btn-secondary"
t-att-data_journal_id='context.journal_id'
>Go to bank statement(s)</button>
</t>
<t t-if="context['active_model'] === 'account.bank.statement'">
<button
class="button_close_statement btn btn-primary"
style="display: inline-block;"
>Close statement</button>
</t>
</p>
</t>
</div>
<t t-name="reconciliation.line">
<t t-set="state" t-value="widget._initialState" />
<div class="o_reconciliation_line" t-att-data-mode="state.mode" tabindex="0">
<table class="accounting_view">
<caption style="caption-side: top;">
<div class="float-right o_buttons">
<button
t-attf-class="o_no_valid btn btn-secondary #{state.balance.type &lt; 0 ? '' : 'd-none'}"
disabled="disabled"
data-toggle="tooltip"
title="Select a partner or choose a counterpart"
accesskey=""
>Validate</button>
<button
t-attf-class="o_validate btn btn-secondary #{!state.balance.type ? '' : 'd-none'}"
>Validate</button>
<button
t-attf-class="o_reconcile btn btn-primary #{state.balance.type &gt; 0 ? '' : 'd-none'}"
>Validate</button>
</div>
</caption>
<thead>
<tr>
<td class="cell_account_code"><t
t-esc="state.st_line.account_code"
/></td>
<td class="cell_due_date"><t t-esc="state.st_line.date" /></td>
<td class="cell_label"><t
t-if="state.st_line.name"
t-esc="state.st_line.name"
/> <t t-if="state.st_line.amount_currency_str"> (<t
t-esc="state.st_line.amount_currency_str"
/>)</t></td>
<td class="cell_left"><t t-if="state.st_line.amount &gt; 0"><t
t-raw="state.st_line.amount_str"
/></t></td>
<td class="cell_right"><t t-if="state.st_line.amount &lt; 0"><t
t-raw="state.st_line.amount_str"
/></t></td>
<td class="cell_info_popover" />
</tr>
</thead>
<tbody>
<t t-foreach="state.reconciliation_proposition" t-as="line"><t
t-call="reconciliation.line.mv_line"
/></t>
</tbody>
<tfoot>
<t t-call="reconciliation.line.balance" />
</tfoot>
</table>
<div class="o_notebook">
<div class="o_notebook_headers">
<ul class="nav nav-tabs ml-0 mr-0">
<li
class="nav-item"
t-attf-title="{{'Match statement with existing lines on receivable/payable accounts&lt;br&gt;* Black line: existing journal entry that should be matched&lt;br&gt;* Blue lines: existing payment that should be matched'}}"
data-toggle="tooltip"
><a
data-toggle="tab"
disable_anchor="true"
t-attf-href="#notebook_page_match_rp_#{state.st_line.id}"
class="nav-link active nav-match_rp"
role="tab"
aria-selected="true"
>Customer/Vendor Matching</a></li>
<li
class="nav-item"
title="Match with entries that are not from receivable/payable accounts"
data-toggle="tooltip"
><a
data-toggle="tab"
disable_anchor="true"
t-attf-href="#notebook_page_match_other_#{state.st_line.id}"
class="nav-link nav-match_other"
role="tab"
aria-selected="false"
>Miscellaneous Matching</a></li>
<li
class="nav-item"
title="Create a counterpart"
data-toggle="tooltip"
><a
data-toggle="tab"
disable_anchor="true"
t-attf-href="#notebook_page_create_#{state.st_line.id}"
class="nav-link nav-create"
role="tab"
aria-selected="false"
>Manual Operations</a></li>
</ul>
</div>
<div class="tab-content">
<div
class="tab-pane active"
t-attf-id="notebook_page_match_rp_#{state.st_line.id}"
>
<div class="match">
<t t-call="reconciliation.line.match" />
</div>
</div>
<div
class="tab-pane"
t-attf-id="notebook_page_match_other_#{state.st_line.id}"
>
<div class="match">
<t t-call="reconciliation.line.match" />
</div>
</div>
<div
class="tab-pane"
t-attf-id="notebook_page_create_#{state.st_line.id}"
>
<div class="create" />
</div>
</div>
</div>
</div>
</t>
<t t-name="reconciliation.manual.line" t-extend="reconciliation.line">
<t t-jquery=".o_buttons" t-operation="replace">
<div class="float-right o_buttons">
<button
t-attf-class="o_validate btn btn-secondary #{!state.balance.type ? '' : 'd-none'}"
>Reconcile</button>
<button
t-attf-class="o_reconcile btn btn-primary #{state.balance.type &gt; 0 ? '' : 'd-none'}"
>Reconcile</button>
<button
t-attf-class="o_no_valid btn btn-secondary #{state.balance.type &lt; 0 ? '' : 'd-none'}"
>Skip</button>
</div>
</t>
<t t-jquery=".accounting_view tbody" t-operation="append">
<t t-if='!_.filter(state.reconciliation_proposition, {"display": true}).length'>
<t t-set="line" t-value='{}' />
<t t-call="reconciliation.line.mv_line" />
</t>
</t>
<t t-jquery=".accounting_view thead tr" t-operation="replace">
<tr>
<td colspan="3"><span /><span
t-if="state.last_time_entries_checked"
>Last Reconciliation: <t
t-esc="state.last_time_entries_checked"
/></span></td>
<td colspan="2"><t t-esc="state.st_line.account_code" /></td>
<td class="cell_info_popover" />
</tr>
</t>
<t t-jquery='div[t-attf-id*="notebook_page_match_rp"]' t-operation="replace" />
<t t-jquery='a[t-attf-href*="notebook_page_match_rp"]' t-operation="replace" />
</t>
<t t-name="reconciliation.line.balance">
<tr t-if="state.balance.show_balance">
<td class="cell_account_code"><t t-esc="state.balance.account_code" /></td>
<td class="cell_due_date" />
<td class="cell_label"><t t-if="state.st_line.partner_id">Open balance</t><t
t-else=""
>Choose counterpart or Create Write-off</t></td>
<td class="cell_left"><t t-if="state.balance.amount_currency &lt; 0"><span
role="img"
t-if="state.balance.amount_currency_str"
t-attf-class="o_multi_currency o_multi_currency_color_#{state.balance.currency_id%8} line_info_button fa fa-money"
t-att-data-content="state.balance.amount_currency_str"
t-att-aria-label="state.balance.amount_currency_str"
t-att-title="state.balance.amount_currency_str"
/><t t-raw="state.balance.amount_str" /></t></td>
<td class="cell_right"><t t-if="state.balance.amount_currency &gt; 0"><span
role="img"
t-if="state.balance.amount_currency_str"
t-attf-class="o_multi_currency o_multi_currency_color_#{state.balance.currency_id%8} line_info_button fa fa-money"
t-att-data-content="state.balance.amount_currency_str"
t-att-aria-label="state.balance.amount_currency_str"
t-att-title="state.balance.amount_currency_str"
/><t t-raw="state.balance.amount_str" /></t></td>
<td class="cell_info_popover" />
</tr>
</t>
<div t-name="reconciliation.line.match">
<div class="match_controls">
<span><input
class="filter o_input"
placeholder="Filter on account, label, partner, amount,..."
type="text"
t-att-value="state['filter_{{state.mode}}']"
/></span>
<button class="btn btn-secondary btn-sm fa fa-search" type="button" />
</div>
<table>
<tbody>
</tbody>
</table>
<div class="load-more text-center">
<a href="#">Load more... (<span /> remaining)</a>
</div>
</div>
<div t-name="reconciliation.line.create">
<div class="quick_add">
<div class="btn-group o_reconcile_models" t-if="state.reconcileModels">
<t t-foreach="state.reconcileModels" t-as="reconcileModel">
<button
class="btn btn-primary"
t-if="reconcileModel.rule_type === 'writeoff_button' &amp;&amp; (reconcileModel.match_journal_ids.length == 0 || reconcileModel.match_journal_ids.includes(state.st_line.journal_id) || state.st_line.journal_id === undefined)"
t-att-data-reconcile-model-id="reconcileModel.id"
>
<t t-esc="reconcileModel.name" />
</button>
</t>
<p
t-if="!state.reconcileModels.length"
style="color: #bbb;"
>To speed up reconciliation, define <a
style="cursor: pointer;"
class="reconcile_model_create"
>reconciliation models</a>.</p>
</div>
<div class="dropdown float-right">
<a data-toggle="dropdown" href="#"><span
class="fa fa-cog"
role="img"
aria-label="Settings"
/></a>
<div
class="dropdown-menu dropdown-menu-right"
role="menu"
aria-label="Presets config"
>
<a
role="menuitem"
class="dropdown-item reconcile_model_create"
href="#"
>Create model</a>
<a
role="menuitem"
class="dropdown-item reconcile_model_edit"
href="#"
>Modify models</a>
</div>
</div>
</div>
<div class="clearfix o_form_sheet">
<div class="o_group">
<table class="o_group o_inner_group o_group_col_6">
<tbody>
<tr class="create_account_id">
<td class="o_td_label"><label
class="o_form_label"
>Account</label></td>
<td class="o_td_field" />
</tr>
<tr class="create_tax_id">
<td class="o_td_label"><label
class="o_form_label"
>Taxes</label></td>
<td class="o_td_field" />
</tr>
<tr class="create_analytic_account_id" t-if="group_acc">
<td class="o_td_label"><label
class="o_form_label"
>Analytic Acc.</label></td>
<td class="o_td_field" />
</tr>
<tr class="create_analytic_tag_ids" t-if="group_tags">
<td class="o_td_label"><label
class="o_form_label"
>Analytic Tags.</label></td>
<td class="o_td_field" />
</tr>
</tbody>
</table>
<table class="o_group o_inner_group o_group_col_6">
<tbody>
<tr class="create_journal_id" style="display: none;">
<td class="o_td_label"><label
class="o_form_label"
>Journal</label></td>
<td class="o_td_field" />
</tr>
<tr class="create_label">
<td class="o_td_label"><label
class="o_form_label"
>Label</label></td>
<td class="o_td_field" />
</tr>
<tr class="create_amount">
<td class="o_td_label"><label
class="o_form_label"
>Amount</label></td>
<td class="o_td_field" />
</tr>
<tr class="create_force_tax_included d-none">
<td class="o_td_label"><label
class="o_form_label"
>Tax Included in Price</label></td>
<td class="o_td_field" />
</tr>
<tr class="create_date d-none">
<td class="o_td_label"><label
class="o_form_label"
>Writeoff Date</label></td>
<td class="o_td_field" />
</tr>
<tr class="create_to_check">
<td class="o_td_label"><label
class="o_form_label"
>To Check</label></td>
<td class="o_td_field" />
</tr>
</tbody>
</table>
</div>
</div>
<div class="add_line_container">
<a
class="add_line"
t-att-style="!state.balance.amout ? 'display: none;' : null"
><i class="fa fa-plus-circle" /> Save and New</a>
</div>
</div>
<t t-name="reconciliation.line.mv_line.amount">
<span
t-att-class="(line.is_move_line &amp;&amp; proposition == true) ? 'cell' : ''"
>
<span class="line_amount">
<span
t-if="line.amount_currency_str"
t-attf-class="o_multi_currency o_multi_currency_color_#{line.currency_id%8} line_info_button fa fa-money"
t-att-data-content="line.amount_currency_str"
/>
<span
t-if="line.partial_amount &amp;&amp; line.partial_amount != line.amount"
class="strike_amount text-muted"
>
<t t-raw="line.amount_str" />
<br />
</span>
</span>
<t t-if="line.is_move_line &amp;&amp; proposition == true">
<i class="fa fa-pencil edit_amount" />
<input class="edit_amount_input text-right d-none" />
</t>
<span class="line_amount">
<t t-if="!line.partial_amount_str" t-raw="line.amount_str" />
<t
t-if="line.partial_amount_str &amp;&amp; line.partial_amount != line.amount"
t-raw="line.partial_amount_str"
/>
</span>
</span>
</t>
<t t-name="reconciliation.line.mv_line">
<tr
t-if="line.display !== false"
t-attf-class="mv_line #{line.already_paid ? ' already_reconciled' : ''} #{line.__invalid ? 'invalid' : ''} #{line.is_tax ? 'is_tax' : ''}"
t-att-data-line-id="line.id"
t-att-data-selected="selected"
>
<td class="cell_account_code"><t
t-esc="line.account_code"
/>&#8203;</td> <!-- zero width space to make empty lines the height of the text -->
<td class="cell_due_date">
<t t-if="typeof(line.id) != 'number' &amp;&amp; line.id">
<span class="badge badge-secondary">New</span>
</t>
<t t-else="" t-esc="line.date_maturity || line.date" />
</td>
<td class="cell_label">
<t
t-if="line.partner_id &amp;&amp; line.partner_id !== state.st_line.partner_id"
>
<t t-if="line.partner_name.length">
<span class="font-weight-bold" t-esc="line.partner_name" />:
</t>
</t>
<t t-esc="line.label || line.name" />
<t t-if="line.ref &amp;&amp; line.ref.length"> : </t>
<t t-esc="line.ref" />
</td>
<td class="cell_left">
<t t-if="line.amount &lt; 0">
<t t-call="reconciliation.line.mv_line.amount" />
</t>
</td>
<td class="cell_right">
<t t-if="line.amount &gt; 0">
<t t-call="reconciliation.line.mv_line.amount" />
</t>
</td>
<td class="cell_info_popover" />
</tr>
</t>
<t t-name="reconciliation.line.mv_line.details">
<table class='details'>
<tr t-if="line.account_code"><td>Account</td><td><t
t-esc="line.account_code"
/> <t t-esc="line.account_name" /></td></tr>
<tr><td>Date</td><td><t t-esc="line.date" /></td></tr>
<tr><td>Due Date</td><td><t t-esc="line.date_maturity || line.date" /></td></tr>
<tr><td>Journal</td><td><t t-esc="line.journal_id.display_name" /></td></tr>
<tr t-if="line.partner_id"><td>Partner</td><td><t
t-esc="line.partner_name"
/></td></tr>
<tr><td>Label</td><td><t t-esc="line.label" /></td></tr>
<tr t-if="line.ref"><td>Ref</td><td><t t-esc="line.ref" /></td></tr>
<tr><td>Amount</td><td><t t-raw="line.total_amount_str" /><t
t-if="line.total_amount_currency_str"
> (<t t-esc="line.total_amount_currency_str" />)</t></td></tr>
<tr t-if="line.is_partially_reconciled"><td>Residual</td><td>
<t t-raw="line.amount_str" /><t t-if="line.amount_currency_str"> (<t
t-esc="line.amount_currency_str"
/>)</t>
</td></tr>
<tr class="one_line_info" t-if='line.already_paid'>
<td colspan="2">This payment is registered but not reconciled.</td>
</tr>
</table>
</t>
<t t-name="reconciliation.line.statement_line.details">
<table class='details'>
<tr><td>Date</td><td><t t-esc="state.st_line.date" /></td></tr>
<tr t-if="state.st_line.partner_name"><td>Partner</td><td><t
t-esc="state.st_line.partner_name"
/></td></tr>
<tr t-if="state.st_line.ref"><td>Transaction</td><td><t
t-esc="state.st_line.ref"
/></td></tr>
<tr><td>Description</td><td><t t-esc="state.st_line.name" /></td></tr>
<tr><td>Amount</td><td><t t-raw="state.st_line.amount_str" /><t
t-if="state.st_line.amount_currency_str"
> (<t t-esc="state.st_line.amount_currency_str" />)</t></td></tr>
<tr><td>Account</td><td><t t-esc="state.st_line.account_code" /> <t
t-esc="state.st_line.account_name"
/></td></tr>
<tr t-if="state.st_line.note"><td>Note</td><td style="white-space: pre;"><t
t-esc="state.st_line.note"
/></td></tr>
</table>
</t>
<t t-name="reconciliation.notification.reconciled">
<t t-if="details !== undefined">
<a
rel="do_action"
href="#"
aria-label="External link"
title="External link"
t-att-data-action_name="details.name"
t-att-data-model="details.model"
t-att-data-ids="details.ids"
>
<t t-esc="nb_reconciled_lines" />
statement lines
</a>
have been reconciled automatically.
</t>
</t>
<t t-name="reconciliation.notification.default">
<t t-esc="message" />
<t t-if="details !== undefined">
<a
class="fa fa-external-link"
rel="do_action"
href="#"
aria-label="External link"
title="External link"
t-att-data-action_name="details.name"
t-att-data-model="details.model"
t-att-data-ids="details.ids"
>
</a>
</t>
</t>
<t t-name="reconciliation.notification">
<div
t-att-class="'notification alert-dismissible alert alert-' + type"
role="alert"
>
<button
type="button"
class="close"
data-dismiss="alert"
aria-label="Close"
><span title="Close" class="fa fa-times" /></button>
<t t-if="template">
<t t-call="{{template}}" />
</t>
<t t-else="">
<t t-call="reconciliation.notification.default" />
</t>
</div>
</t>
</templates>

File diff suppressed because it is too large Load Diff