[IMP] account_reconcile_oca: Finish creation of module

* refactoring JS in order to add logic
* Improve views
* Minor fixes in Odoo code in order to fix all possible options
pull/500/head
Enric Tobella 2023-03-03 23:07:35 +01:00
parent 0ea9421d14
commit 8ad989920e
30 changed files with 573 additions and 145 deletions

View File

@ -27,17 +27,14 @@
"post_init_hook": "post_init_hook", "post_init_hook": "post_init_hook",
"assets": { "assets": {
"web.assets_backend": [ "web.assets_backend": [
"account_reconcile_oca/static/src/js/reconcile_manual_view.esm.js", "account_reconcile_oca/static/src/js/widgets/reconcile_data_widget.esm.js",
"account_reconcile_oca/static/src/js/reconcile_data_widget.esm.js", "account_reconcile_oca/static/src/js/widgets/reconcile_chatter_field.esm.js",
"account_reconcile_oca/static/src/js/reconcile_chatter_field.esm.js", "account_reconcile_oca/static/src/js/widgets/selection_badge_uncheck.esm.js",
"account_reconcile_oca/static/src/js/selection_badge_uncheck.esm.js", "account_reconcile_oca/static/src/js/widgets/reconcile_move_line_widget.esm.js",
"account_reconcile_oca/static/src/js/reconcile_move_line_view.esm.js", "account_reconcile_oca/static/src/js/reconcile_move_line/*.esm.js",
"account_reconcile_oca/static/src/js/reconcile_move_line_widget.esm.js", "account_reconcile_oca/static/src/js/reconcile_form/*.esm.js",
"account_reconcile_oca/static/src/js/reconcile_kanban_record.esm.js", "account_reconcile_oca/static/src/js/reconcile_manual/*.esm.js",
"account_reconcile_oca/static/src/js/reconcile_renderer.esm.js", "account_reconcile_oca/static/src/js/reconcile/*.esm.js",
"account_reconcile_oca/static/src/js/reconcile_controller.esm.js",
"account_reconcile_oca/static/src/js/reconcile_view.esm.js",
"account_reconcile_oca/static/src/js/reconcile_form_view.esm.js",
"account_reconcile_oca/static/src/xml/reconcile.xml", "account_reconcile_oca/static/src/xml/reconcile.xml",
"account_reconcile_oca/static/src/scss/reconcile.scss", "account_reconcile_oca/static/src/scss/reconcile.scss",
], ],

View File

@ -17,11 +17,11 @@ class AccountAccountReconcile(models.Model):
reconcile_data_info = fields.Serialized(inverse="_inverse_reconcile_data_info") reconcile_data_info = fields.Serialized(inverse="_inverse_reconcile_data_info")
partner_id = fields.Many2one("res.partner") partner_id = fields.Many2one("res.partner", readonly=True)
account_id = fields.Many2one("account.account") account_id = fields.Many2one("account.account", readonly=True)
name = fields.Char() name = fields.Char(readonly=True)
is_reconciled = fields.Boolean() is_reconciled = fields.Boolean(readonly=True)
currency_id = fields.Many2one("res.currency") currency_id = fields.Many2one("res.currency", readonly=True)
@property @property
def _table_query(self): def _table_query(self):

View File

@ -40,12 +40,23 @@ class AccountBankStatementLine(models.Model):
) )
manual_partner_id = fields.Many2one( manual_partner_id = fields.Many2one(
"res.partner", "res.partner",
domain=[('parent_id', '=', False)], domain=[("parent_id", "=", False)],
check_company=True, check_company=True,
store=False, store=False,
default=False, default=False,
prefetch=False, prefetch=False,
) )
analytic_distribution = fields.Json(
store=False,
default=False,
prefetch=False,
)
analytic_precision = fields.Integer(
store=False,
default=lambda self: self.env["decimal.precision"].precision_get(
"Percentage Analytic"
),
)
manual_model_id = fields.Many2one( manual_model_id = fields.Many2one(
"account.reconcile.model", "account.reconcile.model",
check_company=True, check_company=True,
@ -54,13 +65,21 @@ class AccountBankStatementLine(models.Model):
prefetch=False, prefetch=False,
domain=[("rule_type", "=", "writeoff_button")], domain=[("rule_type", "=", "writeoff_button")],
) )
manual_delete = fields.Boolean(
store=False,
default=False,
prefetch=False,
)
manual_name = fields.Char(store=False, default=False, prefetch=False) manual_name = fields.Char(store=False, default=False, prefetch=False)
manual_amount = fields.Monetary(store=False, default=False, prefetch=False) manual_amount = fields.Monetary(store=False, default=False, prefetch=False)
manual_original_amount = fields.Monetary(
default=False, store=False, prefetch=False, readonly=True
)
manual_move_type = fields.Selection(
lambda r: r.env["account.move"]._fields["move_type"].selection,
default=False,
store=False,
prefetch=False,
readonly=True,
)
manual_move_id = fields.Many2one(
"account.move", default=False, store=False, prefetch=False, readonly=True
)
can_reconcile = fields.Boolean(sparse="reconcile_data_info") can_reconcile = fields.Boolean(sparse="reconcile_data_info")
def save(self): def save(self):
@ -86,7 +105,8 @@ class AccountBankStatementLine(models.Model):
data, data,
self.manual_model_id, self.manual_model_id,
self.reconcile_data_info["reconcile_auxiliary_id"], self.reconcile_data_info["reconcile_auxiliary_id"],
) ),
self.manual_reference
) )
else: else:
# Refreshing data # Refreshing data
@ -116,12 +136,14 @@ class AccountBankStatementLine(models.Model):
) )
) )
self.reconcile_data_info = self._recompute_suspense_line( self.reconcile_data_info = self._recompute_suspense_line(
new_data, self.reconcile_data_info["reconcile_auxiliary_id"] new_data,
self.reconcile_data_info["reconcile_auxiliary_id"],
self.manual_reference,
) )
self.can_reconcile = self.reconcile_data_info.get("can_reconcile", False) self.can_reconcile = self.reconcile_data_info.get("can_reconcile", False)
self.add_account_move_line_id = False self.add_account_move_line_id = False
def _recompute_suspense_line(self, data, reconcile_auxiliary_id): def _recompute_suspense_line(self, data, reconcile_auxiliary_id, manual_reference):
can_reconcile = True can_reconcile = True
total_amount = 0 total_amount = 0
new_data = [] new_data = []
@ -175,6 +197,7 @@ class AccountBankStatementLine(models.Model):
"counterparts": counterparts, "counterparts": counterparts,
"reconcile_auxiliary_id": reconcile_auxiliary_id, "reconcile_auxiliary_id": reconcile_auxiliary_id,
"can_reconcile": can_reconcile, "can_reconcile": can_reconcile,
"manual_reference": manual_reference,
} }
def _check_line_changed(self, line): def _check_line_changed(self, line):
@ -208,7 +231,11 @@ class AccountBankStatementLine(models.Model):
"manual_name": False, "manual_name": False,
"manual_partner_id": False, "manual_partner_id": False,
"manual_line_id": False, "manual_line_id": False,
"manual_move_id": False,
"manual_move_type": False,
"manual_kind": False, "manual_kind": False,
"manual_original_amount": False,
"analytic_distribution": False,
} }
) )
continue continue
@ -220,10 +247,17 @@ class AccountBankStatementLine(models.Model):
line.get("partner_id") and line["partner_id"][0] line.get("partner_id") and line["partner_id"][0]
) )
self.manual_line_id = line["id"] self.manual_line_id = line["id"]
self.analytic_distribution = line.get("analytic_distribution", {})
if self.manual_line_id:
self.manual_move_id = self.manual_line_id.move_id
self.manual_move_type = self.manual_line_id.move_id.move_type
self.manual_kind = line["kind"] self.manual_kind = line["kind"]
self.manual_original_amount = line.get("original_amount", 0.0)
new_data.append(line) new_data.append(line)
self.reconcile_data_info = self._recompute_suspense_line( self.reconcile_data_info = self._recompute_suspense_line(
new_data, self.reconcile_data_info["reconcile_auxiliary_id"] new_data,
self.reconcile_data_info["reconcile_auxiliary_id"],
self.manual_reference,
) )
self.can_reconcile = self.reconcile_data_info.get("can_reconcile", False) self.can_reconcile = self.reconcile_data_info.get("can_reconcile", False)
@ -232,6 +266,7 @@ class AccountBankStatementLine(models.Model):
"manual_partner_id", "manual_partner_id",
"manual_name", "manual_name",
"manual_amount", "manual_amount",
"analytic_distribution",
) )
def _onchange_manual_reconcile_vals(self): def _onchange_manual_reconcile_vals(self):
self.ensure_one() self.ensure_one()
@ -256,6 +291,7 @@ class AccountBankStatementLine(models.Model):
"debit": self.manual_amount "debit": self.manual_amount
if self.manual_amount > 0 if self.manual_amount > 0
else 0.0, else 0.0,
"analytic_distribution": self.analytic_distribution,
"kind": line["kind"] "kind": line["kind"]
if line["kind"] != "suspense" if line["kind"] != "suspense"
else "other", else "other",
@ -265,7 +301,9 @@ class AccountBankStatementLine(models.Model):
self._update_move_partner() self._update_move_partner()
new_data.append(line) new_data.append(line)
self.reconcile_data_info = self._recompute_suspense_line( self.reconcile_data_info = self._recompute_suspense_line(
new_data, self.reconcile_data_info["reconcile_auxiliary_id"] new_data,
self.reconcile_data_info["reconcile_auxiliary_id"],
self.manual_reference,
) )
self.can_reconcile = self.reconcile_data_info.get("can_reconcile", False) self.can_reconcile = self.reconcile_data_info.get("can_reconcile", False)
@ -310,21 +348,26 @@ class AccountBankStatementLine(models.Model):
for line in reconcile_model._apply_lines_for_bank_widget( for line in reconcile_model._apply_lines_for_bank_widget(
-liquidity_amount, self._retrieve_partner(), self -liquidity_amount, self._retrieve_partner(), self
): ):
amount = line["amount_currency"] new_line = line.copy()
new_line = { amount = line.get("amount_currency")
if self.foreign_currency_id:
amount = self.foreign_currency_id.compute(
amount, self.journal_id.currency_id or self.company_currency_id
)
new_line.update(
{
"reference": "reconcile_auxiliary;%s" % reconcile_auxiliary_id, "reference": "reconcile_auxiliary;%s" % reconcile_auxiliary_id,
"id": False, "id": False,
"amount": amount, "amount": amount,
"debit": amount if amount > 0 else 0.0, "debit": amount if amount > 0 else 0,
"credit": -amount if amount < 0 else 0.0, "credit": -amount if amount < 0 else 0,
"kind": "other", "kind": "other",
"account_id": self.env["account.account"] "account_id": self.env["account.account"]
.browse(line["account_id"]) .browse(line["account_id"])
.name_get()[0], .name_get()[0],
"date": fields.Date.to_string(self.date), "date": fields.Date.to_string(self.date),
"name": line.get("name"),
"currency_id": line.get("currency_id"),
} }
)
reconcile_auxiliary_id += 1 reconcile_auxiliary_id += 1
if line.get("partner_id"): if line.get("partner_id"):
new_line["partner_id"] = ( new_line["partner_id"] = (
@ -377,7 +420,8 @@ class AccountBankStatementLine(models.Model):
return self._recompute_suspense_line( return self._recompute_suspense_line(
*self._reconcile_data_by_model( *self._reconcile_data_by_model(
data, res["model"], reconcile_auxiliary_id data, res["model"], reconcile_auxiliary_id
) ),
self.manual_reference
) )
elif res and res.get("amls"): elif res and res.get("amls"):
amount = self.amount amount = self.amount
@ -387,10 +431,13 @@ class AccountBankStatementLine(models.Model):
) )
amount -= line_data.get("amount") amount -= line_data.get("amount")
data.append(line_data) data.append(line_data)
return self._recompute_suspense_line(data, reconcile_auxiliary_id) return self._recompute_suspense_line(
data, reconcile_auxiliary_id, self.manual_reference
)
return self._recompute_suspense_line( return self._recompute_suspense_line(
data + [self._get_reconcile_line(line, "other") for line in other_lines], data + [self._get_reconcile_line(line, "other") for line in other_lines],
reconcile_auxiliary_id, reconcile_auxiliary_id,
self.manual_reference,
) )
def clean_reconcile(self): def clean_reconcile(self):
@ -414,7 +461,9 @@ class AccountBankStatementLine(models.Model):
to_reconcile = [] to_reconcile = []
with move._check_balanced(container): with move._check_balanced(container):
move.with_context( move.with_context(
skip_account_move_synchronization=True, force_delete=True skip_account_move_synchronization=True,
force_delete=True,
skip_invoice_sync=True,
).write( ).write(
{ {
"line_ids": lines_to_remove, "line_ids": lines_to_remove,
@ -425,7 +474,11 @@ class AccountBankStatementLine(models.Model):
continue continue
line = ( line = (
self.env["account.move.line"] self.env["account.move.line"]
.with_context(check_move_validity=False) .with_context(
check_move_validity=False,
skip_sync_invoice=True,
skip_invoice_sync=True,
)
.create(self._reconcile_move_line_vals(line_vals)) .create(self._reconcile_move_line_vals(line_vals))
) )
if line_vals.get("counterpart_line_id"): if line_vals.get("counterpart_line_id"):
@ -461,7 +514,11 @@ class AccountBankStatementLine(models.Model):
).copy_data({"move_id": move.id})[0] ).copy_data({"move_id": move.id})[0]
to_reconcile[line.account_id.id] |= ( to_reconcile[line.account_id.id] |= (
self.env["account.move.line"] self.env["account.move.line"]
.with_context(check_move_validity=False, skip_invoice_sync=True) .with_context(
check_move_validity=False,
skip_sync_invoice=True,
skip_invoice_sync=True,
)
.create(line_data) .create(line_data)
) )
move.write( move.write(
@ -509,17 +566,7 @@ class AccountBankStatementLine(models.Model):
) )
def _unreconcile_bank_line_edit(self, data): def _unreconcile_bank_line_edit(self, data):
self.move_id.button_draft() self.action_undo_reconciliation()
self.move_id.line_ids.unlink()
self.move_id.write(
{
"line_ids": [
(0, 0, line_vals)
for line_vals in self._prepare_move_line_default_vals()
]
}
)
self.move_id.action_post()
def _unreconcile_bank_line_keep(self, data): def _unreconcile_bank_line_keep(self, data):
raise UserError(_("Keep suspense move lines mode cannot be unreconciled")) raise UserError(_("Keep suspense move lines mode cannot be unreconciled"))
@ -531,6 +578,13 @@ class AccountBankStatementLine(models.Model):
"partner_id": line.get("partner_id") and line["partner_id"][0], "partner_id": line.get("partner_id") and line["partner_id"][0],
"credit": line["credit"], "credit": line["credit"],
"debit": line["debit"], "debit": line["debit"],
"tax_ids": line.get("tax_ids", []),
"tax_tag_ids": line.get("tax_tag_ids", []),
"group_tax_id": line.get("group_tax_id"),
"tax_repartition_line_id": line.get("tax_repartition_line_id"),
"analytic_distribution": line.get("analytic_distribution"),
"name": line.get("name"),
"reconcile_model_id": line.get("reconcile_model_id"),
} }
@api.model_create_multi @api.model_create_multi
@ -556,7 +610,8 @@ class AccountBankStatementLine(models.Model):
data = record._recompute_suspense_line( data = record._recompute_suspense_line(
*record._reconcile_data_by_model( *record._reconcile_data_by_model(
data, res["model"], reconcile_auxiliary_id data, res["model"], reconcile_auxiliary_id
) ),
self.manual_reference
) )
elif res.get("amls"): elif res.get("amls"):
amount = self.amount amount = self.amount
@ -566,10 +621,65 @@ class AccountBankStatementLine(models.Model):
) )
amount -= line_data.get("amount") amount -= line_data.get("amount")
data.append(line_data) data.append(line_data)
data = record._recompute_suspense_line(data, reconcile_auxiliary_id) data = record._recompute_suspense_line(
data, reconcile_auxiliary_id, self.manual_reference
)
if not data.get("can_reconcile"): if not data.get("can_reconcile"):
continue continue
getattr( getattr(
record, "_reconcile_bank_line_%s" % record.journal_id.reconcile_mode record, "_reconcile_bank_line_%s" % record.journal_id.reconcile_mode
)(data["data"]) )(data["data"])
return result return result
def button_manual_reference_full_paid(self):
self.ensure_one()
if not self.reconcile_data_info["manual_reference"]:
return
manual_reference = self.reconcile_data_info["manual_reference"]
data = self.reconcile_data_info.get("data", [])
new_data = []
reconcile_auxiliary_id = self.reconcile_data_info["reconcile_auxiliary_id"]
for line in data:
if line["reference"] == manual_reference and line.get("id"):
total_amount = -line["amount"] + line["original_amount_unsigned"]
original_amount = line["original_amount_unsigned"]
new_data.append(
self._get_reconcile_line(
self.env["account.move.line"].browse(line["id"]),
"other",
is_counterpart=True,
max_amount=original_amount,
)
)
new_data.append(
{
"reference": "reconcile_auxiliary;%s" % reconcile_auxiliary_id,
"id": False,
"account_id": line["account_id"],
"partner_id": line.get("partner_id"),
"date": line["date"],
"name": line["name"],
"amount": -total_amount,
"credit": total_amount if total_amount > 0 else 0.0,
"debit": -total_amount if total_amount < 0 else 0.0,
"kind": "other",
"currency_id": line["currency_id"],
}
)
reconcile_auxiliary_id += 1
else:
new_data.append(line)
self.reconcile_data_info = self._recompute_suspense_line(
new_data, reconcile_auxiliary_id, self.manual_reference
)
self.can_reconcile = self.reconcile_data_info.get("can_reconcile", False)
def action_to_check(self):
self.ensure_one()
self.move_id.to_check = True
if self.can_reconcile and self.journal_id.reconcile_mode == "edit":
self.reconcile_bank_line()
def action_checked(self):
self.ensure_one()
self.move_id.to_check = False

