From ac27f097321c4976f4bdd28be66055b685df7736 Mon Sep 17 00:00:00 2001 From: AaronHForgeFlow Date: Wed, 29 Dec 2021 13:34:20 +0100 Subject: [PATCH] [15.0][MIG]purchase_unreconciled --- purchase_unreconciled/README.rst | 40 ++- purchase_unreconciled/__init__.py | 1 + purchase_unreconciled/__manifest__.py | 12 +- .../models/account_move_line.py | 32 ++- purchase_unreconciled/models/company.py | 9 +- .../models/purchase_order.py | 238 +++++++++++++----- .../models/res_config_settings.py | 11 +- .../security/ir.model.access.csv | 2 + .../static/description/index.html | 50 ++-- .../tests/test_purchase_unreconciled.py | 93 ++++++- .../views/purchase_order_view.xml | 18 +- .../views/res_config_settings_view.xml | 30 ++- purchase_unreconciled/wizards/__init__.py | 1 + .../wizards/purchase_unreconciled_exceeded.py | 36 +++ .../purchase_unreconciled_exceeded_view.xml | 26 ++ 15 files changed, 477 insertions(+), 122 deletions(-) create mode 100644 purchase_unreconciled/security/ir.model.access.csv create mode 100644 purchase_unreconciled/wizards/__init__.py create mode 100644 purchase_unreconciled/wizards/purchase_unreconciled_exceeded.py create mode 100644 purchase_unreconciled/wizards/purchase_unreconciled_exceeded_view.xml diff --git a/purchase_unreconciled/README.rst b/purchase_unreconciled/README.rst index 937df672d..b78b2c2b1 100644 --- a/purchase_unreconciled/README.rst +++ b/purchase_unreconciled/README.rst @@ -2,28 +2,31 @@ Purchase Unreconciled ===================== -.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:8134013d5d37fe8931266d6f916bbc160f48212ac4a9faa43b4efe39d03e6b4c + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png :target: https://odoo-community.org/page/development-status - :alt: Beta + :alt: Alpha .. |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--financial--tools-lightgray.png?logo=github - :target: https://github.com/OCA/account-financial-tools/tree/14.0/purchase_unreconciled + :target: https://github.com/OCA/account-financial-tools/tree/15.0/purchase_unreconciled :alt: OCA/account-financial-tools .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/account-financial-tools-14-0/account-financial-tools-14-0-purchase_unreconciled + :target: https://translation.odoo-community.org/projects/account-financial-tools-15-0/account-financial-tools-15-0-purchase_unreconciled :alt: Translate me on Weblate -.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png - :target: https://runbot.odoo-community.org/runbot/92/14.0 - :alt: Try me on Runbot +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/account-financial-tools&target_branch=15.0 + :alt: Try me on Runboat -|badge1| |badge2| |badge3| |badge4| |badge5| +|badge1| |badge2| |badge3| |badge4| |badge5| This module adds a new fields "Unreconciled" on Purchase Orders, that allows to find PO's with unreconciled journal items related. @@ -31,6 +34,11 @@ to find PO's with unreconciled journal items related. This module allows to reconcile those PO in a single click. In accounting settings users will be able to set up a specific account for write-off. +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + **Table of contents** .. contents:: @@ -51,8 +59,8 @@ 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 `_. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -88,6 +96,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. -This module is part of the `OCA/account-financial-tools `_ project on GitHub. +.. |maintainer-AaronHForgeFlow| image:: https://github.com/AaronHForgeFlow.png?size=40px + :target: https://github.com/AaronHForgeFlow + :alt: AaronHForgeFlow + +Current `maintainer `__: + +|maintainer-AaronHForgeFlow| + +This module is part of the `OCA/account-financial-tools `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/purchase_unreconciled/__init__.py b/purchase_unreconciled/__init__.py index 0650744f6..aee8895e7 100644 --- a/purchase_unreconciled/__init__.py +++ b/purchase_unreconciled/__init__.py @@ -1 +1,2 @@ from . import models +from . import wizards diff --git a/purchase_unreconciled/__manifest__.py b/purchase_unreconciled/__manifest__.py index 0cf6daddd..ec61390c2 100644 --- a/purchase_unreconciled/__manifest__.py +++ b/purchase_unreconciled/__manifest__.py @@ -1,15 +1,19 @@ -# Copyright 2019 ForgeFlow S.L. -# - Lois Rilo Antelo +# Copyright 2019-22 ForgeFlow S.L. # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). { "name": "Purchase Unreconciled", - "version": "14.0.1.0.0", + "version": "15.0.1.0.0", "author": "ForgeFlow S.L., Odoo Community Association (OCA)", "website": "https://github.com/OCA/account-financial-tools", "category": "Purchases", "depends": ["account_move_line_purchase_info", "purchase_stock"], - "data": ["views/purchase_order_view.xml", "views/res_config_settings_view.xml"], + "data": [ + "security/ir.model.access.csv", + "views/purchase_order_view.xml", + "views/res_config_settings_view.xml", + "wizards/purchase_unreconciled_exceeded_view.xml", + ], "license": "AGPL-3", "installable": True, "development_status": "Alpha", diff --git a/purchase_unreconciled/models/account_move_line.py b/purchase_unreconciled/models/account_move_line.py index b97afd521..3c67f3af8 100644 --- a/purchase_unreconciled/models/account_move_line.py +++ b/purchase_unreconciled/models/account_move_line.py @@ -1,4 +1,4 @@ -# Copyright 2019 ForgeFlow S.L. +# Copyright 2019-21 ForgeFlow S.L. # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from datetime import datetime @@ -18,7 +18,9 @@ class AccountMoveLine(models.Model): writeoff_amount_curr = round( sum(line["amount_residual_currency"] for line in self), precision ) - + if writeoff_amount_curr and not writeoff_amount: + # Data inconsistency, do not create the write-off + return (0.0, 0.0, True) first_currency = self[0]["currency_id"] if all([line["currency_id"] == first_currency for line in self]): same_curr = True @@ -37,7 +39,10 @@ class AccountMoveLine(models.Model): amount_writeoff_curr, same_curr, ) = self._get_writeoff_amounts() + if not amount_writeoff: + return self.env["account.move.line"] partners = self.mapped("partner_id") + move_date = writeoff_vals.get("date", datetime.now()) write_off_vals = { "name": _("Automatic writeoff"), "amount_currency": same_curr and amount_writeoff_curr or amount_writeoff, @@ -45,13 +50,16 @@ class AccountMoveLine(models.Model): "credit": amount_writeoff < 0.0 and -amount_writeoff or 0.0, "partner_id": len(partners) == 1 and partners.id or False, "account_id": writeoff_vals["account_id"], - "purchase_order_id": writeoff_vals["purchase_order_id"], + "date": move_date, "journal_id": writeoff_vals["journal_id"], - "currency_id": writeoff_vals["currency_id"], + "currency_id": writeoff_vals.get("currency_id", False), + "product_id": writeoff_vals["product_id"], + "purchase_order_id": writeoff_vals["purchase_order_id"], + "purchase_line_id": writeoff_vals["purchase_line_id"], } counterpart_account = self.mapped("account_id") if len(counterpart_account) != 1: - raise ValidationError(_("CAnnot write-off more than one account")) + raise ValidationError(_("Cannot write-off more than one account")) counter_part = write_off_vals.copy() counter_part["debit"] = write_off_vals["credit"] counter_part["credit"] = write_off_vals["debit"] @@ -60,12 +68,22 @@ class AccountMoveLine(models.Model): move = self.env["account.move"].create( { - "date": datetime.now(), + "date": move_date, "journal_id": writeoff_vals["journal_id"], - "currency_id": writeoff_vals["currency_id"], + "currency_id": writeoff_vals.get("currency_id", False), "line_ids": [(0, 0, write_off_vals), (0, 0, counter_part)], } ) + if writeoff_vals.get("purchase_order_id", False): + # done this way because purchase_order_id is a related field and will + # not being assign on create. Cannot assign purchase_line_id because + # it is a generic write-off for the whole PO + self.env.cr.execute( + """UPDATE account_move_line SET purchase_order_id = %s + WHERE id in %s + """, + (writeoff_vals["purchase_order_id"], tuple(move.line_ids.ids)), + ) move.action_post() return move.line_ids.filtered( lambda l: l.account_id.id == counterpart_account.id diff --git a/purchase_unreconciled/models/company.py b/purchase_unreconciled/models/company.py index 68ce46d51..3fb456129 100644 --- a/purchase_unreconciled/models/company.py +++ b/purchase_unreconciled/models/company.py @@ -1,4 +1,4 @@ -# Copyright 2019 ForgeFlow S.L. +# Copyright 2019-21 ForgeFlow S.L. # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from odoo import fields, models @@ -18,3 +18,10 @@ class ResCompany(models.Model): purchase_reconcile_journal_id = fields.Many2one( "account.journal", string="WriteOff Journal for Purchases" ) + purchase_lock_auto_reconcile = fields.Boolean() + purchase_reconcile_tolerance = fields.Float( + string="Purchase Reconcile Tolerance (%)", + default=0.0, + help="Percentage of tolerance of residual amount vs total amount of the " + "Purchase Order. Leave zero to accept all discrepancies", + ) diff --git a/purchase_unreconciled/models/purchase_order.py b/purchase_unreconciled/models/purchase_order.py index dce72356e..7d0a85dca 100644 --- a/purchase_unreconciled/models/purchase_order.py +++ b/purchase_unreconciled/models/purchase_order.py @@ -1,8 +1,9 @@ -# Copyright 2019 ForgeFlow S.L. +# Copyright 2019-21 ForgeFlow S.L.. # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from odoo import _, api, exceptions, fields, models from odoo.osv import expression +from odoo.tools import float_is_zero class PurchaseOrder(models.Model): @@ -16,57 +17,60 @@ class PurchaseOrder(models.Model): "everything is reconciled or that the related accounts do not " "allow reconciliation", ) - is_shipped = fields.Boolean(search="_search_is_shipped") + amount_unreconciled = fields.Float(compute="_compute_unreconciled") - @api.model - def _get_purchase_unreconciled_base_domain(self): + def _get_account_domain(self): + self.ensure_one() included_accounts = ( ( - self.env["product.category"].search( - [("property_valuation", "=", "real_time")] - ) + self.env["product.category"] + .with_company(self.company_id.id) + .search([("property_valuation", "=", "real_time")]) ) .mapped("property_stock_account_input_categ_id") .ids ) + return [("account_id", "in", included_accounts)] + + @api.model + def _get_purchase_unreconciled_base_domain(self): unreconciled_domain = [ ("account_id.reconcile", "=", True), - ("account_id", "in", included_accounts), + ("account_id.internal_type", "not in", ["receivable", "payable"]), ("move_id.state", "=", "posted"), - # for some reason when amount_residual is zero - # is marked as reconciled, this is better check - ("full_reconcile_id", "=", False), ("company_id", "in", self.env.companies.ids), + # same condition than Odoo Unreconciled filter + ("full_reconcile_id", "=", False), + ("balance", "!=", 0.0), ] return unreconciled_domain - def _search_is_shipped(self, operator, value): - if operator != "=" or not isinstance(value, bool): - raise ValueError(_("Unsupported search operator")) - is_shipped_pos = self.search([("picking_ids.state", "in", ("done", "cancel"))]) - if value: - return [("id", "in", is_shipped_pos.ids)] - else: - return [("id", "not in", is_shipped_pos.ids)] - def _compute_unreconciled(self): acc_item = self.env["account.move.line"] for rec in self: - domain = rec._get_purchase_unreconciled_base_domain() + domain = rec.with_company( + rec.company_id + )._get_purchase_unreconciled_base_domain() + domain_account = rec._get_account_domain() + unreconciled_domain = expression.AND([domain, domain_account]) unreconciled_domain = expression.AND( - [domain, [("purchase_order_id", "=", rec.id)]] + [unreconciled_domain, [("purchase_order_id", "=", rec.id)]] ) unreconciled_items = acc_item.search(unreconciled_domain) rec.unreconciled = len(unreconciled_items) > 0 + rec.amount_unreconciled = sum(unreconciled_items.mapped("amount_residual")) def _search_unreconciled(self, operator, value): - if operator != "=" or not isinstance(value, bool): + if operator not in ("=", "!=") or not isinstance(value, bool): raise ValueError(_("Unsupported search operator")) acc_item = self.env["account.move.line"] domain = self._get_purchase_unreconciled_base_domain() unreconciled_domain = expression.AND( [domain, [("purchase_order_id", "!=", False)]] ) + unreconciled_domain = expression.AND( + [unreconciled_domain, [("company_id", "in", self.env.companies.ids)]] + ) unreconciled_items = acc_item.search(unreconciled_domain) unreconciled_pos = unreconciled_items.mapped("purchase_order_id") if value: @@ -77,10 +81,16 @@ class PurchaseOrder(models.Model): def action_view_unreconciled(self): self.ensure_one() acc_item = self.env["account.move.line"] - domain = self._get_purchase_unreconciled_base_domain() + domain = self.with_company( + self.company_id.id + )._get_purchase_unreconciled_base_domain() + domain_account = self._get_account_domain() + unreconciled_domain = expression.AND([domain, domain_account]) unreconciled_domain = expression.AND( - [domain, [("purchase_order_id", "=", self.id)]] + [unreconciled_domain, [("purchase_order_id", "=", self.id)]] ) + unreconciled_domain.remove(("full_reconcile_id", "=", False)) + unreconciled_domain.remove("&") unreconciled_items = acc_item.search(unreconciled_domain) action = self.env.ref("account.action_account_moves_all") action_dict = action.read()[0] @@ -99,59 +109,169 @@ class PurchaseOrder(models.Model): ) ) self.ensure_one() + res = {} domain = self._get_purchase_unreconciled_base_domain() + domain_account = self._get_account_domain() + unreconciled_domain = expression.AND([domain, domain_account]) unreconciled_domain = expression.AND( [domain, [("purchase_order_id", "=", self.id)]] ) unreconciled_domain = expression.AND( [unreconciled_domain, [("company_id", "=", self.company_id.id)]] ) + writeoff_to_reconcile = self.env["account.move.line"] + all_writeoffs = self.env["account.move.line"] + reconciling_groups = self.env["account.move.line"].read_group( + domain=unreconciled_domain, + fields=["account_id", "product_id", "purchase_line_id"], + groupby=["account_id", "product_id", "purchase_line_id"], + lazy=False, + ) unreconciled_items = self.env["account.move.line"].search(unreconciled_domain) - writeoff_to_reconcile = False - for account in unreconciled_items.mapped("account_id"): - acc_unrec_items = unreconciled_items.filtered( - lambda ml: ml.account_id == account + for group in reconciling_groups: + account_id = group["account_id"][0] + product_id = group["product_id"][0] if group["product_id"] else False + purchase_line_id = ( + group["purchase_line_id"][0] if group["purchase_line_id"] else False ) - all_aml_share_same_currency = all( - [x.currency_id == self[0].currency_id for x in acc_unrec_items] - ) - writeoff_vals = { - "account_id": self.company_id.purchase_reconcile_account_id.id, - "journal_id": self.company_id.purchase_reconcile_journal_id.id, - "purchase_order_id": self.id, - "currency_id": self.currency_id.id, - } - if not all_aml_share_same_currency: - writeoff_vals["amount_currency"] = False - if writeoff_to_reconcile: - writeoff_to_reconcile += unreconciled_items._create_writeoff( - writeoff_vals + unreconciled_items_group = unreconciled_items.filtered( + lambda l: ( + l.account_id.id == account_id and l.product_id.id == product_id ) + ) + # Check which type of force reconciling we are doing: + # - Force reconciling amount_residual + # - Force reconciling amount_residual_currency + amount_residual_currency_reconcile = any( + unreconciled_items_group.filtered( + lambda l: l.amount_residual_currency != 0.0 + and l.account_id.id == account_id + ) + ) + if amount_residual_currency_reconcile: + residual_field = "amount_residual_currency" else: - writeoff_to_reconcile = unreconciled_items._create_writeoff( + residual_field = "amount_residual" + if float_is_zero( + sum(unreconciled_items_group.mapped(residual_field)), + precision_rounding=self.company_id.currency_id.rounding, + ): + moves_to_reconcile = unreconciled_items_group + else: + writeoff_vals = self._get_purchase_writeoff_vals( + unreconciled_items_group, purchase_line_id, product_id + ) + writeoff_to_reconcile = unreconciled_items_group._create_writeoff( writeoff_vals ) - # add writeoff line to reconcile algorithm and finish the reconciliation - if writeoff_to_reconcile: - remaining_moves = unreconciled_items + writeoff_to_reconcile - else: - remaining_moves = unreconciled_items - # Check if reconciliation is total or needs an exchange rate entry to be created - if remaining_moves: - remaining_moves.filtered(lambda l: not l.reconciled).reconcile() - return { + all_writeoffs |= writeoff_to_reconcile + # add writeoff line to reconcile algorithm and finish the reconciliation + moves_to_reconcile = unreconciled_items_group | writeoff_to_reconcile + # Check if reconciliation is total or needs an exchange rate entry to be + # created + if moves_to_reconcile: + moves_to_reconcile.filtered(lambda l: not l.reconciled).reconcile() + reconciled_ids = unreconciled_items | all_writeoffs + res = { "name": _("Reconciled journal items"), "type": "ir.actions.act_window", "view_type": "form", "view_mode": "tree,form", "res_model": "account.move.line", - "domain": [ - ("id", "in", unreconciled_items.ids + writeoff_to_reconcile.ids) - ], + "domain": [("id", "in", reconciled_ids.ids)], } + if self.env.context.get("bypass_unreconciled", False): + # When calling the method from the wizard, lock after reconciling + self.button_done() + return res + + def _get_purchase_writeoff_vals(self, amls, purchase_line_id, product_id): + writeoff_date = self.env.context.get("writeoff_date", False) + aml_date = max(amls.mapped("move_id.date")) + res = { + "account_id": self.company_id.purchase_reconcile_account_id.id, + "journal_id": self.company_id.purchase_reconcile_journal_id.id, + "purchase_order_id": self.id, + "purchase_line_id": purchase_line_id or False, + "product_id": product_id, + "currency_id": self.currency_id.id or self.env.company.currency_id.id, + "date": aml_date, + } + # hook for custom date: + if writeoff_date: + res.update({"date": writeoff_date}) + return res + + def reconcile_criteria(self): + """Gets the criteria where POs are locked or not, by default uses the company + configuration""" + self.ensure_one() + return self.unreconciled and self.company_id.purchase_lock_auto_reconcile def button_done(self): for rec in self: - if rec.unreconciled: - rec.action_reconcile() - return super(PurchaseOrder, self).button_done() + criteria = rec.reconcile_criteria() + if criteria: + if rec.unreconciled: + exception_msg = rec.unreconciled_exception_msg() + if exception_msg: + res = rec.purchase_unreconciled_exception(exception_msg) + return res + else: + rec.action_reconcile() + return super(PurchaseOrder, rec).button_done() + else: + return super(PurchaseOrder, rec).button_done() + else: + return super(PurchaseOrder, rec).button_done() + + def purchase_unreconciled_exception(self, exception_msg=None): + """This mean to be run when the SO cannot be reconciled because it is over + tolerance""" + self.ensure_one() + if exception_msg: + return ( + self.env["purchase.unreconciled.exceeded.wiz"] + .create( + { + "exception_msg": exception_msg, + "purchase_id": self.id, + "origin_reference": "{},{}".format("purchase.order", self.id), + "continue_method": "action_reconcile", + } + ) + .action_show() + ) + + def unreconciled_exception_msg(self): + self.ensure_one() + exception_msg = "" + amount_total = self.amount_total + if self.currency_id and self.company_id.currency_id != self.currency_id: + amount_total = self.currency_id._convert( + amount_total, + self.company_id.currency_id, + self.company_id, + fields.Date.today(), + ) + if ( + self.company_id.purchase_reconcile_tolerance + and amount_total + and abs(self.amount_unreconciled / amount_total) + >= self.company_id.purchase_reconcile_tolerance / 100.0 + ): + params = { + "amount_unreconciled": self.amount_unreconciled, + "amount_allowed": self.amount_total + * self.company_id.purchase_reconcile_tolerance + / 100.0, + } + exception_msg = ( + _( + "Finance Warning: \nUnreconciled amount is too high. Total " + "unreconciled amount: %(amount_unreconciled)s Maximum unreconciled" + " amount accepted: %(amount_allowed)s " + ) + % params + ) + return exception_msg diff --git a/purchase_unreconciled/models/res_config_settings.py b/purchase_unreconciled/models/res_config_settings.py index 4a84f403f..afce10572 100644 --- a/purchase_unreconciled/models/res_config_settings.py +++ b/purchase_unreconciled/models/res_config_settings.py @@ -1,6 +1,5 @@ -# Copyright 2016 Akretion (Alexis de Lattre ) -# Copyright 2018 Tecnativa - Pedro M. Baeza -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# Copyright 2019-21 ForgeFlow S.L.. +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from odoo import fields, models @@ -14,3 +13,9 @@ class ResConfigSettings(models.TransientModel): purchase_reconcile_journal_id = fields.Many2one( related="company_id.purchase_reconcile_journal_id", readonly=False ) + purchase_lock_auto_reconcile = fields.Boolean( + related="company_id.purchase_lock_auto_reconcile", readonly=False + ) + purchase_reconcile_tolerance = fields.Float( + related="company_id.purchase_reconcile_tolerance", readonly=False + ) diff --git a/purchase_unreconciled/security/ir.model.access.csv b/purchase_unreconciled/security/ir.model.access.csv new file mode 100644 index 000000000..11a3d6bf3 --- /dev/null +++ b/purchase_unreconciled/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 +purchase.unreconciled.exceeded.wiz,purchase.unreconciled.exceeded.wiz,purchase_unreconciled.model_purchase_unreconciled_exceeded_wiz,base.group_user,1,1,1,1 diff --git a/purchase_unreconciled/static/description/index.html b/purchase_unreconciled/static/description/index.html index f37fc7e01..2499658cb 100644 --- a/purchase_unreconciled/static/description/index.html +++ b/purchase_unreconciled/static/description/index.html @@ -1,20 +1,20 @@ - + - + Purchase Unreconciled