diff --git a/account_reconcile_checkout/README.rst b/account_reconcile_checkout/README.rst new file mode 100755 index 000000000..f563ce6a2 --- /dev/null +++ b/account_reconcile_checkout/README.rst @@ -0,0 +1,75 @@ +========================================== +Reconcile tools for Compassion CH +========================================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! 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-CompassionCH%2Fcompassion--switzerland-lightgray.png?logo=github + :target: https://github.com/CompassionCH/compassion-switzerland/tree/11.0/account_reconcile_compassion + :alt: CompassionCH/compassion-switzerland + +|badge1| |badge2| |badge3| + + +It finds a matching invoice for the move_line and reconciles only if the amount of the payment corresponds or if it is a multiple of the invoice amount. If many invoices are found, the first reconciled invoice is the current invoice (last invoice that is not in future). Then it reconciles the other invoices from last invoice to first. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + + +Usage +===== + + +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 +~~~~~~~ + +* Compassion CH + +Contributors +~~~~~~~~~~~~ + +* Emanuel Cino + +Maintainers +~~~~~~~~~~~ + +This module is maintained by Compassion Switzerland. + +.. image:: https://upload.wikimedia.org/wikipedia/en/8/83/CompassionInternationalLogo.png + :alt: Compassion Switzerland + :target: https://www.compassion.ch + +Compassion Switzerland is a nonprofit organization whose +mission is to release children from extreme poverty in Jesus name. + +This module is part of the `CompassionCH/compassion-switzerland `_ project on GitHub. + +You are welcome to contribute. diff --git a/account_reconcile_checkout/__init__.py b/account_reconcile_checkout/__init__.py new file mode 100644 index 000000000..838ba1c54 --- /dev/null +++ b/account_reconcile_checkout/__init__.py @@ -0,0 +1,11 @@ +############################################################################## +# +# Copyright (C) 2014 Compassion CH (http://www.compassion.ch) +# Releasing children from poverty in Jesus' name +# @author: Emanuel Cino +# +# The licence is in the file __manifest__.py +# +############################################################################## + +from . import wizards diff --git a/account_reconcile_checkout/__manifest__.py b/account_reconcile_checkout/__manifest__.py new file mode 100644 index 000000000..d7feca100 --- /dev/null +++ b/account_reconcile_checkout/__manifest__.py @@ -0,0 +1,50 @@ +############################################################################## +# +# ______ Releasing children from poverty _ +# / ____/___ ____ ___ ____ ____ ___________(_)___ ____ +# / / / __ \/ __ `__ \/ __ \/ __ `/ ___/ ___/ / __ \/ __ \ +# / /___/ /_/ / / / / / / /_/ / /_/ (__ |__ ) / /_/ / / / / +# \____/\____/_/ /_/ /_/ .___/\__,_/____/____/_/\____/_/ /_/ +# /_/ +# in Jesus' name +# +# Copyright (C) 2014-2017 Compassion CH (http://www.compassion.ch) +# @author: Emanuel Cino +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +# pylint: disable=C8101 +{ + "name": "Reconcile tools for Compassion CH", + "version": "14.0.1.0.0", + "author": "Compassion CH", + "license": "AGPL-3", + "category": "Finance", + "website": "http://www.compassion.ch", + "depends": [ + "account", + "payment_postfinance_flex" # paid-addons + ], + "external_dependencies": {"python": [ + "postfinancecheckout" + ]}, + "data": [ + "security/ir.model.access.csv", + "views/reconcile_outstanding_wizard_view.xml", + ], + "auto_install": False, + "installable": True, +} diff --git a/account_reconcile_checkout/readme/CONFIGURE.rst b/account_reconcile_checkout/readme/CONFIGURE.rst new file mode 100644 index 000000000..604b1a2d8 --- /dev/null +++ b/account_reconcile_checkout/readme/CONFIGURE.rst @@ -0,0 +1,3 @@ +You can add the following system parameter to enable an analytic account to be set on exchange rate move lines: + +* account_reconcile_compassion.currency_exchange_analytic_account diff --git a/account_reconcile_checkout/readme/CONTRIBUTORS.rst b/account_reconcile_checkout/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..6a9ca3183 --- /dev/null +++ b/account_reconcile_checkout/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Emanuel Cino diff --git a/account_reconcile_checkout/readme/DESCRIPTION.rst b/account_reconcile_checkout/readme/DESCRIPTION.rst new file mode 100644 index 000000000..7d8927bb0 --- /dev/null +++ b/account_reconcile_checkout/readme/DESCRIPTION.rst @@ -0,0 +1,6 @@ +Reconcile rules with bvr_ref of invoice for Compassion CH. + +This will add a Product field in the bank statement reconcile view that will allow to create an invoice from a received payment. When choosing a product, an invoice will be created and will be reconciled with the given payment. + +It finds a matching invoice for the move_line and reconciles only if the amount of the payment corresponds or if it is a multiple of the invoice amount. +If many invoices are found, the first reconciled invoice is the current invoice (last invoice that is not in future). Then it reconciles the other invoices from last invoice to first. diff --git a/account_reconcile_checkout/readme/USAGE.rst b/account_reconcile_checkout/readme/USAGE.rst new file mode 100644 index 000000000..b3e512b2b --- /dev/null +++ b/account_reconcile_checkout/readme/USAGE.rst @@ -0,0 +1,3 @@ +To use this module, you need to: + +* Go to Accounting -> Bank Statement -> Reconcile diff --git a/account_reconcile_checkout/security/ir.model.access.csv b/account_reconcile_checkout/security/ir.model.access.csv new file mode 100644 index 000000000..43ddbc2de --- /dev/null +++ b/account_reconcile_checkout/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_reconcile_outstanding_view,Access reconcile outstanding receipts Wizard,model_reconcile_outstanding_wizard,account.group_account_invoice,1,1,1,1 diff --git a/account_reconcile_checkout/views/reconcile_outstanding_wizard_view.xml b/account_reconcile_checkout/views/reconcile_outstanding_wizard_view.xml new file mode 100644 index 000000000..e13d05451 --- /dev/null +++ b/account_reconcile_checkout/views/reconcile_outstanding_wizard_view.xml @@ -0,0 +1,29 @@ + + + + checkout.reconcile.outstanding.wizard + reconcile.outstanding.wizard + +
+ + + + + +
+
+
+
+
+ + + + Reconcile outstanding receipts + reconcile.outstanding.wizard + form + new + + + +
diff --git a/account_reconcile_checkout/wizards/__init__.py b/account_reconcile_checkout/wizards/__init__.py new file mode 100644 index 000000000..5859791c5 --- /dev/null +++ b/account_reconcile_checkout/wizards/__init__.py @@ -0,0 +1,10 @@ +############################################################################## +# +# Copyright (C) 2014 Compassion CH (http://www.compassion.ch) +# Releasing children from poverty in Jesus' name +# @author: Emanuel Cino +# +# The licence is in the file __manifest__.py +# +############################################################################## +from . import reconcile_outstanding_wizard diff --git a/account_reconcile_checkout/wizards/reconcile_outstanding_wizard.py b/account_reconcile_checkout/wizards/reconcile_outstanding_wizard.py new file mode 100644 index 000000000..bcec5d1f2 --- /dev/null +++ b/account_reconcile_checkout/wizards/reconcile_outstanding_wizard.py @@ -0,0 +1,206 @@ +############################################################################## +# +# Copyright (C) 2022 Compassion CH (http://www.compassion.ch) +# Releasing children from poverty in Jesus' name +# @author: Emanuel Cino +# +# The licence is in the file __manifest__.py +# +############################################################################## +from datetime import datetime, timedelta +from enum import Enum +from postfinancecheckout import Configuration +from postfinancecheckout.api.transaction_service_api import TransactionServiceApi + +from odoo import api, models, fields, _ +from odoo.tools import ormcache + + +class Provider(Enum): + WORLDLINE = "WORLDLINE SCHWEIZ AG" + PF_CARD = "POSTFINANCE CARD" + TWINT = "TWINT" + E_FINANCE = "POSTFINANCE E-FINANCE" + + +PF_MAPPING = { + Provider.WORLDLINE: "SIX Acquiring", + Provider.PF_CARD: "PostFinance Acquiring - PostFinance Card", + Provider.E_FINANCE: "PostFinance Acquiring - PostFinance E-Finance", + Provider.TWINT: "TWINT - TWINT Connector" +} + + +class ReconcileOutstandingWizard(models.TransientModel): + + _name = "reconcile.outstanding.wizard" + _description = "Wizard reconcile outstanding payments account" + + account_id = fields.Many2one( + "account.account", "Reconcile account", + default=lambda s: s.env["account.account"].search([ + ("code", "=", "44")], limit=1) + ) + full_reconcile_line_ids = fields.Many2many( + "account.move.line", "reconcile_outstanding_reconciled", string="Reconciled lines", + readonly=True + ) + partial_reconcile_line_ids = fields.Many2many( + "account.move.line", "reconcile_outstanding_partial_reconciled", + string="Partial reconciled lines", readonly=True + ) + missing_donation_line_ids = fields.Many2many( + "account.move.line", "reconcile_outstanding_missing_invoice", + string="Leftover donations", + readonly=True + ) + + def reconcile_outstanding(self): + mvl_obj = self.env["account.move.line"] + credit_lines = mvl_obj.search([ + ("account_id", "=", self.account_id.id), + ("full_reconcile_id", "=", False), + ("credit", ">", 0), + ("date", ">=", "2022-02-09") # Date of activation of pf_checkout + ]) + for cl in credit_lines: + self.reconcile_using_pf_checkout(cl) + + # Compute results + if self.partial_reconcile_line_ids: + oldest_credit = min(self.partial_reconcile_line_ids.mapped("date")) + self.missing_donation_line_ids = mvl_obj.search([ + ("account_id", "=", self.account_id.id), + ("full_reconcile_id", "=", False), + ("debit", ">", 0), + ("date", "<=", fields.Date.to_string(oldest_credit)), + ("date", ">=", "2022-02-09") # Date of activation of pf_checkout + ]).filtered(lambda m: not m.matched_credit_ids) + if self.full_reconcile_line_ids: + self.env.user.notify_success( + message=_("Successfully reconciled %s entries") + % len(self.full_reconcile_line_ids), + sticky=True + ) + else: + if not credit_lines: + self.env.user.notify_success( + message=_("Every credit entry is reconciled")) + else: + self.env.user.notify_warning( + message=_("0 credit entry could be fully reconciled")) + return { + "type": "ir.actions.act_window", + "view_mode": "tree,form", + "view_type": "form", + "res_model": "account.move.line", + "target": "current", + "context": self.env.context, + "domain": [("id", "in", (self.partial_reconcile_line_ids + + self.missing_donation_line_ids).ids)] + } + + def reconcile_using_pf_checkout(self, move_line): + # Transactions are grouped by dates + date_position = -1 + date_length = 8 + search_days_delta = 0 + if Provider.WORLDLINE.value in move_line.name: + date_position = self._search_in_credit_string(move_line, "REFERENCES: ") + search_days_delta = -9 + provider = Provider.WORLDLINE + elif Provider.TWINT.value in move_line.name: + date_position = self._search_in_credit_string(move_line, "Payout ") + search_days_delta = -1 + provider = Provider.TWINT + elif Provider.PF_CARD.value in move_line.name: + date_position = self._search_in_credit_string(move_line, " TRAITEMENT DU ") + date_length = 10 + provider = Provider.PF_CARD + elif Provider.E_FINANCE.value in move_line.name: + date_position = self._search_in_credit_string(move_line, " TRAITEMENT DU ") + date_length = 10 + provider = Provider.E_FINANCE + if date_position == -1: + self.missing_donation_line_ids += move_line + return False + date_transactions = datetime.strptime( + move_line.name[date_position:date_position + date_length], + "%Y%m%d" if date_length == 8 else "%d.%m.%Y" + ) + timedelta(days=search_days_delta) + pf_service, space_id = self.get_pf_service() + debit_match = self.env["account.move.line"] + pf_filter = self.get_pf_filter(date_transactions, provider) + for transaction in pf_service.search(space_id, pf_filter): + # Some references have this TEMPTR- prefix that should be ignored + ref = transaction.merchant_reference.replace("TEMPTR-", "").split("-")[0] + debit_match += self.env["account.move.line"].search([ + ("ref", "like", ref), + ("debit", ">", 0), + ("full_reconcile_id", "=", False), + ("account_id", "=", self.account_id.id) + ]).filtered(lambda m: not m.matched_credit_ids) + # Perform a partial or full reconcile + (move_line + debit_match).reconcile() + if sum(debit_match.mapped("debit")) == move_line.credit: + self.full_reconcile_line_ids += move_line + else: + self.partial_reconcile_line_ids += move_line + return True + + @api.model + def _search_in_credit_string(self, move_line, search_string): + string_position = move_line.name.find(search_string) + if string_position > -1: + string_position += len(search_string) + return string_position + + @ormcache() + def get_pf_service(self): + pf_acquirer = self.env.ref( + "payment_postfinance_flex.payment_acquirer_postfinance") + config = Configuration( + user_id=pf_acquirer.postfinance_api_userid, + api_secret=pf_acquirer.postfinance_api_application_key) + return TransactionServiceApi(configuration=config),\ + pf_acquirer.postfinance_api_spaceid + + @api.model + def get_pf_filter(self, date_search=None, provider=None, state="FULFILL"): + if date_search is None: + date_search = datetime.today() + stop = date_search.replace(hour=23, minute=0, second=0, microsecond=0) + start = stop + timedelta(days=-1) + domain = { + "filter": { + "children": [ + { + "fieldName": "createdOn", + "operator": "GREATER_THAN", + "type": "LEAF", + "value": start.isoformat(), + }, + { + "fieldName": "createdOn", + "operator": "LESS_THAN_OR_EQUAL", + "type": "LEAF", + "value": stop.isoformat(), + }, + { + "fieldName": "state", + "operator": "EQUALS", + "type": "LEAF", + "value": state, + } + ], + "type": "AND", + } + } + if provider is not None: + domain["filter"]["children"].append({ + "fieldName": "paymentConnectorConfiguration.name", + "operator": "CONTAINS", + "type": "LEAF", + "value": PF_MAPPING.get(provider), + }) + return domain