View File

@ -13,14 +13,6 @@ class AccountJournal(models.Model):
required=True, required=True,
) )
def action_open_reconcile_to_check(self):
self.ensure_one()
action = self.env["ir.actions.act_window"]._for_xml_id(
"account_reconcile_oca.action_bank_statement_line_reconcile"
)
action["domain"] = [("id", "=", self.to_check_ids().ids)]
return action
def get_rainbowman_message(self): def get_rainbowman_message(self):
self.ensure_one() self.ensure_one()
if self.get_journal_dashboard_datas()["number_to_reconcile"] > 0: if self.get_journal_dashboard_datas()["number_to_reconcile"] > 0:

View File

@ -53,12 +53,14 @@ class AccountReconcileAbstract(models.AbstractModel):
"credit": -amount if amount < 0 else 0.0, "credit": -amount if amount < 0 else 0.0,
"amount": amount, "amount": amount,
"currency_id": line.currency_id.id, "currency_id": line.currency_id.id,
"analytic_distribution": line.analytic_distribution,
"kind": kind, "kind": kind,
} }
if not float_is_zero( if not float_is_zero(
amount - original_amount, precision_digits=line.currency_id.decimal_places amount - original_amount, precision_digits=line.currency_id.decimal_places
): ):
vals["original_amount"] = abs(original_amount) vals["original_amount"] = abs(original_amount)
vals["original_amount_unsigned"] = original_amount
if is_counterpart: if is_counterpart:
vals["counterpart_line_id"] = line.id vals["counterpart_line_id"] = line.id
return vals return vals

