diff --git a/account_reconciliation_widget/README.rst b/account_reconciliation_widget/README.rst new file mode 100644 index 00000000..0aaf1cc5 --- /dev/null +++ b/account_reconciliation_widget/README.rst @@ -0,0 +1,86 @@ +============================= +account_reconciliation_widget +============================= + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! 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_reconciliation_widget-lightgray.png?logo=github + :target: https://github.com/OCA/account_reconciliation_widget/tree/14.0/account_reconciliation_widget + :alt: OCA/account_reconciliation_widget +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/account_reconciliation_widget-14-0/account_reconciliation_widget-14-0-account_reconciliation_widget + :alt: Translate me on Weblate + +|badge1| |badge2| |badge3| |badge4| + +This module restores account reconciliation widget moved from Odoo community to enterpise in V. 14.0 +Provides two widgets designed to reconcile move lines in a easy way: one focused on bank statements and another for generic use. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +With an user with full accounting features enabled: + +Invoicing --> Accounting --> Actions --> Reconciliation. + +From journal items list view you can select check of them and click Action --> Reconcile. + +From accounting dashboard you can use reconcile button in Bank / Cash journals. + +Also, you can navigate to statements and use the reconcile button. + + +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 +~~~~~~~ + +* Ozono Multimedia + +Contributors +~~~~~~~~~~~~ + +* Tecnativa - Pedro M. Baeza + + +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_reconciliation_widget `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/account_reconciliation_widget/__init__.py b/account_reconciliation_widget/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/account_reconciliation_widget/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/account_reconciliation_widget/__manifest__.py b/account_reconciliation_widget/__manifest__.py new file mode 100644 index 00000000..26e6e107 --- /dev/null +++ b/account_reconciliation_widget/__manifest__.py @@ -0,0 +1,24 @@ +# Copyright 2020 Ozono Multimedia - Iván Antón +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "account_reconciliation_widget", + "version": "14.0.1.0.0", + "category": "Accounting", + "license": "AGPL-3", + "summary": "Account reconciliation widget", + "author": "Odoo, Ozono Multimedia, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/account-reconcile", + "depends": ["account"], + "data": [ + "security/ir.model.access.csv", + "views/assets.xml", + "views/account_view.xml", + "views/account_bank_statement_view.xml", + "views/account_journal_dashboard_view.xml", + ], + "qweb": [ + "static/src/xml/account_reconciliation.xml", + ], + "installable": True, +} diff --git a/account_reconciliation_widget/models/__init__.py b/account_reconciliation_widget/models/__init__.py new file mode 100644 index 00000000..7957603b --- /dev/null +++ b/account_reconciliation_widget/models/__init__.py @@ -0,0 +1,5 @@ +from . import account_move +from . import account_bank_statement +from . import account_journal +from . import reconciliation_widget +from . import res_company diff --git a/account_reconciliation_widget/models/account_bank_statement.py b/account_reconciliation_widget/models/account_bank_statement.py index cee53c90..0916d919 100644 --- a/account_reconciliation_widget/models/account_bank_statement.py +++ b/account_reconciliation_widget/models/account_bank_statement.py @@ -33,6 +33,7 @@ class AccountBankStatementLine(models.Model): _inherit = "account.bank.statement.line" + # FIXME: is this necessary now? move_name = fields.Char( string="Journal Entry Name", readonly=True, @@ -129,22 +130,23 @@ class AccountBankStatementLine(models.Model): and user_type_id not in account_types ): account_types |= user_type_id - if suspense_moves_mode: - if any(not line.journal_entry_ids for line in self): - raise UserError( - _( - "Some selected statement line were not already " - "reconciled with an account move." - ) - ) - else: - if any(line.journal_entry_ids for line in self): - raise UserError( - _( - "A selected statement line was already reconciled with " - "an account move." - ) - ) + # FIXME: review + # if suspense_moves_mode: + # if any(not line.journal_entry_ids for line in self): + # raise UserError( + # _( + # "Some selected statement line were not already " + # "reconciled with an account move." + # ) + # ) + # else: + # if any(line.journal_entry_ids for line in self): + # raise UserError( + # _( + # "A selected statement line was already reconciled with " + # "an account move." + # ) + # ) # Fully reconciled moves are just linked to the bank statement total = self.amount @@ -171,7 +173,7 @@ class AccountBankStatementLine(models.Model): # it. aml_rec.move_id.date = self.date aml_rec.payment_id.payment_date = self.date - aml_rec.move_id.post() + aml_rec.move_id.action_post() # We check the paid status of the invoices reconciled with this # payment for invoice in aml_rec.payment_id.reconciled_invoice_ids: @@ -181,86 +183,10 @@ class AccountBankStatementLine(models.Model): # (eg. invoice), in which case we reconcile the existing and the new # move lines together, or being a write-off. if counterpart_aml_dicts or new_aml_dicts: - - # Create the move - self.sequence = self.statement_id.line_ids.ids.index(self.id) + 1 - move_vals = self._prepare_reconciliation_move(self.statement_id.name) - if suspense_moves_mode: - self.button_cancel_reconciliation() - move = ( - self.env["account.move"] - .with_context(default_journal_id=move_vals["journal_id"]) - .create(move_vals) + counterpart_moves = self._create_counterpart_and_new_aml( + counterpart_moves, counterpart_aml_dicts, new_aml_dicts ) - counterpart_moves = counterpart_moves | move - # Create The payment - payment = self.env["account.payment"] - partner_id = ( - self.partner_id - or (aml_dict.get("move_line") and aml_dict["move_line"].partner_id) - or self.env["res.partner"] - ) - if abs(total) > 0.00001: - payment_vals = self._prepare_payment_vals(total) - if not payment_vals["partner_id"]: - payment_vals["partner_id"] = partner_id.id - if payment_vals["partner_id"] and len(account_types) == 1: - payment_vals["partner_type"] = ( - "customer" - if account_types == receivable_account_type - else "supplier" - ) - payment = payment.create(payment_vals) - - # Complete dicts to create both counterpart move lines and write-offs - to_create = counterpart_aml_dicts + new_aml_dicts - date = self.date or fields.Date.today() - for aml_dict in to_create: - aml_dict["move_id"] = move.id - aml_dict["partner_id"] = self.partner_id.id - aml_dict["statement_line_id"] = self.id - self._prepare_move_line_for_currency(aml_dict, date) - - # Create write-offs - for aml_dict in new_aml_dicts: - aml_dict["payment_id"] = payment and payment.id or False - aml_obj.with_context(check_move_validity=False).create(aml_dict) - - # Create counterpart move lines and reconcile them - for aml_dict in counterpart_aml_dicts: - if ( - aml_dict["move_line"].payment_id - and not aml_dict["move_line"].statement_line_id - ): - aml_dict["move_line"].write({"statement_line_id": self.id}) - if aml_dict["move_line"].partner_id.id: - aml_dict["partner_id"] = aml_dict["move_line"].partner_id.id - aml_dict["account_id"] = aml_dict["move_line"].account_id.id - aml_dict["payment_id"] = payment and payment.id or False - - counterpart_move_line = aml_dict.pop("move_line") - new_aml = aml_obj.with_context(check_move_validity=False).create( - aml_dict - ) - - (new_aml | counterpart_move_line).reconcile() - - self._check_invoice_state(counterpart_move_line.move_id) - - # Balance the move - st_line_amount = -sum([x.balance for x in move.line_ids]) - aml_dict = self._prepare_reconciliation_move_line(move, st_line_amount) - aml_dict["payment_id"] = payment and payment.id or False - aml_obj.with_context(check_move_validity=False).create(aml_dict) - - # Needs to be called manually as lines were created 1 by 1 - move.update_lines_tax_exigibility() - move.post() - # record the move name on the statement line to be able to retrieve - # it in case of unreconciliation - self.write({"move_name": move.name}) - payment and payment.write({"payment_reference": move.name}) elif self.move_name: raise UserError( _( @@ -277,177 +203,71 @@ class AccountBankStatementLine(models.Model): if self.account_number and self.partner_id and not self.bank_account_id: # Search bank account without partner to handle the case the # res.partner.bank already exists but is set on a different partner. - self.bank_account_id = self._find_or_create_bank_account() + self.partner_bank_id = self._find_or_create_bank_account() counterpart_moves._check_balanced() return counterpart_moves - def _prepare_reconciliation_move(self, move_ref): - """Prepare the dict of values to create the move from a statement line. - This method may be overridden to adapt domain logic through model - inheritance (make sure to call super() to establish a clean extension - chain). + def _create_counterpart_and_new_aml( + self, counterpart_moves, counterpart_aml_dicts, new_aml_dicts + ): - :param char move_ref: will be used as the reference of the generated - account move - :return: dict of value to create() the account.move - """ - ref = move_ref or "" - if self.ref: - ref = move_ref + " - " + self.ref if move_ref else self.ref - data = { - "type": "entry", - "journal_id": self.statement_id.journal_id.id, - "currency_id": self.statement_id.currency_id.id, - "date": self.statement_id.accounting_date or self.date, - "partner_id": self.partner_id.id, - "ref": ref, - } - if self.move_name: - data.update(name=self.move_name) - return data + aml_obj = self.env["account.move.line"] - def _prepare_reconciliation_move_line(self, move, amount): - """Prepare the dict of values to balance the move. + # Delete previous move_lines + self.move_id.line_ids.with_context(force_delete=True).unlink() - :param recordset move: the account.move to link the move line - :param dict move: a dict of vals of a account.move which will be created - later - :param float amount: the amount of transaction that wasn't already - reconciled - """ - company_currency = self.journal_id.company_id.currency_id - statement_currency = self.journal_id.currency_id or company_currency - st_line_currency = self.currency_id or statement_currency - amount_currency = False - st_line_currency_rate = ( - self.currency_id and (self.amount_currency / self.amount) or False - ) - if isinstance(move, dict): - amount_sum = sum(x[2].get("amount_currency", 0) for x in move["line_ids"]) - else: - amount_sum = sum(x.amount_currency for x in move.line_ids) - # We have several use case here to compare the currency and amount - # currency of counterpart line to balance the move: - if ( - st_line_currency != company_currency - and st_line_currency == statement_currency - ): - # company in currency A, statement in currency B and transaction in - # currency B - # counterpart line must have currency B and correct amount is - # inverse of already existing lines - amount_currency = -amount_sum - elif ( - st_line_currency != company_currency - and statement_currency == company_currency - ): - # company in currency A, statement in currency A and transaction in - # currency B - # counterpart line must have currency B and correct amount is - # inverse of already existing lines - amount_currency = -amount_sum - elif ( - st_line_currency != company_currency - and st_line_currency != statement_currency - ): - # company in currency A, statement in currency B and transaction in - # currency C - # counterpart line must have currency B and use rate between B and - # C to compute correct amount - amount_currency = -amount_sum / st_line_currency_rate - elif ( - st_line_currency == company_currency - and statement_currency != company_currency - ): - # company in currency A, statement in currency B and transaction in - # currency A - # counterpart line must have currency B and amount is computed using - # the rate between A and B - amount_currency = amount / st_line_currency_rate + # Create liquidity line + liquidity_aml_dict = self._prepare_liquidity_move_line_vals() + aml_obj.with_context(check_move_validity=False).create(liquidity_aml_dict) - # last case is company in currency A, statement in currency A and - # transaction in currency A - # and in this case counterpart line does not need any second currency - # nor amount_currency + self.sequence = self.statement_id.line_ids.ids.index(self.id) + 1 + counterpart_moves = counterpart_moves | self.move_id - # Check if default_debit or default_credit account are properly configured - account_id = ( - amount >= 0 - and self.statement_id.journal_id.default_credit_account_id.id - or self.statement_id.journal_id.default_debit_account_id.id - ) + # Complete dicts to create both counterpart move lines and write-offs + to_create = counterpart_aml_dicts + new_aml_dicts + date = self.date or fields.Date.today() + for aml_dict in to_create: + aml_dict["move_id"] = self.move_id.id + aml_dict["partner_id"] = self.partner_id.id + aml_dict["statement_line_id"] = self.id + self._prepare_move_line_for_currency(aml_dict, date) - if not account_id: - raise UserError( - _( - "No default debit and credit account defined on journal %s " - "(ids: %s)." - % ( - self.statement_id.journal_id.name, - self.statement_id.journal_id.ids, - ) - ) - ) + # Create write-offs + for aml_dict in new_aml_dicts: + aml_obj.with_context(check_move_validity=False).create(aml_dict) - aml_dict = { - "name": self.name, - "partner_id": self.partner_id and self.partner_id.id or False, - "account_id": account_id, - "credit": amount < 0 and -amount or 0.0, - "debit": amount > 0 and amount or 0.0, - "statement_line_id": self.id, - "currency_id": statement_currency != company_currency - and statement_currency.id - or (st_line_currency != company_currency and st_line_currency.id or False), - "amount_currency": amount_currency, - } - if isinstance(move, self.env["account.move"].__class__): - aml_dict["move_id"] = move.id - return aml_dict + # Create counterpart move lines and reconcile them + aml_to_reconcile = [] + for aml_dict in counterpart_aml_dicts: + if not aml_dict["move_line"].statement_line_id: + aml_dict["move_line"].write({"statement_line_id": self.id}) + if aml_dict["move_line"].partner_id.id: + aml_dict["partner_id"] = aml_dict["move_line"].partner_id.id + aml_dict["account_id"] = aml_dict["move_line"].account_id.id - def _get_communication(self, payment_method_id): - return self.name or "" + counterpart_move_line = aml_dict.pop("move_line") + new_aml = aml_obj.with_context(check_move_validity=False).create(aml_dict) - def _prepare_payment_vals(self, total): - """Prepare the dict of values to create the payment from a statement - line. This method may be overridden for update dict - through model inheritance (make sure to call super() to establish a - clean extension chain). + aml_to_reconcile.append((new_aml, counterpart_move_line)) - :param float total: will be used as the amount of the generated payment - :return: dict of value to create() the account.payment - """ - self.ensure_one() - partner_type = False - if self.partner_id: - if total < 0: - partner_type = "supplier" - else: - partner_type = "customer" - if not partner_type and self.env.context.get("default_partner_type"): - partner_type = self.env.context["default_partner_type"] - currency = self.journal_id.currency_id or self.company_id.currency_id - payment_methods = ( - (total > 0) - and self.journal_id.inbound_payment_method_ids - or self.journal_id.outbound_payment_method_ids - ) - return { - "payment_method_id": payment_methods and payment_methods[0].id or False, - "payment_type": total > 0 and "inbound" or "outbound", - "partner_id": self.partner_id.id, - "partner_type": partner_type, - "journal_id": self.statement_id.journal_id.id, - "payment_date": self.date, - "state": "reconciled", - "currency_id": currency.id, - "amount": abs(total), - "communication": self._get_communication( - payment_methods[0] if payment_methods else False - ), - "name": self.statement_id.name or _("Bank Statement %s") % self.date, - } + # Post to allow reconcile + self.move_id.with_context(skip_account_move_synchronization=True).action_post() + + # Reconcile new lines with counterpart + for new_aml, counterpart_move_line in aml_to_reconcile: + (new_aml | counterpart_move_line).reconcile() + + self._check_invoice_state(counterpart_move_line.move_id) + + # Needs to be called manually as lines were created 1 by 1 + self.move_id.update_lines_tax_exigibility() + self.move_id.with_context(skip_account_move_synchronization=True).action_post() + # record the move name on the statement line to be able to retrieve + # it in case of unreconciliation + self.write({"move_name": self.move_id.name}) + + return counterpart_moves def _prepare_move_line_for_currency(self, aml_dict, date): self.ensure_one() diff --git a/account_reconciliation_widget/models/account_journal.py b/account_reconciliation_widget/models/account_journal.py index 261c8bbc..3650bc76 100644 --- a/account_reconciliation_widget/models/account_journal.py +++ b/account_reconciliation_widget/models/account_journal.py @@ -20,3 +20,18 @@ class AccountJournal(models.Model): "company_ids": self.mapped("company_id").ids, }, } + + def action_open_reconcile_to_check(self): + self.ensure_one() + ids = self.to_check_ids().ids + action_context = { + "show_mode_selector": False, + "company_ids": self.mapped("company_id").ids, + } + action_context.update({"suspense_moves_mode": True}) + action_context.update({"statement_line_ids": ids}) + return { + "type": "ir.actions.client", + "tag": "bank_statement_reconciliation_view", + "context": action_context, + } diff --git a/account_reconciliation_widget/models/account_move.py b/account_reconciliation_widget/models/account_move.py index e1a68928..28d07239 100644 --- a/account_reconciliation_widget/models/account_move.py +++ b/account_reconciliation_widget/models/account_move.py @@ -121,7 +121,7 @@ class AccountMoveLine(models.Model): # post all the writeoff moves at once if writeoff_moves: - writeoff_moves.post() + writeoff_moves.action_post() # Return the writeoff move.line which is to be reconciled return line_to_reconcile diff --git a/account_reconciliation_widget/models/reconciliation_widget.py b/account_reconciliation_widget/models/reconciliation_widget.py index ebe2c710..0d0a3d1b 100644 --- a/account_reconciliation_widget/models/reconciliation_widget.py +++ b/account_reconciliation_widget/models/reconciliation_widget.py @@ -1,5 +1,7 @@ import copy +from psycopg2 import sql + from odoo import _, api, models from odoo.exceptions import UserError from odoo.osv import expression @@ -86,8 +88,7 @@ class AccountReconciliation(models.AbstractModel): # Blue lines = payment on bank account not assigned to a statement yet aml_accounts = [ - st_line.journal_id.default_credit_account_id.id, - st_line.journal_id.default_debit_account_id.id, + st_line.journal_id.default_account_id.id, ] if partner_id is None: @@ -106,7 +107,8 @@ class AccountReconciliation(models.AbstractModel): from_clause, where_clause, where_clause_params = ( self.env["account.move.line"]._where_calc(domain).get_sql() ) - query_str = """ + query_str = sql.SQL( + """ SELECT "account_move_line".id FROM {from_clause} {where_str} ORDER BY ("account_move_line".debit - @@ -115,10 +117,11 @@ class AccountReconciliation(models.AbstractModel): "account_move_line".id ASC {limit_str} """.format( - from_clause=from_clause, - where_str=where_clause and (" WHERE %s" % where_clause) or "", - amount=st_line.amount, - limit_str=limit and " LIMIT %s" or "", + from_clause=from_clause, + where_str=where_clause and (" WHERE %s" % where_clause) or "", + amount=st_line.amount, + limit_str=limit and " LIMIT %s" or "", + ) ) params = where_clause_params + (limit and [limit] or []) self.env["account.move"].flush() @@ -290,7 +293,6 @@ class AccountReconciliation(models.AbstractModel): """ if not bank_statement_line_ids: return {} - suspense_moves_mode = self._context.get("suspense_moves_mode") bank_statements = ( self.env["account.bank.statement.line"] .browse(bank_statement_line_ids) @@ -301,17 +303,13 @@ class AccountReconciliation(models.AbstractModel): SELECT line.id FROM account_bank_statement_line line LEFT JOIN res_partner p on p.id = line.partner_id - WHERE line.account_id IS NULL + INNER JOIN account_bank_statement st ON line.statement_id = st.id + AND st.state = 'posted' + WHERE line.is_reconciled = FALSE AND line.amount != 0.0 AND line.id IN %(ids)s - {cond} GROUP BY line.id - """.format( - cond=not suspense_moves_mode - and """AND NOT EXISTS (SELECT 1 from account_move_line aml - WHERE aml.statement_line_id = line.id)""" - or "", - ) + """ self.env.cr.execute(query, {"ids": tuple(bank_statement_line_ids)}) domain = [["id", "in", [line.get("id") for line in self.env.cr.dictfetchall()]]] @@ -327,6 +325,9 @@ class AccountReconciliation(models.AbstractModel): results.update( { + "statement_id": len(bank_statements_left) == 1 + and bank_statements_left.id + or False, "statement_name": len(bank_statements_left) == 1 and bank_statements_left.name or False, @@ -403,7 +404,6 @@ class AccountReconciliation(models.AbstractModel): ) if aml_ids: aml = MoveLine.browse(aml_ids) - aml._check_reconcile_validity() account = aml[0].account_id currency = account.currency_id or account.company_id.currency_id return { @@ -489,8 +489,7 @@ class AccountReconciliation(models.AbstractModel): WHERE l.account_id = a.id {inner_where} AND l.amount_residual != 0 - AND (move.state = 'posted' OR (move.state = 'draft' - AND journal.post_at = 'bank_rec')) + AND move.state = 'posted' ) """.format( inner_where=is_partner and "AND l.partner_id = p.id" or " " @@ -504,8 +503,7 @@ class AccountReconciliation(models.AbstractModel): WHERE l.account_id = a.id {inner_where} AND l.amount_residual > 0 - AND (move.state = 'posted' - OR (move.state = 'draft' AND journal.post_at = 'bank_rec')) + AND move.state = 'posted' ) AND EXISTS ( SELECT NULL @@ -515,13 +513,13 @@ class AccountReconciliation(models.AbstractModel): WHERE l.account_id = a.id {inner_where} AND l.amount_residual < 0 - AND (move.state = 'posted' - OR (move.state = 'draft' AND journal.post_at = 'bank_rec')) + AND move.state = 'posted' ) """.format( inner_where=is_partner and "AND l.partner_id = p.id" or " " ) - query = """ + query = sql.SQL( + """ SELECT {select} account_id, account_name, account_code, max_date FROM ( SELECT {inner_select} @@ -549,35 +547,36 @@ class AccountReconciliation(models.AbstractModel): ) as s {outer_where} """.format( - select=is_partner - and "partner_id, partner_name, to_char(last_time_entries_checked, " - "'YYYY-MM-DD') AS last_time_entries_checked," - or " ", - inner_select=is_partner - and "p.id AS partner_id, p.name AS partner_name, " - "p.last_time_entries_checked AS last_time_entries_checked," - or " ", - inner_from=is_partner - and "RIGHT JOIN res_partner p ON (l.partner_id = p.id)" - or " ", - where1=is_partner - and " " - or "AND ((at.type <> 'payable' AND at.type <> 'receivable') " - "OR l.partner_id IS NULL)", - where2=account_type and "AND at.type = %(account_type)s" or "", - where3=res_ids and "AND " + res_alias + ".id in %(res_ids)s" or "", - company_id=self.env.company.id, - where4=aml_ids and "AND l.id IN %(aml_ids)s" or " ", - where5=all_entries and all_entries_query or only_dual_entries_query, - group_by1=is_partner and "l.partner_id, p.id," or " ", - group_by2=is_partner and ", p.last_time_entries_checked" or " ", - order_by=is_partner - and "ORDER BY p.last_time_entries_checked" - or "ORDER BY a.code", - outer_where=is_partner - and "WHERE (last_time_entries_checked IS NULL " - "OR max_date > last_time_entries_checked)" - or " ", + select=is_partner + and "partner_id, partner_name, to_char(last_time_entries_checked, " + "'YYYY-MM-DD') AS last_time_entries_checked," + or " ", + inner_select=is_partner + and "p.id AS partner_id, p.name AS partner_name, " + "p.last_time_entries_checked AS last_time_entries_checked," + or " ", + inner_from=is_partner + and "RIGHT JOIN res_partner p ON (l.partner_id = p.id)" + or " ", + where1=is_partner + and " " + or "AND ((at.type <> 'payable' AND at.type <> 'receivable') " + "OR l.partner_id IS NULL)", + where2=account_type and "AND at.type = %(account_type)s" or "", + where3=res_ids and "AND " + res_alias + ".id in %(res_ids)s" or "", + company_id=self.env.company.id, + where4=aml_ids and "AND l.id IN %(aml_ids)s" or " ", + where5=all_entries and all_entries_query or only_dual_entries_query, + group_by1=is_partner and "l.partner_id, p.id," or " ", + group_by2=is_partner and ", p.last_time_entries_checked" or " ", + order_by=is_partner + and "ORDER BY p.last_time_entries_checked" + or "ORDER BY a.code", + outer_where=is_partner + and "WHERE (last_time_entries_checked IS NULL " + "OR max_date > last_time_entries_checked)" + or " ", + ) ) self.env["account.move.line"].flush() self.env["account.account"].flush() @@ -822,17 +821,6 @@ class AccountReconciliation(models.AbstractModel): # line domain = expression.AND([domain, [("company_id", "=", st_line.company_id.id)]]) - # take only moves in valid state. Draft is accepted only when "Post At" - # is set to "Bank Reconciliation" in the associated journal - domain_post_at = [ - "|", - "&", - ("move_id.state", "=", "draft"), - ("journal_id.post_at", "=", "bank_rec"), - ("move_id.state", "not in", ["draft", "cancel"]), - ] - domain = expression.AND([domain, domain_post_at]) - if st_line.company_id.account_bank_reconciliation_start: domain = expression.AND( [ @@ -858,11 +846,7 @@ class AccountReconciliation(models.AbstractModel): "&", ("reconciled", "=", False), ("account_id", "=", account_id), - "|", ("move_id.state", "=", "posted"), - "&", - ("move_id.state", "=", "draft"), - ("move_id.journal_id.post_at", "=", "bank_rec"), ] domain = expression.AND([domain, [("balance", "!=", 0.0)]]) if partner_id: @@ -1046,7 +1030,8 @@ class AccountReconciliation(models.AbstractModel): data = { "id": st_line.id, "ref": st_line.ref, - "note": st_line.note or "", + # FIXME: where to fill? + # 'note': st_line.note or "", "name": st_line.name, "date": format_date(self.env, st_line.date), "amount": amount, @@ -1056,11 +1041,11 @@ class AccountReconciliation(models.AbstractModel): "journal_id": st_line.journal_id.id, "statement_id": st_line.statement_id.id, "account_id": [ - st_line.journal_id.default_debit_account_id.id, - st_line.journal_id.default_debit_account_id.display_name, + st_line.journal_id.default_account_id.id, + st_line.journal_id.default_account_id.display_name, ], - "account_code": st_line.journal_id.default_debit_account_id.code, - "account_name": st_line.journal_id.default_debit_account_id.name, + "account_code": st_line.journal_id.default_account_id.code, + "account_name": st_line.journal_id.default_account_id.name, "partner_name": st_line.partner_id.name, "communication_partner_name": st_line.partner_name, # Amount in the statement currency @@ -1091,20 +1076,19 @@ class AccountReconciliation(models.AbstractModel): where_str = where_clause and (" WHERE %s" % where_clause) or "" # Get pairs - query = """ + query = sql.SQL( + """ SELECT a.id, b.id FROM account_move_line a, account_move_line b, account_move move_a, account_move move_b, account_journal journal_a, account_journal journal_b WHERE a.id != b.id AND move_a.id = a.move_id - AND (move_a.state = 'posted' - OR (move_a.state = 'draft' AND journal_a.post_at = 'bank_rec')) + AND move_a.state = 'posted' AND move_a.journal_id = journal_a.id AND move_b.id = b.move_id AND move_b.journal_id = journal_b.id - AND (move_b.state = 'posted' - OR (move_b.state = 'draft' AND journal_b.post_at = 'bank_rec')) + AND move_b.state = 'posted' AND a.amount_residual = -b.amount_residual AND a.balance != 0.0 AND b.balance != 0.0 @@ -1118,7 +1102,8 @@ class AccountReconciliation(models.AbstractModel): ORDER BY a.date desc LIMIT 1 """.format( - from_clause + where_str + from_clause + where_str + ) ) move_line_id = self.env.context.get("move_line_id") or None params = ( diff --git a/account_reconciliation_widget/readme/CONTRIBUTORS.rst b/account_reconciliation_widget/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..207c602b --- /dev/null +++ b/account_reconciliation_widget/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Tecnativa - Pedro M. Baeza diff --git a/account_reconciliation_widget/readme/DESCRIPTION.rst b/account_reconciliation_widget/readme/DESCRIPTION.rst new file mode 100644 index 00000000..573558c8 --- /dev/null +++ b/account_reconciliation_widget/readme/DESCRIPTION.rst @@ -0,0 +1,2 @@ +This module restores account reconciliation widget moved from Odoo community to enterpise in V. 14.0 +Provides two widgets designed to reconcile move lines in a easy way: one focused on bank statements and another for generic use. diff --git a/account_reconciliation_widget/readme/USAGE.rst b/account_reconciliation_widget/readme/USAGE.rst new file mode 100644 index 00000000..4b4b9566 --- /dev/null +++ b/account_reconciliation_widget/readme/USAGE.rst @@ -0,0 +1,9 @@ +With an user with full accounting features enabled: + +Invoicing --> Accounting --> Actions --> Reconciliation. + +From journal items list view you can select check of them and click Action --> Reconcile. + +From accounting dashboard you can use reconcile button in Bank / Cash journals. + +Also, you can navigate to statements and use the reconcile button. diff --git a/account_reconciliation_widget/security/ir.model.access.csv b/account_reconciliation_widget/security/ir.model.access.csv new file mode 100644 index 00000000..d9184f74 --- /dev/null +++ b/account_reconciliation_widget/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_account_reconciliation_widget_group_invoice,account_reconciliation_widget.group_invoice,model_account_reconciliation_widget,account.group_account_invoice,1,1,1,1 diff --git a/account_reconciliation_widget/static/description/index.html b/account_reconciliation_widget/static/description/index.html new file mode 100644 index 00000000..560b763a --- /dev/null +++ b/account_reconciliation_widget/static/description/index.html @@ -0,0 +1,429 @@ + + + + + + +account_reconciliation_widget + + + +
+

