From 8e2365f5ec7456f4ba9d51265ca4e7b5c61a4d54 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Tue, 1 Nov 2022 20:26:19 +0100 Subject: [PATCH 001/210] [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 0000000000..9ec60fd9b1 --- /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 0000000000..cc6b6354ad --- /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 0000000000..b5f8beb4d1 --- /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 0000000000..a27a51e8c1 --- /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 0000000000..dd1094eb20 --- /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 0000000000..8102733afc --- /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 0000000000..6a33d7869a --- /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 0000000000..c7c90e4cb1 --- /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 0000000000..56e3bf342d --- /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 0000000000..659ae5c8d1 --- /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 0000000000..85004765bc --- /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 0000000000..e50452fb2d --- /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 0000000000..24e9bc53f8 --- /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 0000000000..fc080e09b4 --- /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 0000000000..d73fa145dd --- /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 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 z 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 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 0000000000..ca0693c1b3 --- /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 0000000000..c9e6f4e006 --- /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 0000000000..90756cfccb --- /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 0000000000..87f63fa7ea --- /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 0000000000..7a49f3f9a7 --- /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 0000000000..a97c5258ae --- /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 0000000000..99be2b6620 --- /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 0000000000..158f8afaf2 --- /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 0000000000..a539bca43d --- /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 0000000000..d031e2bcd3 --- /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 0000000000..4bcb8adecd --- /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 0000000000..8d238cc640 --- /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 0000000000..840ceb23c3 --- /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 0000000000..63801923e1 --- /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 0000000000..17f193edb5 --- /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 0000000000..f68265ba0d --- /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 0000000000..7d124f2c2c --- /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 0000000000..a8207ce6ef --- /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 0000000000..63cf568765 --- /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 0000000000..dadd124e4d --- /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 0000000000..3947f5c4d9 --- /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 0000000000..04ee2f11be --- /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 0000000000..a5c680ac33 --- /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 2d87c46039..9f8c163db0 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 005/210] [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 0000000000..de3927257a --- /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 87604ed326ccccfbb525ab8b273d8b2511e3bc7e Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Fri, 31 Mar 2023 17:28:53 +0000 Subject: [PATCH 006/210] [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 9ec60fd9b1..1b743b5b1b 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 ca0693c1b3..29b9aba292 100644 --- a/account_reconcile_oca/static/description/index.html +++ b/account_reconcile_oca/static/description/index.html @@ -3,7 +3,7 @@ - + Account Reconcile Oca