View File

@ -17,6 +17,7 @@ export class ReconcileController extends KanbanController {
this.effect = useService("effect"); this.effect = useService("effect");
this.orm = useService("orm"); this.orm = useService("orm");
this.action = useService("action"); this.action = useService("action");
this.router = useService("router");
this.activeActions = this.props.archInfo.activeActions; this.activeActions = this.props.archInfo.activeActions;
this.model.addEventListener("update", () => this.selectRecord(), {once: true}); this.model.addEventListener("update", () => this.selectRecord(), {once: true});
} }
@ -55,7 +56,9 @@ export class ReconcileController extends KanbanController {
} }
async selectRecord(record) { async selectRecord(record) {
var resId = undefined; var resId = undefined;
if (record === undefined) { if (record === undefined && this.props.resId) {
resId = this.props.resId;
} else if (record === undefined) {
var records = this.model.root.records.filter( var records = this.model.root.records.filter(
(modelRecord) => (modelRecord) =>
!modelRecord.data.is_reconciled || modelRecord.data.to_check !modelRecord.data.is_reconciled || modelRecord.data.to_check
@ -85,10 +88,14 @@ export class ReconcileController extends KanbanController {
if (!this.state.selectedRecordId || this.state.selectedRecordId !== resId) { if (!this.state.selectedRecordId || this.state.selectedRecordId !== resId) {
this.state.selectedRecordId = resId; this.state.selectedRecordId = resId;
} }
this.updateURL(resId);
} }
async openRecord(record) { async openRecord(record) {
this.selectRecord(record); this.selectRecord(record);
} }
updateURL(resId) {
this.router.pushState({id: resId});
}
} }
ReconcileController.components = { ReconcileController.components = {
...ReconcileController.components, ...ReconcileController.components,

View File

@ -1,8 +1,6 @@
/** @odoo-module */ /** @odoo-module */
import {FormController} from "@web/views/form/form_controller"; import {FormController} from "@web/views/form/form_controller";
import {formView} from "@web/views/form/form_view";
import {registry} from "@web/core/registry";
import {useService} from "@web/core/utils/hooks"; import {useService} from "@web/core/utils/hooks";
import {useViewButtons} from "@web/views/view_button/view_button_hook"; import {useViewButtons} from "@web/views/view_button/view_button_hook";
const {useRef} = owl; const {useRef} = owl;
@ -42,10 +40,3 @@ export class ReconcileFormController extends FormController {
} }
} }
} }
export const ReconcileFormView = {
...formView,
Controller: ReconcileFormController,
};
registry.category("views").add("reconcile_form", ReconcileFormView);