account_reconciliation_widget

+ + +

Beta License: AGPL-3 OCA/account_reconciliation_widget Translate me on Weblate

+

This module restores account reconciliation widget moved from Odoo community to enterpise in V. 14.0 +Provides two widgets designed to reconcile move lines in a easy way: one focused on bank statements and another for generic use.

+

Table of contents

+ +
+

Usage

+

With an user with full accounting features enabled:

+

Invoicing –> Accounting –> Actions –> Reconciliation.

+

From journal items list view you can select check of them and click Action –> Reconcile.

+

From accounting dashboard you can use reconcile button in Bank / Cash journals.

+

Also, you can navigate to statements and use the reconcile button.

+
+
+

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

+
    +
  • Ozono Multimedia
  • +
+
+
+

Contributors

+
    +
  • Tecnativa - Pedro M. Baeza
  • +
+
+
+

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_reconciliation_widget project on GitHub.

+

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

+
+
+
+ + diff --git a/account_reconciliation_widget/static/src/js/reconciliation/reconciliation_action.js b/account_reconciliation_widget/static/src/js/reconciliation/reconciliation_action.js index 3acf3ba4..6e5a3d3a 100644 --- a/account_reconciliation_widget/static/src/js/reconciliation/reconciliation_action.js +++ b/account_reconciliation_widget/static/src/js/reconciliation/reconciliation_action.js @@ -32,7 +32,6 @@ odoo.define("account.ReconciliationClientAction", function (require) { close_statement: "_onCloseStatement", load_more: "_onLoadMore", reload: "reload", - search: "_onSearch", navigation_move: "_onNavigationMove", }, config: _.extend({}, AbstractAction.prototype.config, { @@ -52,6 +51,7 @@ odoo.define("account.ReconciliationClientAction", function (require) { _onNavigationMove: function (ev) { var non_reconciled_keys = _.keys( + // eslint-disable-next-line no-unused-vars _.pick(this.model.lines, function (value, key, object) { return !value.reconciled; }) @@ -86,7 +86,8 @@ odoo.define("account.ReconciliationClientAction", function (require) { this._super.apply(this, arguments); this.action_manager = parent; this.params = params; - this.controlPanelParams.modelName = "account.bank.statement.line"; + this.searchModelConfig.modelName = "account.bank.statement.line"; + this.controlPanelProps.cp_content = {}; this.model = new this.config.Model(this, { modelName: "account.reconciliation.widget", defaultDisplayQty: @@ -191,7 +192,10 @@ odoo.define("account.ReconciliationClientAction", function (require) { initialState.valuenow = valuenow; initialState.context = self.model.getContext(); self.renderer.showRainbowMan(initialState); - self.remove_cp(); + self.controlPanelProps.cp_content = { + $buttons: $(), + $pager: $(), + }; } else { // Create a notification if some lines have been reconciled automatically. if (initialState.valuenow > 0) @@ -234,12 +238,11 @@ odoo.define("account.ReconciliationClientAction", function (require) { this.$pager = $( QWeb.render("reconciliation.control.pager", {widget: this.renderer}) ); - this.updateControlPanel({ - clear: true, - cp_content: { - $pager: this.$pager, - }, - }); + + this.controlPanelProps.cp_content = { + $buttons: $(), + $pager: this.$pager, + }; this.renderer.$progress = this.$pager; $(this.renderer.$progress) .parent() @@ -248,12 +251,6 @@ odoo.define("account.ReconciliationClientAction", function (require) { } }, - remove_cp: function () { - this.updateControlPanel({ - clear: true, - }); - }, - // -------------------------------------------------------------------------- // Private // -------------------------------------------------------------------------- @@ -384,12 +381,11 @@ odoo.define("account.ReconciliationClientAction", function (require) { /** * @private - * @param {OdooEvent} ev + * @param {Object} searchQuery */ - _onSearch: function (ev) { + _onSearch: function (searchQuery) { var self = this; - ev.stopPropagation(); - this.model.domain = ev.data.domain; + this.model.domain = searchQuery.domain; this.model.display_context = "search"; self.reload().then(function () { self.renderer._updateProgressBar({ @@ -402,7 +398,6 @@ odoo.define("account.ReconciliationClientAction", function (require) { _onActionPartialAmount: function (event) { var self = this; var handle = event.target.handle; - var line = this.model.getLine(handle); var amount = this.model.getPartialReconcileAmount(handle, event.data); self._getWidget(handle).updatePartialAmount(event.data.data, amount); }, @@ -413,12 +408,13 @@ odoo.define("account.ReconciliationClientAction", function (require) { * @private * @param {OdooEvent} event */ + // eslint-disable-next-line no-unused-vars _onCloseStatement: function (event) { var self = this; return this.model.closeStatement().then(function (result) { self.do_action({ name: "Bank Statements", - res_model: "account.bank.statement.line", + res_model: "account.bank.statement", res_id: result, views: [[false, "form"]], type: "ir.actions.act_window", @@ -432,6 +428,7 @@ odoo.define("account.ReconciliationClientAction", function (require) { * * @param {OdooEvent} event */ + // eslint-disable-next-line no-unused-vars _onLoadMore: function (event) { return this._loadMore(this.model.defaultDisplayQty); }, @@ -466,7 +463,7 @@ odoo.define("account.ReconciliationClientAction", function (require) { self.widgets.splice(index, 1); } }); - // Get number of widget and if less than constant and if there are more to laod, load until constant + // Get number of widget and if less than constant and if there are more to load, load until constant if ( self.widgets.length < self.model.defaultDisplayQty && self.model.valuemax - self.model.valuenow >= diff --git a/account_reconciliation_widget/static/src/js/reconciliation/reconciliation_model.js b/account_reconciliation_widget/static/src/js/reconciliation/reconciliation_model.js index 26ca3f09..6eaf53d3 100644 --- a/account_reconciliation_widget/static/src/js/reconciliation/reconciliation_model.js +++ b/account_reconciliation_widget/static/src/js/reconciliation/reconciliation_model.js @@ -5,7 +5,6 @@ odoo.define("account.ReconciliationModel", function (require) { var field_utils = require("web.field_utils"); var utils = require("web.utils"); var session = require("web.session"); - var WarningDialog = require("web.CrashManager").WarningDialog; var core = require("web.core"); var _t = core._t; @@ -50,7 +49,7 @@ odoo.define("account.ReconciliationModel", function (require) { * label: string * amount: number - real amount * amount_str: string - formated amount - * [already_paid]: boolean + * [is_liquidity_line]: boolean * [partner_id]: integer * [partner_name]: string * [account_code]: string @@ -140,7 +139,6 @@ odoo.define("account.ReconciliationModel", function (require) { * @returns {Promise} */ addProposition: function (handle, mv_line_id) { - var self = this; var line = this.getLine(handle); var prop = _.clone(_.find(line["mv_lines_" + line.mode], {id: mv_line_id})); this._addProposition(line, prop); @@ -307,11 +305,11 @@ odoo.define("account.ReconciliationModel", function (require) { closeStatement: function () { var self = this; return this._rpc({ - model: "account.bank.statement.line", - method: "button_confirm_bank", - args: [self.bank_statement_line_id.id], + model: "account.bank.statement", + method: "button_validate", + args: [self.statement.statement_id], }).then(function () { - return self.bank_statement_line_id.id; + return self.statement.statement_id; }); }, /** @@ -807,6 +805,7 @@ odoo.define("account.ReconciliationModel", function (require) { // Check if we have another line with to_check and if yes don't change value of this proposition prop.to_check = line.reconciliation_proposition.some(function ( rec_prop, + // eslint-disable-next-line no-unused-vars index ) { return rec_prop.id !== prop.id && rec_prop.to_check; @@ -928,7 +927,7 @@ odoo.define("account.ReconciliationModel", function (require) { var props = _.filter(line.reconciliation_proposition, function (prop) { return !prop.invalid; }); - var computeLinePromise; + var computeLinePromise = null; if (props.length === 0) { // Usability: if user has not chosen any lines and click validate, it has the same behavior // as creating a write-off of the same amount. @@ -951,13 +950,13 @@ odoo.define("account.ReconciliationModel", function (require) { partner_id: line.st_line.partner_id, counterpart_aml_dicts: _.map( _.filter(props, function (prop) { - return !isNaN(prop.id) && !prop.already_paid; + return !isNaN(prop.id) && !prop.is_liquidity_line; }), self._formatToProcessReconciliation.bind(self, line) ), payment_aml_ids: _.pluck( _.filter(props, function (prop) { - return !isNaN(prop.id) && prop.already_paid; + return !isNaN(prop.id) && prop.is_liquidity_line; }), "id" ), @@ -1124,7 +1123,7 @@ odoo.define("account.ReconciliationModel", function (require) { } return; } - if (!prop.already_paid && parseInt(prop.id)) { + if (!prop.is_liquidity_line && parseInt(prop.id)) { prop.is_move_line = true; } reconciliation_proposition.push(prop); @@ -1135,6 +1134,7 @@ odoo.define("account.ReconciliationModel", function (require) { prop.__tax_to_recompute && prop.base_amount ) { + // (OZM) REVISAR reconciliation_proposition = _.filter( reconciliation_proposition, function (p) { @@ -1170,10 +1170,10 @@ odoo.define("account.ReconciliationModel", function (require) { tax_ids: tax.tax_ids, tax_repartition_line_id: tax.tax_repartition_line_id, - tag_ids: tax.tag_ids, + tax_tag_ids: tax.tag_ids, amount: tax.amount, - label: prop.label - ? prop.label + " " + tax.name + name: prop.name + ? prop.name + " " + tax.name : tax.name, date: prop.date, account_id: tax.account_id @@ -1205,7 +1205,9 @@ odoo.define("account.ReconciliationModel", function (require) { reconciliation_proposition.push(tax_prop); }); - prop.tag_ids = result.base_tags; + prop.tax_tag_ids = self._formatMany2ManyTagsTax( + result.base_tags || [] + ); }) ); } else { @@ -1571,7 +1573,7 @@ odoo.define("account.ReconciliationModel", function (require) { var formatOptions = { currency_id: line.st_line.currency_id, }; - var amount; + var amount = 0; switch (values.amount_type) { case "percentage": amount = (line.balance.amount * values.amount) / 100; @@ -1580,7 +1582,6 @@ odoo.define("account.ReconciliationModel", function (require) { var matching = line.st_line.name.match( new RegExp(values.amount_from_label_regex) ); - amount = 0; if (matching && matching.length == 2) { matching = matching[1].replace( new RegExp("\\D" + values.decimal_separator, "g"), @@ -1714,7 +1715,7 @@ odoo.define("account.ReconciliationModel", function (require) { function (prop) { return _.isNumber(prop.id) ? prop.id : null; } - ).filter((id) => id != null); + ).filter((id) => id !== null); var filter = line["filter_" + mode] || ""; return this._rpc({ model: "account.reconciliation.widget", @@ -1783,8 +1784,8 @@ odoo.define("account.ReconciliationModel", function (require) { * @param {Object[]} data.moves list of processed account.move * @returns {Deferred} */ + // eslint-disable-next-line no-unused-vars _validatePostProcess: function (data) { - var self = this; return Promise.resolve(); }, }); @@ -2288,7 +2289,7 @@ odoo.define("account.ReconciliationModel", function (require) { function (prop) { return _.isNumber(prop.id) ? prop.id : null; } - ).filter((id) => id != null); + ).filter((id) => id !== null); var filter = line.filter_match || ""; var args = [ line.account_id.id, diff --git a/account_reconciliation_widget/static/src/js/reconciliation/reconciliation_renderer.js b/account_reconciliation_widget/static/src/js/reconciliation/reconciliation_renderer.js index 6584ea32..520e90de 100644 --- a/account_reconciliation_widget/static/src/js/reconciliation/reconciliation_renderer.js +++ b/account_reconciliation_widget/static/src/js/reconciliation/reconciliation_renderer.js @@ -35,7 +35,6 @@ odoo.define("account.ReconciliationRenderer", function (require) { * @override */ start: function () { - var self = this; var defs = [this._super.apply(this, arguments)]; this.time = Date.now(); this.$progress = $(""); @@ -98,7 +97,6 @@ odoo.define("account.ReconciliationRenderer", function (require) { * @param {[object]} [state.notifications] */ update: function (state) { - var self = this; this._updateProgressBar(state); if (state.valuenow === state.valuemax && !this.$(".done_message").length) { @@ -147,6 +145,7 @@ odoo.define("account.ReconciliationRenderer", function (require) { * close and then open form view of bank statement * @param {MouseEvent} event */ + // eslint-disable-next-line no-unused-vars _onCloseBankStatement: function (e) { this.trigger_up("close_statement"); }, @@ -215,6 +214,7 @@ odoo.define("account.ReconciliationRenderer", function (require) { * @private * @param {MouseEvent} event */ + // eslint-disable-next-line no-unused-vars _onLoadMore: function (e) { this.trigger_up("load_more"); }, @@ -428,7 +428,6 @@ odoo.define("account.ReconciliationRenderer", function (require) { // Search propositions that could be a partial credit/debit. var props = []; - var balance = state.balance.amount_currency; _.each(state.reconciliation_proposition, function (prop) { if (prop.display) { props.push(prop); @@ -460,8 +459,6 @@ odoo.define("account.ReconciliationRenderer", function (require) { var matching_modes = self.model.modes.filter((x) => x.startsWith("match")); for (let i = 0; i < matching_modes.length; i++) { var stateMvLines = state["mv_lines_" + matching_modes[i]] || []; - var recs_count = - stateMvLines.length > 0 ? stateMvLines[0].recs_count : 0; var remaining = state["remaining_" + matching_modes[i]]; var $mv_lines = this.$( 'div[id*="notebook_page_' + @@ -517,7 +514,7 @@ odoo.define("account.ReconciliationRenderer", function (require) { // Create form if (state.createForm) { - var createPromise; + var createPromise = null; if (!this.fields.account_id) { createPromise = this._renderCreate(state); } @@ -822,6 +819,11 @@ odoo.define("account.ReconciliationRenderer", function (require) { group_acc: self.group_acc, }) ); + + function addRequiredStyle(widget) { + widget.$el.addClass("o_required_modifier"); + } + self.fields.account_id .appendTo($create.find(".create_account_id .o_td_field")) .then(addRequiredStyle.bind(self, self.fields.account_id)); @@ -851,10 +853,6 @@ odoo.define("account.ReconciliationRenderer", function (require) { $create.find(".create_to_check .o_td_field") ); self.$(".create").append($create); - - function addRequiredStyle(widget) { - widget.$el.addClass("o_required_modifier"); - } }); }, @@ -1060,7 +1058,9 @@ odoo.define("account.ReconciliationRenderer", function (require) { * @param {MouseEvent} event */ _onQuickCreateProposition: function (event) { - document.activeElement && document.activeElement.blur(); + if (document.activeElement) { + document.activeElement.blur(); + } this.trigger_up("quick_create_proposition", { data: $(event.target).data("reconcile-model-id"), }); @@ -1069,7 +1069,9 @@ odoo.define("account.ReconciliationRenderer", function (require) { * @private */ _onCreateProposition: function () { - document.activeElement && document.activeElement.blur(); + if (document.activeElement) { + document.activeElement.blur(); + } var invalid = []; _.each(this.fields, function (field) { if (!field.isValid()) { diff --git a/account_reconciliation_widget/static/src/xml/account_reconciliation.xml b/account_reconciliation_widget/static/src/xml/account_reconciliation.xml index e3a946d9..e687e29b 100644 --- a/account_reconciliation_widget/static/src/xml/account_reconciliation.xml +++ b/account_reconciliation_widget/static/src/xml/account_reconciliation.xml @@ -66,7 +66,7 @@ rel="do_action" data-action_name="Unpaid Customer Invoices" data-model="account.move" - data-domain="[('type', 'in', ('out_invoice', 'out_refund'))]" + data-domain="[('move_type', 'in', ('out_invoice', 'out_refund'))]" data-context="{'search_default_unpaid': 1}" >unpaid invoices and follow-up customers
  • Pay your vendor bills
  • Check all + + + + account.bank.statement.inherit.view.form + account.bank.statement + + + + + + + + +
    + +
    + + + +
    +
    + + + + + + diff --git a/account_reconciliation_widget/views/account_view.xml b/account_reconciliation_widget/views/account_view.xml new file mode 100644 index 00000000..2b1f8dab --- /dev/null +++ b/account_reconciliation_widget/views/account_view.xml @@ -0,0 +1,30 @@ + + + + + Reconciliation on Bank Statements + account.bank.statement.line + bank_statement_reconciliation_view + + + + Reconcile + manual_reconciliation_view + + action + list + + + + Reconciliation + manual_reconciliation_view + + + + + diff --git a/account_reconciliation_widget/views/assets.xml b/account_reconciliation_widget/views/assets.xml new file mode 100644 index 00000000..453a0d3b --- /dev/null +++ b/account_reconciliation_widget/views/assets.xml @@ -0,0 +1,42 @@ + + +