[IMP] account_edi_proxy_client,l10n_it_edi: remove edi format dependency
Edi format is going to be removed eventually - `account_edi_proxy_client` should not depend on it and be more flexible in general. This commit: - Removes edi_format field from `account_edi_proxy_client.user` model. We still have to add them in `l10n_it_edi` as it still heavily relies on edi format for now. - Adds a way to have a unique edi user per company, proxy user, edi mode combination. This is a necessary step before adding peppol, which will also depend on the account_edi_proxy_client closes odoo/odoo#116059 Related: odoo/upgrade#4458 Signed-off-by: Laurent Smet <las@odoo.com>
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
'name': 'Proxy features for account_edi',
|
||||
'description': """
|
||||
This module adds generic features to register an Odoo DB on the proxy responsible for receiving data (via requests from web-services).
|
||||
- An edi_proxy_user has a unique identification on a specific format (for example, the vat for Peppol) which
|
||||
- An edi_proxy_user has a unique identification on a specific proxy type (e.g. l10n_it_edi, peppol) which
|
||||
allows to identify him when receiving a document addressed to him. It is linked to a specific company on a specific
|
||||
Odoo database.
|
||||
- Encryption features allows to decrypt all the user's data when receiving it from the proxy.
|
||||
@@ -11,7 +11,7 @@ Odoo database.
|
||||
""",
|
||||
'version': '1.0',
|
||||
'category': 'Accounting/Accounting',
|
||||
'depends': ['account_edi'],
|
||||
'depends': ['account'],
|
||||
'external_dependencies': {
|
||||
'python': ['cryptography']
|
||||
},
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
from . import account_edi_format
|
||||
from . import account_edi_proxy_user
|
||||
from . import res_company
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo import models
|
||||
|
||||
|
||||
class AccountEdiFormat(models.Model):
|
||||
_inherit = 'account.edi.format'
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def _get_proxy_user(self, company):
|
||||
'''Returns the proxy_user associated with this edi format.
|
||||
'''
|
||||
self.ensure_one()
|
||||
return company.account_edi_proxy_client_ids.filtered(lambda u: u.edi_format_id == self)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# To override
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def _get_proxy_identification(self, company):
|
||||
'''Returns the key that will identify company uniquely for this edi format (for example, the vat)
|
||||
or raises a UserError (if the user didn't fill the related field).
|
||||
TO OVERRIDE
|
||||
'''
|
||||
return False
|
||||
@@ -1,4 +1,4 @@
|
||||
from odoo import models, fields, _
|
||||
from odoo import api, models, fields, _
|
||||
from odoo.exceptions import UserError
|
||||
from .account_edi_proxy_auth import OdooEdiProxyAuth
|
||||
|
||||
@@ -17,9 +17,6 @@ import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
DEFAULT_SERVER_URL = 'https://l10n-it-edi.api.odoo.com'
|
||||
DEFAULT_TEST_SERVER_URL = 'https://iap-services-test.odoo.com'
|
||||
TIMEOUT = 30
|
||||
|
||||
|
||||
@@ -45,28 +42,63 @@ class AccountEdiProxyClientUser(models.Model):
|
||||
id_client = fields.Char(required=True)
|
||||
company_id = fields.Many2one('res.company', string='Company', required=True,
|
||||
default=lambda self: self.env.company)
|
||||
edi_format_id = fields.Many2one('account.edi.format', required=True)
|
||||
edi_format_code = fields.Char(related='edi_format_id.code', readonly=True)
|
||||
edi_identification = fields.Char(required=True, help="The unique id that identifies this user for on the edi format, typically the vat")
|
||||
edi_identification = fields.Char(required=True, help="The unique id that identifies this user, typically the vat")
|
||||
private_key = fields.Binary(required=True, attachment=False, groups="base.group_system", help="The key to encrypt all the user's data")
|
||||
private_key_filename = fields.Char(compute='_compute_private_key_filename')
|
||||
refresh_token = fields.Char(groups="base.group_system")
|
||||
proxy_type = fields.Selection(
|
||||
selection=[],
|
||||
string="Proxy type",
|
||||
compute='_compute_proxy_type',
|
||||
readonly=False,
|
||||
store=True,
|
||||
)
|
||||
edi_mode = fields.Selection(
|
||||
selection=[
|
||||
('prod', 'Production mode'),
|
||||
('test', 'Test mode'),
|
||||
('demo', 'Demo mode'),
|
||||
],
|
||||
string='EDI operating mode',
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
('unique_id_client', 'unique(id_client)', 'This id_client is already used on another user.'),
|
||||
('unique_edi_identification_per_for', 'unique(edi_identification, edi_format_id)', 'This edi identification is already assigned to a user'),
|
||||
('unique_edi_identification', 'unique(edi_identification, proxy_type, edi_mode)', 'This edi identification is already assigned to a user'),
|
||||
]
|
||||
|
||||
@api.depends('company_id')
|
||||
def _compute_proxy_type(self):
|
||||
for user in self:
|
||||
user.proxy_type = False
|
||||
|
||||
def _compute_private_key_filename(self):
|
||||
for record in self:
|
||||
record.private_key_filename = f'{record.id_client}_{record.edi_identification}.key'
|
||||
|
||||
def _get_demo_state(self):
|
||||
demo_state = self.env['ir.config_parameter'].sudo().get_param('account_edi_proxy_client.demo', False)
|
||||
return 'prod' if demo_state in ['prod', False] else 'test' if demo_state == 'test' else 'demo'
|
||||
def _get_proxy_urls(self):
|
||||
# To extend
|
||||
return {}
|
||||
|
||||
def _get_server_url(self):
|
||||
return DEFAULT_TEST_SERVER_URL if self._get_demo_state() == 'test' else self.env['ir.config_parameter'].sudo().get_param('account_edi_proxy_client.edi_server_url', DEFAULT_SERVER_URL)
|
||||
def _get_server_url(self, proxy_type=None, edi_mode=None):
|
||||
proxy_type = proxy_type or self.proxy_type
|
||||
edi_mode = edi_mode or self.edi_mode
|
||||
proxy_urls = self._get_proxy_urls()
|
||||
# letting this traceback in case of a KeyError, as that would mean something's wrong with the code
|
||||
return proxy_urls[proxy_type][edi_mode]
|
||||
|
||||
def _get_proxy_users(self, company, proxy_type):
|
||||
'''Returns proxy users associated with the given company and proxy type.
|
||||
'''
|
||||
return company.account_edi_proxy_client_ids.filtered(lambda u: u.proxy_type == proxy_type)
|
||||
|
||||
def _get_proxy_identification(self, company):
|
||||
'''Returns the key that will identify company uniquely
|
||||
within a specific proxy type and edi operating mode.
|
||||
or raises a UserError (if the user didn't fill the related field).
|
||||
TO OVERRIDE
|
||||
'''
|
||||
return False
|
||||
|
||||
def _make_request(self, url, params=False):
|
||||
''' Make a request to proxy and handle the generic elements of the reponse (errors, new refresh token).
|
||||
@@ -78,7 +110,7 @@ class AccountEdiProxyClientUser(models.Model):
|
||||
'id': uuid.uuid4().hex,
|
||||
}
|
||||
|
||||
if self._get_demo_state() == 'demo':
|
||||
if self.edi_mode == 'demo':
|
||||
# Last barrier : in case the demo mode is not handled by the caller, we block access.
|
||||
raise Exception("Can't access the proxy in demo mode")
|
||||
|
||||
@@ -113,7 +145,7 @@ class AccountEdiProxyClientUser(models.Model):
|
||||
|
||||
return response['result']
|
||||
|
||||
def _register_proxy_user(self, company, edi_format, edi_identification):
|
||||
def _register_proxy_user(self, company, proxy_type, edi_mode, edi_identification):
|
||||
''' Generate the public_key/private_key that will be used to encrypt the file, send a request to the proxy
|
||||
to register the user with the public key and create the user with the private key.
|
||||
|
||||
@@ -139,18 +171,18 @@ class AccountEdiProxyClientUser(models.Model):
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
||||
)
|
||||
if self._get_demo_state() == 'demo':
|
||||
if edi_mode == 'demo':
|
||||
# simulate registration
|
||||
response = {'id_client': f'demo{company.id}', 'refresh_token': 'demo'}
|
||||
else:
|
||||
try:
|
||||
# b64encode returns a bytestring, we need it as a string
|
||||
response = self._make_request(self._get_server_url() + '/iap/account_edi/1/create_user', params={
|
||||
response = self._make_request(self._get_server_url(proxy_type, edi_mode) + '/iap/account_edi/2/create_user', params={
|
||||
'dbuuid': company.env['ir.config_parameter'].get_param('database.uuid'),
|
||||
'company_id': company.id,
|
||||
'edi_format_code': edi_format.code,
|
||||
'edi_identification': edi_identification,
|
||||
'public_key': base64.b64encode(public_pem).decode()
|
||||
'public_key': base64.b64encode(public_pem).decode(),
|
||||
'proxy_type': proxy_type,
|
||||
})
|
||||
except AccountEdiProxyError as e:
|
||||
raise UserError(e.message)
|
||||
@@ -160,7 +192,8 @@ class AccountEdiProxyClientUser(models.Model):
|
||||
self.create({
|
||||
'id_client': response['id_client'],
|
||||
'company_id': company.id,
|
||||
'edi_format_id': edi_format.id,
|
||||
'proxy_type': proxy_type,
|
||||
'edi_mode': edi_mode,
|
||||
'edi_identification': edi_identification,
|
||||
'private_key': base64.b64encode(private_pem),
|
||||
'refresh_token': response['refresh_token'],
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<group>
|
||||
<group>
|
||||
<field name="company_id" groups="base.group_multi_company" invisible="1"/>
|
||||
<field name="edi_format_id" readonly="1" options="{'no_open': True}"/>
|
||||
<field name="proxy_type" readonly="1" options="{'no_open': True}"/>
|
||||
<field name="id_client" readonly="1"/>
|
||||
<field name="edi_identification" readonly="1"/>
|
||||
<field name="private_key_filename" invisible="1"/>
|
||||
@@ -30,7 +30,6 @@
|
||||
<field name="arch" type="xml">
|
||||
<tree create="false" delete="false" edit="false">
|
||||
<field name="company_id" groups="base.group_multi_company" invisible="1"/>
|
||||
<field name="edi_format_id" readonly="1"/>
|
||||
<field name="id_client" readonly="1"/>
|
||||
<field name="edi_identification" readonly="1"/>
|
||||
<field name="private_key_filename" invisible="1"/>
|
||||
|
||||
@@ -10,3 +10,4 @@ from . import account_edi_document
|
||||
from . import account_edi_format
|
||||
from . import ddt
|
||||
from . import ir_attachment
|
||||
from . import account_edi_proxy_user
|
||||
|
||||
@@ -292,10 +292,10 @@ class AccountEdiFormat(models.Model):
|
||||
def _cron_receive_fattura_pa(self):
|
||||
''' Check the proxy for incoming invoices for all companies.
|
||||
'''
|
||||
if self.env['account_edi_proxy_client.user']._get_demo_state() == 'demo':
|
||||
return
|
||||
|
||||
for proxy_user in self.env['account_edi_proxy_client.user'].search([('edi_format_code', '=', 'fattura_pa')]):
|
||||
for proxy_user in self.env['account_edi_proxy_client.user'].search([
|
||||
('proxy_type', '=', 'l10n_it_edi'),
|
||||
('edi_mode', '!=', 'demo'),
|
||||
]):
|
||||
self._receive_fattura_pa(proxy_user)
|
||||
|
||||
def _receive_fattura_pa(self, proxy_user):
|
||||
@@ -831,7 +831,7 @@ class AccountEdiFormat(models.Model):
|
||||
|
||||
res.extend(self._l10n_it_edi_check_invoice_configuration(move))
|
||||
|
||||
if not self._get_proxy_user(move.company_id):
|
||||
if not self.env['account_edi_proxy_client.user']._get_proxy_users(move.company_id, 'l10n_it_edi'):
|
||||
res.append(_("You must accept the terms and conditions in the settings to use FatturaPA."))
|
||||
|
||||
return res
|
||||
@@ -870,14 +870,14 @@ class AccountEdiFormat(models.Model):
|
||||
'data': {'filename': filename, 'xml': base64.b64encode(xml.encode()).decode()}}
|
||||
|
||||
company = invoices.company_id
|
||||
proxy_user = self._get_proxy_user(company)
|
||||
proxy_user = self.env['account_edi_proxy_client.user']._get_proxy_users(company, 'l10n_it_edi')
|
||||
if not proxy_user: # proxy user should exist, because there is a check in _check_move_configuration
|
||||
return {invoice: {
|
||||
'error': _("You must accept the terms and conditions in the settings to use FatturaPA."),
|
||||
'blocking_level': 'error'} for invoice in invoices}
|
||||
|
||||
responses = {}
|
||||
if proxy_user._get_demo_state() == 'demo':
|
||||
if proxy_user.edi_mode == 'demo':
|
||||
responses = {i['data']['filename']: {'id_transaction': 'demo'} for i in to_send.values()}
|
||||
else:
|
||||
try:
|
||||
@@ -902,13 +902,13 @@ class AccountEdiFormat(models.Model):
|
||||
to_return = {}
|
||||
company = invoices.company_id
|
||||
|
||||
proxy_user = self._get_proxy_user(company)
|
||||
proxy_user = self.env['account_edi_proxy_client.user']._get_proxy_users(company, 'l10n_it_edi')
|
||||
if not proxy_user: # proxy user should exist, because there is a check in _check_move_configuration
|
||||
return {invoice: {
|
||||
'error': _("You must accept the terms and conditions in the settings to use FatturaPA."),
|
||||
'blocking_level': 'error'} for invoice in invoices}
|
||||
|
||||
if proxy_user._get_demo_state() == 'demo':
|
||||
if proxy_user.edi_mode == 'demo':
|
||||
# simulate success and bypass ack
|
||||
return {invoice: {'attachment': invoice.l10n_it_edi_attachment_id} for invoice in invoices}
|
||||
else:
|
||||
@@ -1049,11 +1049,7 @@ class AccountEdiFormat(models.Model):
|
||||
# -------------------------------------------------------------------------
|
||||
# Proxy methods
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def _get_proxy_identification(self, company):
|
||||
if self.code != 'fattura_pa':
|
||||
return super()._get_proxy_identification()
|
||||
|
||||
if not company.l10n_it_codice_fiscale:
|
||||
raise UserError(_('Please fill your codice fiscale to be able to receive invoices from FatturaPA'))
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class AccountEdiProxyClientUser(models.Model):
|
||||
_inherit = 'account_edi_proxy_client.user'
|
||||
|
||||
proxy_type = fields.Selection(
|
||||
selection_add=[('l10n_it_edi', 'Italian EDI')],
|
||||
)
|
||||
|
||||
def _get_proxy_urls(self):
|
||||
urls = super()._get_proxy_urls()
|
||||
urls['l10n_it_edi'] = {
|
||||
'prod': 'https://l10n-it-edi.api.odoo.com',
|
||||
'test': 'https://iap-services-test.odoo.com',
|
||||
}
|
||||
return urls
|
||||
|
||||
def _compute_proxy_type(self):
|
||||
# Extends account_edi_proxy_client
|
||||
super()._compute_proxy_type()
|
||||
for user in self:
|
||||
if user.company_id.country_code == 'IT':
|
||||
user.proxy_type = 'l10n_it_edi'
|
||||
@@ -18,15 +18,19 @@ class ResConfigSettings(models.TransientModel):
|
||||
inverse='_set_l10n_it_edi_sdicoop_register_demo_mode',
|
||||
readonly=False)
|
||||
|
||||
def _create_proxy_user(self, company_id):
|
||||
def _create_proxy_user(self, company_id, edi_mode):
|
||||
fattura_pa = self.env.ref('l10n_it_edi.edi_fatturaPA')
|
||||
edi_identification = fattura_pa._get_proxy_identification(company_id)
|
||||
self.env['account_edi_proxy_client.user']._register_proxy_user(company_id, fattura_pa, edi_identification)
|
||||
self.env['account_edi_proxy_client.user']._register_proxy_user(company_id, 'l10n_it_edi', edi_mode, edi_identification)
|
||||
|
||||
@api.depends('company_id.account_edi_proxy_client_ids', 'company_id.account_edi_proxy_client_ids.active')
|
||||
def _compute_l10n_it_edi_sdicoop_demo_mode(self):
|
||||
for config in self:
|
||||
config.l10n_it_edi_sdicoop_demo_mode = self.env['account_edi_proxy_client.user']._get_demo_state()
|
||||
edi_user = self.env['account_edi_proxy_client.user'].search([
|
||||
('company_id', '=', config.company_id.id),
|
||||
('proxy_type', '=', 'l10n_it_edi'),
|
||||
], limit=1)
|
||||
config.l10n_it_edi_sdicoop_demo_mode = edi_user.edi_mode
|
||||
|
||||
def _set_l10n_it_edi_sdicoop_demo_mode(self):
|
||||
for config in self:
|
||||
@@ -39,11 +43,10 @@ class ResConfigSettings(models.TransientModel):
|
||||
|
||||
@api.depends('company_id.account_edi_proxy_client_ids', 'company_id.account_edi_proxy_client_ids.active')
|
||||
def _compute_l10n_it_edi_proxy_current_state(self):
|
||||
fattura_pa = self.env.ref('l10n_it_edi.edi_fatturaPA')
|
||||
for config in self:
|
||||
proxy_user = config.company_id.account_edi_proxy_client_ids.search([
|
||||
('company_id', '=', config.company_id.id),
|
||||
('edi_format_id', '=', fattura_pa.id),
|
||||
('proxy_type', '=', 'l10n_it_edi'),
|
||||
], limit=1)
|
||||
|
||||
config.l10n_it_edi_proxy_current_state = 'inactive' if not proxy_user else 'demo' if proxy_user.id_client[:4] == 'demo' else 'active'
|
||||
@@ -54,24 +57,19 @@ class ResConfigSettings(models.TransientModel):
|
||||
self.l10n_it_edi_sdicoop_register = False
|
||||
|
||||
def button_create_proxy_user(self):
|
||||
# For now, only fattura_pa uses the proxy.
|
||||
# To use it for more, we have to either make the activation of the proxy on a format basis
|
||||
# or create a user per format here (but also when installing new formats)
|
||||
fattura_pa = self.env.ref('l10n_it_edi.edi_fatturaPA')
|
||||
edi_identification = fattura_pa._get_proxy_identification(self.company_id)
|
||||
if not edi_identification:
|
||||
return
|
||||
|
||||
self.env['account_edi_proxy_client.user']._register_proxy_user(self.company_id, fattura_pa, edi_identification)
|
||||
self.env['account_edi_proxy_client.user']._register_proxy_user(self.company_id, 'l10n_it_edi', self.l10n_it_edi_sdicoop_demo_mode, edi_identification)
|
||||
|
||||
def _set_l10n_it_edi_sdicoop_register_demo_mode(self):
|
||||
|
||||
fattura_pa = self.env.ref('l10n_it_edi.edi_fatturaPA')
|
||||
for config in self:
|
||||
|
||||
proxy_user = self.env['account_edi_proxy_client.user'].search([
|
||||
('company_id', '=', config.company_id.id),
|
||||
('edi_format_id', '=', fattura_pa.id)
|
||||
('proxy_type', '=', 'l10n_it_edi'),
|
||||
], limit=1)
|
||||
|
||||
real_proxy_users = self.env['account_edi_proxy_client.user'].sudo().search([
|
||||
@@ -79,22 +77,22 @@ class ResConfigSettings(models.TransientModel):
|
||||
])
|
||||
|
||||
# Update the config as per the selected radio button
|
||||
previous_demo_state = proxy_user._get_demo_state()
|
||||
self.env['ir.config_parameter'].set_param('account_edi_proxy_client.demo', config.l10n_it_edi_sdicoop_demo_mode)
|
||||
|
||||
previous_demo_state = proxy_user.edi_mode
|
||||
edi_mode = config.l10n_it_edi_sdicoop_demo_mode
|
||||
self.env['ir.config_parameter'].set_param('account_edi_proxy_client.demo', edi_mode)
|
||||
# If the user is trying to change from a state in which they have a registered official or testing proxy client
|
||||
# to another state, we should stop them
|
||||
if real_proxy_users and previous_demo_state != config.l10n_it_edi_sdicoop_demo_mode:
|
||||
if real_proxy_users and previous_demo_state != edi_mode:
|
||||
raise UserError(_("The company has already registered with the service as 'Test' or 'Official', it cannot change."))
|
||||
|
||||
if config.l10n_it_edi_sdicoop_register:
|
||||
# There should only be one user at a time, if there are no users, register one
|
||||
if not proxy_user:
|
||||
self._create_proxy_user(config.company_id)
|
||||
self._create_proxy_user(config.company_id, edi_mode)
|
||||
return
|
||||
|
||||
# If there is a demo user, and we are transitioning from demo to test or production, we should
|
||||
# delete all demo users and then create the new user.
|
||||
elif proxy_user.id_client[:4] == 'demo' and config.l10n_it_edi_sdicoop_demo_mode != 'demo':
|
||||
elif proxy_user.id_client[:4] == 'demo' and edi_mode != 'demo':
|
||||
self.env['account_edi_proxy_client.user'].search([('id_client', '=like', 'demo%')]).sudo().unlink()
|
||||
self._create_proxy_user(config.company_id)
|
||||
self._create_proxy_user(config.company_id, edi_mode)
|
||||
|
||||
@@ -86,7 +86,6 @@ class TestItEdi(AccountEdiTestCommon):
|
||||
cls.proxy_user = cls.env['account_edi_proxy_client.user'].create({
|
||||
'id_client': 'l10n_it_edi_test',
|
||||
'company_id': cls.company.id,
|
||||
'edi_format_id': cls.edi_format.id,
|
||||
'edi_identification': 'l10n_it_edi_test',
|
||||
'private_key': 'l10n_it_edi_test',
|
||||
})
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<span class="o_form_label">
|
||||
Fattura Elettronica mode
|
||||
</span>
|
||||
<span class="fa fa-lg fa-building-o" title="Values set here are company-specific."/>
|
||||
<div class="text-muted">
|
||||
In demo mode Odoo will just simulate the sending of invoices to the government.<br/>
|
||||
In test mode (experimental) Odoo will send the invoices to a non-production service.
|
||||
|
||||
@@ -32,9 +32,9 @@ class TestDDT(TestSaleCommon):
|
||||
})
|
||||
|
||||
settings = cls.env['res.config.settings'].create({})
|
||||
if hasattr(settings, 'button_create_proxy_user'):
|
||||
if hasattr(settings, '_create_proxy_user'):
|
||||
# Needed when `l10n_it_edi_sdiscoop` is installed
|
||||
settings.button_create_proxy_user()
|
||||
settings._create_proxy_user(cls.company_data['company'], 'demo')
|
||||
|
||||
|
||||
def test_ddt_flow(self):
|
||||
|
||||
Reference in New Issue
Block a user