View File

@ -0,0 +1,32 @@
/** @odoo-module */
import {Notebook} from "@web/core/notebook/notebook";
import {onWillDestroy} from "@odoo/owl";
export class ReconcileFormNotebook extends Notebook {
setup() {
super.setup(...arguments);
const onPageNavigate = this.onPageNavigate.bind(this);
this.env.bus.addEventListener("RECONCILE_PAGE_NAVIGATE", onPageNavigate);
onWillDestroy(() => {
this.env.bus.removeEventListener("RECONCILE_PAGE_NAVIGATE", onPageNavigate);
});
}
onPageNavigate(ev) {
for (const page of this.pages) {
if (
ev.detail.detail.name === page[1].name &&
this.state.currentPage !== page[0]
) {
ev.preventDefault();
ev.detail.detail.originalEv.preventDefault();
this.state.currentPage = page[0];
return;
}
}
}
}
ReconcileFormNotebook.props = {
...Notebook.props,
};

View File

@ -0,0 +1,11 @@
/** @odoo-module */
import {FormRenderer} from "@web/views/form/form_renderer";
import {ReconcileFormNotebook} from "./reconcile_form_notebook.esm.js";
export class ReconcileFormRenderer extends FormRenderer {}
ReconcileFormRenderer.components = {
...ReconcileFormRenderer.components,
Notebook: ReconcileFormNotebook,
};

