[ADD] l10n_it_edi_sdicoop, account_edi_proxy_client: added support for SdiCoop webservice.
- Allows to send and receives invoices from the fatturaPA network via the webservice (SdiCoop). - PEC mail is disabled when SdiCoop is disabled. - Added account_edi_proxy_client, a registered user on the proxy (see l10n_it_edi_proxy on iap-apps), that features encryption. Generates a asymmetric keys, and keep the private_key to be able to decrypt file sent by the proxy (which it encrypted with the public key). - Added a generic way to sign the requests made to the proxy. TASK ID 2358882 closes odoo/odoo#71928 X-original-commit: 4c8afc413a8982b2e77712eab7cf7ddac9f3851f Signed-off-by: Josse Colpaert <jco@openerp.com> Signed-off-by: bfr-o <bfr-o@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
from . import models
|
||||
@@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'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
|
||||
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.
|
||||
- Authentication offers an additionnal level of security to avoid impersonification, in case someone gains to the user's database.
|
||||
""",
|
||||
'version': '1.0',
|
||||
'category': 'Accounting/Accounting',
|
||||
'depends': ['account_edi'],
|
||||
'external_dependencies': {
|
||||
'python': ['cryptography']
|
||||
},
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * account_edi_proxy_client
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 14.0+e\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-06-03 13:44+0000\n"
|
||||
"PO-Revision-Date: 2021-06-03 13:44+0000\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_edi_proxy_client
|
||||
#: model:ir.model,name:account_edi_proxy_client.model_account_edi_proxy_client_user
|
||||
msgid "Account EDI proxy user"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_edi_proxy_client
|
||||
#: model:ir.model.fields,field_description:account_edi_proxy_client.field_res_company__account_edi_proxy_client_ids
|
||||
msgid "Account Edi Proxy Client"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_edi_proxy_client
|
||||
#: model:ir.model.fields,field_description:account_edi_proxy_client.field_account_edi_proxy_client_user__active
|
||||
msgid "Active"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_edi_proxy_client
|
||||
#: model:ir.model.fields,field_description:account_edi_proxy_client.field_account_edi_proxy_client_user__edi_format_code
|
||||
msgid "Code"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_edi_proxy_client
|
||||
#: model:ir.model,name:account_edi_proxy_client.model_res_company
|
||||
msgid "Companies"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_edi_proxy_client
|
||||
#: model:ir.model.fields,field_description:account_edi_proxy_client.field_account_edi_proxy_client_user__company_id
|
||||
msgid "Company"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_edi_proxy_client
|
||||
#: model:ir.model.fields,field_description:account_edi_proxy_client.field_account_edi_proxy_client_user__create_uid
|
||||
msgid "Created by"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_edi_proxy_client
|
||||
#: model:ir.model.fields,field_description:account_edi_proxy_client.field_account_edi_proxy_client_user__create_date
|
||||
msgid "Created on"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_edi_proxy_client
|
||||
#: model:ir.model.fields,field_description:account_edi_proxy_client.field_account_edi_format__display_name
|
||||
#: model:ir.model.fields,field_description:account_edi_proxy_client.field_account_edi_proxy_client_user__display_name
|
||||
#: model:ir.model.fields,field_description:account_edi_proxy_client.field_res_company__display_name
|
||||
msgid "Display Name"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_edi_proxy_client
|
||||
#: model:ir.model,name:account_edi_proxy_client.model_account_edi_format
|
||||
msgid "EDI format"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_edi_proxy_client
|
||||
#: model:ir.model.fields,field_description:account_edi_proxy_client.field_account_edi_proxy_client_user__edi_format_id
|
||||
msgid "Edi Format"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_edi_proxy_client
|
||||
#: model:ir.model.fields,field_description:account_edi_proxy_client.field_account_edi_proxy_client_user__edi_identification
|
||||
msgid "Edi Identification"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_edi_proxy_client
|
||||
#: model:ir.model.fields,field_description:account_edi_proxy_client.field_account_edi_format__id
|
||||
#: model:ir.model.fields,field_description:account_edi_proxy_client.field_account_edi_proxy_client_user__id
|
||||
#: model:ir.model.fields,field_description:account_edi_proxy_client.field_res_company__id
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_edi_proxy_client
|
||||
#: model:ir.model.fields,field_description:account_edi_proxy_client.field_account_edi_proxy_client_user__id_client
|
||||
msgid "Id Client"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_edi_proxy_client
|
||||
#: model:ir.model.fields,field_description:account_edi_proxy_client.field_account_edi_format____last_update
|
||||
#: model:ir.model.fields,field_description:account_edi_proxy_client.field_account_edi_proxy_client_user____last_update
|
||||
#: model:ir.model.fields,field_description:account_edi_proxy_client.field_res_company____last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_edi_proxy_client
|
||||
#: model:ir.model.fields,field_description:account_edi_proxy_client.field_account_edi_proxy_client_user__write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_edi_proxy_client
|
||||
#: model:ir.model.fields,field_description:account_edi_proxy_client.field_account_edi_proxy_client_user__write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_edi_proxy_client
|
||||
#: model:ir.model.fields,field_description:account_edi_proxy_client.field_account_edi_proxy_client_user__private_key
|
||||
msgid "Private Key"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_edi_proxy_client
|
||||
#: model:ir.model.fields,field_description:account_edi_proxy_client.field_account_edi_proxy_client_user__refresh_token
|
||||
msgid "Refresh Token"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_edi_proxy_client
|
||||
#: model:ir.model.fields,help:account_edi_proxy_client.field_account_edi_proxy_client_user__private_key
|
||||
msgid "The key to encrypt all the user's data"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_edi_proxy_client
|
||||
#: model:ir.model.fields,help:account_edi_proxy_client.field_account_edi_proxy_client_user__edi_identification
|
||||
msgid ""
|
||||
"The unique id that identifies this user for on the edi format, typically the"
|
||||
" vat"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_edi_proxy_client
|
||||
#: code:addons/account_edi_proxy_client/models/account_edi_proxy_user.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The url that this service does not exist. The url it tried to contact was %s"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_edi_proxy_client
|
||||
#: code:addons/account_edi_proxy_client/models/account_edi_proxy_user.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The url that this service requested returned an error. The url it tried to "
|
||||
"contact was %s"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_edi_proxy_client
|
||||
#: code:addons/account_edi_proxy_client/models/account_edi_proxy_user.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The url that this service requested returned an error. The url it tried to "
|
||||
"contact was %s. %s"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_edi_proxy_client
|
||||
#: model:ir.model.constraint,message:account_edi_proxy_client.constraint_account_edi_proxy_client_user_unique_edi_identification_per_format
|
||||
msgid "This edi identification is already assigned to a user"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_edi_proxy_client
|
||||
#: model:ir.model.constraint,message:account_edi_proxy_client.constraint_account_edi_proxy_client_user_unique_id_client
|
||||
msgid "This id_client is already used on another user."
|
||||
msgstr ""
|
||||
@@ -0,0 +1,3 @@
|
||||
from . import account_edi_format
|
||||
from . import account_edi_proxy_user
|
||||
from . import res_company
|
||||
@@ -0,0 +1,27 @@
|
||||
# -*- 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
|
||||
@@ -0,0 +1,48 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import requests
|
||||
import time
|
||||
import werkzeug.urls
|
||||
|
||||
|
||||
class OdooEdiProxyAuth(requests.auth.AuthBase):
|
||||
""" For routes that needs to be authenticated and verified for access.
|
||||
Allows:
|
||||
1) to preserve the integrity of the message between the endpoints.
|
||||
2) to check user access rights and account validity
|
||||
3) to avoid that multiple database use the same credentials, via a refresh_token that expire after 24h.
|
||||
"""
|
||||
|
||||
def __init__(self, user=False):
|
||||
self.id_client = user and user.id_client or False
|
||||
self.refresh_token = user and user.refresh_token or False
|
||||
|
||||
def __call__(self, request):
|
||||
# We don't sign request that still don't have a id_client/refresh_token
|
||||
if not self.id_client or not self.refresh_token:
|
||||
return request
|
||||
# craft the message (timestamp|url path|id_client|query params|body content)
|
||||
msg_timestamp = int(time.time())
|
||||
parsed_url = werkzeug.urls.url_parse(request.path_url)
|
||||
|
||||
body = request.body
|
||||
if isinstance(body, bytes):
|
||||
body = body.decode()
|
||||
body = json.loads(body)
|
||||
|
||||
message = '%s|%s|%s|%s|%s' % (
|
||||
msg_timestamp, # timestamp
|
||||
parsed_url.path, # url path
|
||||
self.id_client,
|
||||
json.dumps(werkzeug.urls.url_decode(parsed_url.query), sort_keys=True), # url query params sorted by key
|
||||
json.dumps(body, sort_keys=True)) # http request body
|
||||
h = hmac.new(base64.b64decode(self.refresh_token), message.encode(), digestmod=hashlib.sha256)
|
||||
|
||||
request.headers.update({
|
||||
'odoo-edi-client-id': self.id_client,
|
||||
'odoo-edi-signature': h.hexdigest(),
|
||||
'odoo-edi-timestamp': msg_timestamp,
|
||||
})
|
||||
return request
|
||||
@@ -0,0 +1,183 @@
|
||||
from odoo import models, fields, _
|
||||
from odoo.exceptions import UserError
|
||||
from .account_edi_proxy_auth import OdooEdiProxyAuth
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
from cryptography.fernet import Fernet
|
||||
import requests
|
||||
import uuid
|
||||
import base64
|
||||
import logging
|
||||
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
SERVER_URL = 'https://l10n-it-edi.api.odoo.com'
|
||||
TIMEOUT = 30
|
||||
|
||||
|
||||
class AccountEdiProxyError(Exception):
|
||||
|
||||
def __init__(self, code, message=False):
|
||||
self.code = code
|
||||
self.message = message
|
||||
super().__init__(message or code)
|
||||
|
||||
|
||||
class AccountEdiProxyClientUser(models.Model):
|
||||
"""Represents a user of the proxy for an electronic invoicing format.
|
||||
An edi_proxy_user has a unique identification on a specific format (for example, the vat for 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.
|
||||
It also owns a key with which each file should be decrypted with (the proxy encrypt all the files with the public key).
|
||||
"""
|
||||
_name = 'account_edi_proxy_client.user'
|
||||
_description = 'Account EDI proxy user'
|
||||
|
||||
active = fields.Boolean(default=True)
|
||||
id_client = fields.Char(required=True, index=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")
|
||||
private_key = fields.Binary(required=True, attachment=False, groups="base.group_system", help="The key to encrypt all the user's data")
|
||||
refresh_token = fields.Char(groups="base.group_system")
|
||||
|
||||
_sql_constraints = [
|
||||
('unique_id_client', 'unique(id_client)', 'This id_client is already used on another user.'),
|
||||
('unique_edi_identification_per_format', 'unique(edi_identification, edi_format_id)', 'This edi identification is already assigned to a user'),
|
||||
]
|
||||
|
||||
def _make_request(self, url, params=False):
|
||||
''' Make a request to proxy and handle the generic elements of the reponse (errors, new refresh token).
|
||||
'''
|
||||
payload = {
|
||||
'jsonrpc': '2.0',
|
||||
'method': 'call',
|
||||
'params': params or {},
|
||||
'id': uuid.uuid4().hex,
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
url,
|
||||
json=payload,
|
||||
timeout=TIMEOUT,
|
||||
headers={'content-type': 'application/json'},
|
||||
auth=OdooEdiProxyAuth(user=self)).json()
|
||||
except (ValueError, requests.exceptions.ConnectionError, requests.exceptions.MissingSchema, requests.exceptions.Timeout, requests.exceptions.HTTPError):
|
||||
raise AccountEdiProxyError('connection_error',
|
||||
_('The url that this service requested returned an error. The url it tried to contact was %s', url))
|
||||
|
||||
if 'error' in response:
|
||||
message = _('The url that this service requested returned an error. The url it tried to contact was %s. %s', url, response['error']['message'])
|
||||
if response['error']['code'] == 404:
|
||||
message = _('The url that this service does not exist. The url it tried to contact was %s', url)
|
||||
raise AccountEdiProxyError('connection_error', message)
|
||||
|
||||
proxy_error = response['result'].pop('proxy_error', False)
|
||||
if proxy_error:
|
||||
error_code = proxy_error['code']
|
||||
if error_code == 'refresh_token_expired':
|
||||
self._renew_token()
|
||||
return self._make_request(url, params)
|
||||
if error_code == 'no_such_user':
|
||||
# This error is also raised if the user didn't exchange data and someone else claimed the edi_identificaiton.
|
||||
self.active = False
|
||||
raise AccountEdiProxyError(error_code, proxy_error['message'] or False)
|
||||
|
||||
return response['result']
|
||||
|
||||
def _register_proxy_user(self, company, edi_format, 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.
|
||||
|
||||
:param company: the company of the user.
|
||||
:param edi_identification: The unique ID that identifies this user on this edi network and to which the files will be addressed.
|
||||
Typically the vat.
|
||||
'''
|
||||
# public_exponent=65537 is a default value that should be used most of the time, as per the documentation of cryptography.
|
||||
# key_size=2048 is considered a reasonable default key size, as per the documentation of cryptography.
|
||||
# see https://cryptography.io/en/latest/hazmat/primitives/asymmetric/rsa/
|
||||
private_key = rsa.generate_private_key(
|
||||
public_exponent=65537,
|
||||
key_size=2048,
|
||||
backend=default_backend()
|
||||
)
|
||||
private_pem = private_key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.PKCS8,
|
||||
encryption_algorithm=serialization.NoEncryption()
|
||||
)
|
||||
public_key = private_key.public_key()
|
||||
public_pem = public_key.public_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
||||
)
|
||||
|
||||
try:
|
||||
response = self._make_request(SERVER_URL + '/iap/account_edi/1/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)
|
||||
})
|
||||
except AccountEdiProxyError as e:
|
||||
raise UserError(e.message)
|
||||
if 'error' in response:
|
||||
raise UserError(response['error'])
|
||||
|
||||
self.create({
|
||||
'id_client': response['id_client'],
|
||||
'company_id': company.id,
|
||||
'edi_format_id': edi_format.id,
|
||||
'edi_identification': edi_identification,
|
||||
'private_key': base64.b64encode(private_pem),
|
||||
'refresh_token': response['refresh_token'],
|
||||
})
|
||||
|
||||
def _renew_token(self):
|
||||
''' Request the proxy for a new refresh token.
|
||||
|
||||
Request to the proxy should be made with a refresh token that expire after 24h to avoid
|
||||
that multiple database use the same credentials. When receiving an error for an expired refresh_token,
|
||||
This method makes a request to get a new refresh token.
|
||||
'''
|
||||
response = self._make_request(SERVER_URL + '/iap/account_edi/1/renew_token')
|
||||
if 'error' in response:
|
||||
# can happen if the database was duplicated and the refresh_token was refreshed by the other database.
|
||||
# we don't want two database to be able to query the proxy with the same user
|
||||
# because it could lead to not inconsistent data.
|
||||
_logger.error(response['error'])
|
||||
raise UserError('Proxy error, please contact Odoo (code: 3)')
|
||||
self.refresh_token = response['refresh_token']
|
||||
|
||||
def _decrypt_data(self, data, symmetric_key):
|
||||
''' Decrypt the data. Note that the data is encrypted with a symmetric key, which is encrypted with an asymmetric key.
|
||||
We must therefore decrypt the symmetric key.
|
||||
|
||||
:param data: The data to decrypt.
|
||||
:param symmetric_key: The symmetric_key encrypted with self.private_key.public_key()
|
||||
'''
|
||||
private_key = serialization.load_pem_private_key(
|
||||
base64.b64decode(self.private_key),
|
||||
password=None,
|
||||
backend=default_backend()
|
||||
)
|
||||
key = private_key.decrypt(
|
||||
base64.b64decode(symmetric_key),
|
||||
padding.OAEP(
|
||||
mgf=padding.MGF1(algorithm=hashes.SHA256()),
|
||||
algorithm=hashes.SHA256(),
|
||||
label=None
|
||||
)
|
||||
)
|
||||
f = Fernet(key)
|
||||
return f.decrypt(base64.b64decode(data))
|
||||
@@ -0,0 +1,9 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResCompany(models.Model):
|
||||
_inherit = 'res.company'
|
||||
|
||||
account_edi_proxy_client_ids = fields.One2many('account_edi_proxy_client.user', inverse_name='company_id')
|
||||
@@ -0,0 +1,2 @@
|
||||
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
|
||||
"access_account_edi_proxy_user","access_account_edi_proxy_user","model_account_edi_proxy_client_user","base.group_system",1,1,1,0
|
||||
|
@@ -3,14 +3,14 @@
|
||||
<data noupdate="1">
|
||||
<!-- add VAT, codice fiscal and tax system for main company -->
|
||||
<record id="l10n_it.demo_company_it" model="res.company">
|
||||
<field name="l10n_it_codice_fiscale">0123456789987654</field>
|
||||
<field name="l10n_it_codice_fiscale">09814700101</field>
|
||||
<field name="l10n_it_tax_system">RF01</field>
|
||||
<field name="zip">12345</field>
|
||||
</record>
|
||||
|
||||
<record id="partner_demo_it" model="res.partner">
|
||||
<field name="name">Palazzo dell'Arte</field>
|
||||
<field name="vat">IT12345670124</field>
|
||||
<field name="vat">IT00000010215</field>
|
||||
<field name="street">Piazza Marconi 5</field>
|
||||
<field name="city">Cremona</field>
|
||||
<field name="country_id" ref="base.it"/>
|
||||
@@ -21,10 +21,12 @@
|
||||
</record>
|
||||
|
||||
<record id="base.res_partner_2" model="res.partner">
|
||||
<field name="vat">IT12345670231</field>
|
||||
<field name="vat">IT00079760328</field>
|
||||
<field name="l10n_it_pa_index">XS00001</field>
|
||||
</record>
|
||||
<record id="base.res_partner_12" model="res.partner">
|
||||
<field name="vat">IT12345670348</field>
|
||||
<field name="vat">IT00140390501</field>
|
||||
<field name="l10n_it_pa_index">XS00001</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
<DatiTrasmissione>
|
||||
<IdTrasmittente>
|
||||
<IdPaese t-esc="get_vat_country(record.company_id.vat)"/>
|
||||
<IdCodice t-esc="record.company_id.l10n_it_codice_fiscale"/>
|
||||
<IdCodice t-esc="get_vat_number(record.company_id.vat)"/>
|
||||
</IdTrasmittente>
|
||||
<ProgressivoInvio t-esc="record.name.replace('/','')[-10:]"/>
|
||||
<FormatoTrasmissione t-esc="formato_trasmissione"/>
|
||||
@@ -72,7 +72,7 @@
|
||||
<IdPaese t-esc="get_vat_country(record.company_id.vat)"/>
|
||||
<IdCodice t-esc="get_vat_number(record.company_id.vat)"/>
|
||||
</IdFiscaleIVA>
|
||||
<CodiceFiscale t-if="record.company_id.l10n_it_codice_fiscale" t-esc="record.company_id.l10n_it_codice_fiscale"/>
|
||||
<CodiceFiscale t-if="record.company_id.l10n_it_codice_fiscale and not test_mode" t-esc="record.company_id.l10n_it_codice_fiscale"/>
|
||||
<Anagrafica>
|
||||
<Denominazione t-esc="record.company_id.partner_id.display_name"/>
|
||||
</Anagrafica>
|
||||
|
||||
@@ -20,6 +20,96 @@ DEFAULT_FACTUR_ITALIAN_DATE_FORMAT = '%Y-%m-%d'
|
||||
class AccountEdiFormat(models.Model):
|
||||
_inherit = 'account.edi.format'
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@api.model
|
||||
def _l10n_it_edi_generate_electronic_invoice_filename(self, invoice):
|
||||
'''Returns a name conform to the Fattura pa Specifications:
|
||||
See ES documentation 2.2
|
||||
'''
|
||||
a = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
n = invoice.id
|
||||
progressive_number = ""
|
||||
while n:
|
||||
(n, m) = divmod(n, len(a))
|
||||
progressive_number = a[m] + progressive_number
|
||||
|
||||
return '%(country_code)s%(codice)s_%(progressive_number)s.xml' % {
|
||||
'country_code': invoice.company_id.country_id.code,
|
||||
'codice': invoice.company_id.l10n_it_codice_fiscale.replace(' ', ''),
|
||||
'progressive_number': progressive_number.zfill(5),
|
||||
}
|
||||
|
||||
def _l10n_it_edi_check_invoice_configuration(self, invoice):
|
||||
errors = []
|
||||
seller = invoice.company_id
|
||||
buyer = invoice.commercial_partner_id
|
||||
|
||||
# <1.1.1.1>
|
||||
if not seller.country_id:
|
||||
errors.append(_("%s must have a country", seller.display_name))
|
||||
|
||||
# <1.1.1.2>
|
||||
if not seller.vat:
|
||||
errors.append(_("%s must have a VAT number", seller.display_name))
|
||||
elif len(seller.vat) > 30:
|
||||
errors.append(_("The maximum length for VAT number is 30. %s have a VAT number too long: %s.", seller.display_name, seller.vat))
|
||||
|
||||
# <1.2.1.2>
|
||||
if not seller.l10n_it_codice_fiscale:
|
||||
errors.append(_("%s must have a codice fiscale number", seller.display_name))
|
||||
|
||||
# <1.2.1.8>
|
||||
if not seller.l10n_it_tax_system:
|
||||
errors.append(_("The seller's company must have a tax system."))
|
||||
|
||||
# <1.2.2>
|
||||
if not seller.street and not seller.street2:
|
||||
errors.append(_("%s must have a street.", seller.display_name))
|
||||
if not seller.zip:
|
||||
errors.append(_("%s must have a post code.", seller.display_name))
|
||||
elif len(seller.zip) != 5 and seller.country_id.code == 'IT':
|
||||
errors.append(_("%s must have a post code of length 5.", seller.display_name))
|
||||
if not seller.city:
|
||||
errors.append(_("%s must have a city.", seller.display_name))
|
||||
if not seller.country_id:
|
||||
errors.append(_("%s must have a country.", seller.display_name))
|
||||
|
||||
if seller.l10n_it_has_tax_representative and not seller.l10n_it_tax_representative_partner_id.vat:
|
||||
errors.append(_("Tax representative partner %s of %s must have a tax number.", seller.l10n_it_tax_representative_partner_id.display_name, seller.display_name))
|
||||
|
||||
# <1.4.1>
|
||||
if not buyer.vat and not buyer.l10n_it_codice_fiscale and buyer.country_id.code == 'IT':
|
||||
errors.append(_("The buyer, %s, or his company must have either a VAT number either a tax code (Codice Fiscale).", buyer.display_name))
|
||||
|
||||
# <1.4.2>
|
||||
if not buyer.street and not buyer.street2:
|
||||
errors.append(_("%s must have a street.", buyer.display_name))
|
||||
if not buyer.zip:
|
||||
errors.append(_("%s must have a post code.", buyer.display_name))
|
||||
if len(buyer.zip) != 5 and buyer.country_id.code == 'IT':
|
||||
errors.append(_("%s must have a post code of length 5.", buyer.display_name))
|
||||
if not buyer.city:
|
||||
errors.append(_("%s must have a city.", buyer.display_name))
|
||||
if not buyer.country_id:
|
||||
errors.append(_("%s must have a country.", buyer.display_name))
|
||||
|
||||
# <2.2.1>
|
||||
for invoice_line in invoice.invoice_line_ids:
|
||||
if not invoice_line.display_type and len(invoice_line.tax_ids) != 1:
|
||||
raise UserError(_("You must select one and only one tax by line."))
|
||||
|
||||
for tax_line in invoice.line_ids.filtered(lambda line: line.tax_line_id):
|
||||
if not tax_line.tax_line_id.l10n_it_kind_exoneration and tax_line.tax_line_id.amount == 0:
|
||||
errors.append(_("%s has an amount of 0.0, you must indicate the kind of exoneration.", tax_line.name))
|
||||
|
||||
if not invoice.partner_bank_id:
|
||||
errors.append(_("The seller must have a bank account."))
|
||||
|
||||
return errors
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Export
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -36,25 +126,30 @@ class AccountEdiFormat(models.Model):
|
||||
return super()._is_compatible_with_journal(journal)
|
||||
return journal.type == 'sale' and journal.country_code == 'IT'
|
||||
|
||||
def _l10n_it_edi_is_required_for_invoice(self, invoice):
|
||||
""" Is the edi required for this invoice based on the method (here: PEC mail)
|
||||
Deprecated: in future release PEC mail will be removed.
|
||||
TO OVERRIDE
|
||||
"""
|
||||
return invoice.is_sale_document() and invoice.l10n_it_send_state not in ('sent', 'delivered', 'delivered_accepted') and invoice.country_code == 'IT'
|
||||
|
||||
def _is_required_for_invoice(self, invoice):
|
||||
# OVERRIDE
|
||||
self.ensure_one()
|
||||
if self.code != 'fattura_pa':
|
||||
return super()._is_required_for_invoice(invoice)
|
||||
|
||||
# Determine on which invoices the Mexican CFDI must be generated.
|
||||
return invoice.is_sale_document() and invoice.l10n_it_send_state not in ['sent', 'delivered', 'delivered_accepted'] and invoice.country_code == 'IT'
|
||||
|
||||
def _post_invoice_edi(self, invoices):
|
||||
# OVERRIDE
|
||||
self.ensure_one()
|
||||
edi_result = super()._post_invoice_edi(invoices)
|
||||
if self.code != 'fattura_pa':
|
||||
return edi_result
|
||||
return self._l10n_it_edi_is_required_for_invoice(invoice)
|
||||
|
||||
def _post_fattura_pa(self, invoices):
|
||||
# TO OVERRIDE
|
||||
invoice = invoices # no batching ensure that we only have one invoice
|
||||
invoice.l10n_it_send_state = 'other'
|
||||
invoice._check_before_xml_exporting()
|
||||
if invoice.l10n_it_einvoice_id and invoice.l10n_it_send_state not in ['invalid', 'to_send']:
|
||||
return {'error': _("You can't regenerate an E-Invoice when the first one is sent and there are no errors")}
|
||||
if invoice.l10n_it_einvoice_id:
|
||||
invoice.l10n_it_einvoice_id.unlink()
|
||||
res = invoice.invoice_generate_xml()
|
||||
if len(invoice.commercial_partner_id.l10n_it_pa_index or '') == 6:
|
||||
invoice.message_post(
|
||||
@@ -64,6 +159,15 @@ class AccountEdiFormat(models.Model):
|
||||
invoice.l10n_it_send_state = 'to_send'
|
||||
return {invoice: res}
|
||||
|
||||
def _post_invoice_edi(self, invoices, test_mode=False):
|
||||
# OVERRIDE
|
||||
self.ensure_one()
|
||||
edi_result = super()._post_invoice_edi(invoices)
|
||||
if self.code != 'fattura_pa':
|
||||
return edi_result
|
||||
|
||||
return self._post_fattura_pa(invoices)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Import
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@@ -56,87 +56,14 @@ class AccountMove(models.Model):
|
||||
invoice.l10n_it_einvoice_name = einvoice.attachment_id.name
|
||||
|
||||
def _check_before_xml_exporting(self):
|
||||
self.ensure_one()
|
||||
seller = self.company_id
|
||||
buyer = self.commercial_partner_id
|
||||
|
||||
# <1.1.1.1>
|
||||
if not seller.country_id:
|
||||
raise UserError(_("%s must have a country") % (seller.display_name))
|
||||
|
||||
# <1.1.1.2>
|
||||
if not seller.vat:
|
||||
raise UserError(_("%s must have a VAT number") % (seller.display_name))
|
||||
elif len(seller.vat) > 30:
|
||||
raise UserError(_("The maximum length for VAT number is 30. %s have a VAT number too long: %s.") % (seller.display_name, seller.vat))
|
||||
|
||||
# <1.2.1.2>
|
||||
if not seller.l10n_it_codice_fiscale:
|
||||
raise UserError(_("%s must have a codice fiscale number") % (seller.display_name))
|
||||
|
||||
# <1.2.1.8>
|
||||
if not seller.l10n_it_tax_system:
|
||||
raise UserError(_("The seller's company must have a tax system."))
|
||||
|
||||
# <1.2.2>
|
||||
if not seller.street and not seller.street2:
|
||||
raise UserError(_("%s must have a street.") % (seller.display_name))
|
||||
if not seller.zip:
|
||||
raise UserError(_("%s must have a post code.") % (seller.display_name))
|
||||
if len(seller.zip) != 5 and seller.country_id.code == 'IT':
|
||||
raise UserError(_("%s must have a post code of length 5.") % (seller.display_name))
|
||||
if not seller.city:
|
||||
raise UserError(_("%s must have a city.") % (seller.display_name))
|
||||
if not seller.country_id:
|
||||
raise UserError(_("%s must have a country.") % (seller.display_name))
|
||||
|
||||
if seller.l10n_it_has_tax_representative and not seller.l10n_it_tax_representative_partner_id.vat:
|
||||
raise UserError(_("Tax representative partner %s of %s must have a tax number.") % (seller.l10n_it_tax_representative_partner_id.display_name, seller.display_name))
|
||||
|
||||
# <1.4.1>
|
||||
if not buyer.vat and not buyer.l10n_it_codice_fiscale and buyer.country_id.code == 'IT':
|
||||
raise UserError(_("The buyer, %s, or his company must have either a VAT number either a tax code (Codice Fiscale).") % (buyer.display_name))
|
||||
|
||||
# <1.4.2>
|
||||
if not buyer.street and not buyer.street2:
|
||||
raise UserError(_("%s must have a street.") % (buyer.display_name))
|
||||
if not buyer.zip:
|
||||
raise UserError(_("%s must have a post code.") % (buyer.display_name))
|
||||
if len(buyer.zip) != 5 and buyer.country_id.code == 'IT':
|
||||
raise UserError(_("%s must have a post code of length 5.") % (buyer.display_name))
|
||||
if not buyer.city:
|
||||
raise UserError(_("%s must have a city.") % (buyer.display_name))
|
||||
if not buyer.country_id:
|
||||
raise UserError(_("%s must have a country.") % (buyer.display_name))
|
||||
|
||||
# <2.2.1>
|
||||
for invoice_line in self.invoice_line_ids:
|
||||
if not invoice_line.display_type and len(invoice_line.tax_ids) != 1:
|
||||
raise UserError(_("You must select one and only one tax by line."))
|
||||
|
||||
for tax_line in self.line_ids.filtered(lambda line: line.tax_line_id):
|
||||
if not tax_line.tax_line_id.l10n_it_has_exoneration and tax_line.tax_line_id.amount == 0:
|
||||
raise ValidationError(_("%s has an amount of 0.0, you must indicate the kind of exoneration.", tax_line.name))
|
||||
# DEPRECATED use AccountEdiFormat._l10n_it_edi_check_invoice_configuration instead
|
||||
errors = self.env['account.edi.format']._l10n_it_edi_check_invoice_configuration(self)
|
||||
if errors:
|
||||
raise self.env['account.edi.format']._format_error_message(_("Invalid configuration:"), errors)
|
||||
|
||||
def invoice_generate_xml(self):
|
||||
self.ensure_one()
|
||||
if self.l10n_it_einvoice_id and self.l10n_it_send_state not in ['invalid', 'to_send']:
|
||||
return {'error': _("You can't regenerate an E-Invoice when the first one is sent and there are no errors")}
|
||||
if self.l10n_it_einvoice_id:
|
||||
self.l10n_it_einvoice_id.unlink()
|
||||
|
||||
a = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
n = self.id
|
||||
progressive_number = ""
|
||||
while n:
|
||||
(n,m) = divmod(n,len(a))
|
||||
progressive_number = a[m] + progressive_number
|
||||
|
||||
report_name = '%(country_code)s%(codice)s_%(progressive_number)s.xml' % {
|
||||
'country_code': self.company_id.country_id.code,
|
||||
'codice': self.company_id.l10n_it_codice_fiscale,
|
||||
'progressive_number': progressive_number.zfill(5),
|
||||
}
|
||||
report_name = self.env['account_edi_format']._l10n_it_edi_generate_electronic_invoice_filename(self)
|
||||
|
||||
data = b"<?xml version='1.0' encoding='UTF-8'?>" + self._export_as_xml()
|
||||
description = _('Italian invoice: %s', self.move_type)
|
||||
@@ -155,9 +82,6 @@ class AccountMove(models.Model):
|
||||
return {'attachment': attachment}
|
||||
|
||||
def _prepare_fatturapa_export_values(self):
|
||||
''' Create the xml file content.
|
||||
:return: The XML content as str.
|
||||
'''
|
||||
self.ensure_one()
|
||||
|
||||
def format_date(dt):
|
||||
@@ -252,6 +176,10 @@ class AccountMove(models.Model):
|
||||
return template_values
|
||||
|
||||
def _export_as_xml(self):
|
||||
'''DEPRECATED : this will be moved to AccountEdiFormat in a future version.
|
||||
Create the xml file content.
|
||||
:return: The XML content as str.
|
||||
'''
|
||||
template_values = self._prepare_fatturapa_export_values()
|
||||
content = self.env.ref('l10n_it_edi.account_invoice_it_FatturaPA_export')._render(template_values)
|
||||
return content
|
||||
|
||||
@@ -189,13 +189,11 @@ class FetchmailServer(models.Model):
|
||||
_logger.info('The xml file is badly formatted: %s', att_name)
|
||||
return invoices
|
||||
|
||||
# Create the invoice
|
||||
invoices = self.env.ref('l10n_it_edi.edi_fatturaPA')._create_invoice_from_xml_tree(att_name, tree)
|
||||
if not invoices:
|
||||
_logger.info('E-invoice not found in file: %s', att_name)
|
||||
return invoices
|
||||
|
||||
invoices.l10n_it_send_state = 'new'
|
||||
invoices.l10n_it_send_state = "new"
|
||||
invoices.invoice_source_email = from_address
|
||||
for invoice in invoices:
|
||||
invoice.with_context(no_new_invoice=True, default_res_id=invoice.id) \
|
||||
@@ -409,8 +407,10 @@ class FetchmailServer(models.Model):
|
||||
class IrMailServer(models.Model):
|
||||
_name = "ir.mail_server"
|
||||
_inherit = "ir.mail_server"
|
||||
|
||||
def _get_test_email_addresses(self):
|
||||
self.ensure_one()
|
||||
|
||||
company = self.env["res.company"].search([("l10n_it_mail_pec_server_id", "=", self.id)], limit=1)
|
||||
if not company:
|
||||
# it's not a PEC server
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from . import models
|
||||
|
||||
from odoo import api, SUPERUSER_ID
|
||||
|
||||
|
||||
def _disable_pec_mail_post_init(cr, registry):
|
||||
''' Pec mail cannot be used in conjunction with SdiCoop, so disable the Pec fetchmail servers.
|
||||
'''
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
|
||||
env['fetchmail.server'].search([('l10n_it_is_pec', '=', True)]).l10n_it_is_pec = False
|
||||
env['res.company'].search([('l10n_it_mail_pec_server_id', '!=', None)]).l10n_it_mail_pec_server_id = None
|
||||
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'Italy - E-invoicing (SdiCoop)',
|
||||
'version': '0.3',
|
||||
'depends': [
|
||||
'l10n_it_edi',
|
||||
'account_edi_extended',
|
||||
'account_edi_proxy_client',
|
||||
],
|
||||
'author': 'Odoo',
|
||||
'description': """
|
||||
E-invoice implementation for Italy with the web-service. Ability to send and receive document from SdiCoop. Files sent by SdiCoop are first stored on the proxy
|
||||
and then fetched by this module.
|
||||
""",
|
||||
'category': 'Accounting/Localizations/EDI',
|
||||
'website': 'http://www.odoo.com/',
|
||||
'data': [
|
||||
'data/cron.xml',
|
||||
'views/l10n_it_view.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
],
|
||||
'post_init_hook': '_disable_pec_mail_post_init',
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="ir_cron_receive_fattura_pa_invoice" model="ir.cron">
|
||||
<field name="name">FatturaPA: Receive invoices from the exchange system</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="model_id" ref="account_edi.model_account_edi_format"/>
|
||||
<field name="code">model._cron_receive_fattura_pa()</field>
|
||||
<field name="doall" eval="False"/>
|
||||
<field name="state">code</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,213 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * l10n_it_edi_sdicoop
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 14.0+e\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-06-03 13:44+0000\n"
|
||||
"PO-Revision-Date: 2021-06-03 13:44+0000\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: l10n_it_edi_sdicoop
|
||||
#: model_terms:ir.ui.view,arch_db:l10n_it_edi_sdicoop.res_config_settings_view_form
|
||||
msgid "<span class=\"o_form_label\">Allow Odoo to process invoices</span>"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_it_edi_sdicoop
|
||||
#: model_terms:ir.ui.view,arch_db:l10n_it_edi_sdicoop.res_config_settings_view_form
|
||||
msgid "Already registered"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_it_edi_sdicoop
|
||||
#: code:addons/l10n_it_edi_sdicoop/models/account_edi_format.py:0
|
||||
#, python-format
|
||||
msgid "Attached file is empty"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_it_edi_sdicoop
|
||||
#: model_terms:ir.ui.view,arch_db:l10n_it_edi_sdicoop.res_config_settings_view_form
|
||||
msgid ""
|
||||
"By clicking this button, I hereby accept that Odoo may process my invoices"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_it_edi_sdicoop
|
||||
#: model:ir.model,name:l10n_it_edi_sdicoop.model_res_config_settings
|
||||
msgid "Config Settings"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_it_edi_sdicoop
|
||||
#: model:ir.model.fields,field_description:l10n_it_edi_sdicoop.field_account_edi_format__display_name
|
||||
#: model:ir.model.fields,field_description:l10n_it_edi_sdicoop.field_account_move__display_name
|
||||
#: model:ir.model.fields,field_description:l10n_it_edi_sdicoop.field_res_config_settings__display_name
|
||||
msgid "Display Name"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_it_edi_sdicoop
|
||||
#: model:ir.model,name:l10n_it_edi_sdicoop.model_account_edi_format
|
||||
msgid "EDI format"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_it_edi_sdicoop
|
||||
#: model_terms:ir.ui.view,arch_db:l10n_it_edi_sdicoop.res_config_settings_view_form
|
||||
msgid "Electronic Document Invoicing"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_it_edi_sdicoop
|
||||
#: code:addons/l10n_it_edi_sdicoop/models/account_edi_format.py:0
|
||||
#, python-format
|
||||
msgid "Expiration of the maximum term for communication of acceptance/refusal"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_it_edi_sdicoop
|
||||
#: model:ir.actions.server,name:l10n_it_edi_sdicoop.ir_cron_receive_fattura_pa_invoice_ir_actions_server
|
||||
#: model:ir.cron,cron_name:l10n_it_edi_sdicoop.ir_cron_receive_fattura_pa_invoice
|
||||
#: model:ir.cron,name:l10n_it_edi_sdicoop.ir_cron_receive_fattura_pa_invoice
|
||||
msgid "FatturaPA: Receive invoices from the exchange system"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_it_edi_sdicoop
|
||||
#: model:ir.model.fields,field_description:l10n_it_edi_sdicoop.field_account_edi_format__id
|
||||
#: model:ir.model.fields,field_description:l10n_it_edi_sdicoop.field_account_move__id
|
||||
#: model:ir.model.fields,field_description:l10n_it_edi_sdicoop.field_res_config_settings__id
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_it_edi_sdicoop
|
||||
#: code:addons/l10n_it_edi_sdicoop/models/account_edi_format.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Invoices for PA are not managed by Odoo, you can download the document and "
|
||||
"send it on your own."
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_it_edi_sdicoop
|
||||
#: model:ir.model.fields,field_description:l10n_it_edi_sdicoop.field_res_config_settings__is_edi_proxy_active
|
||||
msgid "Is Edi Proxy Active"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_it_edi_sdicoop
|
||||
#: code:addons/l10n_it_edi_sdicoop/models/account_edi_format.py:0
|
||||
#, python-format
|
||||
msgid "Italian invoice: %s"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_it_edi_sdicoop
|
||||
#: model:ir.model,name:l10n_it_edi_sdicoop.model_account_move
|
||||
msgid "Journal Entry"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_it_edi_sdicoop
|
||||
#: model:ir.model.fields,field_description:l10n_it_edi_sdicoop.field_account_bank_statement_line__l10n_it_edi_attachment_id
|
||||
#: model:ir.model.fields,field_description:l10n_it_edi_sdicoop.field_account_move__l10n_it_edi_attachment_id
|
||||
#: model:ir.model.fields,field_description:l10n_it_edi_sdicoop.field_account_payment__l10n_it_edi_attachment_id
|
||||
msgid "L10N It Edi Attachment"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_it_edi_sdicoop
|
||||
#: model:ir.model.fields,field_description:l10n_it_edi_sdicoop.field_account_bank_statement_line__l10n_it_edi_transaction
|
||||
#: model:ir.model.fields,field_description:l10n_it_edi_sdicoop.field_account_move__l10n_it_edi_transaction
|
||||
#: model:ir.model.fields,field_description:l10n_it_edi_sdicoop.field_account_payment__l10n_it_edi_transaction
|
||||
msgid "L10N It Edi Transaction"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_it_edi_sdicoop
|
||||
#: model:ir.model.fields,field_description:l10n_it_edi_sdicoop.field_account_edi_format____last_update
|
||||
#: model:ir.model.fields,field_description:l10n_it_edi_sdicoop.field_account_move____last_update
|
||||
#: model:ir.model.fields,field_description:l10n_it_edi_sdicoop.field_res_config_settings____last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_it_edi_sdicoop
|
||||
#: code:addons/l10n_it_edi_sdicoop/models/account_edi_format.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Please fill your codice fiscale to be able to receive invoices from "
|
||||
"FatturaPA"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_it_edi_sdicoop
|
||||
#: model_terms:ir.ui.view,arch_db:l10n_it_edi_sdicoop.res_config_settings_view_form
|
||||
msgid "Register"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_it_edi_sdicoop
|
||||
#: code:addons/l10n_it_edi_sdicoop/models/account_edi_format.py:0
|
||||
#, python-format
|
||||
msgid "Service momentarily unavailable"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_it_edi_sdicoop
|
||||
#: code:addons/l10n_it_edi_sdicoop/models/account_edi_format.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The E-invoice is not delivered to the addressee. The Exchange System is"
|
||||
" unable to deliver the file to the Public Administration."
|
||||
" The Exchange System will contact the PA to report the "
|
||||
"problem and request that they provide a solution. During"
|
||||
" the following 15 days, the Exchange System will try to forward the "
|
||||
"FatturaPA file to the Administration in question again."
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_it_edi_sdicoop
|
||||
#: code:addons/l10n_it_edi_sdicoop/models/account_edi_format.py:0
|
||||
#, python-format
|
||||
msgid "The invoice has been refused by the Exchange System"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_it_edi_sdicoop
|
||||
#: code:addons/l10n_it_edi_sdicoop/models/account_edi_format.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The invoice has been succesfully transmitted. The addressee has 15 days to "
|
||||
"accept or reject it."
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_it_edi_sdicoop
|
||||
#: code:addons/l10n_it_edi_sdicoop/models/account_edi_format.py:0
|
||||
#, python-format
|
||||
msgid "The invoice was refused by the addressee."
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_it_edi_sdicoop
|
||||
#: code:addons/l10n_it_edi_sdicoop/models/account_edi_format.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The invoice was successfully transmitted to the Public Administration and we"
|
||||
" are waiting for confirmation"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_it_edi_sdicoop
|
||||
#: code:addons/l10n_it_edi_sdicoop/models/account_edi_format.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The invoice was successfully transmitted to the Public Administration and we"
|
||||
" are waiting for confirmation."
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_it_edi_sdicoop
|
||||
#: code:addons/l10n_it_edi_sdicoop/models/account_edi_format.py:0
|
||||
#, python-format
|
||||
msgid "Unauthorized user"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_it_edi_sdicoop
|
||||
#: code:addons/l10n_it_edi_sdicoop/models/account_edi_format.py:0
|
||||
#, python-format
|
||||
msgid "You are not allowed to check the status of this invoice."
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_it_edi_sdicoop
|
||||
#: code:addons/l10n_it_edi_sdicoop/models/account_edi_format.py:0
|
||||
#: code:addons/l10n_it_edi_sdicoop/models/account_edi_format.py:0
|
||||
#: code:addons/l10n_it_edi_sdicoop/models/account_edi_format.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"You must accept the terms and conditions in the settings to use FatturaPA."
|
||||
msgstr ""
|
||||
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import account_invoice
|
||||
from . import account_edi_format
|
||||
from . import res_config_settings
|
||||
@@ -0,0 +1,267 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, _, _lt
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.addons.account_edi_proxy_client.models.account_edi_proxy_user import AccountEdiProxyError, SERVER_URL
|
||||
|
||||
from lxml import etree
|
||||
import base64
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AccountEdiFormat(models.Model):
|
||||
_inherit = 'account.edi.format'
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Import
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def _cron_receive_fattura_pa(self):
|
||||
''' Check the proxy for incoming invoices.
|
||||
'''
|
||||
proxy_users = self.env['account_edi_proxy_client.user'].search([('edi_format_id', '=', self.env.ref('l10n_it_edi.edi_fatturaPA').id)])
|
||||
for proxy_user in proxy_users:
|
||||
company = proxy_user.company_id
|
||||
try:
|
||||
res = proxy_user._make_request(SERVER_URL + '/api/l10n_it_edi/1/in/RicezioneInvoice',
|
||||
params={'recipient_codice_fiscale': company.l10n_it_codice_fiscale})
|
||||
except AccountEdiProxyError as e:
|
||||
_logger.error('Error while receiving file from SdiCoop: %s', e)
|
||||
|
||||
proxy_acks = []
|
||||
for id_transaction, fattura in res.items():
|
||||
if self.env['ir.attachment'].search([('name', '=', fattura['filename']), ('res_model', '=', 'account.move')], limit=1):
|
||||
# name should be unique, the invoice already exists
|
||||
_logger.info('E-invoice already exist: %s', fattura['filename'])
|
||||
proxy_acks.append(id_transaction)
|
||||
continue
|
||||
|
||||
file = proxy_user._decrypt_data(fattura['file'], fattura['key'])
|
||||
|
||||
try:
|
||||
tree = etree.fromstring(file)
|
||||
except Exception:
|
||||
# should not happen as the file has been checked by SdiCoop
|
||||
_logger.info('Received file badly formatted, skipping: \n %s', file)
|
||||
continue
|
||||
|
||||
invoice = self.env.ref('l10n_it_edi.edi_fatturaPA')._create_invoice_from_xml_tree(fattura['filename'], tree)
|
||||
self.env['ir.attachment'].create({
|
||||
'name': fattura['filename'],
|
||||
'raw': file,
|
||||
'type': 'binary',
|
||||
'res_model': 'account.move',
|
||||
'res_id': invoice.id
|
||||
})
|
||||
|
||||
proxy_acks.append(id_transaction)
|
||||
try:
|
||||
proxy_user._make_request(SERVER_URL + '/api/l10n_it_edi/1/ack',
|
||||
params={'transaction_ids': proxy_acks})
|
||||
except AccountEdiProxyError as e:
|
||||
_logger.error('Error while receiving file from SdiCoop: %s', e)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Export
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def _check_move_configuration(self, move):
|
||||
# OVERRIDE
|
||||
res = super()._check_move_configuration(move)
|
||||
if self.code != 'fattura_pa':
|
||||
return res
|
||||
|
||||
res.extend(self._l10n_it_edi_check_invoice_configuration(move))
|
||||
|
||||
if not self._get_proxy_user(move.company_id):
|
||||
res.append(_("You must accept the terms and conditions in the settings to use FatturaPA."))
|
||||
|
||||
return res
|
||||
|
||||
def _needs_web_services(self):
|
||||
self.ensure_one()
|
||||
return self.code == 'fattura_pa' or super()._needs_web_services()
|
||||
|
||||
def _l10n_it_edi_is_required_for_invoice(self, invoice):
|
||||
""" _is_required_for_invoice for SdiCoop.
|
||||
OVERRIDE
|
||||
"""
|
||||
return invoice.is_sale_document() and invoice.country_code == 'IT'
|
||||
|
||||
def _support_batching(self, move=None, state=None, company=None):
|
||||
# OVERRIDE
|
||||
if self.code == 'fattura_pa':
|
||||
return state == 'to_send' and move.is_invoice()
|
||||
|
||||
return super()._support_batching(move=move, state=state, company=company)
|
||||
|
||||
def _l10n_it_post_invoices_step_1(self, invoices):
|
||||
''' Send the invoices to the proxy.
|
||||
'''
|
||||
to_return = {}
|
||||
|
||||
to_send = {}
|
||||
for invoice in invoices:
|
||||
xml = b"<?xml version='1.0' encoding='UTF-8'?>" + invoice._export_as_xml()
|
||||
filename = self._l10n_it_edi_generate_electronic_invoice_filename(invoice)
|
||||
attachment = self.env['ir.attachment'].create({
|
||||
'name': filename,
|
||||
'res_id': invoice.id,
|
||||
'res_model': invoice._name,
|
||||
'datas': base64.encodebytes(xml),
|
||||
'description': _('Italian invoice: %s', invoice.move_type),
|
||||
'type': 'binary',
|
||||
})
|
||||
invoice.l10n_it_edi_attachment_id = attachment
|
||||
|
||||
if len(invoice.commercial_partner_id.l10n_it_pa_index or '') == 6:
|
||||
invoice.message_post(
|
||||
body=(_("Invoices for PA are not managed by Odoo, you can download the document and send it on your own."))
|
||||
)
|
||||
to_return[invoice] = {'attachment': attachment}
|
||||
else:
|
||||
to_send[filename] = {
|
||||
'invoice': invoice,
|
||||
'data': {'filename': filename, 'xml': base64.b64encode(xml)}}
|
||||
|
||||
company = invoices.company_id
|
||||
proxy_user = self._get_proxy_user(company)
|
||||
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}
|
||||
try:
|
||||
responses = self._l10n_it_edi_upload([i['data'] for i in to_send.values()], proxy_user)
|
||||
except AccountEdiProxyError as e:
|
||||
return {invoice: {'error': e.message, 'blocking_level': 'error'} for invoice in invoices}
|
||||
|
||||
for filename, response in responses.items():
|
||||
invoice = to_send[filename]['invoice']
|
||||
to_return[invoice] = response
|
||||
if 'id_transaction' in response:
|
||||
invoice.l10n_it_edi_transaction = response['id_transaction']
|
||||
to_return[invoice].update({
|
||||
'error': _('The invoice was successfully transmitted to the Public Administration and we are waiting for confirmation.'),
|
||||
'blocking_level': 'info',
|
||||
})
|
||||
return to_return
|
||||
|
||||
def _l10n_it_post_invoices_step_2(self, invoices):
|
||||
''' Check if the sent invoices have been processed by FatturaPA.
|
||||
'''
|
||||
to_check = {i.l10n_it_edi_transaction: i for i in invoices}
|
||||
to_return = {}
|
||||
company = invoices.company_id
|
||||
proxy_user = self._get_proxy_user(company)
|
||||
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}
|
||||
try:
|
||||
responses = proxy_user._make_request(SERVER_URL + '/api/l10n_it_edi/1/in/TrasmissioneFatture',
|
||||
params={'ids_transaction': list(to_check.keys())})
|
||||
except AccountEdiProxyError as e:
|
||||
return {invoice: {'error': e.message, 'blocking_level': 'error'} for invoice in invoices}
|
||||
|
||||
proxy_acks = []
|
||||
for id_transaction, response in responses.items():
|
||||
invoice = to_check[id_transaction]
|
||||
if 'error' in response:
|
||||
to_return[invoice] = response
|
||||
continue
|
||||
|
||||
state = response['state']
|
||||
if state == 'awaiting_outcome':
|
||||
to_return[invoice] = {
|
||||
'error': _('The invoice was successfully transmitted to the Public Administration and we are waiting for confirmation'),
|
||||
'blocking_level': 'info',
|
||||
}
|
||||
proxy_acks.append(id_transaction)
|
||||
continue
|
||||
elif state == 'not_found':
|
||||
# Invoice does not exist on proxy. Either it does not belong to this proxy_user or it was not created correctly when
|
||||
# it was sent to the proxy.
|
||||
to_return[invoice] = {'error': _('You are not allowed to check the status of this invoice.'), 'blocking_level': 'error'}
|
||||
continue
|
||||
|
||||
xml = proxy_user._decrypt_data(response['file'], response['key'])
|
||||
response_tree = etree.fromstring(xml)
|
||||
if state == 'ricevutaConsegna':
|
||||
to_return[invoice] = {'error': _('The invoice has been succesfully transmitted. The addressee has 15 days to accept or reject it.')}
|
||||
elif state == 'notificaScarto':
|
||||
errors = [element.find('Descrizione').text for element in response_tree.xpath('//Errore')]
|
||||
to_return[invoice] = {'error': self._format_error_message(_('The invoice has been refused by the Exchange System'), errors), 'blocking_level': 'error'}
|
||||
elif state == 'notificaMancataConsegna':
|
||||
to_return[invoice] = {
|
||||
'error': _('The E-invoice is not delivered to the addressee. The Exchange System is\
|
||||
unable to deliver the file to the Public Administration. The Exchange System will\
|
||||
contact the PA to report the problem and request that they provide a solution. \
|
||||
During the following 15 days, the Exchange System will try to forward the FatturaPA\
|
||||
file to the Administration in question again.'),
|
||||
}
|
||||
elif state == 'notificaEsito':
|
||||
outcome = response_tree.find('Esito').text
|
||||
if outcome == 'EC01':
|
||||
to_return[invoice] = {'attachment': invoice.l10n_it_edi_attachment_id}
|
||||
else: # ECO2
|
||||
to_return[invoice] = {'error': _('The invoice was refused by the addressee.'), 'blocking_level': 'error'}
|
||||
elif state == 'NotificaDecorrenzaTermini':
|
||||
to_return[invoice] = {'error': _('Expiration of the maximum term for communication of acceptance/refusal'), 'blocking_level': 'error'}
|
||||
proxy_acks.append(id_transaction)
|
||||
|
||||
try:
|
||||
proxy_user._make_request(SERVER_URL + '/api/l10n_it_edi/1/ack',
|
||||
params={'transaction_ids': proxy_acks})
|
||||
except AccountEdiProxyError as e:
|
||||
# Will be ignored and acked again next time.
|
||||
_logger.error('Error while acking file to SdiCoop: %s', e)
|
||||
|
||||
return to_return
|
||||
|
||||
def _post_fattura_pa(self, invoices):
|
||||
# OVERRIDE
|
||||
if not invoices.l10n_it_edi_transaction:
|
||||
return self._l10n_it_post_invoices_step_1(invoices)
|
||||
else:
|
||||
return self._l10n_it_post_invoices_step_2(invoices)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# 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'))
|
||||
|
||||
return company.l10n_it_codice_fiscale
|
||||
|
||||
def _l10n_it_edi_upload(self, files, proxy_user):
|
||||
'''Upload files to fatturapa.
|
||||
|
||||
:param files: A list of dictionary {filename, base64_xml}.
|
||||
:returns: A dictionary.
|
||||
* message: Message from fatturapa.
|
||||
* transactionId: The fatturapa ID of this request.
|
||||
* error: An eventual error.
|
||||
* error_level: Info, warning, error.
|
||||
'''
|
||||
ERRORS = {
|
||||
'EI01': {'error': _lt('Attached file is empty'), 'blocking_level': 'error'},
|
||||
'EI02': {'error': _lt('Service momentarily unavailable'), 'blocking_level': 'warning'},
|
||||
'EI03': {'error': _lt('Unauthorized user'), 'blocking_level': 'error'},
|
||||
}
|
||||
|
||||
result = proxy_user._make_request(SERVER_URL + '/api/l10n_it_edi/1/out/SdiRiceviFile', params={'files': files})
|
||||
|
||||
# Translate the errors.
|
||||
for filename in result.keys():
|
||||
if 'error' in result[filename]:
|
||||
result[filename] = ERRORS.get(result[filename]['error'], {'error': result[filename]['error'], 'blocking_level': 'error'})
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,24 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import logging
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_FACTUR_ITALIAN_DATE_FORMAT = '%Y-%m-%d'
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_inherit = 'account.move'
|
||||
|
||||
l10n_it_edi_transaction = fields.Char()
|
||||
l10n_it_edi_attachment_id = fields.Many2one('ir.attachment')
|
||||
|
||||
def send_pec_mail(self):
|
||||
self.ensure_one()
|
||||
# OVERRIDE
|
||||
# With SdiCoop web-service, no need to send PEC mail.
|
||||
# Set the state to 'other' because the invoice should not be managed par l10n_it_edi.
|
||||
self.l10n_it_send_state = 'other'
|
||||
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import api, models, fields
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
is_edi_proxy_active = fields.Boolean(compute='_compute_is_edi_proxy_active')
|
||||
|
||||
@api.depends('company_id.account_edi_proxy_client_ids', 'company_id.account_edi_proxy_client_ids.active')
|
||||
def _compute_is_edi_proxy_active(self):
|
||||
for config in self:
|
||||
config.is_edi_proxy_active = config.company_id.account_edi_proxy_client_ids
|
||||
|
||||
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)
|
||||
@@ -0,0 +1,75 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="fetchmail_server_form_l10n_it_inherit" model="ir.ui.view">
|
||||
<field name="name">fetchmail.server.form.l10n.it.inherit</field>
|
||||
<field name="model">fetchmail.server</field>
|
||||
<field name="priority">30</field>
|
||||
<field name="inherit_id" ref="l10n_it_edi.fetchmail_server_form_l10n_it"/>
|
||||
<field name="arch" type="xml">
|
||||
<data>
|
||||
<xpath expr="//field[@name='l10n_it_is_pec']" position="replace" />
|
||||
</data>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="res_company_form_l10n_it_inherit" model="ir.ui.view">
|
||||
<field name="name">res.company.form.l10n.it.inherit</field>
|
||||
<field name="model">res.company</field>
|
||||
<field name="priority">30</field>
|
||||
<field name="inherit_id" ref="l10n_it_edi.res_company_form_l10n_it"/>
|
||||
<field name="arch" type="xml">
|
||||
<data>
|
||||
<xpath expr="//field[@name='l10n_it_mail_pec_server_id']" position="replace"/>
|
||||
<xpath expr="//field[@name='l10n_it_address_send_fatturapa']" position="replace"/>
|
||||
<xpath expr="//field[@name='l10n_it_address_recipient_fatturapa']" position="replace"/>
|
||||
</data>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="l10n_it_edi.invoice_supplier_tree_l10n_it" model="ir.ui.view">
|
||||
<field name="arch" type="xml">
|
||||
<!-- Remove the l10n_state_it field. Doing so
|
||||
with xpath might replace other view elements that we shouldn't remove.-->
|
||||
<data/>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="l10n_it_edi.invoice_kanban_l10n_it" model="ir.ui.view">
|
||||
<field name="arch" type="xml">
|
||||
<!-- Remove the l10n_state_it field. Doing so
|
||||
with xpath might replace other view elements that we shouldn't remove.-->
|
||||
<data/>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="l10n_it_edi.account_invoice_form_l10n_it" model="ir.ui.view">
|
||||
<field name="arch" type="xml">
|
||||
<!-- Remove the l10n_state_it field but keep other info tab.
|
||||
Doing so with xpath might repace other view elements that we shouldn't remove. -->
|
||||
<data>
|
||||
<xpath expr="//page[@name='other_info']" position="after">
|
||||
<page string="Electronic Invoicing"
|
||||
name="electronic_invoicing"
|
||||
attrs="{'invisible': [('move_type', 'not in', ('out_invoice', 'out_refund', 'in_invoice', 'in_refund'))]}">
|
||||
<group>
|
||||
<group>
|
||||
<field name="l10n_it_stamp_duty"/>
|
||||
<field name="l10n_it_ddt_id"
|
||||
attrs="{'invisible': [('move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
</data>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="l10n_it_edi.view_account_invoice_filter_l10n_it" model="ir.ui.view">
|
||||
<field name="arch" type="xml">
|
||||
<!-- Remove the filters.-->
|
||||
<data />
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="res_config_settings_view_form" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.inherit.proxy.user</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="account.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@id='account_vendor_bills']" position="after">
|
||||
<div attrs="{'invisible':[('country_code', '!=', 'IT')]}">
|
||||
<h2>Electronic Document Invoicing</h2>
|
||||
<div class="row mt16 o_settings_container" id='account_edi'>
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Allow Odoo to process invoices</span>
|
||||
<div class="text-muted">
|
||||
By clicking this button, I accept that Odoo may process my invoices.
|
||||
</div>
|
||||
<div class="content-group">
|
||||
<field name="is_edi_proxy_active" invisible="True" />
|
||||
<div class="row mt8">
|
||||
<div class="col-lg-6" title="Register">
|
||||
<button name="button_create_proxy_user"
|
||||
type="object"
|
||||
string="Register"
|
||||
class="btn-primary" icon="fa-lg fa-check"
|
||||
attrs="{'invisible':[('is_edi_proxy_active', '=', True)]}"/>
|
||||
<button name="button_create_proxy_user"
|
||||
type="object"
|
||||
string="Already registered"
|
||||
disabled="1"
|
||||
class="btn-lnk" icon="fa-lg fa-check"
|
||||
attrs="{'invisible':[('is_edi_proxy_active', '=', False)]}"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user