From 0df3866d638c92034b80c7249bb0df647c8a618c Mon Sep 17 00:00:00 2001 From: Mihai Fekete Date: Mon, 11 Mar 2024 18:54:37 +0200 Subject: [PATCH] Fix conflicts --- l10n_ro_account_anaf_sync/README.rst | 2 +- l10n_ro_account_anaf_sync/__manifest__.py | 1 + .../controllers/anaf_oauth.py | 35 +++-- l10n_ro_account_anaf_sync/data/neutralize.sql | 3 +- l10n_ro_account_anaf_sync/models/__init__.py | 1 + .../models/l10n_ro_account_anaf_sync.py | 126 ++++++++---------- .../models/l10n_ro_account_anaf_sync_scope.py | 32 +++++ .../models/res_company.py | 28 ++-- l10n_ro_account_anaf_sync/requirements.txt | 1 + .../security/ir.model.access.csv | 2 + .../static/description/index.html | 3 +- .../views/l10n_ro_account_anaf_sync_view.xml | 22 ++- l10n_ro_account_edi_ubl/README.rst | 2 +- l10n_ro_account_edi_ubl/__manifest__.py | 2 +- .../{post_migration.py => post-migration.py} | 0 .../migrations/16.0.1.43.1/post-migration.py | 23 ++++ l10n_ro_account_edi_ubl/models/__init__.py | 1 + .../models/account_edi_format.py | 9 +- .../models/account_move.py | 4 +- .../models/l10n_ro_account_anaf_sync_scope.py | 62 +++++++++ l10n_ro_account_edi_ubl/models/res_company.py | 11 +- .../static/description/index.html | 3 +- .../tests/test_acccount_edi_ubl.py | 37 +++-- requirements.txt | 1 + 24 files changed, 283 insertions(+), 128 deletions(-) create mode 100644 l10n_ro_account_anaf_sync/models/l10n_ro_account_anaf_sync_scope.py create mode 100644 l10n_ro_account_anaf_sync/requirements.txt rename l10n_ro_account_edi_ubl/migrations/16.0.1.40.1/{post_migration.py => post-migration.py} (100%) create mode 100644 l10n_ro_account_edi_ubl/migrations/16.0.1.43.1/post-migration.py create mode 100644 l10n_ro_account_edi_ubl/models/l10n_ro_account_anaf_sync_scope.py diff --git a/l10n_ro_account_anaf_sync/README.rst b/l10n_ro_account_anaf_sync/README.rst index fa038ee8c..f8af5c83c 100644 --- a/l10n_ro_account_anaf_sync/README.rst +++ b/l10n_ro_account_anaf_sync/README.rst @@ -7,7 +7,7 @@ Romania - Account ANAF Sync !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:fd3e4bff53bd0db2cd49ee906caa77ecc3b2f6aae1c62fac37ae91e3193e870e + !! source digest: sha256:5101958b635714560aaac5b75a11aa44a5acaf7311a31f5dff0c5472d4a4b3e6 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Mature-brightgreen.png diff --git a/l10n_ro_account_anaf_sync/__manifest__.py b/l10n_ro_account_anaf_sync/__manifest__.py index cfa566c01..45946bd92 100644 --- a/l10n_ro_account_anaf_sync/__manifest__.py +++ b/l10n_ro_account_anaf_sync/__manifest__.py @@ -20,4 +20,5 @@ "installable": True, "development_status": "Mature", "maintainers": ["feketemihai"], + "external_dependencies": {"python": ["PyJWT"]}, } diff --git a/l10n_ro_account_anaf_sync/controllers/anaf_oauth.py b/l10n_ro_account_anaf_sync/controllers/anaf_oauth.py index 44c457465..17d150fb9 100644 --- a/l10n_ro_account_anaf_sync/controllers/anaf_oauth.py +++ b/l10n_ro_account_anaf_sync/controllers/anaf_oauth.py @@ -1,17 +1,20 @@ # Copyright (C) 2022 NextERP Romania # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# Authorization Endpoint https://logincert.anaf.ro/anaf-oauth2/v1/authorize +# Token Issuance Endpoint https://logincert.anaf.ro/anaf-oauth2/v1/token +# Token Revocation Endpoint https://logincert.anaf.ro/anaf-oauth2/v1/revoke +import logging import secrets from datetime import datetime, timedelta +import jwt import requests from odoo import _, http from odoo.http import request -# Authorization Endpoint https://logincert.anaf.ro/anaf-oauth2/v1/authorize -# Token Issuance Endpoint https://logincert.anaf.ro/anaf-oauth2/v1/token -# Token Revocation Endpoint https://logincert.anaf.ro/anaf-oauth2/v1/revoke +_logger = logging.getLogger(__name__) class AccountANAFSyncWeb(http.Controller): @@ -62,10 +65,13 @@ def redirect_anaf(self, anaf_config_id, **kw): client_id = anaf_config.client_id url = user.get_base_url() odoo_oauth_url = f"{url}/l10n_ro_account_anaf_sync/anaf_oauth/{anaf_config.id}" - redirect_url = "%s?response_type=code&client_id=%s&redirect_uri=%s" % ( - anaf_config.anaf_oauth_url + "/authorize", - client_id, - odoo_oauth_url, + redirect_url = ( + "%s?response_type=code&client_id=%s&redirect_uri=%s&token_content_type=jwt" + % ( + anaf_config.anaf_oauth_url + "/authorize", + client_id, + odoo_oauth_url, + ) ) anaf_request_from_redirect = request.redirect( redirect_url, code=302, local=False @@ -94,7 +100,7 @@ def get_anaf_oauth_code(self, anaf_config_id, **kw): "Returns a text with the result of anaf request from redirect" uid = request.uid user = request.env["res.users"].browse(uid) - now = datetime.now() + datetime.now() ANAF_Configs = request.env["l10n.ro.account.anaf.sync"].sudo() anaf_config = ANAF_Configs.browse(anaf_config_id) @@ -126,6 +132,7 @@ def get_anaf_oauth_code(self, anaf_config_id, **kw): "code": "{}".format(code), "access_key": "{}".format(code), "redirect_uri": "{}".format(redirect_uri), + "token_content_type": "jwt", } response = requests.post( anaf_config.anaf_oauth_url + "/token", @@ -134,12 +141,20 @@ def get_anaf_oauth_code(self, anaf_config_id, **kw): timeout=1.5, ) response_json = response.json() - + acces_token = {} + if response_json.get("access_token", None): + acces_token = jwt.decode( + response_json.get("access_token"), + algorithms=["RS512"], + options={"verify_signature": False}, + ) message = _("The response was finished.\nResponse was: %s") % response_json anaf_config.write( { "code": code, - "client_token_valability": now + timedelta(days=89), + "client_token_valability": datetime.fromtimestamp( + acces_token.get("exp", 0) + ), "access_token": response_json.get("access_token", ""), "refresh_token": response_json.get("refresh_token", ""), } diff --git a/l10n_ro_account_anaf_sync/data/neutralize.sql b/l10n_ro_account_anaf_sync/data/neutralize.sql index 93bafab5e..e225d37ac 100644 --- a/l10n_ro_account_anaf_sync/data/neutralize.sql +++ b/l10n_ro_account_anaf_sync/data/neutralize.sql @@ -1,2 +1 @@ -UPDATE l10n_ro_account_anaf_sync - SET state = 'test', anaf_einvoice_sync_url = 'https://api.anaf.ro/test/FCTEL/rest'; +UPDATE l10n_ro_account_anaf_sync_scope SET state = 'test'; diff --git a/l10n_ro_account_anaf_sync/models/__init__.py b/l10n_ro_account_anaf_sync/models/__init__.py index 893f50357..631bfc7c0 100644 --- a/l10n_ro_account_anaf_sync/models/__init__.py +++ b/l10n_ro_account_anaf_sync/models/__init__.py @@ -1,2 +1,3 @@ from . import l10n_ro_account_anaf_sync +from . import l10n_ro_account_anaf_sync_scope from . import res_company diff --git a/l10n_ro_account_anaf_sync/models/l10n_ro_account_anaf_sync.py b/l10n_ro_account_anaf_sync/models/l10n_ro_account_anaf_sync.py index 9a35d2308..f399c0cfe 100644 --- a/l10n_ro_account_anaf_sync/models/l10n_ro_account_anaf_sync.py +++ b/l10n_ro_account_anaf_sync/models/l10n_ro_account_anaf_sync.py @@ -2,11 +2,12 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). import logging -from datetime import timedelta +from datetime import datetime +import jwt import requests -from odoo import _, api, fields, models +from odoo import _, fields, models from odoo.exceptions import UserError _logger = logging.getLogger(__name__) @@ -17,14 +18,6 @@ class AccountANAFSync(models.Model): _inherit = ["mail.thread", "l10n.ro.mixin"] _description = "Account ANAF Sync" - _sql_constraints = [ - ( - "company_id_uniq", - "unique(company_id)", - "Another ANAF sync for this company already exists!", - ), - ] - def name_get(self): result = [] for anaf_sync in self: @@ -66,11 +59,13 @@ def name_get(self): help="Time when was last time pressed the Get Token From Anaf Website." " It waits for ANAF request for maximum 1 minute", ) - anaf_einvoice_sync_url = fields.Char(default="https://api.anaf.ro/test/FCTEL/rest") state = fields.Selection( [("test", "Test"), ("automatic", "Automatic")], default="test", ) + anaf_scope_ids = fields.One2many( + comodel_name="l10n.ro.account.anaf.sync.scope", inverse_name="anaf_sync_id" + ) def write(self, values): if values.get("company_id"): @@ -105,6 +100,28 @@ def get_token_from_anaf_website(self): "target": "new", } + def _anaf_call_update_token(self, response): + # Extrage token-ul de acces din răspunsul JSON + token_data = response.json() + acces_token = {} + if token_data.get("access_token", None): + acces_token = jwt.decode( + token_data.get("access_token"), + algorithms=["RS512"], + options={"verify_signature": False}, + ) + vals = {} + if token_data.get("access_token"): + vals["access_token"] = token_data.get("access_token") + if token_data.get("refresh_token"): + vals["refresh_token"] = token_data.get("refresh_token") + if acces_token.get("exp"): + vals["client_token_valability"] = datetime.fromtimestamp( + acces_token.get("exp", 0) + ) + vals["last_request_datetime"] = fields.Datetime.now() + self.write(vals) + def handle_anaf_callback(self, authorization_code): # Folosește codul de autorizare pentru a obține token-ul de acces token_url = f"{self.anaf_oauth_url}/token" @@ -114,6 +131,7 @@ def handle_anaf_callback(self, authorization_code): "client_id": self.client_id, "client_secret": self.client_secret, "redirect_uri": self.anaf_callback_url, + "token_content_type": "jwt", } response = requests.post( @@ -124,17 +142,31 @@ def handle_anaf_callback(self, authorization_code): ) if response.status_code == 200: - # Extrage token-ul de acces din răspunsul JSON - token_data = response.json() - self.write( - { - "access_token": token_data.get("access_token"), - "refresh_token": token_data.get("refresh_token"), - "client_token_valability": fields.Date.today() - + timedelta(days=90), # Valabilitate de 90 de zile - "last_request_datetime": fields.Datetime.now(), - } + self._anaf_call_update_token(response) + + def refresh_access_token(self): + self.ensure_one() + if not self.refresh_token: + raise UserError( + _("You don't have ANAF refresh token. Please get it first.") ) + token_url = f"{self.anaf_oauth_url}/token" + data = { + "grant_type": "refresh_token", + "refresh_token": self.refresh_token, + "client_id": self.client_id, + "client_secret": self.client_secret, + } + + response = requests.post( + token_url, + data=data, + timeout=80, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) + + if response.status_code == 200: + self._anaf_call_update_token(response) def revoke_access_token(self): self.ensure_one() @@ -144,7 +176,6 @@ def revoke_access_token(self): "client_id": self.client_id, "client_secret": self.client_secret, "access_token": self.access_token, - # "refresh_token": should function for refresh function "token_type_hint": "access_token", # refresh_token (should work without) } url = self.anaf_oauth_url + "/revoke" @@ -190,54 +221,3 @@ def test_anaf_api(self): else: message = _("Test token response: %s") % response.reason self.message_post(body=message) - - @api.onchange("state") - def _onchange_state(self): - if self.state: - if self.state == "test": - new_url = "https://api.anaf.ro/test/FCTEL/rest" - else: - new_url = "https://api.anaf.ro/prod/FCTEL/rest" - self.anaf_einvoice_sync_url = new_url - - def _l10n_ro_einvoice_call(self, func, params, data=None, method="POST"): - self.ensure_one() - _logger.info("ANAF API call: %s %s" % (func, params)) - url = self.anaf_einvoice_sync_url + func - access_token = self.access_token - headers = { - "Content-Type": "application/xml", - "Authorization": f"Bearer {access_token}", - } - test_data = self.env.context.get("test_data", False) - if test_data: - content = test_data - status_code = 200 - else: - if method == "GET": - response = requests.get( - url, params=params, data=data, headers=headers, timeout=80 - ) - else: - response = requests.post( - url, params=params, data=data, headers=headers, timeout=80 - ) - - content = response.content - status_code = response.status_code - if response.status_code == 400: - content = response.json() - content_type = "" - if response.headers: - content_type = response.headers.get("Content-Type", "") - if content_type == "application/xml": - _logger.info("ANAF API response: %s" % response.text) - if "text/plain" in content_type: - try: - content = response.json() - if content.get("eroare"): - status_code = 400 - except Exception: - _logger.info("ANAF API response: %s" % response.text) - - return content, status_code diff --git a/l10n_ro_account_anaf_sync/models/l10n_ro_account_anaf_sync_scope.py b/l10n_ro_account_anaf_sync/models/l10n_ro_account_anaf_sync_scope.py new file mode 100644 index 000000000..8c3738ef4 --- /dev/null +++ b/l10n_ro_account_anaf_sync/models/l10n_ro_account_anaf_sync_scope.py @@ -0,0 +1,32 @@ +# Copyright (C) 2024 Dakai Soft +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import api, fields, models + + +class AccountANAFSyncScope(models.Model): + _name = "l10n.ro.account.anaf.sync.scope" + _inherit = ["mail.thread", "l10n.ro.mixin"] + _description = "Account ANAF Sync Scope" + + anaf_sync_id = fields.Many2one("l10n.ro.account.anaf.sync") + company_id = fields.Many2one(related="anaf_sync_id.company_id", store=True) + scope = fields.Selection([]) + anaf_sync_production_url = fields.Char(string="API production URL") + anaf_sync_test_url = fields.Char(string="API test URL") + state = fields.Selection( + [("test", "Test"), ("production", "Production")], + default="test", + ) + anaf_sync_url = fields.Char(compute="_compute_anaf_sync_url") + + @api.depends("state") + def _compute_anaf_sync_url(self): + for entry in self: + entry.anaf_sync_url = getattr( + entry, f"anaf_sync_{entry.state}_url", "anaf_sync_test_url" + ) + + @api.onchange("scope") + def _onchange_scope(self): + self._compute_anaf_sync_url() diff --git a/l10n_ro_account_anaf_sync/models/res_company.py b/l10n_ro_account_anaf_sync/models/res_company.py index 9177b2579..e3e30ccfd 100644 --- a/l10n_ro_account_anaf_sync/models/res_company.py +++ b/l10n_ro_account_anaf_sync/models/res_company.py @@ -7,15 +7,19 @@ class ResCompany(models.Model): _inherit = "res.company" - l10n_ro_account_anaf_sync_id = fields.Many2one( - "l10n.ro.account.anaf.sync", - string="Romania - Account ANAF Sync", - compute="_compute_l10n_ro_account_anaf_sync_id", - ) - - def _compute_l10n_ro_account_anaf_sync_id(self): - for company in self: - domain = [("company_id", "=", company.id)] - company.l10n_ro_account_anaf_sync_id = self.env[ - "l10n.ro.account.anaf.sync" - ].search(domain, limit=1) + def _l10n_ro_get_anaf_sync(self, scope=None): + anaf_sync_scope = self.env["l10n.ro.account.anaf.sync.scope"] + if scope: + anaf_sync_scope |= anaf_sync_scope.search( + [ + ("anaf_sync_id.company_id", "=", self.id), + ("scope", "=", scope), + ( + "anaf_sync_id.client_token_valability", + ">", + fields.Date().today(), + ), + ], + limit=1, + ) + return anaf_sync_scope diff --git a/l10n_ro_account_anaf_sync/requirements.txt b/l10n_ro_account_anaf_sync/requirements.txt new file mode 100644 index 000000000..560e69df4 --- /dev/null +++ b/l10n_ro_account_anaf_sync/requirements.txt @@ -0,0 +1 @@ +PyJWT diff --git a/l10n_ro_account_anaf_sync/security/ir.model.access.csv b/l10n_ro_account_anaf_sync/security/ir.model.access.csv index 7141b41bf..49c9e809c 100644 --- a/l10n_ro_account_anaf_sync/security/ir.model.access.csv +++ b/l10n_ro_account_anaf_sync/security/ir.model.access.csv @@ -2,3 +2,5 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_l10n_ro_account_anaf_sync_user,l10n.ro.account.anaf.sync.user,model_l10n_ro_account_anaf_sync,account.group_account_user,1,0,0,0 access_l10n_ro_account_anaf_sync_invoice,l10n.ro.account.anaf.sync.invoice,model_l10n_ro_account_anaf_sync,account.group_account_invoice,1,0,0,0 access_l10n_ro_account_anaf_sync_manager,l10n.ro.account.anaf.sync.manager,model_l10n_ro_account_anaf_sync,account.group_account_manager,1,1,1,1 +access_l10n_ro_account_anaf_sync_scope_user,l10n.ro.account.anaf.sync.scope.invoice,model_l10n_ro_account_anaf_sync_scope,account.group_account_invoice,1,0,0,0 +access_l10n_ro_account_anaf_sync_scope_manager,l10n.ro.account.anaf.sync.scope.manager,model_l10n_ro_account_anaf_sync_scope,account.group_account_manager,1,1,1,1 diff --git a/l10n_ro_account_anaf_sync/static/description/index.html b/l10n_ro_account_anaf_sync/static/description/index.html index 3a6089a47..e89815886 100644 --- a/l10n_ro_account_anaf_sync/static/description/index.html +++ b/l10n_ro_account_anaf_sync/static/description/index.html @@ -1,3 +1,4 @@ + @@ -366,7 +367,7 @@

Romania - Account ANAF Sync

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:fd3e4bff53bd0db2cd49ee906caa77ecc3b2f6aae1c62fac37ae91e3193e870e +!! source digest: sha256:5101958b635714560aaac5b75a11aa44a5acaf7311a31f5dff0c5472d4a4b3e6 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Mature License: AGPL-3 OCA/l10n-romania Translate me on Weblate Try me on Runboat

This module will make posible to send e-invoice / e-transport to Romanian goverment anaf.ro.

diff --git a/l10n_ro_account_anaf_sync/views/l10n_ro_account_anaf_sync_view.xml b/l10n_ro_account_anaf_sync/views/l10n_ro_account_anaf_sync_view.xml index 0119c849b..9ae8b2baf 100644 --- a/l10n_ro_account_anaf_sync/views/l10n_ro_account_anaf_sync_view.xml +++ b/l10n_ro_account_anaf_sync/views/l10n_ro_account_anaf_sync_view.xml @@ -10,7 +10,6 @@ - @@ -29,6 +28,7 @@ type="object" help="At this step you need a digital certified signature and a browser that recognise it" /> +