View File

@ -0,0 +1,14 @@
/** @odoo-module */
import {ReconcileFormController} from "./reconcile_form_controller.esm.js";
import {ReconcileFormRenderer} from "./reconcile_form_renderer.esm.js";
import {formView} from "@web/views/form/form_view";
import {registry} from "@web/core/registry";
export const ReconcileFormView = {
...formView,
Controller: ReconcileFormController,
Renderer: ReconcileFormRenderer,
};
registry.category("views").add("reconcile_form", ReconcileFormView);

View File

@ -1,14 +1,13 @@
/** @odoo-module */ /** @odoo-module */
import {FormController} from "@web/views/form/form_controller"; import {FormController} from "@web/views/form/form_controller";
import {formView} from "@web/views/form/form_view";
import {registry} from "@web/core/registry";
import {useViewButtons} from "@web/views/view_button/view_button_hook"; import {useViewButtons} from "@web/views/view_button/view_button_hook";
const {useRef} = owl; const {useRef} = owl;
export class FormManualReconcileController extends FormController { export class ReconcileManualController extends FormController {
setup() { setup() {
super.setup(...arguments); super.setup(...arguments);
this.env.exposeController(this);
const rootRef = useRef("root"); const rootRef = useRef("root");
useViewButtons(this.model, rootRef, { useViewButtons(this.model, rootRef, {
reload: this.reloadFormController.bind(this), reload: this.reloadFormController.bind(this),
@ -29,10 +28,3 @@ export class FormManualReconcileController extends FormController {
} }
} }
} }
export const FormManualReconcileView = {
...formView,
Controller: FormManualReconcileController,
};
registry.category("views").add("reconcile_manual", FormManualReconcileView);

View File

@ -0,0 +1,12 @@
/** @odoo-module */
import {ReconcileManualController} from "./reconcile_manual_controller.esm.js";
import {formView} from "@web/views/form/form_view";
import {registry} from "@web/core/registry";
export const FormManualReconcileView = {
...formView,
Controller: ReconcileManualController,
};
registry.category("views").add("reconcile_manual", FormManualReconcileView);

View File

@ -0,0 +1,18 @@
/** @odoo-module */
import {ListController} from "@web/views/list/list_controller";
export class ReconcileMoveLineController extends ListController {
async openRecord(record) {
var data = {};
data[this.props.parentField] = [record.resId, record.display_name];
this.props.parentRecord.update(data);
}
}
ReconcileMoveLineController.template = `account_reconcile_oca.ReconcileMoveLineController`;
ReconcileMoveLineController.props = {
...ListController.props,
parentRecord: {type: Object, optional: true},
parentField: {type: String, optional: true},
};

View File

@ -0,0 +1,22 @@
/** @odoo-module */
import {ListRenderer} from "@web/views/list/list_renderer";
export class ReconcileMoveLineRenderer extends ListRenderer {
getRowClass(record) {
var classes = super.getRowClass(record);
if (
this.props.parentRecord.data.reconcile_data_info.counterparts.includes(
record.resId
)
) {
classes += " o_field_account_reconcile_oca_move_line_selected";
}
return classes;
}
}
ReconcileMoveLineRenderer.props = [
...ListRenderer.props,
"parentRecord",
"parentField",
];

View File

@ -0,0 +1,15 @@
/** @odoo-module */
import {ReconcileMoveLineController} from "./reconcile_move_line_controller.esm.js";
import {ReconcileMoveLineRenderer} from "./reconcile_move_line_renderer.esm.js";
import {listView} from "@web/views/list/list_view";
import {registry} from "@web/core/registry";
export const ReconcileMoveLineView = {
...listView,
Controller: ReconcileMoveLineController,
Renderer: ReconcileMoveLineRenderer,
};
registry.category("views").add("reconcile_move_line", ReconcileMoveLineView);

View File

@ -1,46 +0,0 @@
/** @odoo-module */
import {ListController} from "@web/views/list/list_controller";
import {ListRenderer} from "@web/views/list/list_renderer";
import {listView} from "@web/views/list/list_view";
import {registry} from "@web/core/registry";
export class ReconcileMoveLineRenderer extends ListRenderer {
getRowClass(record) {
var classes = super.getRowClass(record);
if (
this.props.parentRecord.data.reconcile_data_info.counterparts.includes(
record.resId
)
) {
classes += " o_field_account_reconcile_oca_move_line_selected";
}
return classes;
}
}
ReconcileMoveLineRenderer.props = [
...ListRenderer.props,
"parentRecord",
"parentField",
];
export class ReconcileMoveLineController extends ListController {
async openRecord(record) {
var data = {};
data[this.props.parentField] = [record.resId, record.display_name];
this.props.parentRecord.update(data);
}
}
ReconcileMoveLineController.template = `account_reconcile_oca.ReconcileMoveLineController`;
ReconcileMoveLineController.props = {
...ListController.props,
parentRecord: {type: Object, optional: true},
parentField: {type: String, optional: true},
};
export const ReconcileMoveLineView = {
...listView,
Controller: ReconcileMoveLineController,
Renderer: ReconcileMoveLineRenderer,
};
registry.category("views").add("reconcile_move_line", ReconcileMoveLineView);

View File

@ -1,7 +1,7 @@
/** @odoo-module **/ /** @odoo-module **/
import {registry} from "@web/core/registry";
import {ChatterContainer} from "@mail/components/chatter_container/chatter_container"; import {ChatterContainer} from "@mail/components/chatter_container/chatter_container";
import {registry} from "@web/core/registry";
const {Component} = owl; const {Component} = owl;

View File

@ -1,8 +1,8 @@
/** @odoo-module **/ /** @odoo-module **/
import fieldUtils from "web.field_utils"; import fieldUtils from "web.field_utils";
import session from "web.session";
import {registry} from "@web/core/registry"; import {registry} from "@web/core/registry";
import session from "web.session";
const {Component} = owl; const {Component} = owl;
@ -56,6 +56,13 @@ export class AccountReconcileDataWidget extends Component {
this.props.record.update({ this.props.record.update({
manual_reference: line.reference, manual_reference: line.reference,
}); });
const triggerEv = new CustomEvent("reconcile-page-navigate", {
detail: {
name: "manual",
originalEv: ev,
},
});
this.env.bus.trigger("RECONCILE_PAGE_NAVIGATE", triggerEv);
} }
} }
AccountReconcileDataWidget.template = "account_reconcile_oca.ReconcileDataWidget"; AccountReconcileDataWidget.template = "account_reconcile_oca.ReconcileDataWidget";

