From 43a019125335b219ca3568aa68018937ba4ff3a2 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Tue, 1 Nov 2022 20:26:19 +0100 Subject: [PATCH 01/94] [ADD] account_reconcile_oca --- account_reconcile_oca/README.rst | 96 +++ account_reconcile_oca/__init__.py | 2 + account_reconcile_oca/__manifest__.py | 45 ++ account_reconcile_oca/demo/demo.xml | 7 + account_reconcile_oca/hooks.py | 8 + account_reconcile_oca/models/__init__.py | 4 + .../models/account_account_reconcile.py | 171 +++++ .../models/account_bank_statement_line.py | 574 ++++++++++++++ .../models/account_journal.py | 28 + .../models/account_reconcile_abstract.py | 64 ++ account_reconcile_oca/readme/CONTRIBUTORS.rst | 1 + account_reconcile_oca/readme/DESCRIPTION.rst | 1 + account_reconcile_oca/readme/ROADMAP.rst | 3 + account_reconcile_oca/readme/USAGE.rst | 12 + .../security/ir.model.access.csv | 3 + .../static/description/icon.png | Bin 0 -> 9455 bytes .../static/description/index.html | 446 +++++++++++ .../src/js/reconcile_chatter_field.esm.js | 15 + .../static/src/js/reconcile_controller.esm.js | 99 +++ .../src/js/reconcile_data_widget.esm.js | 65 ++ .../static/src/js/reconcile_form_view.esm.js | 51 ++ .../src/js/reconcile_kanban_record.esm.js | 14 + .../src/js/reconcile_manual_view.esm.js | 38 + .../src/js/reconcile_move_line_view.esm.js | 46 ++ .../src/js/reconcile_move_line_widget.esm.js | 51 ++ .../static/src/js/reconcile_renderer.esm.js | 12 + .../static/src/js/reconcile_view.esm.js | 16 + .../src/js/selection_badge_uncheck.esm.js | 29 + .../static/src/scss/reconcile.scss | 72 ++ .../static/src/xml/reconcile.xml | 149 ++++ account_reconcile_oca/tests/__init__.py | 2 + .../tests/test_account_reconcile.py | 304 ++++++++ .../tests/test_bank_account_reconcile.py | 706 ++++++++++++++++++ .../views/account_account.xml | 23 + .../views/account_account_reconcile.xml | 165 ++++ .../views/account_bank_statement_line.xml | 326 ++++++++ .../views/account_journal.xml | 70 ++ account_reconcile_oca/views/account_move.xml | 26 + .../views/account_move_line.xml | 98 +++ 39 files changed, 3842 insertions(+) create mode 100644 account_reconcile_oca/README.rst create mode 100644 account_reconcile_oca/__init__.py create mode 100644 account_reconcile_oca/__manifest__.py create mode 100644 account_reconcile_oca/demo/demo.xml create mode 100644 account_reconcile_oca/hooks.py create mode 100644 account_reconcile_oca/models/__init__.py create mode 100644 account_reconcile_oca/models/account_account_reconcile.py create mode 100644 account_reconcile_oca/models/account_bank_statement_line.py create mode 100644 account_reconcile_oca/models/account_journal.py create mode 100644 account_reconcile_oca/models/account_reconcile_abstract.py create mode 100644 account_reconcile_oca/readme/CONTRIBUTORS.rst create mode 100644 account_reconcile_oca/readme/DESCRIPTION.rst create mode 100644 account_reconcile_oca/readme/ROADMAP.rst create mode 100644 account_reconcile_oca/readme/USAGE.rst create mode 100644 account_reconcile_oca/security/ir.model.access.csv create mode 100644 account_reconcile_oca/static/description/icon.png create mode 100644 account_reconcile_oca/static/description/index.html create mode 100644 account_reconcile_oca/static/src/js/reconcile_chatter_field.esm.js create mode 100644 account_reconcile_oca/static/src/js/reconcile_controller.esm.js create mode 100644 account_reconcile_oca/static/src/js/reconcile_data_widget.esm.js create mode 100644 account_reconcile_oca/static/src/js/reconcile_form_view.esm.js create mode 100644 account_reconcile_oca/static/src/js/reconcile_kanban_record.esm.js create mode 100644 account_reconcile_oca/static/src/js/reconcile_manual_view.esm.js create mode 100644 account_reconcile_oca/static/src/js/reconcile_move_line_view.esm.js create mode 100644 account_reconcile_oca/static/src/js/reconcile_move_line_widget.esm.js create mode 100644 account_reconcile_oca/static/src/js/reconcile_renderer.esm.js create mode 100644 account_reconcile_oca/static/src/js/reconcile_view.esm.js create mode 100644 account_reconcile_oca/static/src/js/selection_badge_uncheck.esm.js create mode 100644 account_reconcile_oca/static/src/scss/reconcile.scss create mode 100644 account_reconcile_oca/static/src/xml/reconcile.xml create mode 100644 account_reconcile_oca/tests/__init__.py create mode 100644 account_reconcile_oca/tests/test_account_reconcile.py create mode 100644 account_reconcile_oca/tests/test_bank_account_reconcile.py create mode 100644 account_reconcile_oca/views/account_account.xml create mode 100644 account_reconcile_oca/views/account_account_reconcile.xml create mode 100644 account_reconcile_oca/views/account_bank_statement_line.xml create mode 100644 account_reconcile_oca/views/account_journal.xml create mode 100644 account_reconcile_oca/views/account_move.xml create mode 100644 account_reconcile_oca/views/account_move_line.xml diff --git a/account_reconcile_oca/README.rst b/account_reconcile_oca/README.rst new file mode 100644 index 00000000..9ec60fd9 --- /dev/null +++ b/account_reconcile_oca/README.rst @@ -0,0 +1,96 @@ +===================== +Account Reconcile Oca +===================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Faccount--reconcile-lightgray.png?logo=github + :target: https://github.com/OCA/account-reconcile/tree/16.0/account_reconcile_oca + :alt: OCA/account-reconcile +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/account-reconcile-16-0/account-reconcile-16-0-account_reconcile_oca + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/98/16.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This addon allows to reconcile bank statements and account marked as `reconcile`. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +Bank reconcile +~~~~~~~~~~~~~~ + +Access `Invoicing / Dashboard` with a user with Full Acounting capabilities. +Select reconcile on the journal of your choice. + +Account reconcile +~~~~~~~~~~~~~~~~~ + +Access `Invoicing / Accounting / Actions / Reconcile` +All the possible reconcile options will show and you will be able to reconcile properly. +You can access the same widget from accounts and Partners. + +Known issues / Roadmap +====================== + +The following bugs are already detected: + +* Creation of activities on the chatter do show automatically + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* CreuBlanca + +Contributors +~~~~~~~~~~~~ + +* Enric Tobella + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/account-reconcile `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/account_reconcile_oca/__init__.py b/account_reconcile_oca/__init__.py new file mode 100644 index 00000000..cc6b6354 --- /dev/null +++ b/account_reconcile_oca/__init__.py @@ -0,0 +1,2 @@ +from . import models +from .hooks import post_init_hook diff --git a/account_reconcile_oca/__manifest__.py b/account_reconcile_oca/__manifest__.py new file mode 100644 index 00000000..b5f8beb4 --- /dev/null +++ b/account_reconcile_oca/__manifest__.py @@ -0,0 +1,45 @@ +# Copyright 2022 CreuBlanca +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Account Reconcile Oca", + "summary": """ + Reconcile addons for Odoo CE accounting""", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "author": "CreuBlanca,Odoo Community Association (OCA)", + "maintainers": ["etobella"], + "website": "https://github.com/OCA/account-reconcile", + "depends": [ + "account", + "base_sparse_field", + ], + "data": [ + "security/ir.model.access.csv", + "views/account_account_reconcile.xml", + "views/account_bank_statement_line.xml", + "views/account_move_line.xml", + "views/account_journal.xml", + "views/account_move.xml", + "views/account_account.xml", + ], + "demo": ["demo/demo.xml"], + "post_init_hook": "post_init_hook", + "assets": { + "web.assets_backend": [ + "account_reconcile_oca/static/src/js/reconcile_manual_view.esm.js", + "account_reconcile_oca/static/src/js/reconcile_data_widget.esm.js", + "account_reconcile_oca/static/src/js/reconcile_chatter_field.esm.js", + "account_reconcile_oca/static/src/js/selection_badge_uncheck.esm.js", + "account_reconcile_oca/static/src/js/reconcile_move_line_view.esm.js", + "account_reconcile_oca/static/src/js/reconcile_move_line_widget.esm.js", + "account_reconcile_oca/static/src/js/reconcile_kanban_record.esm.js", + "account_reconcile_oca/static/src/js/reconcile_renderer.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/scss/reconcile.scss", + ], + }, +} diff --git a/account_reconcile_oca/demo/demo.xml b/account_reconcile_oca/demo/demo.xml new file mode 100644 index 00000000..a27a51e8 --- /dev/null +++ b/account_reconcile_oca/demo/demo.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/account_reconcile_oca/hooks.py b/account_reconcile_oca/hooks.py new file mode 100644 index 00000000..dd1094eb --- /dev/null +++ b/account_reconcile_oca/hooks.py @@ -0,0 +1,8 @@ +def post_init_hook(cr, registry): + cr.execute( + """ + UPDATE account_bank_statement_line + SET reconcile_mode = 'edit' + WHERE is_reconciled + """ + ) diff --git a/account_reconcile_oca/models/__init__.py b/account_reconcile_oca/models/__init__.py new file mode 100644 index 00000000..8102733a --- /dev/null +++ b/account_reconcile_oca/models/__init__.py @@ -0,0 +1,4 @@ +from . import account_reconcile_abstract +from . import account_journal +from . import account_bank_statement_line +from . import account_account_reconcile diff --git a/account_reconcile_oca/models/account_account_reconcile.py b/account_reconcile_oca/models/account_account_reconcile.py new file mode 100644 index 00000000..6a33d786 --- /dev/null +++ b/account_reconcile_oca/models/account_account_reconcile.py @@ -0,0 +1,171 @@ +# Copyright 2022 CreuBlanca +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class CharId(fields.Id): + type = "string" + column_type = ("varchar", fields.pg_varchar()) + + +class AccountAccountReconcile(models.Model): + _name = "account.account.reconcile" + _description = "Account Account Reconcile" + _inherit = "account.reconcile.abstract" + _auto = False + + reconcile_data_info = fields.Serialized(inverse="_inverse_reconcile_data_info") + + partner_id = fields.Many2one("res.partner") + account_id = fields.Many2one("account.account") + name = fields.Char() + is_reconciled = fields.Boolean() + currency_id = fields.Many2one("res.currency") + + @property + def _table_query(self): + return "%s %s %s %s %s" % ( + self._select(), + self._from(), + self._where(), + self._groupby(), + self._having(), + ) + + def _select(self): + return """ + SELECT + CAST( + ( + coalesce(aml.partner_id, 0) + a.id + )*( + COALESCE(aml.partner_id, 0)+a.id - 1 + )/2 + COALESCE(aml.partner_id, 0) AS INTEGER + ) as id, + MAX(a.name) as name, + aml.partner_id, + a.id as account_id, + FALSE as is_reconciled, + aml.currency_id as currency_id, + a.company_id + """ + + def _from(self): + return """ + FROM + account_account a + INNER JOIN account_move_line aml ON aml.account_id = a.id + INNER JOIN account_move am ON am.id = aml.move_id + """ + + def _where(self): + return """ + WHERE a.reconcile + AND am.state = 'posted' + AND aml.amount_residual != 0 + """ + + def _groupby(self): + return """ + GROUP BY + a.id, aml.partner_id, aml.currency_id, a.company_id + """ + + def _having(self): + return """ + HAVING + SUM(aml.debit) > 0 + AND SUM(aml.credit) > 0 + """ + + def _compute_reconcile_data_info(self): + data_obj = self.env["account.account.reconcile.data"] + for record in self: + data_record = data_obj.search( + [("user_id", "=", self.env.user.id), ("reconcile_id", "=", record.id)] + ) + if data_record: + record.reconcile_data_info = data_record.data + else: + record.reconcile_data_info = {"data": [], "counterparts": []} + + def _inverse_reconcile_data_info(self): + data_obj = self.env["account.account.reconcile.data"] + for record in self: + data_record = data_obj.search( + [("user_id", "=", self.env.user.id), ("reconcile_id", "=", record.id)] + ) + if data_record: + data_record.data = record.reconcile_data_info + else: + data_obj.create( + { + "reconcile_id": record.id, + "user_id": self.env.user.id, + "data": record.reconcile_data_info, + } + ) + + @api.onchange("add_account_move_line_id") + def _onchange_add_account_move_line(self): + if self.add_account_move_line_id: + data = self.reconcile_data_info + if self.add_account_move_line_id.id not in data["counterparts"]: + data["counterparts"].append(self.add_account_move_line_id.id) + else: + del data["counterparts"][ + data["counterparts"].index(self.add_account_move_line_id.id) + ] + self.reconcile_data_info = self._recompute_data(data) + self.add_account_move_line_id = False + + @api.onchange("manual_reference", "manual_delete") + def _onchange_manual_reconcile_reference(self): + self.ensure_one() + data = self.reconcile_data_info + counterparts = [] + for line in data["data"]: + if line["reference"] == self.manual_reference: + if self.manual_delete: + continue + counterparts.append(line["id"]) + data["counterparts"] = counterparts + self.reconcile_data_info = self._recompute_data(data) + self.manual_delete = False + self.manual_reference = False + + def _recompute_data(self, data): + new_data = {"data": [], "counterparts": data["counterparts"]} + counterparts = data["counterparts"] + amount = 0.0 + for line_id in counterparts: + line = self._get_reconcile_line( + self.env["account.move.line"].browse(line_id), "other", True, amount + ) + new_data["data"].append(line) + amount += line["amount"] + return new_data + + def clean_reconcile(self): + self.ensure_one() + self.reconcile_data_info = {"data": [], "counterparts": []} + + def reconcile(self): + lines = self.env["account.move.line"].browse( + self.reconcile_data_info["counterparts"] + ) + lines.reconcile() + data_record = self.env["account.account.reconcile.data"].search( + [("user_id", "=", self.env.user.id), ("reconcile_id", "=", self.id)] + ) + data_record.unlink() + + +class AccountAccountReconcileData(models.TransientModel): + _name = "account.account.reconcile.data" + _description = "Reconcile data model to store user info" + + user_id = fields.Many2one("res.users", required=True) + reconcile_id = fields.Integer(required=True) + data = fields.Serialized() diff --git a/account_reconcile_oca/models/account_bank_statement_line.py b/account_reconcile_oca/models/account_bank_statement_line.py new file mode 100644 index 00000000..c7c90e4c --- /dev/null +++ b/account_reconcile_oca/models/account_bank_statement_line.py @@ -0,0 +1,574 @@ +# Copyright 2022 CreuBlanca +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from collections import defaultdict + +from odoo import Command, _, api, fields, models +from odoo.exceptions import UserError +from odoo.tools import float_is_zero + + +class AccountBankStatementLine(models.Model): + _name = "account.bank.statement.line" + _inherit = ["account.bank.statement.line", "account.reconcile.abstract"] + + reconcile_data_info = fields.Serialized(inverse="_inverse_reconcile_data_info") + reconcile_mode = fields.Selection( + selection=lambda self: self.env["account.journal"] + ._fields["reconcile_mode"] + .selection + ) + company_id = fields.Many2one(related="journal_id.company_id") + reconcile_data = fields.Serialized() + manual_line_id = fields.Many2one( + "account.move.line", + store=False, + default=False, + prefetch=False, + ) + manual_kind = fields.Char( + store=False, + default=False, + prefetch=False, + ) + manual_account_id = fields.Many2one( + "account.account", + check_company=True, + store=False, + default=False, + prefetch=False, + ) + manual_partner_id = fields.Many2one( + "res.partner", + check_company=True, + store=False, + default=False, + prefetch=False, + ) + manual_model_id = fields.Many2one( + "account.reconcile.model", + check_company=True, + store=False, + default=False, + prefetch=False, + 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_amount = fields.Monetary(store=False, default=False, prefetch=False) + can_reconcile = fields.Boolean(sparse="reconcile_data_info") + + def save(self): + return {"type": "ir.actions.act_window_close"} + + @api.model + def action_new_line(self): + action = self.env["ir.actions.act_window"]._for_xml_id( + "account_reconcile_oca.action_bank_statement_line_create" + ) + action["context"] = self.env.context + return action + + @api.onchange("manual_model_id") + def _onchange_manual_model_id(self): + if self.manual_model_id: + data = [] + for line in self.reconcile_data_info.get("data", []): + if line.get("kind") != "suspense": + data.append(line) + self.reconcile_data_info = self._recompute_suspense_line( + *self._reconcile_data_by_model( + data, + self.manual_model_id, + self.reconcile_data_info["reconcile_auxiliary_id"], + ) + ) + else: + # Refreshing data + self.reconcile_data_info = self.browse( + self.id.origin + )._default_reconcile_data() + self.can_reconcile = self.reconcile_data_info.get("can_reconcile", False) + + @api.onchange("add_account_move_line_id") + def _onchange_add_account_move_line_id(self): + if self.add_account_move_line_id: + data = self.reconcile_data_info["data"] + new_data = [] + is_new_line = True + pending_amount = 0.0 + for line in data: + if line["kind"] != "suspense": + pending_amount += line["amount"] + if line.get("counterpart_line_id") == self.add_account_move_line_id.id: + is_new_line = False + else: + new_data.append(line) + if is_new_line: + new_data.append( + self._get_reconcile_line( + self.add_account_move_line_id, "other", True, pending_amount + ) + ) + self.reconcile_data_info = self._recompute_suspense_line( + new_data, self.reconcile_data_info["reconcile_auxiliary_id"] + ) + self.can_reconcile = self.reconcile_data_info.get("can_reconcile", False) + self.add_account_move_line_id = False + + def _recompute_suspense_line(self, data, reconcile_auxiliary_id): + can_reconcile = True + total_amount = 0 + new_data = [] + suspense_line = False + counterparts = [] + for line in data: + if line.get("counterpart_line_id"): + counterparts.append(line["counterpart_line_id"]) + if ( + line["account_id"][0] == self.journal_id.suspense_account_id.id + or not line["account_id"][0] + ) and line["kind"] != "suspense": + can_reconcile = False + if line["kind"] != "suspense": + new_data.append(line) + total_amount += line["amount"] + else: + suspense_line = line + if not float_is_zero( + total_amount, precision_digits=self.currency_id.decimal_places + ): + can_reconcile = False + if suspense_line: + suspense_line.update( + { + "amount": -total_amount, + "credit": total_amount if total_amount > 0 else 0.0, + "debit": -total_amount if total_amount < 0 else 0.0, + } + ) + else: + suspense_line = { + "reference": "reconcile_auxiliary;%s" % reconcile_auxiliary_id, + "id": False, + "account_id": self.journal_id.suspense_account_id.name_get()[0], + "partner_id": self.partner_id + and self.partner_id.name_get()[0] + or False, + "date": fields.Date.to_string(self.date), + "name": self.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": "suspense", + "currency_id": self.currency_id.id, + } + reconcile_auxiliary_id += 1 + new_data.append(suspense_line) + return { + "data": new_data, + "counterparts": counterparts, + "reconcile_auxiliary_id": reconcile_auxiliary_id, + "can_reconcile": can_reconcile, + } + + def _check_line_changed(self, line): + return ( + not float_is_zero( + self.manual_amount - line["amount"], + precision_digits=self.currency_id.decimal_places, + ) + or self.manual_account_id.id != line["account_id"][0] + or self.manual_name != line["name"] + or ( + self.manual_partner_id and self.manual_partner_id.name_get()[0] or False + ) + != line.get("partner_id") + ) + + @api.onchange("manual_reference", "manual_delete") + def _onchange_manual_reconcile_reference(self): + self.ensure_one() + data = self.reconcile_data_info.get("data", []) + new_data = [] + for line in data: + if line["reference"] == self.manual_reference: + if self.manual_delete: + self.update( + { + "manual_delete": False, + "manual_reference": False, + "manual_account_id": False, + "manual_amount": False, + "manual_name": False, + "manual_partner_id": False, + "manual_line_id": False, + "manual_kind": False, + } + ) + continue + else: + self.manual_account_id = line["account_id"][0] + self.manual_amount = line["amount"] + self.manual_name = line["name"] + self.manual_partner_id = ( + line.get("partner_id") and line["partner_id"][0] + ) + self.manual_line_id = line["id"] + self.manual_kind = line["kind"] + new_data.append(line) + self.reconcile_data_info = self._recompute_suspense_line( + new_data, self.reconcile_data_info["reconcile_auxiliary_id"] + ) + self.can_reconcile = self.reconcile_data_info.get("can_reconcile", False) + + @api.onchange( + "manual_account_id", + "manual_partner_id", + "manual_name", + "manual_amount", + ) + def _onchange_manual_reconcile_vals(self): + self.ensure_one() + data = self.reconcile_data_info.get("data", []) + new_data = [] + for line in data: + if line["reference"] == self.manual_reference: + if self._check_line_changed(line): + line.update( + { + "name": self.manual_name, + "partner_id": self.manual_partner_id + and self.manual_partner_id.name_get()[0] + or False, + "account_id": self.manual_account_id.name_get()[0] + if self.manual_account_id + else [False, _("Undefined")], + "amount": self.manual_amount, + "credit": -self.manual_amount + if self.manual_amount < 0 + else 0.0, + "debit": self.manual_amount + if self.manual_amount > 0 + else 0.0, + "kind": line["kind"] + if line["kind"] != "suspense" + else "other", + } + ) + if line["kind"] == "liquidity": + self._update_move_partner() + new_data.append(line) + self.reconcile_data_info = self._recompute_suspense_line( + new_data, self.reconcile_data_info["reconcile_auxiliary_id"] + ) + self.can_reconcile = self.reconcile_data_info.get("can_reconcile", False) + + def _update_move_partner(self): + if self.partner_id == self.manual_partner_id: + return + self.partner_id = self.manual_partner_id + + @api.depends("reconcile_data") + def _compute_reconcile_data_info(self): + for record in self: + if record.reconcile_data: + record.reconcile_data_info = record.reconcile_data + else: + record.reconcile_data_info = record._default_reconcile_data() + record.can_reconcile = record.reconcile_data_info.get( + "can_reconcile", False + ) + + def action_show_move(self): + self.ensure_one() + action = self.env["ir.actions.act_window"]._for_xml_id( + "account.action_move_journal_line" + ) + action.update( + {"res_id": self.move_id.id, "views": [[False, "form"]], "view_mode": "form"} + ) + return action + + def _inverse_reconcile_data_info(self): + for record in self: + record.reconcile_data = record.reconcile_data_info + + def _reconcile_data_by_model(self, data, reconcile_model, reconcile_auxiliary_id): + new_data = [] + liquidity_amount = 0.0 + for line_data in data: + if line_data["kind"] == "suspense": + continue + new_data.append(line_data) + liquidity_amount += line_data["amount"] + for line in reconcile_model._apply_lines_for_bank_widget( + -liquidity_amount, self._retrieve_partner(), self + ): + amount = line["amount_currency"] + new_line = { + "reference": "reconcile_auxiliary;%s" % reconcile_auxiliary_id, + "id": False, + "amount": amount, + "debit": amount if amount > 0 else 0.0, + "credit": -amount if amount < 0 else 0.0, + "kind": "other", + "account_id": self.env["account.account"] + .browse(line["account_id"]) + .name_get()[0], + "date": fields.Date.to_string(self.date), + "name": line.get("name"), + "currency_id": line.get("currency_id"), + } + reconcile_auxiliary_id += 1 + if line.get("partner_id"): + new_line["partner_id"] = ( + self.env["res.partner"].browse(line["partner_id"]).name_get()[0] + ) + new_data.append(new_line) + return new_data, reconcile_auxiliary_id + + def _compute_exchange_rate(self, data): + reconcile_auxiliary_id = 1 + if not self.foreign_currency_id or self.is_reconciled: + return reconcile_auxiliary_id + currency = self.journal_id.currency_id or self.company_id.currency_id + currency_amount = self.foreign_currency_id._convert( + self.amount_currency, currency, self.company_id, self.date + ) + amount = sum(d["amount"] for d in data) - currency_amount + if not currency.is_zero(amount): + account = self.company_id.expense_currency_exchange_account_id + if amount > 0: + account = self.company_id.income_currency_exchange_account_id + data.append( + { + "reference": "reconcile_auxiliary;%s" % reconcile_auxiliary_id, + "id": False, + "account_id": account.name_get()[0], + "partner_id": False, + "date": fields.Date.to_string(self.date), + "name": self.name, + "amount": -amount, + "credit": amount if amount > 0 else 0.0, + "debit": -amount if amount < 0 else 0.0, + "kind": "other", + "currency_id": self.currency_id.id, + } + ) + reconcile_auxiliary_id += 1 + return reconcile_auxiliary_id + + def _default_reconcile_data(self): + liquidity_lines, suspense_lines, other_lines = self._seek_for_lines() + data = [self._get_reconcile_line(line, "liquidity") for line in liquidity_lines] + reconcile_auxiliary_id = self._compute_exchange_rate(data) + res = ( + self.env["account.reconcile.model"] + .search([("rule_type", "in", ["invoice_matching", "writeoff_suggestion"])]) + ._apply_rules(self, self._retrieve_partner()) + ) + if res and res.get("status", "") == "write_off": + return self._recompute_suspense_line( + *self._reconcile_data_by_model( + data, res["model"], reconcile_auxiliary_id + ) + ) + elif res and res.get("amls"): + amount = self.amount + for line in res.get("amls", []): + line_data = self._get_reconcile_line( + line, "other", is_counterpart=True, max_amount=amount + ) + amount -= line_data.get("amount") + data.append(line_data) + return self._recompute_suspense_line(data, reconcile_auxiliary_id) + return self._recompute_suspense_line( + data + [self._get_reconcile_line(line, "other") for line in other_lines], + reconcile_auxiliary_id, + ) + + def clean_reconcile(self): + self.reconcile_data_info = self._default_reconcile_data() + self.can_reconcile = self.reconcile_data_info.get("can_reconcile", False) + + def reconcile_bank_line(self): + self.ensure_one() + self.reconcile_mode = self.journal_id.reconcile_mode + return getattr(self, "_reconcile_bank_line_%s" % self.reconcile_mode)( + self.reconcile_data_info["data"] + ) + + def _reconcile_bank_line_edit(self, data): + _liquidity_lines, suspense_lines, other_lines = self._seek_for_lines() + lines_to_remove = [(2, line.id) for line in suspense_lines + other_lines] + + # Cleanup previous lines. + move = self.move_id + container = {"records": move, "self": move} + to_reconcile = [] + with move._check_balanced(container): + move.with_context( + skip_account_move_synchronization=True, force_delete=True + ).write( + { + "line_ids": lines_to_remove, + } + ) + for line_vals in data: + if line_vals["kind"] == "liquidity": + continue + line = ( + self.env["account.move.line"] + .with_context(check_move_validity=False) + .create(self._reconcile_move_line_vals(line_vals)) + ) + if line_vals.get("counterpart_line_id"): + to_reconcile.append( + self.env["account.move.line"].browse( + line_vals.get("counterpart_line_id") + ) + + line + ) + for reconcile_items in to_reconcile: + reconcile_items.reconcile() + + def _reconcile_bank_line_keep_move_vals(self): + return { + "journal_id": self.journal_id.id, + } + + def _reconcile_bank_line_keep(self, data): + move = ( + self.env["account.move"] + .with_context(skip_invoice_sync=True) + .create(self._reconcile_bank_line_keep_move_vals()) + ) + _liquidity_lines, suspense_lines, other_lines = self._seek_for_lines() + container = {"records": move, "self": move} + to_reconcile = defaultdict(lambda: self.env["account.move.line"]) + with move._check_balanced(container): + for line in suspense_lines | other_lines: + to_reconcile[line.account_id.id] |= line + line_data = line.with_context( + active_test=False, + include_business_fields=True, + ).copy_data({"move_id": move.id})[0] + to_reconcile[line.account_id.id] |= ( + self.env["account.move.line"] + .with_context(check_move_validity=False, skip_invoice_sync=True) + .create(line_data) + ) + move.write( + { + "line_ids": [ + Command.update( + line.id, + { + "balance": -line.balance, + "amount_currency": -line.amount_currency, + }, + ) + for line in move.line_ids + if line.move_id.move_type == "entry" + or line.display_type == "cogs" + ] + } + ) + for line_vals in data: + if line_vals["kind"] == "liquidity": + continue + if line_vals["kind"] == "suspense": + raise UserError(_("No supense lines are allowed when reconciling")) + line = ( + self.env["account.move.line"] + .with_context(check_move_validity=False, skip_invoice_sync=True) + .create(self._reconcile_move_line_vals(line_vals, move.id)) + ) + if line_vals.get("counterpart_line_id") and line.account_id.reconcile: + to_reconcile[line.account_id.id] |= ( + self.env["account.move.line"].browse( + line_vals.get("counterpart_line_id") + ) + | line + ) + move.invalidate_recordset() + move._post() + for _account, lines in to_reconcile.items(): + lines.reconcile() + + def unreconcile_bank_line(self): + self.ensure_one() + return getattr(self, "_unreconcile_bank_line_%s" % self.reconcile_mode)( + self.reconcile_data_info["data"] + ) + + def _unreconcile_bank_line_edit(self, data): + self.move_id.button_draft() + 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): + raise UserError(_("Keep suspense move lines mode cannot be unreconciled")) + + def _reconcile_move_line_vals(self, line, move_id=False): + return { + "move_id": move_id or self.move_id.id, + "account_id": line["account_id"][0], + "partner_id": line.get("partner_id") and line["partner_id"][0], + "credit": line["credit"], + "debit": line["debit"], + } + + @api.model_create_multi + def create(self, mvals): + result = super().create(mvals) + models = self.env["account.reconcile.model"].search( + [ + ("rule_type", "in", ["invoice_matching", "writeoff_suggestion"]), + ("auto_reconcile", "=", True), + ] + ) + for record in result: + res = models._apply_rules(record, record._retrieve_partner()) + if not res: + continue + liquidity_lines, suspense_lines, other_lines = record._seek_for_lines() + data = [ + record._get_reconcile_line(line, "liquidity") + for line in liquidity_lines + ] + reconcile_auxiliary_id = record._compute_exchange_rate(data) + if res.get("status", "") == "write_off": + data = record._recompute_suspense_line( + *record._reconcile_data_by_model( + data, res["model"], reconcile_auxiliary_id + ) + ) + elif res.get("amls"): + amount = self.amount + for line in res.get("amls", []): + line_data = record._get_reconcile_line( + line, "other", is_counterpart=True, max_amount=amount + ) + amount -= line_data.get("amount") + data.append(line_data) + data = record._recompute_suspense_line(data, reconcile_auxiliary_id) + if not data.get("can_reconcile"): + continue + getattr( + record, "_reconcile_bank_line_%s" % record.journal_id.reconcile_mode + )(data["data"]) + return result diff --git a/account_reconcile_oca/models/account_journal.py b/account_reconcile_oca/models/account_journal.py new file mode 100644 index 00000000..56e3bf34 --- /dev/null +++ b/account_reconcile_oca/models/account_journal.py @@ -0,0 +1,28 @@ +# Copyright 2022 CreuBlanca +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, fields, models + + +class AccountJournal(models.Model): + _inherit = "account.journal" + + reconcile_mode = fields.Selection( + [("edit", "Edit Move"), ("keep", "Keep Suspense Accounts")], + default="edit", + 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): + self.ensure_one() + if self.get_journal_dashboard_datas()["number_to_reconcile"] > 0: + return False + return _("Well done! Everything has been reconciled") diff --git a/account_reconcile_oca/models/account_reconcile_abstract.py b/account_reconcile_oca/models/account_reconcile_abstract.py new file mode 100644 index 00000000..659ae5c8 --- /dev/null +++ b/account_reconcile_oca/models/account_reconcile_abstract.py @@ -0,0 +1,64 @@ +# Copyright 2022 CreuBlanca +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models +from odoo.tools import float_is_zero + + +class AccountReconcileAbstract(models.AbstractModel): + _name = "account.reconcile.abstract" + _description = "Account Reconcile Abstract" + + reconcile_data_info = fields.Serialized( + compute="_compute_reconcile_data_info", + prefetch=False, + ) + company_id = fields.Many2one("res.company") + add_account_move_line_id = fields.Many2one( + "account.move.line", + check_company=True, + store=False, + default=False, + prefetch=False, + ) + manual_reference = fields.Char(store=False, default=False, prefetch=False) + manual_delete = fields.Boolean( + store=False, + default=False, + prefetch=False, + ) + + def _get_reconcile_line(self, line, kind, is_counterpart=False, max_amount=False): + original_amount = amount = line.debit - line.credit + if is_counterpart: + original_amount = amount = ( + line.amount_residual_currency or line.amount_residual + ) + if max_amount: + if amount > max_amount > 0: + amount = max_amount + if amount < max_amount < 0: + amount = max_amount + if is_counterpart: + amount = -amount + original_amount = -original_amount + vals = { + "reference": "account.move.line;%s" % line.id, + "id": line.id, + "account_id": line.account_id.name_get()[0], + "partner_id": line.partner_id and line.partner_id.name_get()[0] or False, + "date": fields.Date.to_string(line.date), + "name": line.name, + "debit": amount if amount > 0 else 0.0, + "credit": -amount if amount < 0 else 0.0, + "amount": amount, + "currency_id": line.currency_id.id, + "kind": kind, + } + if not float_is_zero( + amount - original_amount, precision_digits=line.currency_id.decimal_places + ): + vals["original_amount"] = abs(original_amount) + if is_counterpart: + vals["counterpart_line_id"] = line.id + return vals diff --git a/account_reconcile_oca/readme/CONTRIBUTORS.rst b/account_reconcile_oca/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..85004765 --- /dev/null +++ b/account_reconcile_oca/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Enric Tobella diff --git a/account_reconcile_oca/readme/DESCRIPTION.rst b/account_reconcile_oca/readme/DESCRIPTION.rst new file mode 100644 index 00000000..e50452fb --- /dev/null +++ b/account_reconcile_oca/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This addon allows to reconcile bank statements and account marked as `reconcile`. diff --git a/account_reconcile_oca/readme/ROADMAP.rst b/account_reconcile_oca/readme/ROADMAP.rst new file mode 100644 index 00000000..24e9bc53 --- /dev/null +++ b/account_reconcile_oca/readme/ROADMAP.rst @@ -0,0 +1,3 @@ +The following bugs are already detected: + +* Creation of activities on the chatter do show automatically diff --git a/account_reconcile_oca/readme/USAGE.rst b/account_reconcile_oca/readme/USAGE.rst new file mode 100644 index 00000000..fc080e09 --- /dev/null +++ b/account_reconcile_oca/readme/USAGE.rst @@ -0,0 +1,12 @@ +Bank reconcile +~~~~~~~~~~~~~~ + +Access `Invoicing / Dashboard` with a user with Full Acounting capabilities. +Select reconcile on the journal of your choice. + +Account reconcile +~~~~~~~~~~~~~~~~~ + +Access `Invoicing / Accounting / Actions / Reconcile` +All the possible reconcile options will show and you will be able to reconcile properly. +You can access the same widget from accounts and Partners. diff --git a/account_reconcile_oca/security/ir.model.access.csv b/account_reconcile_oca/security/ir.model.access.csv new file mode 100644 index 00000000..d73fa145 --- /dev/null +++ b/account_reconcile_oca/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_account_account_reconcile,account.account.reconcile,model_account_account_reconcile,account.group_account_user,1,1,0,0 +access_account_account_reconcile_data,account.account.reconcile,model_account_account_reconcile_data,account.group_account_user,1,1,1,1 diff --git a/account_reconcile_oca/static/description/icon.png b/account_reconcile_oca/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 zcmW++2RxMjAAjx~&dlBk9S+%}OXg)AGE&Cb*&}d0jUxM@u(PQx^-s)697TX`ehR4?GS^qbkof1cslKgkU)h65qZ9Oc=ml_0temigYLJfnz{IDzUf>bGs4N!v3=Z3jMq&A#7%rM5eQ#dc?k~! zVpnB`o+K7|Al`Q_U;eD$B zfJtP*jH`siUq~{KE)`jP2|#TUEFGRryE2`i0**z#*^6~AI|YzIWy$Cu#CSLW3q=GA z6`?GZymC;dCPk~rBS%eCb`5OLr;RUZ;D`}um=H)BfVIq%7VhiMr)_#G0N#zrNH|__ zc+blN2UAB0=617@>_u;MPHN;P;N#YoE=)R#i$k_`UAA>WWCcEVMh~L_ zj--gtp&|K1#58Yz*AHCTMziU1Jzt_jG0I@qAOHsk$2}yTmVkBp_eHuY$A9)>P6o~I z%aQ?!(GqeQ-Y+b0I(m9pwgi(IIZZzsbMv+9w{PFtd_<_(LA~0H(xz{=FhLB@(1&qHA5EJw1>>=%q2f&^X>IQ{!GJ4e9U z&KlB)z(84HmNgm2hg2C0>WM{E(DdPr+EeU_N@57;PC2&DmGFW_9kP&%?X4}+xWi)( z;)z%wI5>D4a*5XwD)P--sPkoY(a~WBw;E~AW`Yue4kFa^LM3X`8x|}ZUeMnqr}>kH zG%WWW>3ml$Yez?i%)2pbKPI7?5o?hydokgQyZsNEr{a|mLdt;X2TX(#B1j35xPnPW z*bMSSOauW>o;*=kO8ojw91VX!qoOQb)zHJ!odWB}d+*K?#sY_jqPdg{Sm2HdYzdEx zOGVPhVRTGPtv0o}RfVP;Nd(|CB)I;*t&QO8h zFfekr30S!-LHmV_Su-W+rEwYXJ^;6&3|L$mMC8*bQptyOo9;>Qb9Q9`ySe3%V$A*9 zeKEe+b0{#KWGp$F+tga)0RtI)nhMa-K@JS}2krK~n8vJ=Ngm?R!9G<~RyuU0d?nz# z-5EK$o(!F?hmX*2Yt6+coY`6jGbb7tF#6nHA zuKk=GGJ;ZwON1iAfG$E#Y7MnZVmrY|j0eVI(DN_MNFJmyZ|;w4tf@=CCDZ#5N_0K= z$;R~bbk?}TpfDjfB&aiQ$VA}s?P}xPERJG{kxk5~R`iRS(SK5d+Xs9swCozZISbnS zk!)I0>t=A<-^z(cmSFz3=jZ23u13X><0b)P)^1T_))Kr`e!-pb#q&J*Q`p+B6la%C zuVl&0duN<;uOsB3%T9Fp8t{ED108<+W(nOZd?gDnfNBC3>M8WE61$So|P zVvqH0SNtDTcsUdzaMDpT=Ty0pDHHNL@Z0w$Y`XO z2M-_r1S+GaH%pz#Uy0*w$Vdl=X=rQXEzO}d6J^R6zjM1u&c9vYLvLp?W7w(?np9x1 zE_0JSAJCPB%i7p*Wvg)pn5T`8k3-uR?*NT|J`eS#_#54p>!p(mLDvmc-3o0mX*mp_ zN*AeS<>#^-{S%W<*mz^!X$w_2dHWpcJ6^j64qFBft-o}o_Vx80o0>}Du;>kLts;$8 zC`7q$QI(dKYG`Wa8#wl@V4jVWBRGQ@1dr-hstpQL)Tl+aqVpGpbSfN>5i&QMXfiZ> zaA?T1VGe?rpQ@;+pkrVdd{klI&jVS@I5_iz!=UMpTsa~mBga?1r}aRBm1WS;TT*s0f0lY=JBl66Upy)-k4J}lh=P^8(SXk~0xW=T9v*B|gzIhN z>qsO7dFd~mgxAy4V?&)=5ieYq?zi?ZEoj)&2o)RLy=@hbCRcfT5jigwtQGE{L*8<@Yd{zg;CsL5mvzfDY}P-wos_6PfprFVaeqNE%h zKZhLtcQld;ZD+>=nqN~>GvROfueSzJD&BE*}XfU|H&(FssBqY=hPCt`d zH?@s2>I(|;fcW&YM6#V#!kUIP8$Nkdh0A(bEVj``-AAyYgwY~jB zT|I7Bf@%;7aL7Wf4dZ%VqF$eiaC38OV6oy3Z#TER2G+fOCd9Iaoy6aLYbPTN{XRPz z;U!V|vBf%H!}52L2gH_+j;`bTcQRXB+y9onc^wLm5wi3-Be}U>k_u>2Eg$=k!(l@I zcCg+flakT2Nej3i0yn+g+}%NYb?ta;R?(g5SnwsQ49U8Wng8d|{B+lyRcEDvR3+`O{zfmrmvFrL6acVP%yG98X zo&+VBg@px@i)%o?dG(`T;n*$S5*rnyiR#=wW}}GsAcfyQpE|>a{=$Hjg=-*_K;UtD z#z-)AXwSRY?OPefw^iI+ z)AXz#PfEjlwTes|_{sB?4(O@fg0AJ^g8gP}ex9Ucf*@_^J(s_5jJV}c)s$`Myn|Kd z$6>}#q^n{4vN@+Os$m7KV+`}c%4)4pv@06af4-x5#wj!KKb%caK{A&Y#Rfs z-po?Dcb1({W=6FKIUirH&(yg=*6aLCekcKwyfK^JN5{wcA3nhO(o}SK#!CINhI`-I z1)6&n7O&ZmyFMuNwvEic#IiOAwNkR=u5it{B9n2sAJV5pNhar=j5`*N!Na;c7g!l$ z3aYBqUkqqTJ=Re-;)s!EOeij=7SQZ3Hq}ZRds%IM*PtM$wV z@;rlc*NRK7i3y5BETSKuumEN`Xu_8GP1Ri=OKQ$@I^ko8>H6)4rjiG5{VBM>B|%`&&s^)jS|-_95&yc=GqjNo{zFkw%%HHhS~e=s zD#sfS+-?*t|J!+ozP6KvtOl!R)@@-z24}`9{QaVLD^9VCSR2b`b!KC#o;Ki<+wXB6 zx3&O0LOWcg4&rv4QG0)4yb}7BFSEg~=IR5#ZRj8kg}dS7_V&^%#Do==#`u zpy6{ox?jWuR(;pg+f@mT>#HGWHAJRRDDDv~@(IDw&R>9643kK#HN`!1vBJHnC+RM&yIh8{gG2q zA%e*U3|N0XSRa~oX-3EAneep)@{h2vvd3Xvy$7og(sayr@95+e6~Xvi1tUqnIxoIH zVWo*OwYElb#uyW{Imam6f2rGbjR!Y3`#gPqkv57dB6K^wRGxc9B(t|aYDGS=m$&S!NmCtrMMaUg(c zc2qC=2Z`EEFMW-me5B)24AqF*bV5Dr-M5ig(l-WPS%CgaPzs6p_gnCIvTJ=Y<6!gT zVt@AfYCzjjsMEGi=rDQHo0yc;HqoRNnNFeWZgcm?f;cp(6CNylj36DoL(?TS7eU#+ z7&mfr#y))+CJOXQKUMZ7QIdS9@#-}7y2K1{8)cCt0~-X0O!O?Qx#E4Og+;A2SjalQ zs7r?qn0H044=sDN$SRG$arw~n=+T_DNdSrarmu)V6@|?1-ZB#hRn`uilTGPJ@fqEy zGt(f0B+^JDP&f=r{#Y_wi#AVDf-y!RIXU^0jXsFpf>=Ji*TeqSY!H~AMbJdCGLhC) zn7Rx+sXw6uYj;WRYrLd^5IZq@6JI1C^YkgnedZEYy<&4(z%Q$5yv#Boo{AH8n$a zhb4Y3PWdr269&?V%uI$xMcUrMzl=;w<_nm*qr=c3Rl@i5wWB;e-`t7D&c-mcQl7x! zZWB`UGcw=Y2=}~wzrfLx=uet<;m3~=8I~ZRuzvMQUQdr+yTV|ATf1Uuomr__nDf=X zZ3WYJtHp_ri(}SQAPjv+Y+0=fH4krOP@S&=zZ-t1jW1o@}z;xk8 z(Nz1co&El^HK^NrhVHa-_;&88vTU>_J33=%{if;BEY*J#1n59=07jrGQ#IP>@u#3A z;!q+E1Rj3ZJ+!4bq9F8PXJ@yMgZL;>&gYA0%_Kbi8?S=XGM~dnQZQ!yBSgcZhY96H zrWnU;k)qy`rX&&xlDyA%(a1Hhi5CWkmg(`Gb%m(HKi-7Z!LKGRP_B8@`7&hdDy5n= z`OIxqxiVfX@OX1p(mQu>0Ai*v_cTMiw4qRt3~NBvr9oBy0)r>w3p~V0SCm=An6@3n)>@z!|o-$HvDK z|3D2ZMJkLE5loMKl6R^ez@Zz%S$&mbeoqH5`Bb){Ei21q&VP)hWS2tjShfFtGE+$z zzCR$P#uktu+#!w)cX!lWN1XU%K-r=s{|j?)Akf@q#3b#{6cZCuJ~gCxuMXRmI$nGtnH+-h z+GEi!*X=AP<|fG`1>MBdTb?28JYc=fGvAi2I<$B(rs$;eoJCyR6_bc~p!XR@O-+sD z=eH`-ye})I5ic1eL~TDmtfJ|8`0VJ*Yr=hNCd)G1p2MMz4C3^Mj?7;!w|Ly%JqmuW zlIEW^Ft%z?*|fpXda>Jr^1noFZEwFgVV%|*XhH@acv8rdGxeEX{M$(vG{Zw+x(ei@ zmfXb22}8-?Fi`vo-YVrTH*C?a8%M=Hv9MqVH7H^J$KsD?>!SFZ;ZsvnHr_gn=7acz z#W?0eCdVhVMWN12VV^$>WlQ?f;P^{(&pYTops|btm6aj>_Uz+hqpGwB)vWp0Cf5y< zft8-je~nn?W11plq}N)4A{l8I7$!ks_x$PXW-2XaRFswX_BnF{R#6YIwMhAgd5F9X zGmwdadS6(a^fjHtXg8=l?Rc0Sm%hk6E9!5cLVloEy4eh(=FwgP`)~I^5~pBEWo+F6 zSf2ncyMurJN91#cJTy_u8Y}@%!bq1RkGC~-bV@SXRd4F{R-*V`bS+6;W5vZ(&+I<9$;-V|eNfLa5n-6% z2(}&uGRF;p92eS*sE*oR$@pexaqr*meB)VhmIg@h{uzkk$9~qh#cHhw#>O%)b@+(| z^IQgqzuj~Sk(J;swEM-3TrJAPCq9k^^^`q{IItKBRXYe}e0Tdr=Huf7da3$l4PdpwWDop%^}n;dD#K4s#DYA8SHZ z&1!riV4W4R7R#C))JH1~axJ)RYnM$$lIR%6fIVA@zV{XVyx}C+a-Dt8Y9M)^KU0+H zR4IUb2CJ{Hg>CuaXtD50jB(_Tcx=Z$^WYu2u5kubqmwp%drJ6 z?Fo40g!Qd<-l=TQxqHEOuPX0;^z7iX?Ke^a%XT<13TA^5`4Xcw6D@Ur&VT&CUe0d} z1GjOVF1^L@>O)l@?bD~$wzgf(nxX1OGD8fEV?TdJcZc2KoUe|oP1#=$$7ee|xbY)A zDZq+cuTpc(fFdj^=!;{k03C69lMQ(|>uhRfRu%+!k&YOi-3|1QKB z z?n?eq1XP>p-IM$Z^C;2L3itnbJZAip*Zo0aw2bs8@(s^~*8T9go!%dHcAz2lM;`yp zD=7&xjFV$S&5uDaiScyD?B-i1ze`+CoRtz`Wn+Zl&#s4&}MO{@N!ufrzjG$B79)Y2d3tBk&)TxUTw@QS0TEL_?njX|@vq?Uz(nBFK5Pq7*xj#u*R&i|?7+6# z+|r_n#SW&LXhtheZdah{ZVoqwyT{D>MC3nkFF#N)xLi{p7J1jXlmVeb;cP5?e(=f# zuT7fvjSbjS781v?7{)-X3*?>tq?)Yd)~|1{BDS(pqC zC}~H#WXlkUW*H5CDOo<)#x7%RY)A;ShGhI5s*#cRDA8YgqG(HeKDx+#(ZQ?386dv! zlXCO)w91~Vw4AmOcATuV653fa9R$fyK8ul%rG z-wfS zihugoZyr38Im?Zuh6@RcF~t1anQu7>#lPpb#}4cOA!EM11`%f*07RqOVkmX{p~KJ9 z^zP;K#|)$`^Rb{rnHGH{~>1(fawV0*Z#)}M`m8-?ZJV<+e}s9wE# z)l&az?w^5{)`S(%MRzxdNqrs1n*-=jS^_jqE*5XDrA0+VE`5^*p3CuM<&dZEeCjoz zR;uu_H9ZPZV|fQq`Cyw4nscrVwi!fE6ciMmX$!_hN7uF;jjKG)d2@aC4ropY)8etW=xJvni)8eHi`H$%#zn^WJ5NLc-rqk|u&&4Z6fD_m&JfSI1Bvb?b<*n&sfl0^t z=HnmRl`XrFvMKB%9}>PaA`m-fK6a0(8=qPkWS5bb4=v?XcWi&hRY?O5HdulRi4?fN zlsJ*N-0Qw+Yic@s0(2uy%F@ib;GjXt01Fmx5XbRo6+n|pP(&nodMoap^z{~q ziEeaUT@Mxe3vJSfI6?uLND(CNr=#^W<1b}jzW58bIfyWTDle$mmS(|x-0|2UlX+9k zQ^EX7Nw}?EzVoBfT(-LT|=9N@^hcn-_p&sqG z&*oVs2JSU+N4ZD`FhCAWaS;>|wH2G*Id|?pa#@>tyxX`+4HyIArWDvVrX)2WAOQff z0qyHu&-S@i^MS-+j--!pr4fPBj~_8({~e1bfcl0wI1kaoN>mJL6KUPQm5N7lB(ui1 zE-o%kq)&djzWJ}ob<-GfDlkB;F31j-VHKvQUGQ3sp`CwyGJk_i!y^sD0fqC@$9|jO zOqN!r!8-p==F@ZVP=U$qSpY(gQ0)59P1&t@y?5rvg<}E+GB}26NYPp4f2YFQrQtot5mn3wu_qprZ=>Ig-$ zbW26Ws~IgY>}^5w`vTB(G`PTZaDiGBo5o(tp)qli|NeV( z@H_=R8V39rt5J5YB2Ky?4eJJ#b`_iBe2ot~6%7mLt5t8Vwi^Jy7|jWXqa3amOIoRb zOr}WVFP--DsS`1WpN%~)t3R!arKF^Q$e12KEqU36AWwnCBICpH4XCsfnyrHr>$I$4 z!DpKX$OKLWarN7nv@!uIA+~RNO)l$$w}p(;b>mx8pwYvu;dD_unryX_NhT8*Tj>BTrTTL&!?O+%Rv;b?B??gSzdp?6Uug9{ zd@V08Z$BdI?fpoCS$)t4mg4rT8Q_I}h`0d-vYZ^|dOB*Q^S|xqTV*vIg?@fVFSmMpaw0qtTRbx} z({Pg?#{2`sc9)M5N$*N|4;^t$+QP?#mov zGVC@I*lBVrOU-%2y!7%)fAKjpEFsgQc4{amtiHb95KQEwvf<(3T<9-Zm$xIew#P22 zc2Ix|App^>v6(3L_MCU0d3W##AB0M~3D00EWoKZqsJYT(#@w$Y_H7G22M~ApVFTRHMI_3be)Lkn#0F*V8Pq zc}`Cjy$bE;FJ6H7p=0y#R>`}-m4(0F>%@P|?7fx{=R^uFdISRnZ2W_xQhD{YuR3t< z{6yxu=4~JkeA;|(J6_nv#>Nvs&FuLA&PW^he@t(UwFFE8)|a!R{`E`K`i^ZnyE4$k z;(749Ix|oi$c3QbEJ3b~D_kQsPz~fIUKym($a_7dJ?o+40*OLl^{=&oq$<#Q(yyrp z{J-FAniyAw9tPbe&IhQ|a`DqFTVQGQ&Gq3!C2==4x{6EJwiPZ8zub-iXoUtkJiG{} zPaR&}_fn8_z~(=;5lD-aPWD3z8PZS@AaUiomF!G8I}Mf>e~0g#BelA-5#`cj;O5>N Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/account_reconcile_oca/static/description/index.html b/account_reconcile_oca/static/description/index.html new file mode 100644 index 00000000..ca0693c1 --- /dev/null +++ b/account_reconcile_oca/static/description/index.html @@ -0,0 +1,446 @@ + + + + + + +Account Reconcile Oca + + + +
+

