Merge PR #554 into 16.0

Signed-off-by pedrobaeza
This commit is contained in:
OCA-git-bot
2023-10-18 15:20:44 +00:00
22 changed files with 1928 additions and 3 deletions

View File

@@ -15,9 +15,7 @@ class AccountJournal(models.Model):
@api.model
def _selection_service(self):
OnlineBankStatementProvider = self.env["online.bank.statement.provider"]
return OnlineBankStatementProvider._get_available_services() + [
("dummy", "Dummy")
]
return OnlineBankStatementProvider._selection_service()
# Keep provider fields for compatibility with other modules.
online_bank_statement_provider = fields.Selection(

View File

@@ -0,0 +1,126 @@
===================================
Online Bank Statements: MyPonto.com
===================================
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! 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%2Fbank--statement--import-lightgray.png?logo=github
:target: https://github.com/OCA/bank-statement-import/tree/14.0/account_statement_import_online_ponto
:alt: OCA/bank-statement-import
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/bank-statement-import-14-0/bank-statement-import-14-0-account_statement_import_online_ponto
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
:target: https://runbot.odoo-community.org/runbot/174/14.0
:alt: Try me on Runbot
|badge1| |badge2| |badge3| |badge4| |badge5|
This module provides online bank statements from MyPonto.com.
**Table of contents**
.. contents::
:local:
Configuration
=============
To configure online bank statements provider:
#. Go to *Invoicing > Configuration > Bank Accounts*
#. Open bank account to configure and edit it
#. Set *Bank Feeds* to *Online*
#. Select *MyPonto.com* as online bank statements provider in
*Online Bank Statements (OCA)* section
#. Save the bank account
#. Click on provider and configure provider-specific settings.
or, alternatively:
#. Go to *Invoicing > Overview*
#. Open settings of the corresponding journal account
#. Switch to *Bank Account* tab
#. Set *Bank Feeds* to *Online*
#. Select *MyPonto.com* as online bank statements provider in
*Online Bank Statements (OCA)* section
#. Save the bank account
#. Click on provider and configure provider-specific settings.
To obtain *Login* and *Key*:
#. Open `MyPonto.com <https://myponto.com/>`_.
Check also ``account_bank_statement_import_online`` configuration instructions
for more information.
Usage
=====
To pull historical bank statements:
#. Go to *Invoicing > Configuration > Bank Accounts*
#. Select specific bank accounts
#. Launch *Actions > Online Bank Statements Pull Wizard*
#. Configure date interval and click *Pull*
If historical data is not needed, then just simply wait for the scheduled
activity "Pull Online Bank Statements" to be executed for getting new
transactions.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/bank-statement-import/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 <https://github.com/OCA/bank-statement-import/issues/new?body=module:%20account_statement_import_online_ponto%0Aversion:%2014.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* Florent de Labarre
Contributors
~~~~~~~~~~~~
* Florent de Labarre
* `Tecnativa <https://www.tecnativa.com>`__:
* Pedro M. Baeza
* João Marques
* `Therp BV <https://therp.nl/>`__
* Ronald Portier <ronald@therp.nl>
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/bank-statement-import <https://github.com/OCA/bank-statement-import/tree/14.0/account_statement_import_online_ponto>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

@@ -0,0 +1,3 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import models

View File

@@ -0,0 +1,17 @@
# Copyright 2020 Florent de Labarre.
# Copyright 2020 Tecnativa - João Marques.
# Copyright 2022-2023 Therp BV <https://therp.nl>.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
{
"name": "Online Bank Statements: MyPonto.com",
"version": "16.0.1.0.0",
"category": "Account",
"website": "https://github.com/OCA/bank-statement-import",
"author": "Florent de Labarre, Therp BV, Odoo Community Association (OCA)",
"license": "AGPL-3",
"installable": True,
"depends": ["account_statement_import_online"],
"data": [
"views/online_bank_statement_provider.xml",
],
}

View File

@@ -0,0 +1,87 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * account_statement_import_online_ponto
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 15.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_statement_import_online_ponto
#: code:addons/account_statement_import_online_ponto/models/online_bank_statement_provider_ponto.py:0
#, python-format
msgid ""
"Error during Create Synchronisation {} \n"
"\n"
" {}"
msgstr ""
#. module: account_statement_import_online_ponto
#: code:addons/account_statement_import_online_ponto/models/online_bank_statement_provider_ponto.py:0
#, python-format
msgid ""
"Error during get transaction.\n"
"\n"
"{} \n"
"\n"
" {}"
msgstr ""
#. module: account_statement_import_online_ponto
#: model_terms:ir.ui.view,arch_db:account_statement_import_online_ponto.online_bank_statement_provider_form
msgid "Login"
msgstr ""
#. module: account_statement_import_online_ponto
#: model:ir.model,name:account_statement_import_online_ponto.model_online_bank_statement_provider
msgid "Online Bank Statement Provider"
msgstr ""
#. module: account_statement_import_online_ponto
#: code:addons/account_statement_import_online_ponto/models/online_bank_statement_provider_ponto.py:0
#, python-format
msgid "Please fill login and key."
msgstr ""
#. module: account_statement_import_online_ponto
#: code:addons/account_statement_import_online_ponto/models/online_bank_statement_provider_ponto.py:0
#, python-format
msgid "Ponto : no token"
msgstr ""
#. module: account_statement_import_online_ponto
#: code:addons/account_statement_import_online_ponto/models/online_bank_statement_provider_ponto.py:0
#, python-format
msgid "Ponto : wrong configuration, unknow account %s"
msgstr ""
#. module: account_statement_import_online_ponto
#: model:ir.model.fields,field_description:account_statement_import_online_ponto.field_online_bank_statement_provider__ponto_last_identifier
msgid "Ponto Last Identifier"
msgstr ""
#. module: account_statement_import_online_ponto
#: model:ir.model.fields,field_description:account_statement_import_online_ponto.field_online_bank_statement_provider__ponto_token
msgid "Ponto Token"
msgstr ""
#. module: account_statement_import_online_ponto
#: model:ir.model.fields,field_description:account_statement_import_online_ponto.field_online_bank_statement_provider__ponto_token_expiration
msgid "Ponto Token Expiration"
msgstr ""
#. module: account_statement_import_online_ponto
#: model_terms:ir.ui.view,arch_db:account_statement_import_online_ponto.online_bank_statement_provider_form
msgid "Reset Last identifier."
msgstr ""
#. module: account_statement_import_online_ponto
#: model_terms:ir.ui.view,arch_db:account_statement_import_online_ponto.online_bank_statement_provider_form
msgid "Secret Key"
msgstr ""

View File

@@ -0,0 +1,126 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * account_statement_import_online_ponto
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 14.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2022-01-26 18:52+0000\n"
"Last-Translator: Jaume Planas <jaume.planas@minorisa.net>\n"
"Language-Team: none\n"
"Language: ca\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.3.2\n"
#. module: account_statement_import_online_ponto
#: code:addons/account_statement_import_online_ponto/models/online_bank_statement_provider_ponto.py:0
#: code:addons/account_statement_import_online_ponto/models/online_bank_statement_provider_ponto.py:0
#, python-format
msgid ""
"%s \n"
"\n"
" %s"
msgstr ""
"%s \n"
"\n"
" %s"
#. module: account_statement_import_online_ponto
#: model:ir.model.fields,field_description:account_statement_import_online_ponto.field_online_bank_statement_provider__display_name
msgid "Display Name"
msgstr "Nom"
#. module: account_statement_import_online_ponto
#: code:addons/account_statement_import_online_ponto/models/online_bank_statement_provider_ponto.py:0
#, python-format
msgid ""
"Error during Create Synchronisation %s \n"
"\n"
" %s"
msgstr ""
"Error al crear sincronització %s \n"
"\n"
" %s"
#. module: account_statement_import_online_ponto
#: code:addons/account_statement_import_online_ponto/models/online_bank_statement_provider_ponto.py:0
#, python-format
msgid ""
"Error during get transaction.\n"
"\n"
"%s \n"
"\n"
" %s"
msgstr ""
"Error durant la transacció GET.\n"
"\n"
"%s \n"
"\n"
" %s"
#. module: account_statement_import_online_ponto
#: model:ir.model.fields,field_description:account_statement_import_online_ponto.field_online_bank_statement_provider__id
msgid "ID"
msgstr "ID"
#. module: account_statement_import_online_ponto
#: model:ir.model.fields,field_description:account_statement_import_online_ponto.field_online_bank_statement_provider____last_update
msgid "Last Modified on"
msgstr "Última modificació el"
#. module: account_statement_import_online_ponto
#: model_terms:ir.ui.view,arch_db:account_statement_import_online_ponto.online_bank_statement_provider_form
msgid "Login"
msgstr "Inicia sessió"
#. module: account_statement_import_online_ponto
#: model:ir.model,name:account_statement_import_online_ponto.model_online_bank_statement_provider
msgid "Online Bank Statement Provider"
msgstr "Proveïdor en línia d'extractes bancaris"
#. module: account_statement_import_online_ponto
#: code:addons/account_statement_import_online_ponto/models/online_bank_statement_provider_ponto.py:0
#, python-format
msgid "Please fill login and key."
msgstr "Ompliu l'inici de sessió i la clau."
#. module: account_statement_import_online_ponto
#: code:addons/account_statement_import_online_ponto/models/online_bank_statement_provider_ponto.py:0
#, python-format
msgid "Ponto : no token"
msgstr "Ponto: cap token"
#. module: account_statement_import_online_ponto
#: code:addons/account_statement_import_online_ponto/models/online_bank_statement_provider_ponto.py:0
#, python-format
msgid "Ponto : wrong configuration, unknow account %s"
msgstr "Ponto: configuració errònia, compte %s desconegut"
#. module: account_statement_import_online_ponto
#: model:ir.model.fields,field_description:account_statement_import_online_ponto.field_online_bank_statement_provider__ponto_last_identifier
msgid "Ponto Last Identifier"
msgstr "Ponto Últim identificador"
#. module: account_statement_import_online_ponto
#: model:ir.model.fields,field_description:account_statement_import_online_ponto.field_online_bank_statement_provider__ponto_token
msgid "Ponto Token"
msgstr "Token Ponto"
#. module: account_statement_import_online_ponto
#: model:ir.model.fields,field_description:account_statement_import_online_ponto.field_online_bank_statement_provider__ponto_token_expiration
msgid "Ponto Token Expiration"
msgstr "Caducitat token Ponto"
#. module: account_statement_import_online_ponto
#: model_terms:ir.ui.view,arch_db:account_statement_import_online_ponto.online_bank_statement_provider_form
msgid "Reset Last identifier."
msgstr "Restabliment últim identificador."
#. module: account_statement_import_online_ponto
#: model_terms:ir.ui.view,arch_db:account_statement_import_online_ponto.online_bank_statement_provider_form
msgid "Secret Key"
msgstr "Clau secreta"

View File

@@ -0,0 +1,118 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * account_statement_import_online_ponto
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 14.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2021-11-25 17:36+0000\n"
"Last-Translator: Sergio Zanchetta <primes2h@gmail.com>\n"
"Language-Team: none\n"
"Language: it\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.3.2\n"
#. module: account_statement_import_online_ponto
#: code:addons/account_statement_import_online_ponto/models/online_bank_statement_provider_ponto.py:0
#: code:addons/account_statement_import_online_ponto/models/online_bank_statement_provider_ponto.py:0
#, python-format
msgid ""
"%s \n"
"\n"
" %s"
msgstr ""
"%s \n"
"\n"
" %s"
#. module: account_statement_import_online_ponto
#: model:ir.model.fields,field_description:account_statement_import_online_ponto.field_online_bank_statement_provider__display_name
msgid "Display Name"
msgstr "Nome visualizzato"
#. module: account_statement_import_online_ponto
#: code:addons/account_statement_import_online_ponto/models/online_bank_statement_provider_ponto.py:0
#, python-format
msgid ""
"Error during Create Synchronisation %s \n"
"\n"
" %s"
msgstr ""
#. module: account_statement_import_online_ponto
#: code:addons/account_statement_import_online_ponto/models/online_bank_statement_provider_ponto.py:0
#, python-format
msgid ""
"Error during get transaction.\n"
"\n"
"%s \n"
"\n"
" %s"
msgstr ""
#. module: account_statement_import_online_ponto
#: model:ir.model.fields,field_description:account_statement_import_online_ponto.field_online_bank_statement_provider__id
msgid "ID"
msgstr "ID"
#. module: account_statement_import_online_ponto
#: model:ir.model.fields,field_description:account_statement_import_online_ponto.field_online_bank_statement_provider____last_update
msgid "Last Modified on"
msgstr "Ultima modifica il"
#. module: account_statement_import_online_ponto
#: model_terms:ir.ui.view,arch_db:account_statement_import_online_ponto.online_bank_statement_provider_form
msgid "Login"
msgstr ""
#. module: account_statement_import_online_ponto
#: model:ir.model,name:account_statement_import_online_ponto.model_online_bank_statement_provider
msgid "Online Bank Statement Provider"
msgstr ""
#. module: account_statement_import_online_ponto
#: code:addons/account_statement_import_online_ponto/models/online_bank_statement_provider_ponto.py:0
#, python-format
msgid "Please fill login and key."
msgstr ""
#. module: account_statement_import_online_ponto
#: code:addons/account_statement_import_online_ponto/models/online_bank_statement_provider_ponto.py:0
#, python-format
msgid "Ponto : no token"
msgstr "Ponto : nessun token"
#. module: account_statement_import_online_ponto
#: code:addons/account_statement_import_online_ponto/models/online_bank_statement_provider_ponto.py:0
#, python-format
msgid "Ponto : wrong configuration, unknow account %s"
msgstr ""
#. module: account_statement_import_online_ponto
#: model:ir.model.fields,field_description:account_statement_import_online_ponto.field_online_bank_statement_provider__ponto_last_identifier
msgid "Ponto Last Identifier"
msgstr ""
#. module: account_statement_import_online_ponto
#: model:ir.model.fields,field_description:account_statement_import_online_ponto.field_online_bank_statement_provider__ponto_token
msgid "Ponto Token"
msgstr "Token Ponto"
#. module: account_statement_import_online_ponto
#: model:ir.model.fields,field_description:account_statement_import_online_ponto.field_online_bank_statement_provider__ponto_token_expiration
msgid "Ponto Token Expiration"
msgstr "Scadenza token Ponto"
#. module: account_statement_import_online_ponto
#: model_terms:ir.ui.view,arch_db:account_statement_import_online_ponto.online_bank_statement_provider_form
msgid "Reset Last identifier."
msgstr ""
#. module: account_statement_import_online_ponto
#: model_terms:ir.ui.view,arch_db:account_statement_import_online_ponto.online_bank_statement_provider_form
msgid "Secret Key"
msgstr ""

View File

@@ -0,0 +1,3 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import ponto_interface
from . import online_bank_statement_provider_ponto

View File

@@ -0,0 +1,219 @@
# Copyright 2020 Florent de Labarre
# Copyright 2022-2023 Therp BV <https://therp.nl>.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import json
import logging
import re
from datetime import datetime, timedelta
from operator import itemgetter
import pytz
from odoo import _, api, fields, models
_logger = logging.getLogger(__name__)
class OnlineBankStatementProvider(models.Model):
_inherit = "online.bank.statement.provider"
ponto_last_identifier = fields.Char(readonly=True)
ponto_date_field = fields.Selection(
[
("execution_date", "Execution Date"),
("value_date", "Value Date"),
],
default="execution_date",
help="Select the Ponto date field that will be used for "
"the Odoo bank statement line date.",
)
@api.model
def _get_available_services(self):
"""Each provider model must register its service."""
return super()._get_available_services() + [
("ponto", "MyPonto.com"),
]
def _pull(self, date_since, date_until):
"""For Ponto the pulling of data will not be grouped by statement.
Instead we will pull data from the last available backwards.
For a scheduled pull we will continue until we get to data
already retrieved or there is no more data available.
For a wizard pull we will discard data after date_until and
stop retrieving when either we get before date_since or there is
no more data available.
"""
# pylint: disable=missing-return
ponto_providers = self.filtered(lambda provider: provider.service == "ponto")
super(OnlineBankStatementProvider, self - ponto_providers)._pull(
date_since, date_until
)
for provider in ponto_providers:
provider._ponto_pull(date_since, date_until)
def _ponto_pull(self, date_since, date_until):
"""Translate information from Ponto to Odoo bank statement lines."""
self.ensure_one()
is_scheduled = self.env.context.get("scheduled", False)
if is_scheduled:
_logger.debug(
_(
"Ponto obtain statement data for journal %(journal)s"
" from %(date_since)s to %(date_until)s"
),
dict(
journal=self.journal_id.name,
date_since=date_since,
date_until=date_until,
),
)
else:
_logger.debug(
_("Ponto obtain all new statement data for journal %s"),
self.journal_id.name,
)
lines = self._ponto_retrieve_data(date_since, date_until)
if not lines:
_logger.info(_("No lines were retrieved from Ponto"))
else:
# For scheduled runs, store latest identifier.
if is_scheduled:
self.ponto_last_identifier = lines[0].get("id")
self._ponto_store_lines(lines)
def _ponto_retrieve_data(self, date_since, date_until):
"""Fill buffer with data from Ponto.
We will retrieve data from the latest transactions present in Ponto
backwards, until we find data that has an execution date before date_since,
or until we get to a transaction that we already have.
Note: when reading data they are likely to be in descending order of
execution_date (not seen a guarantee for this in Ponto API). When using
value date, they may well be out of order. So we cannot simply stop
when we have found a transaction date before the date_since.
We will not read transactions more then a week before before date_since.
"""
date_stop = date_since - timedelta(days=7)
is_scheduled = self.env.context.get("scheduled", False)
lines = []
interface_model = self.env["ponto.interface"]
access_data = interface_model._login(self.username, self.password)
interface_model._set_access_account(access_data, self.account_number)
latest_identifier = False
transactions = interface_model._get_transactions(access_data, latest_identifier)
while transactions:
for line in transactions:
identifier = line.get("id")
transaction_datetime = self._ponto_get_transaction_datetime(line)
if is_scheduled:
# Handle all stop conditions for scheduled runs.
if identifier == self.ponto_last_identifier or (
not self.ponto_last_identifier
and transaction_datetime < date_stop
):
return lines
else:
# Handle stop conditions for non scheduled runs.
if transaction_datetime < date_stop:
return lines
if (
transaction_datetime < date_since
or transaction_datetime > date_until
):
continue
line["transaction_datetime"] = transaction_datetime
lines.append(line)
latest_identifier = transactions[-1].get("id")
transactions = interface_model._get_transactions(
access_data, latest_identifier
)
# We get here if we found no transactions before date_since,
# or not equal to stored last identifier.
return lines
def _ponto_store_lines(self, lines):
"""Store transactions retrieved from Ponto in statements."""
lines = sorted(lines, key=itemgetter("transaction_datetime"))
# Group statement lines by date per period (date range)
grouped_periods = {}
for line in lines:
date_since = line["transaction_datetime"]
statement_date_since = self._get_statement_date_since(date_since)
statement_date_until = (
statement_date_since + self._get_statement_date_step()
)
if (statement_date_since, statement_date_until) not in grouped_periods:
grouped_periods[(statement_date_since, statement_date_until)] = []
line.pop("transaction_datetime")
vals_line = self._ponto_get_transaction_vals(line)
grouped_periods[(statement_date_since, statement_date_until)].append(
vals_line
)
# For each period, create or update statement lines
for period, statement_lines in grouped_periods.items():
(date_since, date_until) = period
statement = self._create_or_update_statement(
(statement_lines, {}), date_since, date_until
)
for line in statement.line_ids.filtered(lambda l: not l.partner_id):
line.partner_id = line._retrieve_partner()
def _ponto_get_transaction_vals(self, transaction):
"""Translate information from Ponto to statement line vals."""
attributes = transaction.get("attributes", {})
ref_list = [
attributes.get(x)
for x in {
"description",
"counterpartName",
"counterpartReference",
}
if attributes.get(x)
]
ref = " ".join(ref_list)
date = self._ponto_get_transaction_datetime(transaction)
vals_line = {
"sequence": 1, # Sequence is not meaningfull for Ponto.
"date": date,
"ref": re.sub(" +", " ", ref) or "/",
"payment_ref": attributes.get("remittanceInformation", ref),
"unique_import_id": transaction["id"],
"amount": attributes["amount"],
"raw_data": json.dumps(transaction),
}
if attributes.get("counterpartReference"):
vals_line["account_number"] = attributes["counterpartReference"]
if attributes.get("counterpartName"):
vals_line["partner_name"] = attributes["counterpartName"]
return vals_line
def _ponto_get_transaction_datetime(self, transaction):
"""Get execution datetime for a transaction.
Odoo often names variables containing date and time just xxx_date or
date_xxx. We try to avoid this misleading naming by using datetime as
much for variables and fields of type datetime.
"""
attributes = transaction.get("attributes", {})
if self.ponto_date_field == "value_date":
datetime_str = attributes.get("valueDate")
else:
datetime_str = attributes.get("executionDate")
return self._ponto_datetime_from_string(datetime_str)
def _ponto_datetime_from_string(self, datetime_str):
"""Dates in Ponto are expressed in UTC, so we need to convert them
to supplied tz for proper classification.
"""
dt = datetime.strptime(datetime_str, "%Y-%m-%dT%H:%M:%S.%fZ")
dt = dt.replace(tzinfo=pytz.utc).astimezone(pytz.timezone(self.tz or "utc"))
return dt.replace(tzinfo=None)

View File

@@ -0,0 +1,166 @@
# Copyright 2020 Florent de Labarre
# Copyright 2022 Therp BV <https://therp.nl>.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import base64
import json
import logging
import requests
from dateutil.relativedelta import relativedelta
from odoo import _, fields, models
from odoo.exceptions import UserError
from odoo.addons.base.models.res_bank import sanitize_account_number
_logger = logging.getLogger(__name__)
PONTO_ENDPOINT = "https://api.myponto.com"
class PontoInterface(models.AbstractModel):
_name = "ponto.interface"
_description = "Interface to all interactions with Ponto API"
def _login(self, username, password):
"""Ponto login returns an access dictionary for further requests."""
url = PONTO_ENDPOINT + "/oauth2/token"
if not (username and password):
raise UserError(_("Please fill login and key."))
login = ":".join([username, password])
login = base64.b64encode(login.encode("UTF-8")).decode("UTF-8")
login_headers = {
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "application/json",
"Authorization": "Basic {login}".format(login=login),
}
_logger.debug(_("POST request on %(url)s"), dict(url=url))
response = requests.post(
url,
params={"grant_type": "client_credentials"},
headers=login_headers,
timeout=60,
)
data = self._get_response_data(response)
access_token = data.get("access_token", False)
if not access_token:
raise UserError(_("Ponto : no token"))
token_expiration = fields.Datetime.now() + relativedelta(
seconds=data.get("expires_in", False)
)
return {
"username": username,
"password": password,
"access_token": access_token,
"token_expiration": token_expiration,
}
def _get_request_headers(self, access_data):
"""Get headers with authorization for further ponto requests."""
if access_data["token_expiration"] <= fields.Datetime.now():
updated_data = self._login(access_data["username"], access_data["password"])
access_data.update(updated_data)
return {
"Accept": "application/json",
"Authorization": "Bearer {access_token}".format(
access_token=access_data["access_token"]
),
}
def _set_access_account(self, access_data, account_number):
"""Set ponto account for bank account in access_data."""
url = PONTO_ENDPOINT + "/accounts"
_logger.debug(_("GET request on %(url)s"), dict(url=url))
response = requests.get(
url,
params={"limit": 100},
headers=self._get_request_headers(access_data),
timeout=60,
)
data = self._get_response_data(response)
for ponto_account in data.get("data", []):
ponto_iban = sanitize_account_number(
ponto_account.get("attributes", {}).get("reference", "")
)
if ponto_iban == account_number:
access_data["ponto_account"] = ponto_account.get("id")
return
# If we get here, we did not find Ponto account for bank account.
raise UserError(
_(
"Ponto : wrong configuration, account {account} not found in {data}"
).format(account=account_number, data=data)
)
def _get_transactions(self, access_data, last_identifier):
"""Get transactions from ponto, using last_identifier as pointer.
Note that Ponto has the transactions in descending order. The first
transaction, retrieved by not passing an identifier, is the latest
present in Ponto. If you read transactions 'after' a certain identifier
(Ponto id), you will get transactions with an earlier date.
"""
url = (
PONTO_ENDPOINT
+ "/accounts/"
+ access_data["ponto_account"]
+ "/transactions"
)
params = {"limit": 100}
if last_identifier:
params["after"] = last_identifier
data = self._get_request(access_data, url, params)
transactions = self._get_transactions_from_data(data)
return transactions
def _get_transactions_from_data(self, data):
"""Get all transactions that are in the ponto response data."""
transactions = data.get("data", [])
if not transactions:
_logger.debug(
_("No transactions where found in data %(data)s"),
dict(data=data),
)
else:
_logger.debug(
_("%d transactions present in response data"),
len(transactions),
)
return transactions
def _get_request(self, access_data, url, params):
"""Interact with Ponto to get next page of data."""
headers = self._get_request_headers(access_data)
_logger.debug(
_("GET request to %(url)s with headers %(headers)s and params %(params)s"),
dict(
url=url,
headers=headers,
params=params,
),
)
response = requests.get(
url,
params=params,
headers=headers,
timeout=(60, 300),
)
return self._get_response_data(response)
def _get_response_data(self, response):
"""Get response data for GET or POST request."""
_logger.debug(
_("HTTP answer code %(response_code)s from Ponto"),
dict(response_code=response.status_code),
)
if response.status_code not in (200, 201):
raise UserError(
_(
"Server returned status code {response_code}: {response_text}"
).format(
response_code=response.status_code,
response_text=response.text,
)
)
return json.loads(response.text)

View File

@@ -0,0 +1,27 @@
To configure online bank statements provider:
#. Go to *Invoicing > Configuration > Bank Accounts*
#. Open bank account to configure and edit it
#. Set *Bank Feeds* to *Online*
#. Select *MyPonto.com* as online bank statements provider in
*Online Bank Statements (OCA)* section
#. Save the bank account
#. Click on provider and configure provider-specific settings.
or, alternatively:
#. Go to *Invoicing > Overview*
#. Open settings of the corresponding journal account
#. Switch to *Bank Account* tab
#. Set *Bank Feeds* to *Online*
#. Select *MyPonto.com* as online bank statements provider in
*Online Bank Statements (OCA)* section
#. Save the bank account
#. Click on provider and configure provider-specific settings.
To obtain *Login* and *Key*:
#. Open `MyPonto.com <https://myponto.com/>`_.
Check also ``account_bank_statement_import_online`` configuration instructions
for more information.

View File

@@ -0,0 +1,8 @@
* Florent de Labarre
* `Tecnativa <https://www.tecnativa.com>`__:
* Pedro M. Baeza
* João Marques
* `Therp BV <https://therp.nl>`__:
* Ronald Portier <ronald@therp.nl>

View File

@@ -0,0 +1 @@
This module provides online bank statements from MyPonto.com.

View File

@@ -0,0 +1,10 @@
To pull historical bank statements:
#. Go to *Invoicing > Configuration > Bank Accounts*
#. Select specific bank accounts
#. Launch *Actions > Online Bank Statements Pull Wizard*
#. Configure date interval and click *Pull*
If historical data is not needed, then just simply wait for the scheduled
activity "Pull Online Bank Statements" to be executed for getting new
transactions.

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,473 @@
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils 0.15.1: http://docutils.sourceforge.net/" />
<title>Online Bank Statements: MyPonto.com</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: grey; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="online-bank-statements-myponto-com">
<h1 class="title">Online Bank Statements: MyPonto.com</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/bank-statement-import/tree/14.0/account_statement_import_online_ponto"><img alt="OCA/bank-statement-import" src="https://img.shields.io/badge/github-OCA%2Fbank--statement--import-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/bank-statement-import-14-0/bank-statement-import-14-0-account_statement_import_online_ponto"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/174/14.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p>
<p>This module provides online bank statements from MyPonto.com.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#configuration" id="id1">Configuration</a></li>
<li><a class="reference internal" href="#usage" id="id2">Usage</a></li>
<li><a class="reference internal" href="#bug-tracker" id="id3">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="id4">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="id5">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="id6">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="id7">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="configuration">
<h1><a class="toc-backref" href="#id1">Configuration</a></h1>
<p>To configure online bank statements provider:</p>
<ol class="arabic simple">
<li>Go to <em>Invoicing &gt; Configuration &gt; Bank Accounts</em></li>
<li>Open bank account to configure and edit it</li>
<li>Set <em>Bank Feeds</em> to <em>Online</em></li>
<li>Select <em>MyPonto.com</em> as online bank statements provider in
<em>Online Bank Statements (OCA)</em> section</li>
<li>Save the bank account</li>
<li>Click on provider and configure provider-specific settings.</li>
</ol>
<p>or, alternatively:</p>
<ol class="arabic simple">
<li>Go to <em>Invoicing &gt; Overview</em></li>
<li>Open settings of the corresponding journal account</li>
<li>Switch to <em>Bank Account</em> tab</li>
<li>Set <em>Bank Feeds</em> to <em>Online</em></li>
<li>Select <em>MyPonto.com</em> as online bank statements provider in
<em>Online Bank Statements (OCA)</em> section</li>
<li>Save the bank account</li>
<li>Click on provider and configure provider-specific settings.</li>
</ol>
<p>To obtain <em>Login</em> and <em>Key</em>:</p>
<ol class="arabic simple">
<li>Open <a class="reference external" href="https://myponto.com/">MyPonto.com</a>.</li>
</ol>
<p>Check also <tt class="docutils literal">account_bank_statement_import_online</tt> configuration instructions
for more information.</p>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#id2">Usage</a></h1>
<p>To pull historical bank statements:</p>
<ol class="arabic simple">
<li>Go to <em>Invoicing &gt; Configuration &gt; Bank Accounts</em></li>
<li>Select specific bank accounts</li>
<li>Launch <em>Actions &gt; Online Bank Statements Pull Wizard</em></li>
<li>Configure date interval and click <em>Pull</em></li>
</ol>
<p>If historical data is not needed, then just simply wait for the scheduled
activity “Pull Online Bank Statements” to be executed for getting new
transactions.</p>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#id3">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/bank-statement-import/issues">GitHub Issues</a>.
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
<a class="reference external" href="https://github.com/OCA/bank-statement-import/issues/new?body=module:%20account_statement_import_online_ponto%0Aversion:%2014.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#id4">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#id5">Authors</a></h2>
<ul class="simple">
<li>Florent de Labarre</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#id6">Contributors</a></h2>
<ul class="simple">
<li>Florent de Labarre</li>
<li><a class="reference external" href="https://www.tecnativa.com">Tecnativa</a>:<ul>
<li>Pedro M. Baeza</li>
<li>João Marques</li>
</ul>
</li>
<li><a class="reference external" href="https://therp.nl/">Therp BV</a><ul>
<li>Ronald Portier &lt;<a class="reference external" href="mailto:ronald&#64;therp.nl">ronald&#64;therp.nl</a>&gt;</li>
</ul>
</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#id7">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
<p>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.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/bank-statement-import/tree/14.0/account_statement_import_online_ponto">OCA/bank-statement-import</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,4 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import test_ponto_interface
from . import test_account_statement_import_online_ponto

View File

@@ -0,0 +1,409 @@
# Copyright 2020 Florent de Labarre
# Copyright 2022 Therp BV <https://therp.nl>.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import logging
from datetime import datetime
from unittest import mock
from odoo import _, fields
from odoo.tests import common
_logger = logging.getLogger(__name__)
_module_ns = "odoo.addons.account_statement_import_online_ponto"
_interface_class = _module_ns + ".models.ponto_interface" + ".PontoInterface"
# Transactions should be ordered by descending executionDate.
FOUR_TRANSACTIONS = [
# First transaction will be after date_until.
{
"type": "transaction",
"relationships": {
"account": {
"links": {"related": "https://api.myponto.com/accounts/"},
"data": {
"type": "account",
"id": "fd3d5b1d-fca9-4310-a5c8-76f2a9dc7c75",
},
}
},
"id": "1552c32f-e63f-4ce6-a974-f270e6cd53a9",
"attributes": {
"valueDate": "2019-12-04T12:30:00.000Z",
"remittanceInformationType": "unstructured",
"remittanceInformation": "Arresto Momentum",
"executionDate": "2019-12-04T10:25:00.000Z",
"description": "Wire transfer after execution",
"currency": "EUR",
"counterpartReference": "BE10325927501996",
"counterpartName": "Some other customer",
"amount": 8.95,
},
},
# Next transaction has valueDate before, executionDate after date_until.
{
"type": "transaction",
"relationships": {
"account": {
"links": {"related": "https://api.myponto.com/accounts/"},
"data": {
"type": "account",
"id": "fd3d5b1d-fca9-4310-a5c8-76f2a9dc7c75",
},
}
},
"id": "701ab965-21c4-46ca-b157-306c0646e0e2",
"attributes": {
"valueDate": "2019-11-18T00:00:00.000Z",
"remittanceInformationType": "unstructured",
"remittanceInformation": "Minima vitae totam!",
"executionDate": "2019-11-20T00:00:00.000Z",
"description": "Wire transfer",
"currency": "EUR",
"counterpartReference": "BE26089479973169",
"counterpartName": "Osinski Group",
"amount": 6.08,
},
},
{
"type": "transaction",
"relationships": {
"account": {
"links": {"related": "https://api.myponto.com/accounts/"},
"data": {
"type": "account",
"id": "fd3d5b1d-fca9-4310-a5c8-76f2a9dc7c75",
},
}
},
"id": "9ac50483-16dc-4a82-aa60-df56077405cd",
"attributes": {
"valueDate": "2019-11-04T00:00:00.000Z",
"remittanceInformationType": "unstructured",
"remittanceInformation": "Quia voluptatem blanditiis.",
"executionDate": "2019-11-06T00:00:00.000Z",
"description": "Wire transfer",
"currency": "EUR",
"counterpartReference": "BE97201830401438",
"counterpartName": "Stokes-Miller",
"amount": 5.48,
},
},
{
"type": "transaction",
"relationships": {
"account": {
"links": {"related": "https://api.myponto.com/accounts/"},
"data": {
"type": "account",
"id": "fd3d5b1d-fca9-4310-a5c8-76f2a9dc7c75",
},
}
},
"id": "b21a6c65-1c52-4ba6-8cbc-127d2b2d85ff",
"attributes": {
"valueDate": "2019-11-04T00:00:00.000Z",
"remittanceInformationType": "unstructured",
"remittanceInformation": "Laboriosam repelo?",
"executionDate": "2019-11-04T00:00:00.000Z",
"description": "Wire transfer",
"currency": "EUR",
"counterpartReference": "BE10325927501996",
"counterpartName": "Strosin-Veum",
"amount": 5.83,
},
},
]
EMPTY_TRANSACTIONS = []
EARLY_TRANSACTIONS = [
# First transaction in october 2019, month before other transactions.
{
"type": "transaction",
"relationships": {
"account": {
"links": {"related": "https://api.myponto.com/accounts/"},
"data": {
"type": "account",
"id": "fd3d5b1d-fca9-4310-a5c8-76f2a9dc7c75",
},
}
},
"id": "1552c32f-e63f-4ce6-a974-f270e6cd5301",
"attributes": {
"valueDate": "2019-10-04T12:29:00.000Z",
"remittanceInformationType": "unstructured",
"remittanceInformation": "Arresto Momentum",
"executionDate": "2019-10-04T10:24:00.000Z",
"description": "Wire transfer after execution",
"currency": "EUR",
"counterpartReference": "BE10325927501996",
"counterpartName": "Some other customer",
"amount": 4.25,
},
},
# Second transaction in september 2019.
{
"type": "transaction",
"relationships": {
"account": {
"links": {"related": "https://api.myponto.com/accounts/"},
"data": {
"type": "account",
"id": "fd3d5b1d-fca9-4310-a5c8-76f2a9dc7c75",
},
}
},
"id": "701ab965-21c4-46ca-b157-306c0646e002",
"attributes": {
"valueDate": "2019-09-18T01:00:00.000Z",
"remittanceInformationType": "unstructured",
"remittanceInformation": "Minima vitae totam!",
"executionDate": "2019-09-20T01:00:00.000Z",
"description": "Wire transfer",
"currency": "EUR",
"counterpartReference": "BE26089479973169",
"counterpartName": "Osinski Group",
"amount": 4.08,
},
},
]
transaction_amounts = [5.48, 5.83, 6.08, 8.95]
class TestAccountStatementImportOnlinePonto(common.TransactionCase):
post_install = True
def setUp(self):
super().setUp()
self.now = fields.Datetime.now()
self.currency_eur = self.env.ref("base.EUR")
self.currency_usd = self.env.ref("base.USD")
self.AccountJournal = self.env["account.journal"]
self.ResPartnerBank = self.env["res.partner.bank"]
self.OnlineBankStatementProvider = self.env["online.bank.statement.provider"]
self.AccountBankStatement = self.env["account.bank.statement"]
self.AccountBankStatementLine = self.env["account.bank.statement.line"]
self.AccountStatementPull = self.env["online.bank.statement.pull.wizard"]
self.currency_eur.write({"active": True})
self.bank_account = self.ResPartnerBank.create(
{
"acc_number": "FR0214508000302245362775K46",
"partner_id": self.env.user.company_id.partner_id.id,
}
)
self.journal = self.AccountJournal.create(
{
"name": "Bank",
"type": "bank",
"code": "BANK",
"currency_id": self.currency_eur.id,
"bank_statements_source": "online",
"bank_account_id": self.bank_account.id,
}
)
self.provider = self.OnlineBankStatementProvider.create(
{
"name": "Ponto Provider",
"service": "ponto",
"journal_id": self.journal.id,
# To get all the moves in a month at once
"statement_creation_mode": "monthly",
}
)
self.mock_login = lambda: mock.patch(
_interface_class + "._login",
return_value={
"username": "test_user",
"password": "very_secret",
"access_token": "abcd1234",
"token_expiration": datetime(2099, 12, 31, 23, 59, 59),
},
)
self.mock_set_access_account = lambda: mock.patch(
_interface_class + "._set_access_account",
return_value=None,
)
# return list of transactions on first call, empty list on second call.
self.mock_get_transactions = lambda: mock.patch(
_interface_class + "._get_transactions",
side_effect=[
FOUR_TRANSACTIONS,
EMPTY_TRANSACTIONS,
],
)
# return two times list of transactions, empty list on third call.
self.mock_get_transactions_multi = lambda: mock.patch(
_interface_class + "._get_transactions",
side_effect=[
FOUR_TRANSACTIONS,
EARLY_TRANSACTIONS,
EMPTY_TRANSACTIONS,
],
)
def test_balance_start(self):
"""Test wether end balance of last statement, taken as start balance of new."""
statement_date = datetime(2019, 11, 1)
data = self._get_statement_line_data(statement_date)
self.provider.statement_creation_mode = "daily"
self.provider._create_or_update_statement(
data, statement_date, datetime(2019, 11, 2)
)
with self.mock_login(), self.mock_set_access_account(), self.mock_get_transactions(): # noqa: B950
vals = {
"date_since": datetime(2019, 11, 4),
"date_until": datetime(2019, 11, 5),
}
wizard = self.AccountStatementPull.with_context(
active_model=self.provider._name,
active_id=self.provider.id,
).create(vals)
wizard.action_pull()
statements = self.AccountBankStatement.search(
[("journal_id", "=", self.journal.id)], order="name"
)
self.assertEqual(len(statements), 2)
new_statement = statements[1]
self.assertEqual(len(new_statement.line_ids), 1)
self.assertEqual(new_statement.balance_start, 100)
self.assertEqual(new_statement.balance_end, 105.83)
def test_ponto_execution_date(self):
with self.mock_login(), self.mock_set_access_account(), self.mock_get_transactions(): # noqa: B950
# First base selection on execution date.
self.provider.ponto_date_field = "execution_date"
statement = self._get_statements_from_wizard() # Will get 1 statement
self._check_line_count(statement.line_ids, expected_count=2)
self._check_statement_amounts(statement, transaction_amounts[:2])
def test_ponto_value_date(self):
with self.mock_login(), self.mock_set_access_account(), self.mock_get_transactions(): # noqa: B950
# First base selection on execution date.
self.provider.ponto_date_field = "value_date"
statement = self._get_statements_from_wizard() # Will get 1 statement
self._check_line_count(statement.line_ids, expected_count=3)
self._check_statement_amounts(statement, transaction_amounts[:3])
def test_ponto_get_transactions_multi(self):
with self.mock_login(), self.mock_set_access_account(), self.mock_get_transactions_multi(): # noqa: B950
# First base selection on execution date.
self.provider.ponto_date_field = "execution_date"
# Expect statements for october and november.
statements = self._get_statements_from_wizard(
expected_statement_count=2, date_since=datetime(2019, 9, 25)
)
self._check_line_count(statements[0].line_ids, expected_count=1) # october
self._check_line_count(statements[1].line_ids, expected_count=2) # november
self._check_statement_amounts(statements[0], [4.25])
self._check_statement_amounts(
statements[1],
transaction_amounts[:2],
expected_balance_end=15.56, # Includes 4.25 from statement before.
)
def test_ponto_scheduled(self):
with self.mock_login(), self.mock_set_access_account(), self.mock_get_transactions(): # noqa: B950
# Scheduled should get all transaction, ignoring date_until.
self.provider.ponto_last_identifier = False
date_since = datetime(2019, 11, 3)
date_until = datetime(2019, 11, 18)
self.provider.with_context(scheduled=True)._pull(date_since, date_until)
statements = self._get_statements_from_journal(expected_count=2)
self._check_line_count(statements[0].line_ids, expected_count=3)
self._check_statement_amounts(statements[0], transaction_amounts[:3])
self._check_line_count(statements[1].line_ids, expected_count=1)
# Expected balance_end will include amounts of previous statement.
self._check_statement_amounts(
statements[1], transaction_amounts[3:], expected_balance_end=26.34
)
def test_ponto_scheduled_from_identifier(self):
with self.mock_login(), self.mock_set_access_account(), self.mock_get_transactions(): # noqa: B950
# Scheduled should get all transactions after last identifier.
self.provider.ponto_last_identifier = "9ac50483-16dc-4a82-aa60-df56077405cd"
date_since = datetime(2019, 11, 3)
date_until = datetime(2019, 11, 18)
self.provider.with_context(scheduled=True)._pull(date_since, date_until)
# First two transactions for statement 0 should have been ignored.
statements = self._get_statements_from_journal(expected_count=2)
self._check_line_count(statements[0].line_ids, expected_count=1)
self._check_statement_amounts(statements[0], transaction_amounts[2:3])
self._check_line_count(statements[1].line_ids, expected_count=1)
# Expected balance_end will include amounts of previous statement.
self._check_statement_amounts(
statements[1], transaction_amounts[3:], expected_balance_end=15.03
)
def _get_statements_from_wizard(self, expected_statement_count=1, date_since=None):
"""Run wizard to pull data and return statement."""
date_since = date_since if date_since else datetime(2019, 11, 3)
vals = {
"date_since": date_since,
"date_until": datetime(2019, 11, 18),
}
wizard = self.AccountStatementPull.with_context(
active_model=self.provider._name,
active_id=self.provider.id,
).create(vals)
wizard.action_pull()
return self._get_statements_from_journal(
expected_count=expected_statement_count
)
def _get_statements_from_journal(self, expected_count=0):
"""We only expect statements created by our tests."""
statements = self.AccountBankStatement.search(
[("journal_id", "=", self.journal.id)],
order="date asc",
)
self.assertEqual(len(statements), expected_count)
return statements
def _check_line_count(self, lines, expected_count=0):
"""Check wether lines contain expected number of transactions.
If count differs, show the unique id's of lines that are present.
"""
# If we do not get all lines, show lines we did get:
line_count = len(lines)
if line_count != expected_count:
_logger.info(
_("Statement contains transactions: %s"),
" ".join(lines.mapped("unique_import_id")),
)
self.assertEqual(line_count, expected_count)
def _check_statement_amounts(
self, statement, expected_amounts, expected_balance_end=0.0
):
"""Check wether amount in lines and end_balance as expected."""
sorted_amounts = sorted([round(line.amount, 2) for line in statement.line_ids])
sorted_expected_amounts = sorted(
[round(amount, 2) for amount in expected_amounts]
)
self.assertEqual(sorted_amounts, sorted_expected_amounts)
if not expected_balance_end:
expected_balance_end = sum(expected_amounts)
self.assertEqual(
round(statement.balance_end, 2), round(expected_balance_end, 2)
)
def _get_statement_line_data(self, statement_date):
return [
{
"payment_ref": "payment",
"amount": 100,
"date": statement_date,
"unique_import_id": str(statement_date),
"partner_name": "John Doe",
"account_number": "XX00 0000 0000 0000",
}
], {}

View File

@@ -0,0 +1,98 @@
# Copyright 2022 Therp BV <https://therp.nl>.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import json
from unittest.mock import MagicMock, patch
from dateutil.relativedelta import relativedelta
from odoo import fields
from odoo.tests import common
from .test_account_statement_import_online_ponto import FOUR_TRANSACTIONS
class TestPontoInterface(common.TransactionCase):
post_install = True
@patch("requests.post")
def test_login(self, requests_post):
"""Check Ponto API login."""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.text = json.dumps(
{
"access_token": "live_the_token",
"expires_in": 1799,
"scope": "ai",
"token_type": "bearer",
}
)
requests_post.return_value = mock_response
interface_model = self.env["ponto.interface"]
access_data = interface_model._login("uncle_john", "secret")
self.assertEqual(access_data["access_token"], "live_the_token")
self.assertIn("token_expiration", access_data)
@patch("requests.get")
def test_set_access_account(self, requests_get):
"""Test getting account data for Ponto access."""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.text = json.dumps(
{
"data": [
{
"id": "wrong_id",
"attributes": {
"reference": "NL66ABNA123456789",
},
},
{
"id": "2ad3df83-be01-47cf-a6be-cf0de5cb4c99",
"attributes": {
"reference": "NL66RABO123456789",
},
},
],
}
)
requests_get.return_value = mock_response
# Start of actual test.
access_data = self._get_access_dict(include_account=False)
interface_model = self.env["ponto.interface"]
interface_model._set_access_account(access_data, "NL66RABO123456789")
self.assertIn("ponto_account", access_data)
self.assertEqual(
access_data["ponto_account"], "2ad3df83-be01-47cf-a6be-cf0de5cb4c99"
)
@patch("requests.get")
def test_get_transactions(self, requests_get):
"""Test getting transactions from Ponto."""
mock_response = MagicMock()
mock_response.status_code = 200
# Key "data" will contain a list of transactions.
mock_response.text = json.dumps({"data": FOUR_TRANSACTIONS})
requests_get.return_value = mock_response
# Start of actual test.
access_data = self._get_access_dict()
interface_model = self.env["ponto.interface"]
transactions = interface_model._get_transactions(access_data, False)
self.assertEqual(len(transactions), 4)
self.assertEqual(transactions[3]["id"], "b21a6c65-1c52-4ba6-8cbc-127d2b2d85ff")
self.assertEqual(
transactions[3]["attributes"]["counterpartReference"], "BE10325927501996"
)
def _get_access_dict(self, include_account=True):
"""Get access dict that caches login/account information."""
token_expiration = fields.Datetime.now() + relativedelta(seconds=1800)
access_data = {
"username": "uncle_john",
"password": "secret",
"access_token": "live_the_token",
"token_expiration": token_expiration,
}
if include_account:
access_data["ponto_account"] = "2ad3df83-be01-47cf-a6be-cf0de5cb4c99"
return access_data

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record model="ir.ui.view" id="online_bank_statement_provider_form">
<field name="name">online.bank.statement.provider.form</field>
<field name="model">online.bank.statement.provider</field>
<field
name="inherit_id"
ref="account_statement_import_online.online_bank_statement_provider_form"
/>
<field name="arch" type="xml">
<xpath expr="//group[@name='main']" position="inside">
<group
name="ponto"
string="Ponto Config"
attrs="{'invisible':[('service','!=','ponto')]}"
>
<field name="username" string="Login" />
<field name="password" string="Secret Key" />
<field name="ponto_date_field" />
<field name="ponto_last_identifier" />
</group>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1 @@
../../../../account_statement_import_online_ponto

View File

@@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)