View File

@ -63,6 +63,9 @@
&.liquidity { &.liquidity {
font-weight: bold; font-weight: bold;
} }
&.selected {
background-color: rgba($o-brand-primary, 0.2);
}
} }
} }
} }

View File

@ -74,7 +74,7 @@
> >
<tr <tr
t-on-click="(ev) => this.selectReconcileLine(ev, reconcile_line)" t-on-click="(ev) => this.selectReconcileLine(ev, reconcile_line)"
t-att-class="'o_reconcile_widget_line ' + reconcile_line.kind" t-att-class="'o_reconcile_widget_line ' + reconcile_line.kind + (props.record.data.manual_reference == reconcile_line.reference ? ' selected ' : ' ')"
> >
<td t-esc="reconcile_line.account_id[1]" /> <td t-esc="reconcile_line.account_id[1]" />
<td> <td>

View File

@ -64,6 +64,87 @@ class TestReconciliationWidget(TestAccountReconciliationCommon):
# Testing reconcile action # Testing reconcile action
def test_reconcile_invoice_currency(self):
inv1 = self.create_invoice(currency_id=self.currency_usd_id, invoice_amount=100)
bank_stmt = self.acc_bank_stmt_model.create(
{
"company_id": self.env.ref("base.main_company").id,
"journal_id": self.bank_journal_euro.id,
"date": time.strftime("%Y-07-15"),
"name": "test",
}
)
bank_stmt_line = self.acc_bank_stmt_line_model.create(
{
"name": "testLine",
"journal_id": self.bank_journal_euro.id,
"statement_id": bank_stmt.id,
"amount": 50,
"amount_currency": 100,
"foreign_currency_id": self.currency_usd_id,
"date": time.strftime("%Y-07-15"),
}
)
with Form(
bank_stmt_line,
view="account_reconcile_oca.bank_statement_line_form_reconcile_view",
) as f:
self.assertFalse(f.can_reconcile)
f.add_account_move_line_id = inv1.line_ids.filtered(
lambda l: l.account_id.account_type == "asset_receivable"
)
self.assertFalse(f.add_account_move_line_id)
self.assertTrue(f.can_reconcile)
def test_reconcile_invoice_reconcile_full(self):
"""
We want to test the reconcile widget for bank statements on invoices.
As we use edit mode by default, we will also check what happens when
we press unreconcile
"""
inv1 = self.create_invoice(
currency_id=self.currency_euro_id, invoice_amount=100
)
bank_stmt = self.acc_bank_stmt_model.create(
{
"company_id": self.env.ref("base.main_company").id,
"journal_id": self.bank_journal_euro.id,
"date": time.strftime("%Y-07-15"),
"name": "test",
}
)
bank_stmt_line = self.acc_bank_stmt_line_model.create(
{
"name": "testLine",
"journal_id": self.bank_journal_euro.id,
"statement_id": bank_stmt.id,
"amount": 50,
"date": time.strftime("%Y-07-15"),
}
)
receivable1 = inv1.line_ids.filtered(
lambda l: l.account_id.account_type == "asset_receivable"
)
with Form(
bank_stmt_line,
view="account_reconcile_oca.bank_statement_line_form_reconcile_view",
) as f:
self.assertFalse(f.can_reconcile)
f.add_account_move_line_id = receivable1
self.assertFalse(f.add_account_move_line_id)
self.assertTrue(f.can_reconcile)
f.manual_reference = "account.move.line;%s" % receivable1.id
self.assertEqual(-50, f.manual_amount)
self.assertEqual(2, len(bank_stmt_line.reconcile_data_info["data"]))
bank_stmt_line.button_manual_reference_full_paid()
self.assertEqual(3, len(bank_stmt_line.reconcile_data_info["data"]))
with Form(
bank_stmt_line,
view="account_reconcile_oca.bank_statement_line_form_reconcile_view",
) as f:
f.manual_reference = "account.move.line;%s" % receivable1.id
self.assertEqual(-100, f.manual_amount)
def test_reconcile_invoice_unreconcile(self): def test_reconcile_invoice_unreconcile(self):
""" """
We want to test the reconcile widget for bank statements on invoices. We want to test the reconcile widget for bank statements on invoices.
@ -348,6 +429,85 @@ class TestReconciliationWidget(TestAccountReconciliationCommon):
with self.assertRaises(UserError): with self.assertRaises(UserError):
bank_stmt_line.unreconcile_bank_line() bank_stmt_line.unreconcile_bank_line()
# Testing to check functionality
def test_reconcile_invoice_to_check_reconciled(self):
"""
We want to test the reconcile widget for bank statements on invoices.
As we use edit mode by default, we will also check what happens when
we press unreconcile
"""
inv1 = self.create_invoice(
currency_id=self.currency_euro_id, invoice_amount=100
)
bank_stmt = self.acc_bank_stmt_model.create(
{
"company_id": self.env.ref("base.main_company").id,
"journal_id": self.bank_journal_euro.id,
"date": time.strftime("%Y-07-15"),
"name": "test",
}
)
bank_stmt_line = self.acc_bank_stmt_line_model.create(
{
"name": "testLine",
"journal_id": self.bank_journal_euro.id,
"statement_id": bank_stmt.id,
"amount": 100,
"date": time.strftime("%Y-07-15"),
}
)
receivable1 = inv1.line_ids.filtered(
lambda l: l.account_id.account_type == "asset_receivable"
)
with Form(
bank_stmt_line,
view="account_reconcile_oca.bank_statement_line_form_reconcile_view",
) as f:
self.assertFalse(f.can_reconcile)
f.add_account_move_line_id = receivable1
self.assertTrue(f.can_reconcile)
self.assertFalse(bank_stmt_line.is_reconciled)
self.assertFalse(bank_stmt_line.to_check)
bank_stmt_line.action_to_check()
self.assertTrue(bank_stmt_line.is_reconciled)
self.assertTrue(bank_stmt_line.to_check)
bank_stmt_line.action_checked()
self.assertTrue(bank_stmt_line.is_reconciled)
self.assertFalse(bank_stmt_line.to_check)
def test_reconcile_invoice_to_check_not_reconciled(self):
"""
We want to test the reconcile widget for bank statements on invoices.
As we use edit mode by default, we will also check what happens when
we press unreconcile
"""
bank_stmt = self.acc_bank_stmt_model.create(
{
"company_id": self.env.ref("base.main_company").id,
"journal_id": self.bank_journal_euro.id,
"date": time.strftime("%Y-07-15"),
"name": "test",
}
)
bank_stmt_line = self.acc_bank_stmt_line_model.create(
{
"name": "testLine",
"journal_id": self.bank_journal_euro.id,
"statement_id": bank_stmt.id,
"amount": 100,
"date": time.strftime("%Y-07-15"),
}
)
self.assertFalse(bank_stmt_line.is_reconciled)
self.assertFalse(bank_stmt_line.to_check)
bank_stmt_line.action_to_check()
self.assertFalse(bank_stmt_line.is_reconciled)
self.assertTrue(bank_stmt_line.to_check)
bank_stmt_line.action_checked()
self.assertFalse(bank_stmt_line.is_reconciled)
self.assertFalse(bank_stmt_line.to_check)
# Testing widget # Testing widget
def test_widget_invoice_clean(self): def test_widget_invoice_clean(self):
@ -545,10 +705,6 @@ class TestReconciliationWidget(TestAccountReconciliationCommon):
# Testing actions # Testing actions
def test_bank_statement_action_to_check(self):
action = self.bank_journal_euro.action_open_reconcile_to_check()
self.assertFalse(self.env[action["res_model"]].search(action["domain"]))
def test_bank_statement_rainbowman(self): def test_bank_statement_rainbowman(self):
message = self.bank_journal_euro.get_rainbowman_message() message = self.bank_journal_euro.get_rainbowman_message()
self.assertTrue(message) self.assertTrue(message)