Account Reconcile Oca

+ + +

Beta License: AGPL-3 OCA/account-reconcile Translate me on Weblate Try me on Runbot

+

This addon allows to reconcile bank statements and account marked as reconcile.

+

Table of contents

+ +
+

Usage

+
+

Bank reconcile

+

Access Invoicing / Dashboard with a user with Full Acounting capabilities. +Select reconcile on the journal of your choice.

+
+
+

Account reconcile

+

Access Invoicing / Accounting / Actions / Reconcile +All the possible reconcile options will show and you will be able to reconcile properly. +You can access the same widget from accounts and Partners.

+
+
+
+

Known issues / Roadmap

+

The following bugs are already detected:

+
    +
  • Creation of activities on the chatter do show automatically
  • +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • CreuBlanca
  • +
+
+
+

Contributors

+
    +
  • Enric Tobella
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/account-reconcile project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/account_reconcile_oca/static/src/js/reconcile_chatter_field.esm.js b/account_reconcile_oca/static/src/js/reconcile_chatter_field.esm.js new file mode 100644 index 00000000..c9e6f4e0 --- /dev/null +++ b/account_reconcile_oca/static/src/js/reconcile_chatter_field.esm.js @@ -0,0 +1,15 @@ +/** @odoo-module **/ + +import {registry} from "@web/core/registry"; +import {ChatterContainer} from "@mail/components/chatter_container/chatter_container"; + +const {Component} = owl; + +export class AccountReconcileChatterWidget extends Component {} +AccountReconcileChatterWidget.template = + "account_reconcile_oca.AccountReconcileChatterWidget"; +AccountReconcileChatterWidget.components = {...Component.components, ChatterContainer}; + +registry + .category("fields") + .add("account_reconcile_oca_chatter", AccountReconcileChatterWidget); diff --git a/account_reconcile_oca/static/src/js/reconcile_controller.esm.js b/account_reconcile_oca/static/src/js/reconcile_controller.esm.js new file mode 100644 index 00000000..90756cfc --- /dev/null +++ b/account_reconcile_oca/static/src/js/reconcile_controller.esm.js @@ -0,0 +1,99 @@ +/** @odoo-module */ +const {useState, useSubEnv} = owl; +import {KanbanController} from "@web/views/kanban/kanban_controller"; +import {View} from "@web/views/view"; +import {useService} from "@web/core/utils/hooks"; + +export class ReconcileController extends KanbanController { + async setup() { + super.setup(); + this.state = useState({ + selectedRecordId: null, + }); + useSubEnv({ + parentController: this, + exposeController: this.exposeController.bind(this), + }); + this.effect = useService("effect"); + this.orm = useService("orm"); + this.action = useService("action"); + this.activeActions = this.props.archInfo.activeActions; + this.model.addEventListener("update", () => this.selectRecord(), {once: true}); + } + exposeController(controller) { + this.form_controller = controller; + } + async onClickNewButton() { + const action = await this.orm.call(this.props.resModel, "action_new_line", [], { + context: this.props.context, + }); + this.action.doAction(action, { + onClose: async () => { + await this.model.root.load(); + this.render(true); + }, + }); + } + async setRainbowMan(message) { + this.effect.add({ + message, + type: "rainbow_man", + }); + } + get viewReconcileInfo() { + return { + resId: this.state.selectedRecordId, + type: "form", + context: { + ...(this.props.context || {}), + form_view_ref: this.props.context.view_ref, + }, + display: {controlPanel: false}, + mode: this.props.mode || "edit", + resModel: this.props.resModel, + }; + } + async selectRecord(record) { + var resId = undefined; + if (record === undefined) { + var records = this.model.root.records.filter( + (modelRecord) => + !modelRecord.data.is_reconciled || modelRecord.data.to_check + ); + if (records.length === 0) { + records = this.model.root.records; + if (records.length === 0) { + this.state.selectedRecordId = false; + return; + } + } + resId = records[0].resId; + } else { + resId = record.resId; + } + if (this.state.selectedRecordId && this.state.selectedRecordId !== resId) { + if (this.form_controller.model.root.isDirty) { + await this.form_controller.model.root.save({ + noReload: true, + stayInEdition: true, + useSaveErrorDialog: true, + }); + await this.model.root.load(); + await this.render(true); + } + } + if (!this.state.selectedRecordId || this.state.selectedRecordId !== resId) { + this.state.selectedRecordId = resId; + } + } + async openRecord(record) { + this.selectRecord(record); + } +} +ReconcileController.components = { + ...ReconcileController.components, + View, +}; + +ReconcileController.template = "account_reconcile_oca.ReconcileController"; +ReconcileController.defaultProps = {}; diff --git a/account_reconcile_oca/static/src/js/reconcile_data_widget.esm.js b/account_reconcile_oca/static/src/js/reconcile_data_widget.esm.js new file mode 100644 index 00000000..87f63fa7 --- /dev/null +++ b/account_reconcile_oca/static/src/js/reconcile_data_widget.esm.js @@ -0,0 +1,65 @@ +/** @odoo-module **/ + +import fieldUtils from "web.field_utils"; +import session from "web.session"; +import {registry} from "@web/core/registry"; + +const {Component} = owl; + +export class AccountReconcileDataWidget extends Component { + getReconcileLines() { + var data = this.props.record.data[this.props.name].data; + for (var line in data) { + data[line].amount_format = fieldUtils.format.monetary( + data[line].amount, + undefined, + { + currency: session.get_currency(data[line].currency_id), + } + ); + data[line].debit_format = fieldUtils.format.monetary( + data[line].debit, + undefined, + { + currency: session.get_currency(data[line].currency_id), + } + ); + data[line].credit_format = fieldUtils.format.monetary( + data[line].credit, + undefined, + { + currency: session.get_currency(data[line].currency_id), + } + ); + if (data[line].original_amount) { + data[line].original_amount_format = fieldUtils.format.monetary( + data[line].original_amount, + undefined, + { + currency: session.get_currency(data[line].currency_id), + } + ); + } + data[line].date_format = fieldUtils.format.date( + fieldUtils.parse.date(data[line].date, undefined, {isUTC: true}) + ); + } + return data; + } + onTrashLine(ev, line) { + this.props.record.update({ + manual_reference: line.reference, + manual_delete: true, + }); + } + selectReconcileLine(ev, line) { + this.props.record.update({ + manual_reference: line.reference, + }); + } +} +AccountReconcileDataWidget.template = "account_reconcile_oca.ReconcileDataWidget"; + +registry + .category("fields") + .add("account_reconcile_oca_data", AccountReconcileDataWidget); diff --git a/account_reconcile_oca/static/src/js/reconcile_form_view.esm.js b/account_reconcile_oca/static/src/js/reconcile_form_view.esm.js new file mode 100644 index 00000000..7a49f3f9 --- /dev/null +++ b/account_reconcile_oca/static/src/js/reconcile_form_view.esm.js @@ -0,0 +1,51 @@ +/** @odoo-module */ + +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 {useViewButtons} from "@web/views/view_button/view_button_hook"; +const {useRef} = owl; + +export class ReconcileFormController extends FormController { + setup() { + super.setup(...arguments); + this.env.exposeController(this); + this.orm = useService("orm"); + const rootRef = useRef("root"); + useViewButtons(this.model, rootRef, { + reload: this.reloadFormController.bind(this), + beforeExecuteAction: this.beforeExecuteActionButton.bind(this), + afterExecuteAction: this.afterExecuteActionButton.bind(this), + }); + } + async reloadFormController() { + var is_reconciled = this.model.root.data.is_reconciled; + await this.model.root.load(); + if (!is_reconciled && this.model.root.data.is_reconciled) { + // This only happens when we press the reconcile button + if (this.env.parentController) { + // Showing rainbow man + const message = await this.orm.call( + "account.journal", + "get_rainbowman_message", + [[this.model.root.data.journal_id[0]]] + ); + if (message) { + this.env.parentController.setRainbowMan(message); + } + // Refreshing + await this.env.parentController.model.root.load(); + await this.env.parentController.render(true); + this.env.parentController.selectRecord(); + } + } + } +} + +export const ReconcileFormView = { + ...formView, + Controller: ReconcileFormController, +}; + +registry.category("views").add("reconcile_form", ReconcileFormView); diff --git a/account_reconcile_oca/static/src/js/reconcile_kanban_record.esm.js b/account_reconcile_oca/static/src/js/reconcile_kanban_record.esm.js new file mode 100644 index 00000000..a97c5258 --- /dev/null +++ b/account_reconcile_oca/static/src/js/reconcile_kanban_record.esm.js @@ -0,0 +1,14 @@ +/** @odoo-module */ + +import {KanbanRecord} from "@web/views/kanban/kanban_record"; + +export class ReconcileKanbanRecord extends KanbanRecord { + getRecordClasses() { + var result = super.getRecordClasses(); + if (this.props.selectedRecordId === this.props.record.resId) { + result += " o_kanban_record_reconcile_oca_selected"; + } + return result; + } +} +ReconcileKanbanRecord.props = [...KanbanRecord.props, "selectedRecordId?"]; diff --git a/account_reconcile_oca/static/src/js/reconcile_manual_view.esm.js b/account_reconcile_oca/static/src/js/reconcile_manual_view.esm.js new file mode 100644 index 00000000..99be2b66 --- /dev/null +++ b/account_reconcile_oca/static/src/js/reconcile_manual_view.esm.js @@ -0,0 +1,38 @@ +/** @odoo-module */ + +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"; +const {useRef} = owl; + +export class FormManualReconcileController extends FormController { + setup() { + super.setup(...arguments); + const rootRef = useRef("root"); + useViewButtons(this.model, rootRef, { + reload: this.reloadFormController.bind(this), + beforeExecuteAction: this.beforeExecuteActionButton.bind(this), + afterExecuteAction: this.afterExecuteActionButton.bind(this), + }); + } + async reloadFormController() { + try { + await this.model.root.load(); + } catch (error) { + // This should happen when we reconcile a line (no more possible data...) + if (this.env.parentController) { + await this.env.parentController.model.root.load(); + await this.env.parentController.render(true); + this.env.parentController.selectRecord(); + } + } + } +} + +export const FormManualReconcileView = { + ...formView, + Controller: FormManualReconcileController, +}; + +registry.category("views").add("reconcile_manual", FormManualReconcileView); diff --git a/account_reconcile_oca/static/src/js/reconcile_move_line_view.esm.js b/account_reconcile_oca/static/src/js/reconcile_move_line_view.esm.js new file mode 100644 index 00000000..158f8afa --- /dev/null +++ b/account_reconcile_oca/static/src/js/reconcile_move_line_view.esm.js @@ -0,0 +1,46 @@ +/** @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); diff --git a/account_reconcile_oca/static/src/js/reconcile_move_line_widget.esm.js b/account_reconcile_oca/static/src/js/reconcile_move_line_widget.esm.js new file mode 100644 index 00000000..a539bca4 --- /dev/null +++ b/account_reconcile_oca/static/src/js/reconcile_move_line_widget.esm.js @@ -0,0 +1,51 @@ +/** @odoo-module **/ + +import {View} from "@web/views/view"; +import {registry} from "@web/core/registry"; + +const {Component, useSubEnv} = owl; + +export class AccountReconcileMatchWidget extends Component { + setup() { + // Necessary in order to avoid a loop + super.setup(...arguments); + useSubEnv({ + config: {}, + parentController: this.env.parentController, + }); + } + get listViewProperties() { + return { + type: "list", + display: { + controlPanel: { + // Hiding the control panel buttons + "top-left": false, + "bottom-left": false, + }, + }, + resModel: this.props.record.fields[this.props.name].relation, + searchMenuTypes: ["filter"], + domain: this.props.record.getFieldDomain(this.props.name).toList(), + context: { + ...this.props.record.getFieldContext(this.props.name), + }, + // Disables de selector + allowSelectors: false, + // We need to force the search view in order to show the right one + searchViewId: false, + parentRecord: this.props.record, + parentField: this.props.name, + }; + } +} +AccountReconcileMatchWidget.template = "account_reconcile_oca.ReconcileMatchWidget"; + +AccountReconcileMatchWidget.components = { + ...AccountReconcileMatchWidget.components, + View, +}; + +registry + .category("fields") + .add("account_reconcile_oca_match", AccountReconcileMatchWidget); diff --git a/account_reconcile_oca/static/src/js/reconcile_renderer.esm.js b/account_reconcile_oca/static/src/js/reconcile_renderer.esm.js new file mode 100644 index 00000000..d031e2bc --- /dev/null +++ b/account_reconcile_oca/static/src/js/reconcile_renderer.esm.js @@ -0,0 +1,12 @@ +/** @odoo-module */ + +import {KanbanRenderer} from "@web/views/kanban/kanban_renderer"; +import {ReconcileKanbanRecord} from "./reconcile_kanban_record.esm.js"; +export class ReconcileRenderer extends KanbanRenderer {} + +ReconcileRenderer.components = { + ...KanbanRenderer.components, + KanbanRecord: ReconcileKanbanRecord, +}; +ReconcileRenderer.template = "account_reconcile_oca.ReconcileRenderer"; +ReconcileRenderer.props = [...KanbanRenderer.props, "selectedRecordId?"]; diff --git a/account_reconcile_oca/static/src/js/reconcile_view.esm.js b/account_reconcile_oca/static/src/js/reconcile_view.esm.js new file mode 100644 index 00000000..4bcb8ade --- /dev/null +++ b/account_reconcile_oca/static/src/js/reconcile_view.esm.js @@ -0,0 +1,16 @@ +/** @odoo-module */ + +import {ReconcileController} from "./reconcile_controller.esm.js"; +import {ReconcileRenderer} from "./reconcile_renderer.esm.js"; +import {kanbanView} from "@web/views/kanban/kanban_view"; +import {registry} from "@web/core/registry"; + +export const reconcileView = { + ...kanbanView, + Renderer: ReconcileRenderer, + Controller: ReconcileController, + buttonTemplate: "account_reconcile.ReconcileView.Buttons", + searchMenuTypes: ["filter"], +}; + +registry.category("views").add("reconcile", reconcileView); diff --git a/account_reconcile_oca/static/src/js/selection_badge_uncheck.esm.js b/account_reconcile_oca/static/src/js/selection_badge_uncheck.esm.js new file mode 100644 index 00000000..8d238cc6 --- /dev/null +++ b/account_reconcile_oca/static/src/js/selection_badge_uncheck.esm.js @@ -0,0 +1,29 @@ +/** @odoo-module **/ +import { + BadgeSelectionField, + preloadSelection, +} from "@web/views/fields/badge_selection/badge_selection_field"; +import {registry} from "@web/core/registry"; + +export class FieldSelectionBadgeUncheck extends BadgeSelectionField { + async onChange(value) { + var old_value = this.props.value; + if (this.props.type === "many2one") { + old_value = old_value[0]; + } + if (value === old_value) { + this.props.update(false); + return; + } + super.onChange(...arguments); + } +} + +FieldSelectionBadgeUncheck.supportedTypes = ["many2one", "selection"]; +FieldSelectionBadgeUncheck.additionalClasses = ["o_field_selection_badge"]; +registry.category("fields").add("selection_badge_uncheck", FieldSelectionBadgeUncheck); + +registry.category("preloadedData").add("selection_badge_uncheck", { + loadOnTypes: ["many2one"], + preload: preloadSelection, +}); diff --git a/account_reconcile_oca/static/src/scss/reconcile.scss b/account_reconcile_oca/static/src/scss/reconcile.scss new file mode 100644 index 00000000..840ceb23 --- /dev/null +++ b/account_reconcile_oca/static/src/scss/reconcile.scss @@ -0,0 +1,72 @@ +.o_account_reconcile_oca { + display: -webkit-box; + display: -webkit-flex; + display: flex; + -webkit-flex-flow: row wrap; + flex-flow: row wrap; + height: 100%; + .o_kanban_renderer.o_kanban_ungrouped .o_kanban_record { + margin: 0 0 0; + > div { + border-right: thick solid rgba(0, 0, 0, 0); + } + &.o_kanban_record_reconcile_oca_selected > div { + border-right: thick solid $o-brand-primary; + } + } + .o_account_reconcile_oca_selector { + width: 30%; + height: 100%; + padding: 0; + position: relative; + border-right: 1px solid $o-gray-300; + } + .o_account_reconcile_oca_info { + width: 70%; + height: 100%; + } + .o_form_view { + .btn-info:not(.dropdown-toggle):not(.dropdown-item) { + text-transform: uppercase; + } + .o_form_statusbar.o_account_reconcile_oca_statusbar { + .btn:not(.dropdown-toggle):not(.dropdown-item) { + text-transform: uppercase; + } + height: 40px; + > .o_statusbar_buttons { + height: 100%; + > .btn { + margin: 0; + height: 100%; + padding: 10px; + border-radius: 0; + } + } + } + .o_field_account_reconcile_oca_data { + .o_field_account_reconcile_oca_balance_float { + .o_field_account_reconcile_oca_balance_original_float { + text-decoration: line-through; + } + } + } + .o_field_widget.o_field_account_reconcile_oca_match { + display: inline; + } + .o_field_account_reconcile_oca_move_line_selected { + background-color: rgba($o-brand-primary, 0.2); + color: #000; + } + .o_reconcile_widget_table { + .o_reconcile_widget_line { + &.liquidity { + font-weight: bold; + } + } + } + } +} +.o_field_account_reconcile_oca_chatter { + width: 100%; +} diff --git a/account_reconcile_oca/static/src/xml/reconcile.xml b/account_reconcile_oca/static/src/xml/reconcile.xml new file mode 100644 index 00000000..63801923 --- /dev/null +++ b/account_reconcile_oca/static/src/xml/reconcile.xml @@ -0,0 +1,149 @@ + + + + + + + + + props.selectedRecordId + + + + + + state.selectedRecordId + + + model.useSampleModel ? 'o_view_sample_data o_account_reconcile_oca' : 'o_account_reconcile_oca' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AccountPartnerDateLabelDebitCredit +
+ + + + + + +
+
+ + + + + + props.parentRecord + props.parentField + + +
diff --git a/account_reconcile_oca/tests/__init__.py b/account_reconcile_oca/tests/__init__.py new file mode 100644 index 00000000..17f193ed --- /dev/null +++ b/account_reconcile_oca/tests/__init__.py @@ -0,0 +1,2 @@ +from . import test_bank_account_reconcile +from . import test_account_reconcile diff --git a/account_reconcile_oca/tests/test_account_reconcile.py b/account_reconcile_oca/tests/test_account_reconcile.py new file mode 100644 index 00000000..f68265ba --- /dev/null +++ b/account_reconcile_oca/tests/test_account_reconcile.py @@ -0,0 +1,304 @@ +from odoo.tests import Form, tagged + +from odoo.addons.account.tests.common import TestAccountReconciliationCommon + + +@tagged("post_install", "-at_install") +class TestReconciliationWidget(TestAccountReconciliationCommon): + @classmethod + def setUpClass(cls, chart_template_ref=None): + super().setUpClass(chart_template_ref=chart_template_ref) + + cls.acc_bank_stmt_model = cls.env["account.bank.statement"] + cls.acc_bank_stmt_line_model = cls.env["account.bank.statement.line"] + cls.bank_journal_usd.suspense_account_id = ( + cls.company.account_journal_suspense_account_id + ) + cls.bank_journal_euro.suspense_account_id = ( + cls.company.account_journal_suspense_account_id + ) + cls.current_assets_account = ( + cls.env["account.account"] + .search( + [ + ("account_type", "=", "asset_current"), + ("company_id", "=", cls.company.id), + ], + limit=1, + ) + .copy() + ) + cls.current_assets_account.reconcile = True + cls.asset_receivable_account = ( + cls.env["account.account"] + .search( + [ + ("account_type", "=", "asset_receivable"), + ("company_id", "=", cls.company.id), + ], + limit=1, + ) + .copy() + ) + cls.asset_receivable_account.reconcile = True + cls.equity_account = ( + cls.env["account.account"] + .search( + [ + ("account_type", "=", "equity"), + ("company_id", "=", cls.company.id), + ], + limit=1, + ) + .copy() + ) + cls.non_current_assets_account = ( + cls.env["account.account"] + .search( + [ + ("account_type", "=", "asset_non_current"), + ("company_id", "=", cls.company.id), + ], + limit=1, + ) + .copy() + ) + cls.non_current_assets_account.reconcile = True + cls.move_1 = cls.env["account.move"].create( + { + "line_ids": [ + ( + 0, + 0, + { + "account_id": cls.current_assets_account.id, + "name": "DEMO", + "credit": 100, + }, + ), + ( + 0, + 0, + { + "account_id": cls.non_current_assets_account.id, + "name": "DEMO", + "debit": 100, + }, + ), + ] + } + ) + cls.move_1.action_post() + cls.move_2 = cls.env["account.move"].create( + { + "line_ids": [ + ( + 0, + 0, + { + "account_id": cls.non_current_assets_account.id, + "name": "DEMO", + "credit": 50, + }, + ), + ( + 0, + 0, + { + "account_id": cls.equity_account.id, + "name": "DEMO", + "debit": 50, + }, + ), + ] + } + ) + cls.move_2.action_post() + cls.move_3 = cls.env["account.move"].create( + { + "line_ids": [ + ( + 0, + 0, + { + "account_id": cls.non_current_assets_account.id, + "name": "DEMO", + "credit": 50, + }, + ), + ( + 0, + 0, + { + "account_id": cls.equity_account.id, + "name": "DEMO", + "debit": 50, + }, + ), + ] + } + ) + cls.move_3.action_post() + + def test_reconcile(self): + account = self.non_current_assets_account + reconcile_account = self.env["account.account.reconcile"].search( + [("account_id", "=", account.id)] + ) + self.assertTrue(reconcile_account) + with Form(reconcile_account) as f: + f.add_account_move_line_id = self.move_1.line_ids.filtered( + lambda r: r.account_id == account + ) + f.add_account_move_line_id = self.move_2.line_ids.filtered( + lambda r: r.account_id == account + ) + reconcile_account.reconcile() + reconcile_account = self.env["account.account.reconcile"].search( + [("account_id", "=", account.id)] + ) + self.assertTrue(reconcile_account) + with Form(reconcile_account) as f: + f.add_account_move_line_id = self.move_1.line_ids.filtered( + lambda r: r.account_id == account + ) + f.add_account_move_line_id = self.move_3.line_ids.filtered( + lambda r: r.account_id == account + ) + reconcile_account.reconcile() + reconcile_account = self.env["account.account.reconcile"].search( + [("account_id", "=", account.id)] + ) + self.assertFalse(reconcile_account) + + def test_clean_reconcile(self): + account = self.non_current_assets_account + reconcile_account = self.env["account.account.reconcile"].search( + [("account_id", "=", account.id)] + ) + self.assertTrue(reconcile_account) + with Form(reconcile_account) as f: + f.add_account_move_line_id = self.move_1.line_ids.filtered( + lambda r: r.account_id == account + ) + f.add_account_move_line_id = self.move_2.line_ids.filtered( + lambda r: r.account_id == account + ) + self.assertTrue(reconcile_account.reconcile_data_info.get("counterparts")) + reconcile_account.clean_reconcile() + self.assertFalse(reconcile_account.reconcile_data_info.get("counterparts")) + + def test_cannot_reconcile(self): + """ + There is not enough records to reconcile for this account + """ + reconcile_account = self.env["account.account.reconcile"].search( + [("account_id", "=", self.current_assets_account.id)] + ) + self.assertFalse(reconcile_account) + + def test_cannot_reconcile_different_partners(self): + """ + We can only reconcile lines with the same account and partner. + """ + reconcile_account = self.env["account.account.reconcile"].search( + [ + ("account_id", "=", self.asset_receivable_account.id), + ] + ) + self.assertFalse(reconcile_account) + move_1 = self.env["account.move"].create( + { + "line_ids": [ + ( + 0, + 0, + { + "account_id": self.current_assets_account.id, + "name": "DEMO", + "credit": 100, + }, + ), + ( + 0, + 0, + { + "account_id": self.asset_receivable_account.id, + "partner_id": self.env.user.partner_id.id, + "name": "DEMO", + "debit": 100, + }, + ), + ] + } + ) + move_1.action_post() + self.env.flush_all() + move_2 = self.env["account.move"].create( + { + "line_ids": [ + ( + 0, + 0, + { + "account_id": self.current_assets_account.id, + "name": "DEMO", + "debit": 100, + }, + ), + ( + 0, + 0, + { + "account_id": self.asset_receivable_account.id, + "partner_id": self.company.partner_id.id, + "name": "DEMO", + "credit": 100, + }, + ), + ] + } + ) + move_2.action_post() + self.env.flush_all() + reconcile_account = self.env["account.account.reconcile"].search( + [ + ("account_id", "=", self.asset_receivable_account.id), + ] + ) + self.assertFalse(reconcile_account) + + move_3 = self.env["account.move"].create( + { + "line_ids": [ + ( + 0, + 0, + { + "account_id": self.current_assets_account.id, + "name": "DEMO", + "debit": 100, + }, + ), + ( + 0, + 0, + { + "account_id": self.asset_receivable_account.id, + "partner_id": self.env.user.partner_id.id, + "name": "DEMO", + "credit": 100, + }, + ), + ] + } + ) + move_3.action_post() + self.env.flush_all() + reconcile_account = self.env["account.account.reconcile"].search( + [ + ("account_id", "=", self.asset_receivable_account.id), + ] + ) + self.assertTrue(reconcile_account) + self.assertEqual(reconcile_account.partner_id, self.env.user.partner_id) diff --git a/account_reconcile_oca/tests/test_bank_account_reconcile.py b/account_reconcile_oca/tests/test_bank_account_reconcile.py new file mode 100644 index 00000000..7d124f2c --- /dev/null +++ b/account_reconcile_oca/tests/test_bank_account_reconcile.py @@ -0,0 +1,706 @@ +import time + +from odoo.exceptions import UserError +from odoo.tests import Form, tagged + +from odoo.addons.account.tests.common import TestAccountReconciliationCommon + + +@tagged("post_install", "-at_install") +class TestReconciliationWidget(TestAccountReconciliationCommon): + @classmethod + def setUpClass(cls, chart_template_ref=None): + super().setUpClass(chart_template_ref=chart_template_ref) + + cls.acc_bank_stmt_model = cls.env["account.bank.statement"] + cls.acc_bank_stmt_line_model = cls.env["account.bank.statement.line"] + cls.bank_journal_usd.suspense_account_id = ( + cls.company.account_journal_suspense_account_id + ) + cls.bank_journal_euro.suspense_account_id = ( + cls.company.account_journal_suspense_account_id + ) + cls.current_assets_account = cls.env["account.account"].search( + [ + ("account_type", "=", "asset_current"), + ("company_id", "=", cls.company.id), + ], + limit=1, + ) + cls.current_assets_account.reconcile = True + + cls.rule = cls.env["account.reconcile.model"].create( + { + "name": "write-off model", + "rule_type": "writeoff_button", + "match_partner": True, + "match_partner_ids": [], + "line_ids": [(0, 0, {"account_id": cls.current_assets_account.id})], + } + ) + # We need to make some fields visible in order to make the tests work + cls.env["ir.ui.view"].create( + { + "name": "DEMO Account bank statement", + "model": "account.bank.statement.line", + "inherit_id": cls.env.ref( + "account_reconcile_oca.bank_statement_line_form_reconcile_view" + ).id, + "arch": """ + + + 0 + + + 0 + + + 0 + + + """, + } + ) + + # Testing reconcile action + + def test_reconcile_invoice_unreconcile(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"), + } + ) + 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) + self.assertFalse(bank_stmt_line.is_reconciled) + self.assertTrue( + bank_stmt_line.move_id.line_ids.filtered( + lambda r: r.account_id == self.bank_journal_euro.suspense_account_id + ) + ) + bank_stmt_line.reconcile_bank_line() + self.assertTrue(bank_stmt_line.is_reconciled) + self.assertFalse( + bank_stmt_line.move_id.line_ids.filtered( + lambda r: r.account_id == self.bank_journal_euro.suspense_account_id + ) + ) + bank_stmt_line.unreconcile_bank_line() + self.assertFalse(bank_stmt_line.is_reconciled) + self.assertTrue( + bank_stmt_line.move_id.line_ids.filtered( + lambda r: r.account_id == self.bank_journal_euro.suspense_account_id + ) + ) + + def test_reconcile_invoice_partial(self): + """ + We want to partially reconcile two invoices from a single payment. + As a result, both invoices must be partially reconciled + """ + inv1 = self.create_invoice( + currency_id=self.currency_euro_id, invoice_amount=100 + ) + inv2 = 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" + ) + receivable2 = inv2.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(f.manual_amount, -100) + f.manual_amount = -70 + self.assertFalse(f.can_reconcile) + f.add_account_move_line_id = receivable2 + f.manual_reference = "account.move.line;%s" % receivable2.id + self.assertEqual(f.manual_amount, -30) + self.assertTrue(f.can_reconcile) + self.assertEqual(inv1.amount_residual, 100) + self.assertEqual(inv2.amount_residual, 100) + bank_stmt_line.reconcile_bank_line() + self.assertEqual(inv1.amount_residual, 30) + self.assertEqual(inv2.amount_residual, 70) + + def test_reconcile_model(self): + """ + We want to test what happens when we select an reconcile model to fill a + bank statement. + """ + 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"), + } + ) + with Form( + bank_stmt_line, + view="account_reconcile_oca.bank_statement_line_form_reconcile_view", + ) as f: + self.assertFalse(f.can_reconcile) + f.manual_model_id = self.rule + self.assertTrue(f.can_reconcile) + bank_stmt_line.reconcile_bank_line() + self.assertTrue( + bank_stmt_line.move_id.line_ids.filtered( + lambda r: r.account_id == self.current_assets_account + ) + ) + + def test_reconcile_invoice_model(self): + """ + We want to test what happens when we select a reconcile model to fill a + bank statement prefilled with an invoice. + + The result should be the reconcile of the invoice, and the rest set to the model + """ + + inv1 = self.create_invoice(currency_id=self.currency_euro_id) + + receivable1 = inv1.line_ids.filtered( + lambda l: l.account_id.account_type == "asset_receivable" + ) + 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"), + } + ) + 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.can_reconcile) + f.manual_model_id = self.rule + self.assertTrue(f.can_reconcile) + bank_stmt_line.reconcile_bank_line() + self.assertNotEqual(self.current_assets_account, receivable1.account_id) + self.assertTrue( + bank_stmt_line.move_id.line_ids.filtered( + lambda r: r.account_id == self.current_assets_account + ) + ) + self.assertTrue( + bank_stmt_line.move_id.line_ids.filtered( + lambda r: r.account_id == receivable1.account_id + ) + ) + self.assertEqual(0, inv1.amount_residual) + + def test_reconcile_rule_on_create(self): + """ + Testing the fill of the bank statment line with + writeoff suggestion reconcile model with auto_reconcile + """ + self.env["account.reconcile.model"].create( + { + "name": "write-off model suggestion", + "rule_type": "writeoff_suggestion", + "match_label": "contains", + "match_label_param": "DEMO WRITEOFF", + "auto_reconcile": True, + "line_ids": [(0, 0, {"account_id": self.current_assets_account.id})], + } + ) + + 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": "DEMO WRITEOFF", + "payment_ref": "DEMO WRITEOFF", + "journal_id": self.bank_journal_euro.id, + "statement_id": bank_stmt.id, + "amount": 100, + "date": time.strftime("%Y-07-15"), + } + ) + self.assertTrue(bank_stmt_line.is_reconciled) + + def test_reconcile_invoice_keep(self): + """ + We want to test how the keep mode works, keeping the original move lines. + However, the unreconcile will not work properly + """ + self.bank_journal_euro.reconcile_mode = "keep" + self.bank_journal_euro.suspense_account_id.reconcile = True + 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.assertFalse(f.add_account_move_line_id) + self.assertTrue(bank_stmt_line.can_reconcile) + bank_stmt_line.reconcile_bank_line() + self.assertIn( + self.bank_journal_euro.suspense_account_id, + bank_stmt_line.mapped("move_id.line_ids.account_id"), + ) + with self.assertRaises(UserError): + bank_stmt_line.unreconcile_bank_line() + + # Testing widget + + def test_widget_invoice_clean(self): + """ + We want to test how the clean works on an already defined bank statement + """ + 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.assertFalse(f.add_account_move_line_id) + self.assertTrue(bank_stmt_line.can_reconcile) + bank_stmt_line.clean_reconcile() + self.assertFalse(bank_stmt_line.can_reconcile) + + def test_widget_invoice_delete(self): + """ + We need to test the possibility to remove a line from the reconcile widget + """ + 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.assertFalse(f.add_account_move_line_id) + self.assertTrue(f.can_reconcile) + f.manual_reference = "account.move.line;%s" % receivable1.id + self.assertEqual(f.manual_amount, -100) + f.manual_delete = True + self.assertFalse(f.can_reconcile) + + def test_widget_invoice_unselect(self): + """ + We want to test how selection and unselection of an account move lines is managed + by the system. + """ + 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"), + } + ) + 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) + 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.assertFalse(f.can_reconcile) + + def test_widget_invoice_change_partner(self): + """ + We want to know how the change of partner of + a bank statement line is managed + """ + 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"), + } + ) + liquidity_lines, suspense_lines, other_lines = bank_stmt_line._seek_for_lines() + with Form( + bank_stmt_line, + view="account_reconcile_oca.bank_statement_line_form_reconcile_view", + ) as f: + self.assertFalse(f.can_reconcile) + self.assertFalse(f.partner_id) + f.manual_reference = "account.move.line;%s" % liquidity_lines.id + f.manual_partner_id = inv1.partner_id + self.assertEqual(f.partner_id, inv1.partner_id) + bank_stmt_line.clean_reconcile() + # As we have a set a partner, the cleaning should assign the invoice automatically + self.assertTrue(bank_stmt_line.can_reconcile) + + def test_widget_model_clean(self): + """ + We want to test what happens when we select an reconcile model to fill a + bank statement. + """ + 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"), + } + ) + with Form( + bank_stmt_line, + view="account_reconcile_oca.bank_statement_line_form_reconcile_view", + ) as f: + self.assertFalse(f.can_reconcile) + f.manual_model_id = self.rule + self.assertTrue(f.can_reconcile) + # We need to check what happens when we uncheck it too + f.manual_model_id = self.env["account.reconcile.model"] + self.assertFalse(f.can_reconcile) + f.manual_model_id = self.rule + self.assertTrue(f.can_reconcile) + + # 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): + message = self.bank_journal_euro.get_rainbowman_message() + self.assertTrue(message) + self.acc_bank_stmt_line_model.create( + { + "name": "testLine", + "journal_id": self.bank_journal_euro.id, + "amount": 100, + "date": time.strftime("%Y-07-15"), + } + ) + self.env.flush_all() + message = self.bank_journal_euro.get_rainbowman_message() + self.assertFalse(message) + + def test_bank_statement_line_actions(self): + """ + Testing the actions of bank statement + """ + 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"), + } + ) + move_action = bank_stmt_line.action_show_move() + self.assertEqual( + bank_stmt_line.move_id, + self.env[move_action["res_model"]].browse(move_action["res_id"]), + ) + + # Testing filters + + def test_filter_partner(self): + """ + When a partner is set, the system might try to define an existent + invoice automatically + """ + inv1 = self.create_invoice(currency_id=self.currency_euro_id) + inv2 = self.create_invoice(currency_id=self.currency_euro_id) + partner = inv1.partner_id + + receivable1 = inv1.line_ids.filtered( + lambda l: l.account_id.account_type == "asset_receivable" + ) + self.assertTrue(receivable1) + receivable2 = inv2.line_ids.filtered( + lambda l: l.account_id.account_type == "asset_receivable" + ) + self.assertTrue(receivable2) + + 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"), + } + ) + + # Without a partner set, No default data + + bkstmt_data = bank_stmt_line.reconcile_data_info + mv_lines_ids = bkstmt_data["counterparts"] + self.assertNotIn(receivable1.id, mv_lines_ids) + self.assertNotIn(receivable2.id, mv_lines_ids) + + # This is like input a partner in the widget + + bank_stmt_line.partner_id = partner + bank_stmt_line.flush_recordset() + bank_stmt_line.invalidate_recordset() + bkstmt_data = bank_stmt_line.reconcile_data_info + mv_lines_ids = bkstmt_data["counterparts"] + + self.assertIn(receivable1.id, mv_lines_ids) + self.assertIn(receivable2.id, mv_lines_ids) + + # With a partner set, type the invoice reference in the filter + bank_stmt_line.payment_ref = inv1.payment_reference + bank_stmt_line.flush_recordset() + bank_stmt_line.invalidate_recordset() + bkstmt_data = bank_stmt_line.reconcile_data_info + mv_lines_ids = bkstmt_data["counterparts"] + + self.assertIn(receivable1.id, mv_lines_ids) + self.assertNotIn(receivable2.id, mv_lines_ids) + + def test_partner_name_with_parent(self): + parent_partner = self.env["res.partner"].create( + { + "name": "test", + } + ) + child_partner = self.env["res.partner"].create( + { + "name": "test", + "parent_id": parent_partner.id, + "type": "delivery", + } + ) + self.create_invoice_partner( + currency_id=self.currency_euro_id, partner_id=child_partner.id + ) + + 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", + "statement_id": bank_stmt.id, + "journal_id": self.bank_journal_euro.id, + "amount": 100, + "date": time.strftime("%Y-07-15"), + "payment_ref": "test", + "partner_name": "test", + } + ) + + bkstmt_data = bank_stmt_line.reconcile_data_info + self.assertEqual(len(bkstmt_data["counterparts"]), 1) + self.assertEqual( + self.env["account.move.line"] + .browse(bkstmt_data["counterparts"]) + .partner_id, + parent_partner, + ) diff --git a/account_reconcile_oca/views/account_account.xml b/account_reconcile_oca/views/account_account.xml new file mode 100644 index 00000000..a8207ce6 --- /dev/null +++ b/account_reconcile_oca/views/account_account.xml @@ -0,0 +1,23 @@ + + + + + + account.account.tree (in account_reconcile_oca) + account.account + + + + + + + diff --git a/account_reconcile_oca/views/account_account_reconcile.xml b/account_reconcile_oca/views/account_account_reconcile.xml new file mode 100644 index 00000000..63cf5687 --- /dev/null +++ b/account_reconcile_oca/views/account_account_reconcile.xml @@ -0,0 +1,165 @@ + + + + + + account.account.reconcile.form (in account_reconcile_oca) + account.account.reconcile + +
+ + + + + + + + + + + + + + + + + + + + +
+ + + account.account.reconcile.search (in account_reconcile_oca) + account.account.reconcile + + + + + + + + + account.account.reconcile.tree (in account_reconcile_oca) + account.account.reconcile + + + + + + + + + + + + account.account.reconcile.kanban (in account_reconcile_oca) + account.account.reconcile + + + + +
+
+ +
+
+ +
+
+
+
+
+
+
+ + + Reconcile + account.account.reconcile + kanban + [] + {'view_ref': 'account_reconcile_oca.account_account_reconcile_form_view'} + + + Reconcile + account.account.reconcile + kanban + [("partner_id", "=", active_id)] + {'view_ref': 'account_reconcile_oca.account_account_reconcile_form_view'} + + form + + + + Reconcile + account.account.reconcile + kanban + [("account_id", "=", active_id)] + {'view_ref': 'account_reconcile_oca.account_account_reconcile_form_view'} + + form + + + + Reconcile + + + + + +
diff --git a/account_reconcile_oca/views/account_bank_statement_line.xml b/account_reconcile_oca/views/account_bank_statement_line.xml new file mode 100644 index 00000000..dadd124e --- /dev/null +++ b/account_reconcile_oca/views/account_bank_statement_line.xml @@ -0,0 +1,326 @@ + + + + + + account.bank.statement.line.reconcile + account.bank.statement.line + + + + + + + + + + + + + + + + account.bank.statement.line.reconcile + account.bank.statement.line + + + + + + + +
+
+
+ +
+
+ + +
+ +
+
+ +
+
+
+ +
+
+
+ Reconciled +
+
+
+
+
+
+
+
+
+ + + account.bank.statement.line.form + account.bank.statement.line + +
+ + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + account.bank.statement.line.reconcile + account.bank.statement.line + 99 + +
+ + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+ + Reconcile bank statement lines + account.bank.statement.line + [('journal_id', '=', active_id)] + {'default_journal_id': active_id, 'search_default_not_reconciled': True, 'view_ref': 'account_reconcile_oca.bank_statement_line_form_reconcile_view'} + tree + + +

+ Nothing to reconcile +

+
+
+ + Reconcile bank statement lines + account.bank.statement.line + [('journal_id', '=', active_id)] + {'default_journal_id': active_id, 'view_ref': 'account_reconcile_oca.bank_statement_line_form_reconcile_view'} + tree + + +

+ Nothing to reconcile +

+
+
+ + + Reconcile bank statement lines + account.bank.statement.line + {'search_default_move_id': active_id} + tree + + +

+ Nothing to reconcile +

+
+
+ + Add an Statement + account.bank.statement.line + form + + new + +
diff --git a/account_reconcile_oca/views/account_journal.xml b/account_reconcile_oca/views/account_journal.xml new file mode 100644 index 00000000..3947f5c4 --- /dev/null +++ b/account_reconcile_oca/views/account_journal.xml @@ -0,0 +1,70 @@ + + + + + + account.journal.inherit.dashboard.kanban + account.journal + + + + + + + + + account.journal.inherit.dashboard.kanban + account.journal + + + + + + + + + +
+ +
+ + + +
+
+
+
+ + + + +
+
+
diff --git a/account_reconcile_oca/views/account_move.xml b/account_reconcile_oca/views/account_move.xml new file mode 100644 index 00000000..04ee2f11 --- /dev/null +++ b/account_reconcile_oca/views/account_move.xml @@ -0,0 +1,26 @@ + + + + + + account.move.form (in account_reconcile_oca) + account.move + + +
+
+
+
+ + + +
diff --git a/account_reconcile_oca/views/account_move_line.xml b/account_reconcile_oca/views/account_move_line.xml new file mode 100644 index 00000000..a5c680ac --- /dev/null +++ b/account_reconcile_oca/views/account_move_line.xml @@ -0,0 +1,98 @@ + + + + + + account.move.line.tree.reconcile + account.move.line + 99 + + + + + + + + . + @@ -268,6 +330,28 @@

+ + Reconcile bank statement lines + account.bank.statement.line + [('journal_id', '=', active_id)] + {'default_journal_id': active_id, 'search_default_to_check': True, 'view_ref': 'account_reconcile_oca.bank_statement_line_form_reconcile_view'} + tree + + +

+ Nothing to check +

+
+
diff --git a/account_reconcile_oca/views/account_move_line.xml b/account_reconcile_oca/views/account_move_line.xml index 2d87c460..9f8c163d 100644 --- a/account_reconcile_oca/views/account_move_line.xml +++ b/account_reconcile_oca/views/account_move_line.xml @@ -41,7 +41,7 @@ - + Date: Fri, 31 Mar 2023 17:25:38 +0000 Subject: [PATCH 05/94] [UPD] Update account_reconcile_oca.pot --- .../i18n/account_reconcile_oca.pot | 532 ++++++++++++++++++ 1 file changed, 532 insertions(+) create mode 100644 account_reconcile_oca/i18n/account_reconcile_oca.pot diff --git a/account_reconcile_oca/i18n/account_reconcile_oca.pot b/account_reconcile_oca/i18n/account_reconcile_oca.pot new file mode 100644 index 00000000..de392725 --- /dev/null +++ b/account_reconcile_oca/i18n/account_reconcile_oca.pot @@ -0,0 +1,532 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_reconcile_oca +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: account_reconcile_oca +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.bank_statement_line_form_reconcile_view +msgid "" +".\n" +"
\n" +" You might want to set the invoice as" +msgstr "" + +#. module: account_reconcile_oca +#. odoo-javascript +#: code:addons/account_reconcile_oca/static/src/xml/reconcile.xml:0 +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_account_reconcile__account_id +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.bank_statement_line_form_reconcile_view +#, python-format +msgid "Account" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model,name:account_reconcile_oca.model_account_account_reconcile +msgid "Account Account Reconcile" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model,name:account_reconcile_oca.model_account_reconcile_abstract +msgid "Account Reconcile Abstract" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_account_reconcile__add_account_move_line_id +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_bank_statement_line__add_account_move_line_id +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_reconcile_abstract__add_account_move_line_id +msgid "Add Account Move Line" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.actions.act_window,name:account_reconcile_oca.action_bank_statement_line_create +msgid "Add Bank Statement Line" +msgstr "" + +#. module: account_reconcile_oca +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.bank_statement_line_form_reconcile_view +msgid "Amount" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_bank_statement_line__analytic_distribution +msgid "Analytic Distribution" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_bank_statement_line__analytic_precision +msgid "Analytic Precision" +msgstr "" + +#. module: account_reconcile_oca +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.bank_statement_line_form_reconcile_view +msgid "Are you sure that the move should be unreconciled?" +msgstr "" + +#. module: account_reconcile_oca +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.account_move_line_search_reconcile_view +msgid "Bank" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model,name:account_reconcile_oca.model_account_bank_statement_line +msgid "Bank Statement Line" +msgstr "" + +#. module: account_reconcile_oca +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.account_move_form_view +msgid "Bank reconcile" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_bank_statement_line__can_reconcile +msgid "Can Reconcile" +msgstr "" + +#. module: account_reconcile_oca +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.bank_statement_line_form_add_view +msgid "Cancel" +msgstr "" + +#. module: account_reconcile_oca +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.account_move_line_search_reconcile_view +msgid "Cash" +msgstr "" + +#. module: account_reconcile_oca +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.bank_statement_line_form_reconcile_view +msgid "Chatter" +msgstr "" + +#. module: account_reconcile_oca +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.account_account_reconcile_form_view +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.bank_statement_line_form_reconcile_view +msgid "Clean" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_account_reconcile__company_id +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_bank_statement_line__company_id +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_reconcile_abstract__company_id +msgid "Company" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model.fields,help:account_reconcile_oca.field_account_bank_statement_line__company_id +msgid "Company related to this journal" +msgstr "" + +#. module: account_reconcile_oca +#. odoo-javascript +#: code:addons/account_reconcile_oca/static/src/xml/reconcile.xml:0 +#, python-format +msgid "Create" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_account_reconcile_data__create_uid +msgid "Created by" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_account_reconcile_data__create_date +msgid "Created on" +msgstr "" + +#. module: account_reconcile_oca +#. odoo-javascript +#: code:addons/account_reconcile_oca/static/src/xml/reconcile.xml:0 +#, python-format +msgid "Credit" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_account_reconcile__currency_id +msgid "Currency" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_account_reconcile_data__data +msgid "Data" +msgstr "" + +#. module: account_reconcile_oca +#. odoo-javascript +#: code:addons/account_reconcile_oca/static/src/xml/reconcile.xml:0 +#, python-format +msgid "Date" +msgstr "" + +#. module: account_reconcile_oca +#. odoo-javascript +#: code:addons/account_reconcile_oca/static/src/xml/reconcile.xml:0 +#, python-format +msgid "Debit" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_account_reconcile__display_name +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_account_reconcile_data__display_name +msgid "Display Name" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model.fields.selection,name:account_reconcile_oca.selection__account_journal__reconcile_mode__edit +msgid "Edit Move" +msgstr "" + +#. module: account_reconcile_oca +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.account_move_line_search_reconcile_view +msgid "From Trade Payable accounts" +msgstr "" + +#. module: account_reconcile_oca +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.account_move_line_search_reconcile_view +msgid "From Trade Receivable accounts" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_account_reconcile__id +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_account_reconcile_data__id +msgid "ID" +msgstr "" + +#. module: account_reconcile_oca +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.bank_statement_line_form_reconcile_view +msgid "Invoice" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_account_reconcile__is_reconciled +msgid "Is Reconciled" +msgstr "" + +#. module: account_reconcile_oca +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.account_journal_dashboard_kanban_view +msgid "Items" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model,name:account_reconcile_oca.model_account_journal +msgid "Journal" +msgstr "" + +#. module: account_reconcile_oca +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.account_move_line_search_reconcile_view +msgid "Journal Entry" +msgstr "" + +#. module: account_reconcile_oca +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.account_move_line_search_reconcile_view +msgid "Journal Item" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model.fields.selection,name:account_reconcile_oca.selection__account_journal__reconcile_mode__keep +msgid "Keep Suspense Accounts" +msgstr "" + +#. module: account_reconcile_oca +#. odoo-python +#: code:addons/account_reconcile_oca/models/account_bank_statement_line.py:0 +#, python-format +msgid "Keep suspense move lines mode cannot be unreconciled" +msgstr "" + +#. module: account_reconcile_oca +#. odoo-javascript +#: code:addons/account_reconcile_oca/static/src/xml/reconcile.xml:0 +#, python-format +msgid "Label" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_account_reconcile____last_update +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_account_reconcile_data____last_update +msgid "Last Modified on" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_account_reconcile_data__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_account_reconcile_data__write_date +msgid "Last Updated on" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_bank_statement_line__manual_account_id +msgid "Manual Account" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_bank_statement_line__manual_amount +msgid "Manual Amount" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_account_reconcile__manual_delete +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_bank_statement_line__manual_delete +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_reconcile_abstract__manual_delete +msgid "Manual Delete" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_bank_statement_line__manual_kind +msgid "Manual Kind" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_bank_statement_line__manual_line_id +msgid "Manual Line" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_bank_statement_line__manual_model_id +msgid "Manual Model" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_bank_statement_line__manual_move_id +msgid "Manual Move" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_bank_statement_line__manual_move_type +msgid "Manual Move Type" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_bank_statement_line__manual_name +msgid "Manual Name" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_bank_statement_line__manual_original_amount +msgid "Manual Original Amount" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_bank_statement_line__manual_partner_id +msgid "Manual Partner" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_account_reconcile__manual_reference +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_bank_statement_line__manual_reference +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_reconcile_abstract__manual_reference +msgid "Manual Reference" +msgstr "" + +#. module: account_reconcile_oca +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.bank_statement_line_form_reconcile_view +msgid "Manual operation" +msgstr "" + +#. module: account_reconcile_oca +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.account_move_line_search_reconcile_view +msgid "Miscellaneous" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_account_reconcile__name +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.bank_statement_line_form_reconcile_view +msgid "Name" +msgstr "" + +#. module: account_reconcile_oca +#. odoo-python +#: code:addons/account_reconcile_oca/models/account_bank_statement_line.py:0 +#, python-format +msgid "No supense lines are allowed when reconciling" +msgstr "" + +#. module: account_reconcile_oca +#: model_terms:ir.actions.act_window,help:account_reconcile_oca.action_bank_statement_line_reconcile_to_check +msgid "Nothing to check" +msgstr "" + +#. module: account_reconcile_oca +#: model_terms:ir.actions.act_window,help:account_reconcile_oca.action_bank_statement_line_move_view_reconcile +#: model_terms:ir.actions.act_window,help:account_reconcile_oca.action_bank_statement_line_reconcile +#: model_terms:ir.actions.act_window,help:account_reconcile_oca.action_bank_statement_line_reconcile_all +msgid "Nothing to reconcile" +msgstr "" + +#. module: account_reconcile_oca +#. odoo-javascript +#: code:addons/account_reconcile_oca/static/src/xml/reconcile.xml:0 +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_account_reconcile__partner_id +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.bank_statement_line_form_reconcile_view +#, python-format +msgid "Partner" +msgstr "" + +#. module: account_reconcile_oca +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.account_move_line_search_reconcile_view +msgid "Payable" +msgstr "" + +#. module: account_reconcile_oca +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.account_move_line_search_reconcile_view +msgid "Purchases" +msgstr "" + +#. module: account_reconcile_oca +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.account_move_line_search_reconcile_view +msgid "Receivable" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.actions.act_window,name:account_reconcile_oca.account_account_account_account_reconcile_act_window +#: model:ir.actions.act_window,name:account_reconcile_oca.account_account_reconcile_act_window +#: model:ir.actions.act_window,name:account_reconcile_oca.res_partner_account_account_reconcile_act_window +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_account_reconcile_data__reconcile_id +#: model:ir.ui.menu,name:account_reconcile_oca.account_account_reconcile_menu +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.account_account_reconcile_form_view +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.account_journal_dashboard_kanban_view +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.bank_statement_line_form_reconcile_view +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.view_account_list +msgid "Reconcile" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_bank_statement_line__reconcile_data +msgid "Reconcile Data" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_account_reconcile__reconcile_data_info +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_bank_statement_line__reconcile_data_info +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_reconcile_abstract__reconcile_data_info +msgid "Reconcile Data Info" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_bank_statement_line__reconcile_mode +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_journal__reconcile_mode +msgid "Reconcile Mode" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.actions.act_window,name:account_reconcile_oca.action_bank_statement_line_move_view_reconcile +#: model:ir.actions.act_window,name:account_reconcile_oca.action_bank_statement_line_reconcile +#: model:ir.actions.act_window,name:account_reconcile_oca.action_bank_statement_line_reconcile_all +#: model:ir.actions.act_window,name:account_reconcile_oca.action_bank_statement_line_reconcile_to_check +msgid "Reconcile bank statement lines" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model,name:account_reconcile_oca.model_account_account_reconcile_data +msgid "Reconcile data model to store user info" +msgstr "" + +#. module: account_reconcile_oca +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.bank_statement_line_reconcile_view +msgid "Reconciled" +msgstr "" + +#. module: account_reconcile_oca +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.bank_statement_line_form_reconcile_view +msgid "Reset" +msgstr "" + +#. module: account_reconcile_oca +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.account_move_line_search_reconcile_view +msgid "Sales" +msgstr "" + +#. module: account_reconcile_oca +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.bank_statement_line_form_add_view +msgid "Save" +msgstr "" + +#. module: account_reconcile_oca +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.account_move_line_search_reconcile_view +msgid "Search Journal Items" +msgstr "" + +#. module: account_reconcile_oca +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.bank_statement_line_form_reconcile_view +msgid "Set as Checked" +msgstr "" + +#. module: account_reconcile_oca +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.bank_statement_line_form_reconcile_view +msgid "To Check" +msgstr "" + +#. module: account_reconcile_oca +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.bank_statement_line_reconcile_view +msgid "To check" +msgstr "" + +#. module: account_reconcile_oca +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.account_journal_dashboard_kanban_view +msgid "Transactions" +msgstr "" + +#. module: account_reconcile_oca +#. odoo-python +#: code:addons/account_reconcile_oca/models/account_bank_statement_line.py:0 +#, python-format +msgid "Undefined" +msgstr "" + +#. module: account_reconcile_oca +#: model:ir.model.fields,field_description:account_reconcile_oca.field_account_account_reconcile_data__user_id +msgid "User" +msgstr "" + +#. module: account_reconcile_oca +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.bank_statement_line_form_reconcile_view +msgid "Validate" +msgstr "" + +#. module: account_reconcile_oca +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.account_move_line_tree_reconcile_view +msgid "View" +msgstr "" + +#. module: account_reconcile_oca +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.bank_statement_line_form_reconcile_view +msgid "View move" +msgstr "" + +#. module: account_reconcile_oca +#. odoo-python +#: code:addons/account_reconcile_oca/models/account_journal.py:0 +#, python-format +msgid "Well done! Everything has been reconciled" +msgstr "" + +#. module: account_reconcile_oca +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.bank_statement_line_form_reconcile_view +msgid "fully paid" +msgstr "" + +#. module: account_reconcile_oca +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.account_journal_dashboard_kanban_view +msgid "to check" +msgstr "" + +#. module: account_reconcile_oca +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.bank_statement_line_form_reconcile_view +msgid "will be reduced by" +msgstr "" + +#. module: account_reconcile_oca +#: model_terms:ir.ui.view,arch_db:account_reconcile_oca.bank_statement_line_form_reconcile_view +msgid "with an open amount" +msgstr "" From 813a9c107e4bcab1b4b0d8dd724039bcdc507021 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Fri, 31 Mar 2023 17:28:53 +0000 Subject: [PATCH 06/94] [UPD] README.rst --- account_reconcile_oca/README.rst | 8 ++++++++ account_reconcile_oca/static/description/index.html | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/account_reconcile_oca/README.rst b/account_reconcile_oca/README.rst index 9ec60fd9..1b743b5b 100644 --- a/account_reconcile_oca/README.rst +++ b/account_reconcile_oca/README.rst @@ -91,6 +91,14 @@ OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use. +.. |maintainer-etobella| image:: https://github.com/etobella.png?size=40px + :target: https://github.com/etobella + :alt: etobella + +Current `maintainer `__: + +|maintainer-etobella| + This module is part of the `OCA/account-reconcile `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/account_reconcile_oca/static/description/index.html b/account_reconcile_oca/static/description/index.html index ca0693c1..29b9aba2 100644 --- a/account_reconcile_oca/static/description/index.html +++ b/account_reconcile_oca/static/description/index.html @@ -3,7 +3,7 @@ - + Account Reconcile Oca + + +
+

Account Reconcile Model Oca

+ + +

Beta License: LGPL-3 OCA/account-reconcile Translate me on Weblate Try me on Runboat

+

This module restores account reconciliation models functions moved from +Odoo community to enterpise in V. 17.0

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Dixmit
  • +
  • Odoo
  • +
+
+
+

Contributors

+
    +
  • Dixmit
      +
    • Enric Tobella
    • +
    +
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/account-reconcile project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/account_reconcile_model_oca/tests/__init__.py b/account_reconcile_model_oca/tests/__init__.py new file mode 100644 index 00000000..4198497c --- /dev/null +++ b/account_reconcile_model_oca/tests/__init__.py @@ -0,0 +1 @@ +from . import test_reconciliation_match diff --git a/account_reconcile_model_oca/tests/common.py b/account_reconcile_model_oca/tests/common.py new file mode 100644 index 00000000..7b9a804c --- /dev/null +++ b/account_reconcile_model_oca/tests/common.py @@ -0,0 +1,243 @@ +import time + +from odoo import Command + +from odoo.addons.account.tests.common import AccountTestInvoicingCommon + + +class TestAccountReconciliationCommon(AccountTestInvoicingCommon): + + """Tests for reconciliation (account.tax) + + Test used to check that when doing a sale or purchase invoice in a different currency, + the result will be balanced. + """ + + @classmethod + def setUpClass(cls, chart_template_ref=None): + super().setUpClass(chart_template_ref=chart_template_ref) + + cls.company = cls.company_data["company"] + cls.company.currency_id = cls.env.ref("base.EUR") + + cls.partner_agrolait = cls.env["res.partner"].create( + { + "name": "Deco Addict", + "is_company": True, + "country_id": cls.env.ref("base.us").id, + } + ) + cls.partner_agrolait_id = cls.partner_agrolait.id + cls.currency_swiss_id = cls.env.ref("base.CHF").id + cls.currency_usd_id = cls.env.ref("base.USD").id + cls.currency_euro_id = cls.env.ref("base.EUR").id + cls.account_rcv = cls.company_data["default_account_receivable"] + cls.account_rsa = cls.company_data["default_account_payable"] + cls.product = cls.env["product.product"].create( + { + "name": "Product Product 4", + "standard_price": 500.0, + "list_price": 750.0, + "type": "consu", + "categ_id": cls.env.ref("product.product_category_all").id, + } + ) + + cls.bank_journal_euro = cls.env["account.journal"].create( + {"name": "Bank", "type": "bank", "code": "BNK67"} + ) + cls.account_euro = cls.bank_journal_euro.default_account_id + + cls.bank_journal_usd = cls.env["account.journal"].create( + { + "name": "Bank US", + "type": "bank", + "code": "BNK68", + "currency_id": cls.currency_usd_id, + } + ) + cls.account_usd = cls.bank_journal_usd.default_account_id + + cls.fx_journal = cls.company.currency_exchange_journal_id + cls.diff_income_account = cls.company.income_currency_exchange_account_id + cls.diff_expense_account = cls.company.expense_currency_exchange_account_id + + cls.expense_account = cls.company_data["default_account_expense"] + # cash basis intermediary account + cls.tax_waiting_account = cls.env["account.account"].create( + { + "name": "TAX_WAIT", + "code": "TWAIT", + "account_type": "liability_current", + "reconcile": True, + "company_id": cls.company.id, + } + ) + # cash basis final account + cls.tax_final_account = cls.env["account.account"].create( + { + "name": "TAX_TO_DEDUCT", + "code": "TDEDUCT", + "account_type": "asset_current", + "company_id": cls.company.id, + } + ) + cls.tax_base_amount_account = cls.env["account.account"].create( + { + "name": "TAX_BASE", + "code": "TBASE", + "account_type": "asset_current", + "company_id": cls.company.id, + } + ) + cls.company.account_cash_basis_base_account_id = cls.tax_base_amount_account.id + + # Journals + cls.purchase_journal = cls.company_data["default_journal_purchase"] + cls.cash_basis_journal = cls.env["account.journal"].create( + { + "name": "Test CABA", + "code": "tCABA", + "type": "general", + } + ) + cls.general_journal = cls.company_data["default_journal_misc"] + + # Tax Cash Basis + cls.tax_cash_basis = cls.env["account.tax"].create( + { + "name": "cash basis 20%", + "type_tax_use": "purchase", + "company_id": cls.company.id, + "country_id": cls.company.account_fiscal_country_id.id, + "amount": 20, + "tax_exigibility": "on_payment", + "cash_basis_transition_account_id": cls.tax_waiting_account.id, + "invoice_repartition_line_ids": [ + ( + 0, + 0, + { + "repartition_type": "base", + }, + ), + ( + 0, + 0, + { + "repartition_type": "tax", + "account_id": cls.tax_final_account.id, + }, + ), + ], + "refund_repartition_line_ids": [ + ( + 0, + 0, + { + "repartition_type": "base", + }, + ), + ( + 0, + 0, + { + "repartition_type": "tax", + "account_id": cls.tax_final_account.id, + }, + ), + ], + } + ) + cls.env["res.currency.rate"].create( + [ + { + "currency_id": cls.env.ref("base.EUR").id, + "name": "2010-01-02", + "rate": 1.0, + }, + { + "currency_id": cls.env.ref("base.USD").id, + "name": "2010-01-02", + "rate": 1.2834, + }, + { + "currency_id": cls.env.ref("base.USD").id, + "name": time.strftime("%Y-06-05"), + "rate": 1.5289, + }, + ] + ) + + def _create_invoice( + self, + move_type="out_invoice", + invoice_amount=50, + currency_id=None, + partner_id=None, + date_invoice=None, + payment_term_id=False, + auto_validate=False, + ): + date_invoice = date_invoice or time.strftime("%Y") + "-07-01" + + invoice_vals = { + "move_type": move_type, + "partner_id": partner_id or self.partner_agrolait_id, + "invoice_date": date_invoice, + "date": date_invoice, + "invoice_line_ids": [ + ( + 0, + 0, + { + "name": "product that cost %s" % invoice_amount, + "quantity": 1, + "price_unit": invoice_amount, + "tax_ids": [Command.set([])], + }, + ) + ], + } + + if payment_term_id: + invoice_vals["invoice_payment_term_id"] = payment_term_id + + if currency_id: + invoice_vals["currency_id"] = currency_id + + invoice = ( + self.env["account.move"] + .with_context(default_move_type=move_type) + .create(invoice_vals) + ) + if auto_validate: + invoice.action_post() + return invoice + + def create_invoice( + self, move_type="out_invoice", invoice_amount=50, currency_id=None + ): + return self._create_invoice( + move_type=move_type, + invoice_amount=invoice_amount, + currency_id=currency_id, + auto_validate=True, + ) + + def create_invoice_partner( + self, + move_type="out_invoice", + invoice_amount=50, + currency_id=None, + partner_id=False, + payment_term_id=False, + ): + return self._create_invoice( + move_type=move_type, + invoice_amount=invoice_amount, + currency_id=currency_id, + partner_id=partner_id, + payment_term_id=payment_term_id, + auto_validate=True, + ) diff --git a/account_reconcile_model_oca/tests/test_reconciliation_match.py b/account_reconcile_model_oca/tests/test_reconciliation_match.py new file mode 100644 index 00000000..26704d15 --- /dev/null +++ b/account_reconcile_model_oca/tests/test_reconciliation_match.py @@ -0,0 +1,1491 @@ +from freezegun import freeze_time + +from odoo import Command +from odoo.tests import tagged +from odoo.tests.common import Form + +from odoo.addons.account.tests.common import AccountTestInvoicingCommon + + +@tagged("post_install", "-at_install") +class TestReconciliationMatchingRules(AccountTestInvoicingCommon): + @classmethod + def setUpClass(cls, chart_template_ref=None): + super().setUpClass(chart_template_ref=chart_template_ref) + + ################# + # Company setup # + ################# + cls.currency_data_2 = cls.setup_multi_currency_data( + { + "name": "Dark Chocolate Coin", + "symbol": "🍫", + "currency_unit_label": "Dark Choco", + "currency_subunit_label": "Dark Cacao Powder", + }, + rate2016=10.0, + rate2017=20.0, + ) + + cls.company = cls.company_data["company"] + + cls.account_pay = cls.company_data["default_account_payable"] + cls.current_assets_account = cls.env["account.account"].search( + [ + ("account_type", "=", "asset_current"), + ("company_id", "=", cls.company.id), + ], + limit=1, + ) + + cls.bank_journal = cls.env["account.journal"].search( + [("type", "=", "bank"), ("company_id", "=", cls.company.id)], limit=1 + ) + cls.cash_journal = cls.env["account.journal"].search( + [("type", "=", "cash"), ("company_id", "=", cls.company.id)], limit=1 + ) + + cls.tax21 = cls.env["account.tax"].create( + { + "name": "21%", + "type_tax_use": "purchase", + "amount": 21, + } + ) + + cls.tax12 = cls.env["account.tax"].create( + { + "name": "12%", + "type_tax_use": "purchase", + "amount": 12, + } + ) + + cls.partner_1 = cls.env["res.partner"].create( + {"name": "partner_1", "company_id": cls.company.id} + ) + cls.partner_2 = cls.env["res.partner"].create( + {"name": "partner_2", "company_id": cls.company.id} + ) + cls.partner_3 = cls.env["res.partner"].create( + {"name": "partner_3", "company_id": cls.company.id} + ) + + ############### + # Rules setup # + ############### + cls.rule_1 = cls.env["account.reconcile.model"].create( + { + "name": "Invoices Matching Rule", + "sequence": "1", + "rule_type": "invoice_matching", + "auto_reconcile": False, + "match_nature": "both", + "match_same_currency": True, + "allow_payment_tolerance": True, + "payment_tolerance_type": "percentage", + "payment_tolerance_param": 0.0, + "match_partner": True, + "match_partner_ids": [ + (6, 0, (cls.partner_1 + cls.partner_2 + cls.partner_3).ids) + ], + "company_id": cls.company.id, + "line_ids": [(0, 0, {"account_id": cls.current_assets_account.id})], + } + ) + cls.rule_2 = cls.env["account.reconcile.model"].create( + { + "name": "write-off model", + "rule_type": "writeoff_suggestion", + "match_partner": True, + "match_partner_ids": [], + "line_ids": [(0, 0, {"account_id": cls.current_assets_account.id})], + } + ) + + ################## + # Invoices setup # + ################## + cls.invoice_line_1 = cls._create_invoice_line(100, cls.partner_1, "out_invoice") + cls.invoice_line_2 = cls._create_invoice_line(200, cls.partner_1, "out_invoice") + cls.invoice_line_3 = cls._create_invoice_line( + 300, cls.partner_1, "in_refund", name="RBILL/2019/09/0013" + ) + cls.invoice_line_4 = cls._create_invoice_line(1000, cls.partner_2, "in_invoice") + cls.invoice_line_5 = cls._create_invoice_line(600, cls.partner_3, "out_invoice") + cls.invoice_line_6 = cls._create_invoice_line( + 600, cls.partner_3, "out_invoice", ref="RF12 3456" + ) + cls.invoice_line_7 = cls._create_invoice_line( + 200, cls.partner_3, "out_invoice", pay_reference="RF12 3456" + ) + + #################### + # Statements setup # + #################### + # TODO : account_number, partner_name, transaction_type, narration + invoice_number = cls.invoice_line_1.move_id.name + ( + cls.bank_line_1, + cls.bank_line_2, + cls.bank_line_3, + cls.bank_line_4, + cls.bank_line_5, + cls.cash_line_1, + ) = cls.env["account.bank.statement.line"].create( + [ + { + "journal_id": cls.bank_journal.id, + "date": "2020-01-01", + "payment_ref": "invoice {}-{}".format( + *invoice_number.split("/")[1:] + ), + "partner_id": cls.partner_1.id, + "amount": 100, + "sequence": 1, + }, + { + "journal_id": cls.bank_journal.id, + "date": "2020-01-01", + "payment_ref": "xxxxx", + "partner_id": cls.partner_1.id, + "amount": 600, + "sequence": 2, + }, + { + "journal_id": cls.bank_journal.id, + "date": "2020-01-01", + "payment_ref": "nawak", + "narration": "Communication: RF12 3456", + "partner_id": cls.partner_3.id, + "amount": 600, + "sequence": 1, + }, + { + "journal_id": cls.bank_journal.id, + "date": "2020-01-01", + "payment_ref": "RF12 3456", + "partner_id": cls.partner_3.id, + "amount": 600, + "sequence": 2, + }, + { + "journal_id": cls.bank_journal.id, + "date": "2020-01-01", + "payment_ref": "baaaaah", + "ref": "RF12 3456", + "partner_id": cls.partner_3.id, + "amount": 600, + "sequence": 2, + }, + { + "journal_id": cls.cash_journal.id, + "date": "2020-01-01", + "payment_ref": "yyyyy", + "partner_id": cls.partner_2.id, + "amount": -1000, + "sequence": 1, + }, + ] + ) + + @classmethod + def _create_invoice_line( + cls, + amount, + partner, + move_type, + currency=None, + pay_reference=None, + ref=None, + name=None, + inv_date="2019-09-01", + ): + """Create an invoice on the fly.""" + invoice_form = Form( + cls.env["account.move"].with_context( + default_move_type=move_type, + default_invoice_date=inv_date, + default_date=inv_date, + ) + ) + invoice_form.partner_id = partner + if currency: + invoice_form.currency_id = currency + if pay_reference: + invoice_form.payment_reference = pay_reference + if ref: + invoice_form.ref = ref + if name: + invoice_form.name = name + with invoice_form.invoice_line_ids.new() as invoice_line_form: + invoice_line_form.name = "xxxx" + invoice_line_form.quantity = 1 + invoice_line_form.price_unit = amount + invoice_line_form.tax_ids.clear() + invoice = invoice_form.save() + invoice.action_post() + lines = invoice.line_ids + return lines.filtered( + lambda line: line.account_id.account_type + in ("asset_receivable", "liability_payable") + ) + + @classmethod + def _create_st_line( + cls, amount=1000.0, date="2019-01-01", payment_ref="turlututu", **kwargs + ): + st_line = cls.env["account.bank.statement.line"].create( + { + "journal_id": kwargs.get("journal_id", cls.bank_journal.id), + "amount": amount, + "date": date, + "payment_ref": payment_ref, + "partner_id": cls.partner_a.id, + **kwargs, + } + ) + return st_line + + @classmethod + def _create_reconcile_model(cls, **kwargs): + return cls.env["account.reconcile.model"].create( + { + "name": "test", + "rule_type": "invoice_matching", + "allow_payment_tolerance": True, + "payment_tolerance_type": "percentage", + "payment_tolerance_param": 0.0, + **kwargs, + "line_ids": [ + Command.create( + { + "account_id": cls.company_data[ + "default_account_revenue" + ].id, + "amount_type": "percentage", + "label": f"test {i}", + **line_vals, + } + ) + for i, line_vals in enumerate(kwargs.get("line_ids", [])) + ], + "partner_mapping_line_ids": [ + Command.create(line_vals) + for i, line_vals in enumerate( + kwargs.get("partner_mapping_line_ids", []) + ) + ], + } + ) + + @freeze_time("2020-01-01") + def _check_statement_matching(self, rules, expected_values_list): + for statement_line, expected_values in expected_values_list.items(): + res = rules._apply_rules(statement_line, statement_line._retrieve_partner()) + self.assertDictEqual(res, expected_values) + + def test_matching_fields(self): + # Check without restriction. + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: {"amls": self.invoice_line_1, "model": self.rule_1}, + self.bank_line_2: { + "amls": self.invoice_line_1 + + self.invoice_line_2 + + self.invoice_line_3, + "model": self.rule_1, + }, + self.cash_line_1: {"amls": self.invoice_line_4, "model": self.rule_1}, + }, + ) + + @freeze_time("2020-01-01") + def test_matching_fields_match_text_location(self): + st_line = self._create_st_line( + payment_ref="1111", ref="2222 3333", narration="4444 5555 6666" + ) + + inv1 = self._create_invoice_line( + 1000, self.partner_a, "out_invoice", pay_reference="bernard 1111 gagnant" + ) + inv2 = self._create_invoice_line( + 1000, self.partner_a, "out_invoice", pay_reference="2222 turlututu 3333" + ) + inv3 = self._create_invoice_line( + 1000, + self.partner_a, + "out_invoice", + pay_reference="4444 tsoin 5555 tsoin 6666", + ) + + rule = self._create_reconcile_model( + allow_payment_tolerance=False, + match_text_location_label=True, + match_text_location_reference=False, + match_text_location_note=False, + ) + self.assertDictEqual( + rule._apply_rules(st_line, st_line._retrieve_partner()), + {"amls": inv1, "model": rule}, + ) + + rule.match_text_location_reference = True + self.assertDictEqual( + rule._apply_rules(st_line, st_line._retrieve_partner()), + {"amls": inv2, "model": rule}, + ) + + rule.match_text_location_note = True + self.assertDictEqual( + rule._apply_rules(st_line, st_line._retrieve_partner()), + {"amls": inv3, "model": rule}, + ) + + def test_matching_fields_match_text_location_no_partner(self): + self.bank_line_2.unlink() # One line is enough for this test + self.bank_line_1.partner_id = None + + self.partner_1.name = "Bernard Gagnant" + + self.rule_1.write( + { + "match_partner": False, + "match_partner_ids": [(5, 0, 0)], + "line_ids": [(5, 0, 0)], + } + ) + + st_line_initial_vals = { + "ref": None, + "payment_ref": "nothing", + "narration": None, + } + recmod_initial_vals = { + "match_text_location_label": False, + "match_text_location_note": False, + "match_text_location_reference": False, + } + + rec_mod_options_to_fields = { + "match_text_location_label": "payment_ref", + "match_text_location_note": "narration", + "match_text_location_reference": "ref", + } + + for rec_mod_field, st_line_field in rec_mod_options_to_fields.items(): + self.rule_1.write({**recmod_initial_vals, rec_mod_field: True}) + # Fully reinitialize the statement line + self.bank_line_1.write(st_line_initial_vals) + + # Nothing should match + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: {}, + }, + ) + + # Test matching with the invoice ref + self.bank_line_1.write( + {st_line_field: self.invoice_line_1.move_id.payment_reference} + ) + + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: { + "amls": self.invoice_line_1, + "model": self.rule_1, + }, + }, + ) + + # Test matching with the partner name (reinitializing the statement line first) + self.bank_line_1.write( + {**st_line_initial_vals, st_line_field: self.partner_1.name} + ) + + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: { + "amls": self.invoice_line_1, + "model": self.rule_1, + }, + }, + ) + + def test_matching_fields_match_journal_ids(self): + self.rule_1.match_journal_ids |= self.cash_line_1.journal_id + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: {}, + self.bank_line_2: {}, + self.cash_line_1: {"amls": self.invoice_line_4, "model": self.rule_1}, + }, + ) + + def test_matching_fields_match_nature(self): + self.rule_1.match_nature = "amount_received" + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: {"amls": self.invoice_line_1, "model": self.rule_1}, + self.bank_line_2: { + "amls": self.invoice_line_2 + + self.invoice_line_3 + + self.invoice_line_1, + "model": self.rule_1, + }, + self.cash_line_1: {}, + }, + ) + self.rule_1.match_nature = "amount_paid" + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: {}, + self.bank_line_2: {}, + self.cash_line_1: {"amls": self.invoice_line_4, "model": self.rule_1}, + }, + ) + + def test_matching_fields_match_amount(self): + self.rule_1.match_amount = "lower" + self.rule_1.match_amount_max = 150 + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: {"amls": self.invoice_line_1, "model": self.rule_1}, + self.bank_line_2: {}, + self.cash_line_1: {}, + }, + ) + self.rule_1.match_amount = "greater" + self.rule_1.match_amount_min = 200 + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: {}, + self.bank_line_2: { + "amls": self.invoice_line_1 + + self.invoice_line_2 + + self.invoice_line_3, + "model": self.rule_1, + }, + self.cash_line_1: {"amls": self.invoice_line_4, "model": self.rule_1}, + }, + ) + self.rule_1.match_amount = "between" + self.rule_1.match_amount_min = 200 + self.rule_1.match_amount_max = 800 + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: {}, + self.bank_line_2: { + "amls": self.invoice_line_1 + + self.invoice_line_2 + + self.invoice_line_3, + "model": self.rule_1, + }, + self.cash_line_1: {}, + }, + ) + + def test_matching_fields_match_label(self): + self.rule_1.match_label = "contains" + self.rule_1.match_label_param = "yyyyy" + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: {}, + self.bank_line_2: {}, + self.cash_line_1: {"amls": self.invoice_line_4, "model": self.rule_1}, + }, + ) + self.rule_1.match_label = "not_contains" + self.rule_1.match_label_param = "xxxxx" + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: {"amls": self.invoice_line_1, "model": self.rule_1}, + self.bank_line_2: {}, + self.cash_line_1: {"amls": self.invoice_line_4, "model": self.rule_1}, + }, + ) + self.rule_1.match_label = "match_regex" + self.rule_1.match_label_param = "xxxxx|yyyyy" + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: {}, + self.bank_line_2: { + "amls": self.invoice_line_1 + + self.invoice_line_2 + + self.invoice_line_3, + "model": self.rule_1, + }, + self.cash_line_1: {"amls": self.invoice_line_4, "model": self.rule_1}, + }, + ) + + @freeze_time("2019-01-01") + def test_zero_payment_tolerance(self): + rule = self._create_reconcile_model(line_ids=[{}]) + + for inv_type, bsl_sign in (("out_invoice", 1), ("in_invoice", -1)): + invl = self._create_invoice_line( + 1000.0, self.partner_a, inv_type, inv_date="2019-01-01" + ) + + # Exact matching. + st_line = self._create_st_line(amount=bsl_sign * 1000.0) + self._check_statement_matching( + rule, + {st_line: {"amls": invl, "model": rule}}, + ) + + # No matching because there is no tolerance. + st_line = self._create_st_line(amount=bsl_sign * 990.0) + self._check_statement_matching( + rule, + {st_line: {}}, + ) + + # The payment amount is higher than the invoice one. + st_line = self._create_st_line(amount=bsl_sign * 1010.0) + self._check_statement_matching( + rule, + {st_line: {"amls": invl, "model": rule}}, + ) + + @freeze_time("2019-01-01") + def test_zero_payment_tolerance_auto_reconcile(self): + rule = self._create_reconcile_model( + auto_reconcile=True, + line_ids=[{}], + ) + + for inv_type, bsl_sign in (("out_invoice", 1), ("in_invoice", -1)): + invl = self._create_invoice_line( + 1000.0, + self.partner_a, + inv_type, + pay_reference="123456", + inv_date="2019-01-01", + ) + + # No matching because there is no tolerance. + st_line = self._create_st_line(amount=bsl_sign * 990.0) + self._check_statement_matching( + rule, + {st_line: {}}, + ) + + # The payment amount is higher than the invoice one. + st_line = self._create_st_line( + amount=bsl_sign * 1010.0, payment_ref="123456" + ) + self._check_statement_matching( + rule, + {st_line: {"amls": invl, "model": rule, "auto_reconcile": True}}, + ) + + @freeze_time("2019-01-01") + def test_not_enough_payment_tolerance(self): + rule = self._create_reconcile_model( + payment_tolerance_param=0.5, + line_ids=[{}], + ) + + for inv_type, bsl_sign in (("out_invoice", 1), ("in_invoice", -1)): + with self.subTest(inv_type=inv_type, bsl_sign=bsl_sign): + invl = self._create_invoice_line( + 1000.0, self.partner_a, inv_type, inv_date="2019-01-01" + ) + + # No matching because there is no enough tolerance. + st_line = self._create_st_line(amount=bsl_sign * 990.0) + self._check_statement_matching( + rule, + {st_line: {}}, + ) + + # The payment amount is higher than the invoice one. + # However, since the invoice amount is lower than the payment amount, + # the tolerance is not checked and the invoice line is matched. + st_line = self._create_st_line(amount=bsl_sign * 1010.0) + self._check_statement_matching( + rule, + {st_line: {"amls": invl, "model": rule}}, + ) + + @freeze_time("2019-01-01") + def test_enough_payment_tolerance(self): + rule = self._create_reconcile_model( + payment_tolerance_param=1.0, + line_ids=[{}], + ) + + for inv_type, bsl_sign in (("out_invoice", 1), ("in_invoice", -1)): + invl = self._create_invoice_line( + 1000.0, self.partner_a, inv_type, inv_date="2019-01-01" + ) + + # Enough tolerance to match the invoice line. + st_line = self._create_st_line(amount=bsl_sign * 990.0) + self._check_statement_matching( + rule, + {st_line: {"amls": invl, "model": rule, "status": "write_off"}}, + ) + + # The payment amount is higher than the invoice one. + # However, since the invoice amount is lower than the payment amount, + # the tolerance is not checked and the invoice line is matched. + st_line = self._create_st_line(amount=bsl_sign * 1010.0) + self._check_statement_matching( + rule, + {st_line: {"amls": invl, "model": rule}}, + ) + + @freeze_time("2019-01-01") + def test_enough_payment_tolerance_auto_reconcile_not_full(self): + rule = self._create_reconcile_model( + payment_tolerance_param=1.0, + auto_reconcile=True, + line_ids=[{"amount_type": "percentage_st_line", "amount_string": "200.0"}], + ) + + for inv_type, bsl_sign in (("out_invoice", 1), ("in_invoice", -1)): + invl = self._create_invoice_line( + 1000.0, + self.partner_a, + inv_type, + pay_reference="123456", + inv_date="2019-01-01", + ) + + # Enough tolerance to match the invoice line. + st_line = self._create_st_line( + amount=bsl_sign * 990.0, payment_ref="123456" + ) + self._check_statement_matching( + rule, + { + st_line: { + "amls": invl, + "model": rule, + "status": "write_off", + "auto_reconcile": True, + } + }, + ) + + @freeze_time("2019-01-01") + def test_allow_payment_tolerance_lower_amount(self): + rule = self._create_reconcile_model( + line_ids=[{"amount_type": "percentage_st_line"}] + ) + + for inv_type, bsl_sign in (("out_invoice", 1), ("in_invoice", -1)): + invl = self._create_invoice_line( + 990.0, self.partner_a, inv_type, inv_date="2019-01-01" + ) + st_line = self._create_st_line(amount=bsl_sign * 1000) + + # Partial reconciliation. + self._check_statement_matching( + rule, + {st_line: {"amls": invl, "model": rule}}, + ) + + @freeze_time("2019-01-01") + def test_enough_payment_tolerance_auto_reconcile(self): + rule = self._create_reconcile_model( + payment_tolerance_param=1.0, + auto_reconcile=True, + line_ids=[{}], + ) + + for inv_type, bsl_sign in (("out_invoice", 1), ("in_invoice", -1)): + invl = self._create_invoice_line( + 1000.0, + self.partner_a, + inv_type, + pay_reference="123456", + inv_date="2019-01-01", + ) + + # Enough tolerance to match the invoice line. + st_line = self._create_st_line( + amount=bsl_sign * 990.0, payment_ref="123456" + ) + self._check_statement_matching( + rule, + { + st_line: { + "amls": invl, + "model": rule, + "status": "write_off", + "auto_reconcile": True, + } + }, + ) + + @freeze_time("2019-01-01") + def test_percentage_st_line_auto_reconcile(self): + rule = self._create_reconcile_model( + payment_tolerance_param=1.0, + rule_type="writeoff_suggestion", + auto_reconcile=True, + line_ids=[ + { + "amount_type": "percentage_st_line", + "amount_string": "100.0", + "label": "A", + }, + { + "amount_type": "percentage_st_line", + "amount_string": "-100.0", + "label": "B", + }, + { + "amount_type": "percentage_st_line", + "amount_string": "100.0", + "label": "C", + }, + ], + ) + + for bsl_sign in (1, -1): + st_line = self._create_st_line(amount=bsl_sign * 1000.0) + self._check_statement_matching( + rule, + { + st_line: { + "model": rule, + "status": "write_off", + "auto_reconcile": True, + } + }, + ) + + def test_matching_fields_match_partner_category_ids(self): + test_category = self.env["res.partner.category"].create( + {"name": "Consulting Services"} + ) + self.partner_2.category_id = test_category + self.rule_1.match_partner_category_ids |= test_category + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: {}, + self.bank_line_2: {}, + self.cash_line_1: {"amls": self.invoice_line_4, "model": self.rule_1}, + }, + ) + self.rule_1.match_partner_category_ids = False + + def test_mixin_rules(self): + """Test usage of rules together.""" + # rule_1 is used before rule_2. + self.rule_1.sequence = 1 + self.rule_2.sequence = 2 + + self._check_statement_matching( + self.rule_1 + self.rule_2, + { + self.bank_line_1: { + "amls": self.invoice_line_1, + "model": self.rule_1, + }, + self.bank_line_2: { + "amls": self.invoice_line_2 + + self.invoice_line_3 + + self.invoice_line_1, + "model": self.rule_1, + }, + self.cash_line_1: {"amls": self.invoice_line_4, "model": self.rule_1}, + }, + ) + + # rule_2 is used before rule_1. + self.rule_1.sequence = 2 + self.rule_2.sequence = 1 + + self._check_statement_matching( + self.rule_1 + self.rule_2, + { + self.bank_line_1: { + "model": self.rule_2, + "auto_reconcile": False, + "status": "write_off", + }, + self.bank_line_2: { + "model": self.rule_2, + "auto_reconcile": False, + "status": "write_off", + }, + self.cash_line_1: { + "model": self.rule_2, + "auto_reconcile": False, + "status": "write_off", + }, + }, + ) + + # rule_2 is used before rule_1 but only on partner_1. + self.rule_2.match_partner_ids |= self.partner_1 + + self._check_statement_matching( + self.rule_1 + self.rule_2, + { + self.bank_line_1: { + "model": self.rule_2, + "auto_reconcile": False, + "status": "write_off", + }, + self.bank_line_2: { + "model": self.rule_2, + "auto_reconcile": False, + "status": "write_off", + }, + self.cash_line_1: {"amls": self.invoice_line_4, "model": self.rule_1}, + }, + ) + + def test_auto_reconcile(self): + """Test auto reconciliation.""" + self.bank_line_1.amount += 5 + + self.rule_1.sequence = 2 + self.rule_1.auto_reconcile = True + self.rule_1.payment_tolerance_param = 10.0 + self.rule_2.sequence = 1 + self.rule_2.match_partner_ids |= self.partner_2 + self.rule_2.auto_reconcile = True + + self._check_statement_matching( + self.rule_1 + self.rule_2, + { + self.bank_line_1: { + "amls": self.invoice_line_1, + "model": self.rule_1, + "auto_reconcile": True, + }, + self.bank_line_2: { + "amls": self.invoice_line_1 + + self.invoice_line_2 + + self.invoice_line_3, + "model": self.rule_1, + }, + self.cash_line_1: { + "model": self.rule_2, + "status": "write_off", + "auto_reconcile": True, + }, + }, + ) + + def test_larger_invoice_auto_reconcile(self): + """Test auto reconciliation with an invoice with larger amount than the + statement line's, for rules without write-offs.""" + self.bank_line_1.amount = 40 + self.invoice_line_1.move_id.payment_reference = self.bank_line_1.payment_ref + + self.rule_1.sequence = 2 + self.rule_1.allow_payment_tolerance = False + self.rule_1.auto_reconcile = True + self.rule_1.line_ids = [(5, 0, 0)] + + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: { + "amls": self.invoice_line_1, + "model": self.rule_1, + "auto_reconcile": True, + }, + self.bank_line_2: { + "amls": self.invoice_line_1 + + self.invoice_line_2 + + self.invoice_line_3, + "model": self.rule_1, + }, + }, + ) + + def test_auto_reconcile_with_tax(self): + """Test auto reconciliation with a tax amount included in the bank statement line""" + self.rule_1.write( + { + "auto_reconcile": True, + "rule_type": "writeoff_suggestion", + "line_ids": [ + ( + 1, + self.rule_1.line_ids.id, + { + "amount": 50, + "force_tax_included": True, + "tax_ids": [(6, 0, self.tax21.ids)], + }, + ), + ( + 0, + 0, + { + "amount": 100, + "force_tax_included": False, + "tax_ids": [(6, 0, self.tax12.ids)], + "account_id": self.current_assets_account.id, + }, + ), + ], + } + ) + + self.bank_line_1.amount = -121 + + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: { + "model": self.rule_1, + "status": "write_off", + "auto_reconcile": True, + }, + self.bank_line_2: { + "model": self.rule_1, + "status": "write_off", + "auto_reconcile": True, + }, + }, + ) + + def test_auto_reconcile_with_tax_fpos(self): + """Test the fiscal positions are applied by reconcile models when using taxes.""" + self.rule_1.write( + { + "auto_reconcile": True, + "rule_type": "writeoff_suggestion", + "line_ids": [ + ( + 1, + self.rule_1.line_ids.id, + { + "amount": 100, + "force_tax_included": True, + "tax_ids": [(6, 0, self.tax21.ids)], + }, + ) + ], + } + ) + + self.partner_1.country_id = self.env.ref("base.lu") + belgium = self.env.ref("base.be") + self.partner_2.country_id = belgium + + self.bank_line_2.partner_id = self.partner_2 + + self.bank_line_1.amount = -121 + self.bank_line_2.amount = -112 + + self.env["account.fiscal.position"].create( + { + "name": "Test", + "country_id": belgium.id, + "auto_apply": True, + "tax_ids": [ + Command.create( + { + "tax_src_id": self.tax21.id, + "tax_dest_id": self.tax12.id, + } + ), + ], + } + ) + + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: { + "model": self.rule_1, + "status": "write_off", + "auto_reconcile": True, + }, + self.bank_line_2: { + "model": self.rule_1, + "status": "write_off", + "auto_reconcile": True, + }, + }, + ) + + def test_reverted_move_matching(self): + partner = self.partner_1 + AccountMove = self.env["account.move"] + move = AccountMove.create( + { + "journal_id": self.bank_journal.id, + "line_ids": [ + ( + 0, + 0, + { + "account_id": self.account_pay.id, + "partner_id": partner.id, + "name": "One of these days", + "debit": 10, + }, + ), + ( + 0, + 0, + { + "account_id": self.bank_journal.company_id.account_journal_payment_credit_account_id.id, + "partner_id": partner.id, + "name": "I'm gonna cut you into little pieces", + "credit": 10, + }, + ), + ], + } + ) + + payment_bnk_line = move.line_ids.filtered( + lambda line: line.account_id + == self.bank_journal.company_id.account_journal_payment_credit_account_id + ) + + move.action_post() + move_reversed = move._reverse_moves() + self.assertTrue(move_reversed.exists()) + + self.bank_line_1.write( + { + "payment_ref": "8", + "partner_id": partner.id, + "amount": -10, + } + ) + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: {"amls": payment_bnk_line, "model": self.rule_1}, + self.bank_line_2: { + "amls": self.invoice_line_1 + + self.invoice_line_2 + + self.invoice_line_3, + "model": self.rule_1, + }, + }, + ) + + def test_match_different_currencies(self): + partner = self.env["res.partner"].create({"name": "Bernard Gagnant"}) + self.rule_1.write( + {"match_partner_ids": [(6, 0, partner.ids)], "match_same_currency": False} + ) + + currency_inv = self.env.ref("base.EUR") + currency_inv.active = True + currency_statement = self.env.ref("base.JPY") + + currency_statement.active = True + + invoice_line = self._create_invoice_line( + 100, partner, "out_invoice", currency=currency_inv + ) + + self.bank_line_1.write( + { + "partner_id": partner.id, + "foreign_currency_id": currency_statement.id, + "amount_currency": 100, + "payment_ref": "test", + } + ) + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: {"amls": invoice_line, "model": self.rule_1}, + self.bank_line_2: {}, + }, + ) + + def test_invoice_matching_rule_no_partner(self): + """Tests that a statement line without any partner can be matched to the + right invoice if they have the same payment reference. + """ + self.invoice_line_1.move_id.write({"payment_reference": "Tournicoti66"}) + self.rule_1.allow_payment_tolerance = False + + self.bank_line_1.write( + { + "payment_ref": "Tournicoti66", + "partner_id": None, + "amount": 95, + } + ) + + self.rule_1.write( + { + "line_ids": [(5, 0, 0)], + "match_partner": False, + "match_label": "contains", + "match_label_param": "Tournicoti", # So that we only match what we want to test + } + ) + + # TODO: 'invoice_line_1' has no reason to match 'bank_line_1' here... to check + # self._check_statement_matching(self.rule_1, { + # self.bank_line_1: {'amls': self.invoice_line_1, 'model': self.rule_1}, + # self.bank_line_2: {'amls': []}, + # }, self.bank_st) + + def test_inv_matching_rule_auto_rec_no_partner_with_writeoff(self): + self.invoice_line_1.move_id.ref = "doudlidou3555" + + self.bank_line_1.write( + { + "payment_ref": "doudlidou3555", + "partner_id": None, + "amount": 95, + } + ) + + self.rule_1.write( + { + "match_partner": False, + "match_label": "contains", + "match_label_param": "doudlidou", # So that we only match what we want to test + "payment_tolerance_param": 10.0, + "auto_reconcile": True, + } + ) + + # Check bank reconciliation + + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: { + "amls": self.invoice_line_1, + "model": self.rule_1, + "status": "write_off", + "auto_reconcile": True, + }, + self.bank_line_2: {}, + }, + ) + + def test_partner_mapping_rule(self): + st_line = self._create_st_line(partner_id=None, payment_ref="toto42") + + rule = self._create_reconcile_model( + partner_mapping_line_ids=[ + { + "partner_id": self.partner_1.id, + "payment_ref_regex": "toto.*", + } + ], + ) + + # Matching using the regex on payment_ref. + self.assertEqual(st_line._retrieve_partner(), self.partner_1) + + rule.partner_mapping_line_ids.narration_regex = ".*coincoin" + + # No match because the narration is not matching the regex. + self.assertEqual(st_line._retrieve_partner(), self.env["res.partner"]) + + st_line.narration = "42coincoin" + + # Matching is back thanks to "coincoin". + self.assertEqual(st_line._retrieve_partner(), self.partner_1) + + def test_partner_name_in_communication(self): + self.invoice_line_1.partner_id.write({"name": "Archibald Haddock"}) + self.bank_line_1.write( + {"partner_id": None, "payment_ref": "1234//HADDOCK-Archibald"} + ) + self.bank_line_2.write({"partner_id": None}) + self.rule_1.write({"match_partner": False}) + + # bank_line_1 should match, as its communication contains the invoice's partner name + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: {"amls": self.invoice_line_1, "model": self.rule_1}, + self.bank_line_2: {}, + }, + ) + + def test_partner_name_with_regexp_chars(self): + self.invoice_line_1.partner_id.write({"name": "Archibald + Haddock"}) + self.bank_line_1.write( + {"partner_id": None, "payment_ref": "1234//HADDOCK+Archibald"} + ) + self.bank_line_2.write({"partner_id": None}) + self.rule_1.write({"match_partner": False}) + + # The query should still work + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: {"amls": self.invoice_line_1, "model": self.rule_1}, + self.bank_line_2: {}, + }, + ) + + def test_match_multi_currencies(self): + """Ensure the matching of candidates is made using the right statement line currency. + In this test, the value of the statement line is 100 USD = 300 GOL = 900 DAR and we want to match two journal + items of: + - 100 USD = 200 GOL (= 600 DAR from the statement line point of view) + - 14 USD = 280 DAR + Both journal items should be suggested to the user because they represents 98% of the statement line amount + (DAR). + """ + partner = self.env["res.partner"].create({"name": "Bernard Perdant"}) + + journal = self.env["account.journal"].create( + { + "name": "test_match_multi_currencies", + "code": "xxxx", + "type": "bank", + "currency_id": self.currency_data["currency"].id, + } + ) + + matching_rule = self.env["account.reconcile.model"].create( + { + "name": "test_match_multi_currencies", + "rule_type": "invoice_matching", + "match_partner": True, + "match_partner_ids": [(6, 0, partner.ids)], + "allow_payment_tolerance": True, + "payment_tolerance_type": "percentage", + "payment_tolerance_param": 5.0, + "match_same_currency": False, + "company_id": self.company_data["company"].id, + "past_months_limit": False, + } + ) + + statement_line = self.env[ + "account.bank.statement.line" + ].create( + { + "journal_id": journal.id, + "date": "2016-01-01", + "payment_ref": "line", + "partner_id": partner.id, + "foreign_currency_id": self.currency_data_2["currency"].id, + "amount": 300.0, # Rate is 3 GOL = 1 USD in 2016. + "amount_currency": 900.0, # Rate is 10 DAR = 1 USD in 2016 but the rate used by the bank is 9:1. + } + ) + + move = self.env["account.move"].create( + { + "move_type": "entry", + "date": "2017-01-01", + "journal_id": self.company_data["default_journal_misc"].id, + "line_ids": [ + # Rate is 2 GOL = 1 USD in 2017. + # The statement line will consider this line equivalent to 600 DAR. + ( + 0, + 0, + { + "account_id": self.company_data[ + "default_account_receivable" + ].id, + "partner_id": partner.id, + "currency_id": self.currency_data["currency"].id, + "debit": 100.0, + "credit": 0.0, + "amount_currency": 200.0, + }, + ), + # Rate is 20 GOL = 1 USD in 2017. + ( + 0, + 0, + { + "account_id": self.company_data[ + "default_account_receivable" + ].id, + "partner_id": partner.id, + "currency_id": self.currency_data_2["currency"].id, + "debit": 14.0, + "credit": 0.0, + "amount_currency": 280.0, + }, + ), + # Line to balance the journal entry: + ( + 0, + 0, + { + "account_id": self.company_data[ + "default_account_revenue" + ].id, + "debit": 0.0, + "credit": 114.0, + }, + ), + ], + } + ) + move.action_post() + + move_line_1 = move.line_ids.filtered(lambda line: line.debit == 100.0) + move_line_2 = move.line_ids.filtered(lambda line: line.debit == 14.0) + + self._check_statement_matching( + matching_rule, + { + statement_line: { + "amls": move_line_1 + move_line_2, + "model": matching_rule, + } + }, + ) + + @freeze_time("2020-01-01") + def test_matching_with_write_off_foreign_currency(self): + journal_foreign_curr = self.company_data["default_journal_bank"].copy() + journal_foreign_curr.currency_id = self.currency_data["currency"] + + reco_model = self._create_reconcile_model( + auto_reconcile=True, + rule_type="writeoff_suggestion", + line_ids=[ + { + "amount_type": "percentage", + "amount": 100.0, + "account_id": self.company_data["default_account_revenue"].id, + } + ], + ) + + st_line = self._create_st_line( + amount=100.0, payment_ref="123456", journal_id=journal_foreign_curr.id + ) + self._check_statement_matching( + reco_model, + { + st_line: { + "model": reco_model, + "status": "write_off", + "auto_reconcile": True, + }, + }, + ) + + def test_payment_similar_communications(self): + def create_payment_line(amount, memo, partner): + payment = self.env["account.payment"].create( + { + "amount": amount, + "payment_type": "inbound", + "partner_type": "customer", + "partner_id": partner.id, + "ref": memo, + "destination_account_id": self.company_data[ + "default_account_receivable" + ].id, + } + ) + payment.action_post() + + return payment.line_ids.filtered( + lambda x: x.account_id.account_type + not in {"asset_receivable", "liability_payable"} + ) + + payment_partner = self.env["res.partner"].create( + { + "name": "Bernard Gagnant", + } + ) + + self.rule_1.match_partner_ids = [(6, 0, payment_partner.ids)] + + pmt_line_1 = create_payment_line(500, "a1b2c3", payment_partner) + pmt_line_2 = create_payment_line(500, "a1b2c3", payment_partner) + create_payment_line(500, "d1e2f3", payment_partner) + + self.bank_line_1.write( + { + "amount": 1000, + "payment_ref": "a1b2c3", + "partner_id": payment_partner.id, + } + ) + self.bank_line_2.unlink() + self.rule_1.allow_payment_tolerance = False + + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: { + "amls": pmt_line_1 + pmt_line_2, + "model": self.rule_1, + "status": "write_off", + }, + }, + ) + + def test_no_amount_check_keep_first(self): + """In case the reconciliation model doesn't check the total amount of the candidates, + we still don't want to suggest more than are necessary to match the statement. + For example, if a statement line amounts to 250 and is to be matched with three invoices + of 100, 200 and 300 (retrieved in this order), only 100 and 200 should be proposed. + """ + self.rule_1.allow_payment_tolerance = False + self.bank_line_2.amount = 250 + self.bank_line_1.partner_id = None + + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: {}, + self.bank_line_2: { + "amls": self.invoice_line_1 + self.invoice_line_2, + "model": self.rule_1, + "status": "write_off", + }, + }, + ) + + def test_no_amount_check_exact_match(self): + """If a reconciliation model finds enough candidates for a full reconciliation, + it should still check the following candidates, in case one of them exactly + matches the amount of the statement line. If such a candidate exist, all the + other ones are disregarded. + """ + self.rule_1.allow_payment_tolerance = False + self.bank_line_2.amount = 300 + self.bank_line_1.partner_id = None + + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: {}, + self.bank_line_2: { + "amls": self.invoice_line_3, + "model": self.rule_1, + "status": "write_off", + }, + }, + ) From 132a333c468fd238fb8bacda3793b500fc9a1a11 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Fri, 9 Feb 2024 15:12:01 +0100 Subject: [PATCH 78/94] [FIX] account_reconcile_oca: make widget work --- account_reconcile_oca/models/account_account_reconcile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account_reconcile_oca/models/account_account_reconcile.py b/account_reconcile_oca/models/account_account_reconcile.py index 9c8d1540..4171315e 100644 --- a/account_reconcile_oca/models/account_account_reconcile.py +++ b/account_reconcile_oca/models/account_account_reconcile.py @@ -52,7 +52,7 @@ class AccountAccountReconcile(models.Model): FALSE as is_reconciled, aml.currency_id as currency_id, a.company_id, - false as foreign_currency_id + null as foreign_currency_id """ def _from(self): From 7881a52b02f7af80729df4053911cc2679bb3c34 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Mon, 26 Feb 2024 23:03:53 +0100 Subject: [PATCH 79/94] fixup! Account_reconcile_oca: remove console log --- .../static/src/js/widgets/reconcile_chatter_field.esm.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/account_reconcile_oca/static/src/js/widgets/reconcile_chatter_field.esm.js b/account_reconcile_oca/static/src/js/widgets/reconcile_chatter_field.esm.js index 72a045a4..4f401d5c 100644 --- a/account_reconcile_oca/static/src/js/widgets/reconcile_chatter_field.esm.js +++ b/account_reconcile_oca/static/src/js/widgets/reconcile_chatter_field.esm.js @@ -8,12 +8,7 @@ import {Chatter} from "@mail/core/web/chatter"; const {Component} = owl; -export class AccountReconcileChatterWidget extends Component { - setup() { - super.setup(); - console.log(this); - } -} +export class AccountReconcileChatterWidget extends Component {} AccountReconcileChatterWidget.props = {...standardFieldProps}; AccountReconcileChatterWidget.template = "account_reconcile_oca.AccountReconcileChatterWidget"; From c8f85a2775037eb0c6a48821457649bdb1c39983 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Mon, 26 Feb 2024 23:09:20 +0100 Subject: [PATCH 80/94] fixup! account_reconcile_oca: use the proper tags --- account_reconcile_oca/views/account_move_line.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/account_reconcile_oca/views/account_move_line.xml b/account_reconcile_oca/views/account_move_line.xml index bef2829b..9ad56ee7 100644 --- a/account_reconcile_oca/views/account_move_line.xml +++ b/account_reconcile_oca/views/account_move_line.xml @@ -19,8 +19,8 @@ - - + + From b24ca9a834df00e3cfc0b5243461750e7c295edc Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Mon, 26 Feb 2024 23:18:37 +0100 Subject: [PATCH 81/94] fixup! account_reconcile_oca: Fix opening --- .../js/reconcile/reconcile_controller.esm.js | 3 +- .../static/src/scss/reconcile.scss | 15 ---------- .../views/account_account_reconcile.xml | 4 +-- .../views/account_bank_statement_line.xml | 30 ++++++++++++++----- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/account_reconcile_oca/static/src/js/reconcile/reconcile_controller.esm.js b/account_reconcile_oca/static/src/js/reconcile/reconcile_controller.esm.js index cf80ed8f..65dfa94c 100644 --- a/account_reconcile_oca/static/src/js/reconcile/reconcile_controller.esm.js +++ b/account_reconcile_oca/static/src/js/reconcile/reconcile_controller.esm.js @@ -1,5 +1,5 @@ /** @odoo-module */ -const {useState, useSubEnv} = owl; +const {useState, useSubEnv, onMounted} = owl; import {useBus, useService} from "@web/core/utils/hooks"; import {KanbanController} from "@web/views/kanban/kanban_controller"; import {View} from "@web/views/view"; @@ -22,6 +22,7 @@ export class ReconcileController extends KanbanController { useBus(this.model.bus, "update", () => { this.selectRecord(); }); + onMounted(() => this.selectRecord()); } exposeController(controller) { this.form_controller = controller; diff --git a/account_reconcile_oca/static/src/scss/reconcile.scss b/account_reconcile_oca/static/src/scss/reconcile.scss index bd33d4c2..bf1d0175 100644 --- a/account_reconcile_oca/static/src/scss/reconcile.scss +++ b/account_reconcile_oca/static/src/scss/reconcile.scss @@ -26,23 +26,8 @@ height: 100%; } .o_form_view { - .btn-info:not(.dropdown-toggle):not(.dropdown-item) { - text-transform: uppercase; - } .o_form_statusbar.o_account_reconcile_oca_statusbar { - .btn:not(.dropdown-toggle):not(.dropdown-item) { - text-transform: uppercase; - } height: 40px; - > .o_statusbar_buttons { - height: 100%; - > .btn { - margin: 0; - height: 100%; - padding: 10px; - border-radius: 0; - } - } } .o_field_account_reconcile_oca_data { .o_field_account_reconcile_oca_balance_float { diff --git a/account_reconcile_oca/views/account_account_reconcile.xml b/account_reconcile_oca/views/account_account_reconcile.xml index 58c2d7f5..3ed3a540 100644 --- a/account_reconcile_oca/views/account_account_reconcile.xml +++ b/account_reconcile_oca/views/account_account_reconcile.xml @@ -21,14 +21,14 @@ name="reconcile" type="object" string="Reconcile" - class="btn btn-primary" + class="btn btn-primary mx-1" invisible="is_reconciled == True" />
diff --git a/account_reconcile_oca/views/account_bank_statement_line.xml b/account_reconcile_oca/views/account_bank_statement_line.xml index eb5a1f36..debca12d 100644 --- a/account_reconcile_oca/views/account_bank_statement_line.xml +++ b/account_reconcile_oca/views/account_bank_statement_line.xml @@ -117,6 +117,19 @@ + + account.bank.statement.line.search + account.bank.statement.line + + + + + + + account.bank.statement.line.reconcile account.bank.statement.line @@ -130,14 +143,14 @@ type="object" string="Validate" accesskey="v" - class="btn btn-primary" + class="btn btn-primary mx-1" invisible="is_reconciled == True or can_reconcile == False" />
@@ -367,7 +381,7 @@ > Reconcile bank statement lines account.bank.statement.line - {'search_default_move_id': id} + {'search_default_move_id': active_id} kanban Date: Sat, 20 Apr 2024 23:48:56 +0200 Subject: [PATCH 82/94] [FIX] account_reconcile_oca: Fix kanban record division --- account_reconcile_oca/static/src/scss/reconcile.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/account_reconcile_oca/static/src/scss/reconcile.scss b/account_reconcile_oca/static/src/scss/reconcile.scss index bf1d0175..f8fa60fe 100644 --- a/account_reconcile_oca/static/src/scss/reconcile.scss +++ b/account_reconcile_oca/static/src/scss/reconcile.scss @@ -7,6 +7,8 @@ height: 100%; .o_kanban_renderer.o_kanban_ungrouped .o_kanban_record { margin: 0 0 0; + min-width: fit-content; + width: 100%; > div { border-right: thick solid rgba(0, 0, 0, 0); } From 2430b8082cbf02579b0dec1334b94549c41f7d20 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Sun, 21 Apr 2024 02:09:20 +0200 Subject: [PATCH 83/94] [IMP] account_reconcile_oca: Add balance --- .../models/account_journal.py | 1 + .../js/reconcile/reconcile_controller.esm.js | 44 ++++++++++++++++++- .../js/reconcile/reconcile_renderer.esm.js | 6 ++- .../reconcile_form_controller.esm.js | 3 ++ .../js/widgets/reconcile_data_widget.esm.js | 8 ++-- .../static/src/xml/reconcile.xml | 11 +++++ .../views/account_bank_statement_line.xml | 2 +- 7 files changed, 66 insertions(+), 9 deletions(-) diff --git a/account_reconcile_oca/models/account_journal.py b/account_reconcile_oca/models/account_journal.py index 8c6409c5..bc4ded92 100644 --- a/account_reconcile_oca/models/account_journal.py +++ b/account_reconcile_oca/models/account_journal.py @@ -12,6 +12,7 @@ class AccountJournal(models.Model): default="edit", required=True, ) + company_currency_id = fields.Many2one(related="company_id.currency_id") def get_rainbowman_message(self): self.ensure_one() diff --git a/account_reconcile_oca/static/src/js/reconcile/reconcile_controller.esm.js b/account_reconcile_oca/static/src/js/reconcile/reconcile_controller.esm.js index 65dfa94c..4639d6a2 100644 --- a/account_reconcile_oca/static/src/js/reconcile/reconcile_controller.esm.js +++ b/account_reconcile_oca/static/src/js/reconcile/reconcile_controller.esm.js @@ -1,14 +1,17 @@ /** @odoo-module */ -const {useState, useSubEnv, onMounted} = owl; +const {onMounted, onWillStart, useState, useSubEnv} = owl; import {useBus, useService} from "@web/core/utils/hooks"; import {KanbanController} from "@web/views/kanban/kanban_controller"; import {View} from "@web/views/view"; +import {formatMonetary} from "@web/views/fields/formatters"; export class ReconcileController extends KanbanController { async setup() { super.setup(); this.state = useState({ selectedRecordId: null, + journalBalance: 0, + currency: false, }); useSubEnv({ parentController: this, @@ -22,7 +25,43 @@ export class ReconcileController extends KanbanController { useBus(this.model.bus, "update", () => { this.selectRecord(); }); - onMounted(() => this.selectRecord()); + onWillStart(() => { + this.updateJournalInfo(); + }); + onMounted(() => { + this.selectRecord(); + }); + } + get journalId() { + if (this.props.resModel === "account.bank.statement.line") { + return this.props.context.active_id; + } + return false; + } + async updateJournalInfo() { + var journalId = this.journalId; + if (!journalId) { + return; + } + var result = await this.orm.call("account.journal", "read", [ + [journalId], + ["current_statement_balance", "currency_id", "company_currency_id"], + ]); + this.state.journalBalance = result[0].current_statement_balance; + this.state.currency = (result[0].currency_id || + result[0].company_currency_id)[0]; + } + get journalBalanceStr() { + if (!this.state.journalBalance) { + return ""; + } + console.log(this.state, { + currencyId: this.state.currency, + humanReadable: true, + }); + return formatMonetary(this.state.journalBalance, { + currencyId: this.state.currency, + }); } exposeController(controller) { this.form_controller = controller; @@ -48,6 +87,7 @@ export class ReconcileController extends KanbanController { return { resId: this.state.selectedRecordId, type: "form", + noBreadcrumbs: true, context: { ...(this.props.context || {}), form_view_ref: this.props.context.view_ref, diff --git a/account_reconcile_oca/static/src/js/reconcile/reconcile_renderer.esm.js b/account_reconcile_oca/static/src/js/reconcile/reconcile_renderer.esm.js index d031e2bc..3dafa4ec 100644 --- a/account_reconcile_oca/static/src/js/reconcile/reconcile_renderer.esm.js +++ b/account_reconcile_oca/static/src/js/reconcile/reconcile_renderer.esm.js @@ -2,7 +2,11 @@ import {KanbanRenderer} from "@web/views/kanban/kanban_renderer"; import {ReconcileKanbanRecord} from "./reconcile_kanban_record.esm.js"; -export class ReconcileRenderer extends KanbanRenderer {} +export class ReconcileRenderer extends KanbanRenderer { + get journalBalanceStr() { + console.log(this); + } +} ReconcileRenderer.components = { ...KanbanRenderer.components, diff --git a/account_reconcile_oca/static/src/js/reconcile_form/reconcile_form_controller.esm.js b/account_reconcile_oca/static/src/js/reconcile_form/reconcile_form_controller.esm.js index 571b4673..d91b4bda 100644 --- a/account_reconcile_oca/static/src/js/reconcile_form/reconcile_form_controller.esm.js +++ b/account_reconcile_oca/static/src/js/reconcile_form/reconcile_form_controller.esm.js @@ -17,6 +17,9 @@ export class ReconcileFormController extends FormController { afterExecuteAction: this.afterExecuteActionButton.bind(this), }); } + displayName() { + return this.env.config.getDisplayName(); + } async reloadFormController() { var is_reconciled = this.model.root.data.is_reconciled; await this.model.root.load(); diff --git a/account_reconcile_oca/static/src/js/widgets/reconcile_data_widget.esm.js b/account_reconcile_oca/static/src/js/widgets/reconcile_data_widget.esm.js index 3deb8499..1f858f27 100644 --- a/account_reconcile_oca/static/src/js/widgets/reconcile_data_widget.esm.js +++ b/account_reconcile_oca/static/src/js/widgets/reconcile_data_widget.esm.js @@ -21,18 +21,17 @@ export class AccountReconcileDataWidget extends Component { getReconcileLines() { var data = this.props.record.data[this.props.name].data; for (var line in data) { - data[line].amount_format = formatMonetary(data[line].amount, undefined, { + data[line].amount_format = formatMonetary(data[line].amount, { currency: data[line].currency_id, }); - data[line].debit_format = formatMonetary(data[line].debit, undefined, { + data[line].debit_format = formatMonetary(data[line].debit, { currency: data[line].currency_id, }); - data[line].credit_format = formatMonetary(data[line].credit, undefined, { + data[line].credit_format = formatMonetary(data[line].credit, { currency: data[line].currency_id, }); data[line].amount_currency_format = formatMonetary( data[line].currency_amount, - undefined, { currency: data[line].line_currency_id, } @@ -40,7 +39,6 @@ export class AccountReconcileDataWidget extends Component { if (data[line].original_amount) { data[line].original_amount_format = formatMonetary( data[line].original_amount, - undefined, { currency: data[line].currency_id, } diff --git a/account_reconcile_oca/static/src/xml/reconcile.xml b/account_reconcile_oca/static/src/xml/reconcile.xml index 7fbe4632..d79159a2 100644 --- a/account_reconcile_oca/static/src/xml/reconcile.xml +++ b/account_reconcile_oca/static/src/xml/reconcile.xml @@ -5,6 +5,17 @@ t-inherit="web.KanbanRenderer" t-inherit-mode="primary" > + +
+ Balance + +
+
- Reconcile bank statement lines + Statement lines account.bank.statement.line [('journal_id', '=', active_id)] From 4a5e33919d0d968a33b239312c262d3033a2ee9d Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Thu, 7 Mar 2024 19:33:44 +0100 Subject: [PATCH 84/94] [FIX] account_reconcile_oca: Fix foreign currency --- .../models/account_bank_statement_line.py | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/account_reconcile_oca/models/account_bank_statement_line.py b/account_reconcile_oca/models/account_bank_statement_line.py index f0ff494f..fcddb636 100644 --- a/account_reconcile_oca/models/account_bank_statement_line.py +++ b/account_reconcile_oca/models/account_bank_statement_line.py @@ -144,13 +144,16 @@ class AccountBankStatementLine(models.Model): new_data, self.reconcile_data_info["reconcile_auxiliary_id"], self.manual_reference, + exchange_recompute=True, ) self.can_reconcile = self.reconcile_data_info.get("can_reconcile", False) self.add_account_move_line_id = False - def _recompute_suspense_line(self, data, reconcile_auxiliary_id, manual_reference): + def _recompute_suspense_line( + self, data, reconcile_auxiliary_id, manual_reference, exchange_recompute=False + ): reconcile_auxiliary_id = self._compute_exchange_rate( - data, reconcile_auxiliary_id + data, reconcile_auxiliary_id, exchange_recompute=exchange_recompute ) can_reconcile = True total_amount = 0 @@ -406,7 +409,11 @@ class AccountBankStatementLine(models.Model): new_data.append(new_line) return new_data, reconcile_auxiliary_id - def _compute_exchange_rate(self, data, reconcile_auxiliary_id): + def _compute_exchange_rate( + self, data, reconcile_auxiliary_id, exchange_recompute=False + ): + if not exchange_recompute: + return reconcile_auxiliary_id foreign_currency = ( self.currency_id != self.company_id.currency_id or self.foreign_currency_id @@ -429,6 +436,7 @@ class AccountBankStatementLine(models.Model): "date": fields.Date.to_string(self.date), "name": self.payment_ref or self.name, "amount": -amount, + "net_amount": -amount, "credit": amount if amount > 0 else 0.0, "debit": -amount if amount < 0 else 0.0, "kind": "other", @@ -458,6 +466,7 @@ class AccountBankStatementLine(models.Model): data, res["model"], reconcile_auxiliary_id ), self.manual_reference, + exchange_recompute=True, ) elif res and res.get("amls"): amount = self.amount_total_signed @@ -468,7 +477,10 @@ class AccountBankStatementLine(models.Model): amount -= line_data.get("amount") data.append(line_data) return self._recompute_suspense_line( - data, reconcile_auxiliary_id, self.manual_reference + data, + reconcile_auxiliary_id, + self.manual_reference, + exchange_recompute=True, ) return self._recompute_suspense_line( data @@ -661,6 +673,7 @@ class AccountBankStatementLine(models.Model): data, res["model"], reconcile_auxiliary_id ), self.manual_reference, + exchange_recompute=True, ) elif res.get("amls"): amount = self.amount @@ -671,7 +684,10 @@ class AccountBankStatementLine(models.Model): amount -= line_data.get("amount") data.append(line_data) data = record._recompute_suspense_line( - data, reconcile_auxiliary_id, self.manual_reference + data, + reconcile_auxiliary_id, + self.manual_reference, + exchange_recompute=True, ) if not data.get("can_reconcile"): continue @@ -721,7 +737,10 @@ class AccountBankStatementLine(models.Model): else: new_data.append(line) self.reconcile_data_info = self._recompute_suspense_line( - new_data, reconcile_auxiliary_id, self.manual_reference + new_data, + reconcile_auxiliary_id, + self.manual_reference, + exchange_recompute=True, ) self.can_reconcile = self.reconcile_data_info.get("can_reconcile", False) From 3b09e1144c7d92a8a134dc1a9521323704b9ad06 Mon Sep 17 00:00:00 2001 From: lk-eska Date: Fri, 15 Mar 2024 23:57:57 +0100 Subject: [PATCH 85/94] [IMP] account_reconile_oca: Set tree views Make listed and editable --- .../views/account_bank_statement_line.xml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/account_reconcile_oca/views/account_bank_statement_line.xml b/account_reconcile_oca/views/account_bank_statement_line.xml index 8b720660..e81ead57 100644 --- a/account_reconcile_oca/views/account_bank_statement_line.xml +++ b/account_reconcile_oca/views/account_bank_statement_line.xml @@ -321,11 +321,12 @@ {'default_journal_id': active_id, 'search_default_not_reconciled': True, 'view_ref': 'account_reconcile_oca.bank_statement_line_form_reconcile_view'} - kanban + kanban,tree

@@ -337,14 +338,15 @@ Reconcile bank statement lines account.bank.statement.line [('journal_id', '=', active_id)] - - {'default_journal_id': active_id, 'view_ref': 'account_reconcile_oca.bank_statement_line_form_reconcile_view'} - - kanban + {'default_journal_id': active_id, 'view_ref': 'account_reconcile_oca.bank_statement_line_form_reconcile_view'} + kanban,tree

From c408c0f059ba8b26f8db2c19a7cb09695c1695f6 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Fri, 22 Mar 2024 10:49:32 +0100 Subject: [PATCH 86/94] [FIX] account_reconcile_oca: Use counterparts properly --- .../models/account_bank_statement_line.py | 16 +++++++++------- .../models/account_reconcile_abstract.py | 6 +++--- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/account_reconcile_oca/models/account_bank_statement_line.py b/account_reconcile_oca/models/account_bank_statement_line.py index fcddb636..b5fab233 100644 --- a/account_reconcile_oca/models/account_bank_statement_line.py +++ b/account_reconcile_oca/models/account_bank_statement_line.py @@ -130,7 +130,9 @@ class AccountBankStatementLine(models.Model): for line in data: if line["kind"] != "suspense": pending_amount += line["amount"] - if line.get("counterpart_line_id") == self.add_account_move_line_id.id: + if self.add_account_move_line_id.id in line.get( + "counterpart_line_ids", [] + ): is_new_line = False else: new_data.append(line) @@ -161,8 +163,8 @@ class AccountBankStatementLine(models.Model): suspense_line = False counterparts = [] for line in data: - if line.get("counterpart_line_id"): - counterparts.append(line["counterpart_line_id"]) + if line.get("counterpart_line_ids"): + counterparts += line["counterpart_line_ids"] if ( line["account_id"][0] == self.journal_id.suspense_account_id.id or not line["account_id"][0] @@ -537,10 +539,10 @@ class AccountBankStatementLine(models.Model): ) .create(self._reconcile_move_line_vals(line_vals)) ) - if line_vals.get("counterpart_line_id"): + if line_vals.get("counterpart_line_ids"): to_reconcile.append( self.env["account.move.line"].browse( - line_vals.get("counterpart_line_id") + line_vals.get("counterpart_line_ids") ) + line ) @@ -603,10 +605,10 @@ class AccountBankStatementLine(models.Model): .with_context(check_move_validity=False, skip_invoice_sync=True) .create(self._reconcile_move_line_vals(line_vals, move.id)) ) - if line_vals.get("counterpart_line_id") and line.account_id.reconcile: + if line_vals.get("counterpart_line_ids") and line.account_id.reconcile: to_reconcile[line.account_id.id] |= ( self.env["account.move.line"].browse( - line_vals.get("counterpart_line_id") + line_vals.get("counterpart_line_ids") ) | line ) diff --git a/account_reconcile_oca/models/account_reconcile_abstract.py b/account_reconcile_oca/models/account_reconcile_abstract.py index edb9c427..026b57c8 100644 --- a/account_reconcile_oca/models/account_reconcile_abstract.py +++ b/account_reconcile_oca/models/account_reconcile_abstract.py @@ -88,10 +88,10 @@ class AccountReconcileAbstract(models.AbstractModel): vals.update( { "id": False, - "counterpart_line_id": ( + "counterpart_line_ids": ( line.matched_debit_ids.mapped("debit_move_id") | line.matched_credit_ids.mapped("credit_move_id") - ).id, + ).ids, } ) if not float_is_zero( @@ -100,5 +100,5 @@ class AccountReconcileAbstract(models.AbstractModel): vals["original_amount"] = abs(original_amount) vals["original_amount_unsigned"] = original_amount if is_counterpart: - vals["counterpart_line_id"] = line.id + vals["counterpart_line_ids"] = line.ids return vals From 0cc1cca5ff0b51a565e143017053fced4968684d Mon Sep 17 00:00:00 2001 From: "Pedro M. Baeza" Date: Tue, 26 Mar 2024 21:43:47 +0100 Subject: [PATCH 87/94] [FIX] account_reconcile_oca: Don't show partner label if none Avoid to show the ugly `false` text. --- account_reconcile_oca/static/src/xml/reconcile.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account_reconcile_oca/static/src/xml/reconcile.xml b/account_reconcile_oca/static/src/xml/reconcile.xml index d79159a2..1852d7b8 100644 --- a/account_reconcile_oca/static/src/xml/reconcile.xml +++ b/account_reconcile_oca/static/src/xml/reconcile.xml @@ -92,7 +92,7 @@ From 38b1f2987d543348aca5e5345cc47641ce11b66e Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Sun, 21 Apr 2024 02:18:03 +0200 Subject: [PATCH 88/94] [FIX] account_reconcile_oca: Fix monetary --- .../static/src/js/widgets/reconcile_data_widget.esm.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/account_reconcile_oca/static/src/js/widgets/reconcile_data_widget.esm.js b/account_reconcile_oca/static/src/js/widgets/reconcile_data_widget.esm.js index 1f858f27..bc5cd475 100644 --- a/account_reconcile_oca/static/src/js/widgets/reconcile_data_widget.esm.js +++ b/account_reconcile_oca/static/src/js/widgets/reconcile_data_widget.esm.js @@ -22,25 +22,25 @@ export class AccountReconcileDataWidget extends Component { var data = this.props.record.data[this.props.name].data; for (var line in data) { data[line].amount_format = formatMonetary(data[line].amount, { - currency: data[line].currency_id, + currencyId: data[line].currency_id, }); data[line].debit_format = formatMonetary(data[line].debit, { - currency: data[line].currency_id, + currencyId: data[line].currency_id, }); data[line].credit_format = formatMonetary(data[line].credit, { - currency: data[line].currency_id, + currencyId: data[line].currency_id, }); data[line].amount_currency_format = formatMonetary( data[line].currency_amount, { - currency: data[line].line_currency_id, + currencyId: data[line].line_currency_id, } ); if (data[line].original_amount) { data[line].original_amount_format = formatMonetary( data[line].original_amount, { - currency: data[line].currency_id, + currencyId: data[line].currency_id, } ); } From 1321579f59cceb831b5c2daf311d3a183ee45fc0 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Sun, 21 Apr 2024 02:21:16 +0200 Subject: [PATCH 89/94] [IMP] account_reconcile_oca: Fix colors --- .../js/reconcile_move_line/reconcile_move_line_renderer.esm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account_reconcile_oca/static/src/js/reconcile_move_line/reconcile_move_line_renderer.esm.js b/account_reconcile_oca/static/src/js/reconcile_move_line/reconcile_move_line_renderer.esm.js index 10958e49..682328b7 100644 --- a/account_reconcile_oca/static/src/js/reconcile_move_line/reconcile_move_line_renderer.esm.js +++ b/account_reconcile_oca/static/src/js/reconcile_move_line/reconcile_move_line_renderer.esm.js @@ -10,7 +10,7 @@ export class ReconcileMoveLineRenderer extends ListRenderer { record.resId ) ) { - classes += " o_field_account_reconcile_oca_move_line_selected"; + classes += " o_field_account_reconcile_oca_move_line_selected table-info"; } return classes; } From 9eccc9e96827d4ce7bab07fd49ac8096b9183b76 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Sun, 21 Apr 2024 02:26:13 +0200 Subject: [PATCH 90/94] [FIX] account_reconcile_oca: Fix view name change --- .../src/js/reconcile_manual/reconcile_manual_controller.esm.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/account_reconcile_oca/static/src/js/reconcile_manual/reconcile_manual_controller.esm.js b/account_reconcile_oca/static/src/js/reconcile_manual/reconcile_manual_controller.esm.js index ea49dbdb..f5feb11a 100644 --- a/account_reconcile_oca/static/src/js/reconcile_manual/reconcile_manual_controller.esm.js +++ b/account_reconcile_oca/static/src/js/reconcile_manual/reconcile_manual_controller.esm.js @@ -15,6 +15,9 @@ export class ReconcileManualController extends FormController { afterExecuteAction: this.afterExecuteActionButton.bind(this), }); } + displayName() { + return this.env.config.getDisplayName(); + } async reloadFormController() { try { await this.model.root.load(); From 9db67392c307bbc6e0cea866c7766a508c3d17bb Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Sun, 21 Apr 2024 15:05:15 +0200 Subject: [PATCH 91/94] [IMP] account_reconcile_oca: show balances --- .../js/reconcile/reconcile_controller.esm.js | 4 --- .../js/reconcile/reconcile_renderer.esm.js | 31 +++++++++++++++++-- .../static/src/xml/reconcile.xml | 18 +++++++++++ .../views/account_bank_statement_line.xml | 3 ++ 4 files changed, 50 insertions(+), 6 deletions(-) diff --git a/account_reconcile_oca/static/src/js/reconcile/reconcile_controller.esm.js b/account_reconcile_oca/static/src/js/reconcile/reconcile_controller.esm.js index 4639d6a2..fd9f7cb9 100644 --- a/account_reconcile_oca/static/src/js/reconcile/reconcile_controller.esm.js +++ b/account_reconcile_oca/static/src/js/reconcile/reconcile_controller.esm.js @@ -55,10 +55,6 @@ export class ReconcileController extends KanbanController { if (!this.state.journalBalance) { return ""; } - console.log(this.state, { - currencyId: this.state.currency, - humanReadable: true, - }); return formatMonetary(this.state.journalBalance, { currencyId: this.state.currency, }); diff --git a/account_reconcile_oca/static/src/js/reconcile/reconcile_renderer.esm.js b/account_reconcile_oca/static/src/js/reconcile/reconcile_renderer.esm.js index 3dafa4ec..7bf47166 100644 --- a/account_reconcile_oca/static/src/js/reconcile/reconcile_renderer.esm.js +++ b/account_reconcile_oca/static/src/js/reconcile/reconcile_renderer.esm.js @@ -2,9 +2,36 @@ import {KanbanRenderer} from "@web/views/kanban/kanban_renderer"; import {ReconcileKanbanRecord} from "./reconcile_kanban_record.esm.js"; +import {formatMonetary} from "@web/views/fields/formatters"; + export class ReconcileRenderer extends KanbanRenderer { - get journalBalanceStr() { - console.log(this); + getStatements() { + console.log(this.props); + if ( + this.env.parentController.props.resModel !== "account.bank.statement.line" + ) { + return []; + } + const {list} = this.props; + const statements = []; + for (const record of list.records) { + const statementId = record.data.statement_id && record.data.statement_id[0]; + if ( + statementId && + (!statements.length || statements[0].id !== statementId) + ) { + statements.push({ + id: statementId, + name: record.data.statement_name, + balance: record.data.statement_balance_end_real, + balanceStr: formatMonetary(record.data.statement_balance_end_real, { + currencyId: record.data.currency_id[0], + }), + }); + } + } + console.log(statements); + return statements; } } diff --git a/account_reconcile_oca/static/src/xml/reconcile.xml b/account_reconcile_oca/static/src/xml/reconcile.xml index 1852d7b8..b41a22b6 100644 --- a/account_reconcile_oca/static/src/xml/reconcile.xml +++ b/account_reconcile_oca/static/src/xml/reconcile.xml @@ -15,6 +15,24 @@ t-esc="env.parentController.journalBalanceStr" /> + + + + + +

+ + +
+ + + + From 7a7b28f155d9354c6957b791f842823220a864d2 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Sun, 21 Apr 2024 23:22:31 +0200 Subject: [PATCH 92/94] [IMP] account_reconcile_oca: Add statements information --- account_reconcile_oca/__manifest__.py | 1 + account_reconcile_oca/models/__init__.py | 1 + .../models/account_bank_statement.py | 16 +++++++ .../js/reconcile/reconcile_renderer.esm.js | 24 +++++++++- .../static/src/xml/reconcile.xml | 3 +- .../views/account_bank_statement.xml | 45 +++++++++++++++++++ 6 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 account_reconcile_oca/models/account_bank_statement.py create mode 100644 account_reconcile_oca/views/account_bank_statement.xml diff --git a/account_reconcile_oca/__manifest__.py b/account_reconcile_oca/__manifest__.py index 49c467be..5b5ea66b 100644 --- a/account_reconcile_oca/__manifest__.py +++ b/account_reconcile_oca/__manifest__.py @@ -23,6 +23,7 @@ "views/account_journal.xml", "views/account_move.xml", "views/account_account.xml", + "views/account_bank_statement.xml", ], "demo": ["demo/demo.xml"], "post_init_hook": "post_init_hook", diff --git a/account_reconcile_oca/models/__init__.py b/account_reconcile_oca/models/__init__.py index 5be31a27..7845dccd 100644 --- a/account_reconcile_oca/models/__init__.py +++ b/account_reconcile_oca/models/__init__.py @@ -1,5 +1,6 @@ from . import account_reconcile_abstract from . import account_journal from . import account_bank_statement_line +from . import account_bank_statement from . import account_account_reconcile from . import account_move_line diff --git a/account_reconcile_oca/models/account_bank_statement.py b/account_reconcile_oca/models/account_bank_statement.py new file mode 100644 index 00000000..52a64a2f --- /dev/null +++ b/account_reconcile_oca/models/account_bank_statement.py @@ -0,0 +1,16 @@ +# Copyright 2024 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import models + + +class AccountBankStatement(models.Model): + _inherit = "account.bank.statement" + + def action_open_statement(self): + self.ensure_one() + action = self.env["ir.actions.act_window"]._for_xml_id( + "account_reconcile_oca.account_bank_statement_action_edit" + ) + print(action) + action["res_id"] = self.id + return action diff --git a/account_reconcile_oca/static/src/js/reconcile/reconcile_renderer.esm.js b/account_reconcile_oca/static/src/js/reconcile/reconcile_renderer.esm.js index 7bf47166..552de9ba 100644 --- a/account_reconcile_oca/static/src/js/reconcile/reconcile_renderer.esm.js +++ b/account_reconcile_oca/static/src/js/reconcile/reconcile_renderer.esm.js @@ -3,10 +3,15 @@ import {KanbanRenderer} from "@web/views/kanban/kanban_renderer"; import {ReconcileKanbanRecord} from "./reconcile_kanban_record.esm.js"; import {formatMonetary} from "@web/views/fields/formatters"; +import {useService} from "@web/core/utils/hooks"; export class ReconcileRenderer extends KanbanRenderer { + setup() { + super.setup(); + this.action = useService("action"); + this.orm = useService("orm"); + } getStatements() { - console.log(this.props); if ( this.env.parentController.props.resModel !== "account.bank.statement.line" ) { @@ -30,9 +35,24 @@ export class ReconcileRenderer extends KanbanRenderer { }); } } - console.log(statements); return statements; } + async onClickStatement(statementId) { + const action = await this.orm.call( + "account.bank.statement", + "action_open_statement", + [[statementId]], + { + context: this.props.context, + } + ); + const model = this.props.list.model; + this.action.doAction(action, { + async onClose() { + model.root.load(); + }, + }); + } } ReconcileRenderer.components = { diff --git a/account_reconcile_oca/static/src/xml/reconcile.xml b/account_reconcile_oca/static/src/xml/reconcile.xml index b41a22b6..74a15e47 100644 --- a/account_reconcile_oca/static/src/xml/reconcile.xml +++ b/account_reconcile_oca/static/src/xml/reconcile.xml @@ -28,7 +28,8 @@ t-esc="statement.name" /> diff --git a/account_reconcile_oca/views/account_bank_statement.xml b/account_reconcile_oca/views/account_bank_statement.xml new file mode 100644 index 00000000..d081b50b --- /dev/null +++ b/account_reconcile_oca/views/account_bank_statement.xml @@ -0,0 +1,45 @@ + + + + + + Edit Bank statement + account.bank.statement + 99 + + + + + + + + + + + + + + + + + + Edit Bank Statement + account.bank.statement + form + + new + + From b64ffc6edac7b98aa611353f6f3dada675cd92e0 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Sun, 21 Apr 2024 23:55:05 +0200 Subject: [PATCH 93/94] [IMP] account_reconcile_oca: Allow to define the statement directly --- .../models/account_bank_statement.py | 1 - .../models/account_bank_statement_line.py | 22 +++++++++++++++++++ .../static/src/scss/reconcile.scss | 14 ++++++++++++ .../views/account_bank_statement.xml | 1 + .../views/account_bank_statement_line.xml | 14 ++++++++++++ 5 files changed, 51 insertions(+), 1 deletion(-) diff --git a/account_reconcile_oca/models/account_bank_statement.py b/account_reconcile_oca/models/account_bank_statement.py index 52a64a2f..19b9bec1 100644 --- a/account_reconcile_oca/models/account_bank_statement.py +++ b/account_reconcile_oca/models/account_bank_statement.py @@ -11,6 +11,5 @@ class AccountBankStatement(models.Model): action = self.env["ir.actions.act_window"]._for_xml_id( "account_reconcile_oca.account_bank_statement_action_edit" ) - print(action) action["res_id"] = self.id return action diff --git a/account_reconcile_oca/models/account_bank_statement_line.py b/account_reconcile_oca/models/account_bank_statement_line.py index b5fab233..36971169 100644 --- a/account_reconcile_oca/models/account_bank_statement_line.py +++ b/account_reconcile_oca/models/account_bank_statement_line.py @@ -769,3 +769,25 @@ class AccountBankStatementLine(models.Model): if vals["partner_id"] is False and self.partner_name: vals["partner_id"] = (False, self.partner_name) return vals + + def add_statement(self): + self.ensure_one() + action = self.env["ir.actions.act_window"]._for_xml_id( + "account_reconcile_oca.account_bank_statement_action_edit" + ) + previous_line_with_statement = self.env["account.bank.statement.line"].search( + [ + ("internal_index", "<", self.internal_index), + ("journal_id", "=", self.journal_id.id), + ("state", "=", "posted"), + ("statement_id", "!=", self.statement_id.id), + ("statement_id", "!=", False), + ], + limit=1, + ) + action["context"] = { + "default_journal_id": self.journal_id.id, + "default_balance_start": previous_line_with_statement.statement_id.balance_end_real, + "split_line_id": self.id, + } + return action diff --git a/account_reconcile_oca/static/src/scss/reconcile.scss b/account_reconcile_oca/static/src/scss/reconcile.scss index f8fa60fe..9a574985 100644 --- a/account_reconcile_oca/static/src/scss/reconcile.scss +++ b/account_reconcile_oca/static/src/scss/reconcile.scss @@ -6,9 +6,23 @@ flex-flow: row wrap; height: 100%; .o_kanban_renderer.o_kanban_ungrouped .o_kanban_record { + &:hover { + .o_reconcile_create_statement { + opacity: 100; + } + } margin: 0 0 0; min-width: fit-content; width: 100%; + .o_reconcile_create_statement { + position: absolute; + height: 4px; + margin: 0; + padding: 2px 0 0 0; + border: 0; + top: -14px; + opacity: 0; + } > div { border-right: thick solid rgba(0, 0, 0, 0); } diff --git a/account_reconcile_oca/views/account_bank_statement.xml b/account_reconcile_oca/views/account_bank_statement.xml index d081b50b..ac615d7f 100644 --- a/account_reconcile_oca/views/account_bank_statement.xml +++ b/account_reconcile_oca/views/account_bank_statement.xml @@ -24,6 +24,7 @@ + diff --git a/account_reconcile_oca/views/account_bank_statement_line.xml b/account_reconcile_oca/views/account_bank_statement_line.xml index 95f8263c..fc8d16b8 100644 --- a/account_reconcile_oca/views/account_bank_statement_line.xml +++ b/account_reconcile_oca/views/account_bank_statement_line.xml @@ -17,6 +17,20 @@ +
Date: Mon, 22 Apr 2024 13:24:16 +0200 Subject: [PATCH 94/94] [IMP] account_reconcile_oca: Allow aggregation for more parameters --- account_reconcile_oca/__manifest__.py | 1 + account_reconcile_oca/models/__init__.py | 2 + .../models/account_bank_statement_line.py | 40 +++++++++++++++++++ .../models/account_journal.py | 10 +++++ account_reconcile_oca/models/res_company.py | 14 +++++++ .../models/res_config_settings.py | 12 ++++++ .../js/reconcile/reconcile_renderer.esm.js | 18 ++++----- .../static/src/xml/reconcile.xml | 23 +++++++---- .../views/account_bank_statement_line.xml | 6 ++- .../views/account_journal.xml | 5 +++ .../views/res_config_settings.xml | 24 +++++++++++ 11 files changed, 136 insertions(+), 19 deletions(-) create mode 100644 account_reconcile_oca/models/res_company.py create mode 100644 account_reconcile_oca/models/res_config_settings.py create mode 100644 account_reconcile_oca/views/res_config_settings.xml diff --git a/account_reconcile_oca/__manifest__.py b/account_reconcile_oca/__manifest__.py index 5b5ea66b..2e53b4fa 100644 --- a/account_reconcile_oca/__manifest__.py +++ b/account_reconcile_oca/__manifest__.py @@ -16,6 +16,7 @@ "base_sparse_field", ], "data": [ + "views/res_config_settings.xml", "security/ir.model.access.csv", "views/account_account_reconcile.xml", "views/account_bank_statement_line.xml", diff --git a/account_reconcile_oca/models/__init__.py b/account_reconcile_oca/models/__init__.py index 7845dccd..da04aa8d 100644 --- a/account_reconcile_oca/models/__init__.py +++ b/account_reconcile_oca/models/__init__.py @@ -4,3 +4,5 @@ from . import account_bank_statement_line from . import account_bank_statement from . import account_account_reconcile from . import account_move_line +from . import res_company +from . import res_config_settings diff --git a/account_reconcile_oca/models/account_bank_statement_line.py b/account_reconcile_oca/models/account_bank_statement_line.py index 36971169..d51f2bd7 100644 --- a/account_reconcile_oca/models/account_bank_statement_line.py +++ b/account_reconcile_oca/models/account_bank_statement_line.py @@ -3,6 +3,9 @@ from collections import defaultdict +from dateutil import rrule +from dateutil.relativedelta import relativedelta + from odoo import Command, _, api, fields, models, tools from odoo.exceptions import UserError from odoo.tools import float_is_zero @@ -86,6 +89,43 @@ class AccountBankStatementLine(models.Model): "account.move", default=False, store=False, prefetch=False, readonly=True ) can_reconcile = fields.Boolean(sparse="reconcile_data_info") + reconcile_aggregate = fields.Char(compute="_compute_reconcile_aggregate") + aggregate_id = fields.Integer(compute="_compute_reconcile_aggregate") + aggregate_name = fields.Char(compute="_compute_reconcile_aggregate") + + @api.model + def _reconcile_aggregate_map(self): + lang = self.env["res.lang"]._lang_get(self.env.user.lang) + week_start = rrule.weekday(int(lang.week_start) - 1) + return { + False: lambda s: (False, False), + "statement": lambda s: (s.statement_id.id, s.statement_id.name), + "day": lambda s: (s.date.toordinal(), s.date.strftime(lang.date_format)), + "week": lambda s: ( + (s.date + relativedelta(weekday=week_start(-1))).toordinal(), + (s.date + relativedelta(weekday=week_start(-1))).strftime( + lang.date_format + ), + ), + "month": lambda s: ( + s.date.replace(day=1).toordinal(), + s.date.replace(day=1).strftime(lang.date_format), + ), + } + + @api.depends("company_id", "journal_id") + def _compute_reconcile_aggregate(self): + reconcile_aggregate_map = self._reconcile_aggregate_map() + for record in self: + reconcile_aggregate = ( + record.journal_id.reconcile_aggregate + or record.company_id.reconcile_aggregate + ) + record.reconcile_aggregate = reconcile_aggregate + print(record.date, reconcile_aggregate_map[reconcile_aggregate](record)) + record.aggregate_id, record.aggregate_name = reconcile_aggregate_map[ + reconcile_aggregate + ](record) def save(self): return {"type": "ir.actions.act_window_close"} diff --git a/account_reconcile_oca/models/account_journal.py b/account_reconcile_oca/models/account_journal.py index bc4ded92..a88e106f 100644 --- a/account_reconcile_oca/models/account_journal.py +++ b/account_reconcile_oca/models/account_journal.py @@ -13,6 +13,16 @@ class AccountJournal(models.Model): required=True, ) company_currency_id = fields.Many2one(related="company_id.currency_id") + reconcile_aggregate = fields.Selection( + [ + ("statement", "Statement"), + ("day", "Day"), + ("week", "Week"), + ("month", "Month"), + ], + string="Reconcile aggregation", + help="Aggregation to use on reconcile view", + ) def get_rainbowman_message(self): self.ensure_one() diff --git a/account_reconcile_oca/models/res_company.py b/account_reconcile_oca/models/res_company.py new file mode 100644 index 00000000..3ed88cb6 --- /dev/null +++ b/account_reconcile_oca/models/res_company.py @@ -0,0 +1,14 @@ +# Copyright 2024 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + reconcile_aggregate = fields.Selection( + selection=lambda self: self.env["account.journal"] + ._fields["reconcile_aggregate"] + .selection + ) diff --git a/account_reconcile_oca/models/res_config_settings.py b/account_reconcile_oca/models/res_config_settings.py new file mode 100644 index 00000000..8bcc2185 --- /dev/null +++ b/account_reconcile_oca/models/res_config_settings.py @@ -0,0 +1,12 @@ +# Copyright 2024 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + reconcile_aggregate = fields.Selection( + related="company_id.reconcile_aggregate", readonly=False + ) diff --git a/account_reconcile_oca/static/src/js/reconcile/reconcile_renderer.esm.js b/account_reconcile_oca/static/src/js/reconcile/reconcile_renderer.esm.js index 552de9ba..25fe2ac4 100644 --- a/account_reconcile_oca/static/src/js/reconcile/reconcile_renderer.esm.js +++ b/account_reconcile_oca/static/src/js/reconcile/reconcile_renderer.esm.js @@ -11,23 +11,23 @@ export class ReconcileRenderer extends KanbanRenderer { this.action = useService("action"); this.orm = useService("orm"); } - getStatements() { + getAggregates() { if ( this.env.parentController.props.resModel !== "account.bank.statement.line" ) { return []; } const {list} = this.props; - const statements = []; + const aggregates = []; for (const record of list.records) { - const statementId = record.data.statement_id && record.data.statement_id[0]; + const aggregateId = record.data.aggregate_id && record.data.aggregate_id; if ( - statementId && - (!statements.length || statements[0].id !== statementId) + aggregateId && + (!aggregates.length || aggregates[0].id !== aggregateId) ) { - statements.push({ - id: statementId, - name: record.data.statement_name, + aggregates.push({ + id: aggregateId, + name: record.data.aggregate_name, balance: record.data.statement_balance_end_real, balanceStr: formatMonetary(record.data.statement_balance_end_real, { currencyId: record.data.currency_id[0], @@ -35,7 +35,7 @@ export class ReconcileRenderer extends KanbanRenderer { }); } } - return statements; + return aggregates; } async onClickStatement(statementId) { const action = await this.orm.call( diff --git a/account_reconcile_oca/static/src/xml/reconcile.xml b/account_reconcile_oca/static/src/xml/reconcile.xml index 74a15e47..5e8990d3 100644 --- a/account_reconcile_oca/static/src/xml/reconcile.xml +++ b/account_reconcile_oca/static/src/xml/reconcile.xml @@ -9,29 +9,36 @@
Balance + >Global Balance
- + - -
+ +
+
diff --git a/account_reconcile_oca/views/account_bank_statement_line.xml b/account_reconcile_oca/views/account_bank_statement_line.xml index fc8d16b8..4f4e5ab2 100644 --- a/account_reconcile_oca/views/account_bank_statement_line.xml +++ b/account_reconcile_oca/views/account_bank_statement_line.xml @@ -13,12 +13,14 @@ - + + +