View File

@ -11,6 +11,7 @@
<field name="is_reconciled" /> <field name="is_reconciled" />
<field name="currency_id" /> <field name="currency_id" />
<field name="foreign_currency_id" /> <field name="foreign_currency_id" />
<field name="to_check" />
<templates> <templates>
<t t-name="kanban-box"> <t t-name="kanban-box">
<div <div
@ -43,6 +44,12 @@
<field name="payment_ref" /> <field name="payment_ref" />
</div> </div>
<div class="col-4" style="text-align:right"> <div class="col-4" style="text-align:right">
<div
t-if="record.to_check.raw_value"
class="badge text-bg-warning"
>
To check
</div>
<div <div
t-if="record.is_reconciled.raw_value" t-if="record.is_reconciled.raw_value"
class="badge text-bg-success" class="badge text-bg-success"
@ -121,7 +128,8 @@
<button <button
name="reconcile_bank_line" name="reconcile_bank_line"
type="object" type="object"
string="Reconcile" string="Validate"
accesskey="v"
class="btn btn-primary" class="btn btn-primary"
attrs="{'invisible': ['|', ('is_reconciled', '=', True), ('can_reconcile', '=', False)]}" attrs="{'invisible': ['|', ('is_reconciled', '=', True), ('can_reconcile', '=', False)]}"
/> />
@ -135,7 +143,8 @@
<button <button
name="unreconcile_bank_line" name="unreconcile_bank_line"
type="object" type="object"
string="Unreconcile" string="Reset"
accesskey="r"
class="btn btn-warning" class="btn btn-warning"
attrs="{'invisible': [('is_reconciled', '=', False)]}" attrs="{'invisible': [('is_reconciled', '=', False)]}"
confirm="Are you sure that the move should be unreconciled?" confirm="Are you sure that the move should be unreconciled?"
@ -147,9 +156,25 @@
class="btn btn-secondary" class="btn btn-secondary"
attrs="{'invisible': [('is_reconciled', '=', True)]}" attrs="{'invisible': [('is_reconciled', '=', True)]}"
/> />
<button
name="action_to_check"
string="To Check"
class="btn btn-secondary"
accesskey="c"
type="object"
attrs="{'invisible': [('to_check', '=', True)]}"
/>
<button
name="action_checked"
string="Set as Checked"
accesskey="c"
type="object"
attrs="{'invisible': [('to_check', '=', False)]}"
/>
<button <button
name="action_show_move" name="action_show_move"
type="object" type="object"
accesskey="m"
string="View move" string="View move"
class="btn btn-info" class="btn btn-info"
/> />
@ -158,6 +183,7 @@
<field name="id" invisible="1" /> <field name="id" invisible="1" />
<field name="name" invisible="1" /> <field name="name" invisible="1" />
<field name="can_reconcile" invisible="1" /> <field name="can_reconcile" invisible="1" />
<field name="to_check" invisible="1" />
<field name="partner_id" invisible="1" /> <field name="partner_id" invisible="1" />
<field name="company_id" invisible="1" /> <field name="company_id" invisible="1" />
<field name="journal_id" invisible="1" /> <field name="journal_id" invisible="1" />
@ -208,6 +234,13 @@
string="Partner" string="Partner"
attrs="{'readonly': ['|', '|', ('manual_reference', '=', False), ('is_reconciled', '=', True), '&amp;', ('manual_line_id', '!=', False), ('manual_kind', '!=', 'liquidity')]}" attrs="{'readonly': ['|', '|', ('manual_reference', '=', False), ('is_reconciled', '=', True), '&amp;', ('manual_line_id', '!=', False), ('manual_kind', '!=', 'liquidity')]}"
/> />
<field
name="analytic_distribution"
widget="analytic_distribution"
groups="analytic.group_analytic_accounting"
options="{'account_field': 'manual_account_id', 'business_domain': 'general'}"
attrs="{'invisible': ['|', ('manual_kind', '=', 'liquidity'), ('manual_reference', '=', False)], 'readonly': [('is_reconciled', '=', True)]}"
/>
</group> </group>
<group> <group>
<field <field
@ -220,6 +253,35 @@
string="Amount" string="Amount"
attrs="{'readonly': ['|', ('manual_reference', '=', False), ('is_reconciled', '=', True)]}" attrs="{'readonly': ['|', ('manual_reference', '=', False), ('is_reconciled', '=', True)]}"
/> />
<field name="manual_original_amount" invisible="1" />
<field name="manual_move_type" invisible="1" />
<label
for="manual_move_id"
string=""
attrs="{'invisible': ['|', ('manual_move_type', 'not in', ['in_invoice', 'in_refund', 'out_invoice', 'out_refund']), ('manual_original_amount', '=', 0)]}"
/>
<div
attrs="{'invisible': ['|', ('manual_move_type', 'not in', ['in_invoice', 'in_refund', 'out_invoice', 'out_refund']), ('manual_original_amount', '=', 0)]}"
>
Invoice <field
class="oe_inline"
name="manual_move_id"
/>
with an open amount <field
class="oe_inline"
name="manual_original_amount"
/> will be reduced by <field
class="oe_inline"
name="manual_amount"
readonly="1"
/>.
<br />
You might want to set the invoice as <button
name="button_manual_reference_full_paid"
type="object"
method_args="[1]"
>fully paid</button>.
</div>
</group> </group>
</group> </group>
</page> </page>
@ -268,6 +330,28 @@
</p> </p>
</field> </field>
</record> </record>
<record
id="action_bank_statement_line_reconcile_to_check"
model="ir.actions.act_window"
>
<field name="name">Reconcile bank statement lines</field>
<field name="res_model">account.bank.statement.line</field>
<field name="domain">[('journal_id', '=', active_id)]</field>
<field
name="context"
>{'default_journal_id': active_id, 'search_default_to_check': True, 'view_ref': 'account_reconcile_oca.bank_statement_line_form_reconcile_view'}</field>
<field name="view_mode">tree</field>
<field
name="view_ids"
eval="[(5, 0, 0),
(0, 0, {'view_mode': 'tree', 'view_id': ref('bank_statement_line_reconcile_view')})]"
/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Nothing to check
</p>
</field>
</record>
<record <record
id="action_bank_statement_line_move_view_reconcile" id="action_bank_statement_line_move_view_reconcile"

View File

@ -44,7 +44,10 @@
<t t-if="dashboard.number_to_check > 0"> <t t-if="dashboard.number_to_check > 0">
<div class="row"> <div class="row">
<div class="col overflow-hidden text-left"> <div class="col overflow-hidden text-left">
<a type="object" name="action_open_reconcile_to_check"> <a
type="action"
name="%(account_reconcile_oca.action_bank_statement_line_reconcile_to_check)s"
>
<t t-esc="dashboard.number_to_check" /> to check</a> <t t-esc="dashboard.number_to_check" /> to check</a>
</div> </div>
<div class="col-auto text-right"> <div class="col-auto text-right">

View File

@ -117,6 +117,12 @@
domain="[('is_reconciled', '=', False)]" domain="[('is_reconciled', '=', False)]"
/> />
<separator /> <separator />
<filter
name="to_check"
string="To check"
domain="[('to_check', '=', True)]"
/>
<separator />
<filter name="date" string="Date" date="date" /> <filter name="date" string="Date" date="date" />
<group name="groupby"> <group name="groupby">
<filter <filter