[IMP] account_peppol: improve participant onboarding
This commit adds several improvements in Peppol onboarding process: - Adds a phone number verification process - Adds a possibility to migrate away/cancel the registration - Adds a possibility to update contact details for the participant - UX improvements like autosave - Set 0208 as the default EAS code for Belgium - Add a check for certain peppol endpoint numbers (based on Peppol schematron rules) task-3060814 closes odoo/odoo#127092 Signed-off-by: Florian Gilbert (flg) <flg@odoo.com>
This commit is contained in:
@@ -46,7 +46,7 @@ EAS_MAPPING = {
|
||||
'AT': {'9915': 'vat'},
|
||||
'AU': {'0151': 'vat'},
|
||||
'BA': {'9924': 'vat'},
|
||||
'BE': {'9925': 'vat'},
|
||||
'BE': {'9925': 'vat', '0208': 'company_registry'},
|
||||
'BG': {'9926': 'vat'},
|
||||
'CH': {'9927': 'vat'},
|
||||
'CY': {'9928': 'vat'},
|
||||
|
||||
@@ -24,4 +24,9 @@
|
||||
'wizard/account_move_send_views.xml',
|
||||
],
|
||||
'license': 'LGPL-3',
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'account_peppol/static/src/components/**/*',
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import fields, models, tools, _
|
||||
from odoo.addons.account_edi_proxy_client.models.account_edi_proxy_user import AccountEdiProxyError
|
||||
from odoo import _, fields, models, tools
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.addons.account_edi_proxy_client.models.account_edi_proxy_user import AccountEdiProxyError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -13,10 +13,16 @@ _logger = logging.getLogger(__name__)
|
||||
class AccountEdiProxyClientUser(models.Model):
|
||||
_inherit = 'account_edi_proxy_client.user'
|
||||
|
||||
peppol_migration_key = fields.Char(string="Migration Key")
|
||||
peppol_verification_code = fields.Char(string='SMS verification code')
|
||||
proxy_type = fields.Selection(
|
||||
selection_add=[('peppol', 'PEPPOL')],
|
||||
)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# HELPER METHODS
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def _get_proxy_urls(self):
|
||||
urls = super()._get_proxy_urls()
|
||||
urls['peppol'] = {
|
||||
@@ -38,6 +44,15 @@ class AccountEdiProxyClientUser(models.Model):
|
||||
|
||||
return super()._get_server_url(proxy_type, edi_mode)
|
||||
|
||||
def _get_proxy_identification(self, company):
|
||||
if not company.peppol_eas or not company.peppol_endpoint:
|
||||
raise UserError(_("Please fill in the EAS code and the Participant ID code."))
|
||||
return f'{company.peppol_eas}:{company.peppol_endpoint}'
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# COMPUTE METHODS
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def _compute_proxy_type(self):
|
||||
# Extends account_edi_proxy_client
|
||||
super()._compute_proxy_type()
|
||||
@@ -45,11 +60,9 @@ class AccountEdiProxyClientUser(models.Model):
|
||||
if user.company_id.is_account_peppol_participant:
|
||||
user.proxy_type = 'peppol'
|
||||
|
||||
def _get_proxy_identification(self, company):
|
||||
if not company.peppol_eas or not company.peppol_endpoint:
|
||||
raise UserError(
|
||||
_("Please fill in the EAS code and the Participant ID code in the company's partner form."))
|
||||
return f'{company.peppol_eas}:{company.peppol_endpoint}'
|
||||
# -------------------------------------------------------------------------
|
||||
# BUSINESS ACTIONS
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def _cron_peppol_get_new_documents(self):
|
||||
# Retrieve all new Peppol documents for every edi user in the database
|
||||
@@ -136,6 +149,7 @@ class AccountEdiProxyClientUser(models.Model):
|
||||
else:
|
||||
move._message_log(
|
||||
body=_('Peppol document has been received successfully'))
|
||||
# pylint: disable=broad-except
|
||||
except Exception:
|
||||
# if the invoice creation fails for any reason,
|
||||
# we want to create an empty invoice with the attachment
|
||||
@@ -205,7 +219,7 @@ class AccountEdiProxyClientUser(models.Model):
|
||||
|
||||
def _cron_peppol_get_participant_status(self):
|
||||
edi_users = self.env['account_edi_proxy_client.user'].search(
|
||||
[('company_id.account_peppol_proxy_state', 'in', ('pending', 'manually_approved'))])
|
||||
[('company_id.account_peppol_proxy_state', '=', 'pending')])
|
||||
|
||||
for edi_user in edi_users:
|
||||
try:
|
||||
@@ -215,5 +229,5 @@ class AccountEdiProxyClientUser(models.Model):
|
||||
_logger.error('Error while updating Peppol participant status: %s', e)
|
||||
continue
|
||||
|
||||
if proxy_user['peppol_state'] != 'draft':
|
||||
if proxy_user['peppol_state'] in {'active', 'rejected', 'canceled'}:
|
||||
edi_user.company_id.account_peppol_proxy_state = proxy_user['peppol_state']
|
||||
|
||||
@@ -1,23 +1,66 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
import re
|
||||
from stdnum import get_cc_module, ean
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.addons.account_edi_ubl_cii.models.account_edi_common import EAS_MAPPING
|
||||
|
||||
try:
|
||||
import phonenumbers
|
||||
except ImportError:
|
||||
phonenumbers = None
|
||||
|
||||
# at the moment, only phone numbers in European countries are accepted
|
||||
PHONE_ALLOWED_COUNTRIES = set(EAS_MAPPING.keys()) - {'AU', 'SG', 'NZ'}
|
||||
|
||||
PEPPOL_ENDPOINT_RULES = {
|
||||
'0007': ['se', 'orgnr'],
|
||||
'0088': ['ean'],
|
||||
'0184': ['dk', 'cvr'],
|
||||
'0192': ['no', 'orgnr'],
|
||||
'0208': ['be', 'vat'],
|
||||
}
|
||||
|
||||
PEPPOL_ENDPOINT_WARNING = {
|
||||
'0201': ['regex', '[0-9a-zA-Z]{6}$'],
|
||||
'0210': ['it', 'codicefiscale'],
|
||||
'0211': ['it', 'iva'],
|
||||
'9906': ['it', 'iva'],
|
||||
'9907': ['it', 'codicefiscale'],
|
||||
'0151': ['au', 'abn'],
|
||||
}
|
||||
|
||||
|
||||
class ResCompany(models.Model):
|
||||
_inherit = 'res.company'
|
||||
|
||||
# to be removed once the module is available
|
||||
account_peppol_attachment_ids = fields.Many2many(
|
||||
comodel_name='ir.attachment',
|
||||
string='Peppol Identification Documents',
|
||||
)
|
||||
account_peppol_contact_email = fields.Char(
|
||||
string='Primary contact email',
|
||||
compute='_compute_account_peppol_contact_email', store=True, readonly=False,
|
||||
help='Primary contact email for Peppol-related communication',
|
||||
)
|
||||
account_peppol_phone_number = fields.Char(
|
||||
string='Phone number (for validation)',
|
||||
help='You will receive a verification code to this phone number',
|
||||
)
|
||||
account_peppol_proxy_state = fields.Selection(
|
||||
selection=[
|
||||
('not_registered', 'Not registered'),
|
||||
('not_verified', 'Not verified'),
|
||||
('sent_verification', 'Verification code sent'),
|
||||
('pending', 'Pending'),
|
||||
('manually_approved', 'Approved'),
|
||||
('active', 'Active'),
|
||||
('rejected', 'Rejected'),
|
||||
('canceled', 'Canceled'),
|
||||
],
|
||||
string='PEPPOL status',
|
||||
compute='_compute_account_peppol_proxy_state', required=True, readonly=False, store=True, precompute=True,
|
||||
@@ -32,6 +75,73 @@ class ResCompany(models.Model):
|
||||
compute='_compute_peppol_purchase_journal_id', store=True, readonly=False,
|
||||
)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# HELPER METHODS
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def _sanitize_peppol_phone_number(self):
|
||||
self.ensure_one()
|
||||
|
||||
error_message = _(
|
||||
"Please enter the phone number in the correct international format.\n"
|
||||
"For example: +32123456789, where +32 is the country code.\n"
|
||||
"Currently, only European countries are supported.")
|
||||
|
||||
if not phonenumbers:
|
||||
raise ValidationError(error_message)
|
||||
|
||||
phone_number = self.account_peppol_phone_number
|
||||
if not phone_number.startswith('+'):
|
||||
phone_number = f'+{phone_number}'
|
||||
|
||||
try:
|
||||
phone_nbr = phonenumbers.parse(phone_number)
|
||||
except phonenumbers.phonenumberutil.NumberParseException:
|
||||
raise ValidationError(error_message)
|
||||
|
||||
country_code = phonenumbers.phonenumberutil.region_code_for_number(phone_nbr)
|
||||
if country_code not in PHONE_ALLOWED_COUNTRIES or not phonenumbers.is_valid_number(phone_nbr):
|
||||
raise ValidationError(error_message)
|
||||
|
||||
def _check_peppol_endpoint_number(self, warning=False):
|
||||
self.ensure_one()
|
||||
|
||||
peppol_dict = PEPPOL_ENDPOINT_WARNING if warning else PEPPOL_ENDPOINT_RULES
|
||||
endpoint_rule = peppol_dict.get(self.peppol_eas)
|
||||
if not endpoint_rule:
|
||||
return True
|
||||
|
||||
if endpoint_rule[0] == 'regex':
|
||||
return bool(re.match(endpoint_rule[1], self.peppol_endpoint))
|
||||
|
||||
if endpoint_rule[0] == 'ean':
|
||||
check_module = ean
|
||||
else:
|
||||
check_module = get_cc_module(endpoint_rule[0], endpoint_rule[1])
|
||||
return check_module.is_valid(self.peppol_endpoint)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# CONSTRAINTS
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@api.constrains('account_peppol_phone_number')
|
||||
def _check_account_peppol_phone_number(self):
|
||||
for company in self:
|
||||
if company.account_peppol_phone_number:
|
||||
company._sanitize_peppol_phone_number()
|
||||
|
||||
@api.constrains('peppol_endpoint')
|
||||
def _check_peppol_endpoint(self):
|
||||
for company in self:
|
||||
if not company.peppol_endpoint:
|
||||
continue
|
||||
if not company._check_peppol_endpoint_number(PEPPOL_ENDPOINT_RULES):
|
||||
raise ValidationError(_("The Peppol endpoint identification number is not correct."))
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# COMPUTE METHODS
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@api.depends('is_account_peppol_participant')
|
||||
def _compute_account_peppol_proxy_state(self):
|
||||
for company in self:
|
||||
@@ -48,3 +158,40 @@ class ResCompany(models.Model):
|
||||
)
|
||||
else:
|
||||
company.peppol_purchase_journal_id = False
|
||||
|
||||
@api.depends('email')
|
||||
def _compute_account_peppol_contact_email(self):
|
||||
for company in self:
|
||||
if not company.account_peppol_contact_email:
|
||||
company.account_peppol_contact_email = company.email
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# LOW-LEVEL METHODS
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@api.model
|
||||
def _sanitize_peppol_endpoint(self, vals, eas=False, endpoint=False):
|
||||
if 'peppol_eas' not in vals and 'peppol_endpoint' not in vals:
|
||||
return vals
|
||||
|
||||
peppol_eas = vals['peppol_eas'] if 'peppol_eas' in vals else eas # let users remove the value
|
||||
peppol_endpoint = vals['peppol_endpoint'] if 'peppol_endpoint' in vals else endpoint
|
||||
if not peppol_eas or not peppol_endpoint:
|
||||
return vals
|
||||
|
||||
if peppol_eas == '0208':
|
||||
cbe_match = re.search('[0-9]{10}', peppol_endpoint)
|
||||
if bool(cbe_match):
|
||||
vals['peppol_endpoint'] = cbe_match.group(0)
|
||||
return vals
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
vals = self._sanitize_peppol_endpoint(vals)
|
||||
return super().create(vals_list)
|
||||
|
||||
def write(self, vals):
|
||||
for company in self:
|
||||
vals = self._sanitize_peppol_endpoint(vals, company.peppol_eas, company.peppol_endpoint)
|
||||
return super().write(vals)
|
||||
|
||||
@@ -1,25 +1,43 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from base64 import b64encode
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.addons.account_edi_proxy_client.models.account_edi_proxy_user import AccountEdiProxyError
|
||||
from odoo import _, api, fields, models, modules, tools
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
|
||||
from odoo.addons.account_edi_proxy_client.models.account_edi_proxy_user import AccountEdiProxyError
|
||||
from odoo.addons.account_edi_ubl_cii.models.account_edi_common import EAS_MAPPING
|
||||
|
||||
# at the moment, only European countries are accepted
|
||||
ALLOWED_COUNTRIES = set(EAS_MAPPING.keys()) - {'AU', 'SG', 'NZ'}
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
account_peppol_edi_user = fields.Many2one(
|
||||
comodel_name='account_edi_proxy_client.user',
|
||||
string='EDI user',
|
||||
compute='_compute_account_peppol_edi_user',
|
||||
)
|
||||
account_peppol_contact_email = fields.Char(related='company_id.account_peppol_contact_email', readonly=False)
|
||||
account_peppol_eas = fields.Selection(related='company_id.peppol_eas', readonly=False)
|
||||
account_peppol_edi_identification = fields.Char(related='account_peppol_edi_user.edi_identification')
|
||||
account_peppol_endpoint = fields.Char(related='company_id.peppol_endpoint', readonly=False)
|
||||
account_peppol_proxy_state = fields.Selection(related='company_id.account_peppol_proxy_state')
|
||||
account_peppol_endpoint_warning = fields.Char(
|
||||
string="Warning",
|
||||
compute="_compute_account_peppol_endpoint_warning",
|
||||
)
|
||||
account_peppol_migration_key = fields.Char(related='account_peppol_edi_user.peppol_migration_key', readonly=False)
|
||||
account_peppol_phone_number = fields.Char(related='company_id.account_peppol_phone_number', readonly=False)
|
||||
account_peppol_proxy_state = fields.Selection(related='company_id.account_peppol_proxy_state', readonly=False)
|
||||
account_peppol_purchase_journal_id = fields.Many2one(related='company_id.peppol_purchase_journal_id', readonly=False)
|
||||
# to be removed once the module is available
|
||||
account_peppol_attachment_ids = fields.Many2many(
|
||||
comodel_name='ir.attachment',
|
||||
string='Peppol Identification Documents',
|
||||
related='company_id.account_peppol_attachment_ids', readonly=False,
|
||||
)
|
||||
account_peppol_verification_code = fields.Char(related='account_peppol_edi_user.peppol_verification_code', readonly=False)
|
||||
is_account_peppol_eligible = fields.Boolean(
|
||||
string='PEPPOL eligible',
|
||||
compute='_compute_is_account_peppol_eligible',
|
||||
@@ -30,46 +48,238 @@ class ResConfigSettings(models.TransientModel):
|
||||
help='Register as a PEPPOL user',
|
||||
)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# HELPER METHODS
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def _call_peppol_proxy(self, endpoint, params, edi_user=None):
|
||||
if not edi_user:
|
||||
edi_user = self.company_id.account_edi_proxy_client_ids[0]
|
||||
|
||||
try:
|
||||
response = edi_user._make_request(
|
||||
f"{edi_user._get_server_url()}{endpoint}",
|
||||
params=params,
|
||||
)
|
||||
except AccountEdiProxyError as e:
|
||||
raise UserError(e.message)
|
||||
|
||||
if 'error' in response:
|
||||
raise UserError(response['error'].get('message') or response['error']['data']['message'])
|
||||
return response
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# COMPUTE METHODS
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@api.depends("company_id.country_id")
|
||||
def _compute_is_account_peppol_eligible(self):
|
||||
# we want to show Peppol settings only to BE and LU customers at first
|
||||
# we want to show Peppol settings only to customers that are eligible for Peppol,
|
||||
# except countries that are not in Europe
|
||||
# but keeping an option to see them for testing purposes using a config param
|
||||
for config in self:
|
||||
peppol_param = config.env['ir.config_parameter'].sudo().get_param(
|
||||
'account_peppol.edi.mode', False
|
||||
)
|
||||
config.is_account_peppol_eligible = config.company_id.country_id.code in {'BE', 'LU'} \
|
||||
config.is_account_peppol_eligible = config.company_id.country_id.code in ALLOWED_COUNTRIES \
|
||||
or peppol_param == 'test'
|
||||
|
||||
@api.depends("company_id.account_edi_proxy_client_ids")
|
||||
def _compute_account_peppol_edi_user(self):
|
||||
for config in self:
|
||||
config.account_peppol_edi_user = config.company_id.account_edi_proxy_client_ids.filtered(
|
||||
lambda u: u.proxy_type == 'peppol')
|
||||
|
||||
@api.depends('account_peppol_eas', 'account_peppol_endpoint')
|
||||
def _compute_account_peppol_endpoint_warning(self):
|
||||
for config in self:
|
||||
if (
|
||||
not config.account_peppol_eas
|
||||
or config.company_id._check_peppol_endpoint_number(warning=True)
|
||||
):
|
||||
config.account_peppol_endpoint_warning = False
|
||||
else:
|
||||
config.account_peppol_endpoint_warning = _("The endpoint number might not be correct. "
|
||||
"Please check if you entered the right identification number.")
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# BUSINESS ACTIONS
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def button_create_peppol_proxy_user(self):
|
||||
"""
|
||||
The first step of the Peppol onboarding.
|
||||
- Creates an EDI proxy user on the iap side, then the client side
|
||||
- Calls /activate_participant to mark the EDI user as peppol user
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
if self.account_peppol_proxy_state != 'not_registered':
|
||||
raise UserError(
|
||||
_('Cannot register a user with a %s application', self.account_peppol_proxy_state))
|
||||
|
||||
if not self.company_id.account_peppol_attachment_ids:
|
||||
raise ValidationError(
|
||||
_('Please upload a document that would help verifying your application'))
|
||||
if not self.account_peppol_phone_number:
|
||||
raise ValidationError(_("Please enter a phone number to verify your application."))
|
||||
|
||||
company = self.company_id
|
||||
edi_proxy_client = self.env['account_edi_proxy_client.user']
|
||||
edi_identification = edi_proxy_client._get_proxy_identification(self.company_id)
|
||||
edi_identification = edi_proxy_client._get_proxy_identification(company)
|
||||
edi_user = edi_proxy_client.sudo()._register_proxy_user(
|
||||
self.company_id, 'peppol', 'prod', edi_identification)
|
||||
company, 'peppol', 'prod', edi_identification)
|
||||
self.account_peppol_proxy_state = 'not_verified'
|
||||
|
||||
params = {'documents': []}
|
||||
for attachment in self.company_id.account_peppol_attachment_ids:
|
||||
params['documents'].append((attachment.name, b64encode(attachment.raw).decode()))
|
||||
# if there is an error when activating the participant below,
|
||||
# the client side is rolled back and the edi user is deleted on the client side
|
||||
# but remains on the proxy side.
|
||||
# it is important to keep these two in sync, so commit before activating.
|
||||
if not tools.config['test_enable'] and not modules.module.current_test:
|
||||
self.env.cr.commit()
|
||||
|
||||
try:
|
||||
response = edi_user._make_request(
|
||||
f"{edi_user._get_server_url()}/api/peppol/1/activate_participant",
|
||||
company_details = {
|
||||
'peppol_company_name': company.display_name,
|
||||
'peppol_company_vat': company.vat,
|
||||
'peppol_company_street': company.street,
|
||||
'peppol_company_city': company.city,
|
||||
'peppol_company_zip': company.zip,
|
||||
'peppol_country_code': company.country_id.code,
|
||||
'peppol_phone_number': self.account_peppol_phone_number,
|
||||
'peppol_contact_email': self.account_peppol_contact_email,
|
||||
}
|
||||
|
||||
params = {
|
||||
'migration_key': self.account_peppol_migration_key,
|
||||
'company_details': company_details,
|
||||
}
|
||||
|
||||
self._call_peppol_proxy(
|
||||
endpoint='/api/peppol/1/activate_participant',
|
||||
params=params,
|
||||
edi_user=edi_user,
|
||||
)
|
||||
# once we sent the migration key over, we don't need it
|
||||
# but we need the field for future in case the user decided to migrate away from Odoo
|
||||
self.account_peppol_migration_key = False
|
||||
|
||||
def button_update_peppol_user_data(self):
|
||||
"""
|
||||
Action for the user to be able to update their contact details any time
|
||||
Calls /update_user on the iap server
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
if not self.account_peppol_contact_email or not self.account_peppol_phone_number:
|
||||
raise ValidationError(_("Contact email and phone number are required."))
|
||||
|
||||
params = {
|
||||
'update_data': {
|
||||
'peppol_phone_number': self.account_peppol_phone_number,
|
||||
'peppol_contact_email': self.account_peppol_contact_email,
|
||||
}
|
||||
}
|
||||
|
||||
self._call_peppol_proxy(
|
||||
endpoint='/api/peppol/1/update_user',
|
||||
params=params,
|
||||
)
|
||||
|
||||
def button_send_peppol_verification_code(self):
|
||||
"""
|
||||
Request user verification via SMS
|
||||
Calls the /send_verification_code to send the 6-digit verification code
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
# update contact details in case the user made changes
|
||||
self.button_update_peppol_user_data()
|
||||
|
||||
self._call_peppol_proxy(
|
||||
endpoint='/api/peppol/1/send_verification_code',
|
||||
params={'message': _("Your confirmation code is")},
|
||||
)
|
||||
self.account_peppol_proxy_state = 'sent_verification'
|
||||
|
||||
def button_check_peppol_verification_code(self):
|
||||
"""
|
||||
Calls /verify_phone_number to compare user's input and the
|
||||
code generated on the IAP server
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
if len(self.account_peppol_verification_code) != 6:
|
||||
raise ValidationError(_("The verification code should contain six digits."))
|
||||
|
||||
self._call_peppol_proxy(
|
||||
endpoint='/api/peppol/1/verify_phone_number',
|
||||
params={'verification_code': self.account_peppol_verification_code},
|
||||
)
|
||||
self.account_peppol_proxy_state = 'pending'
|
||||
self.account_peppol_verification_code = False
|
||||
|
||||
def button_cancel_peppol_registration(self):
|
||||
"""
|
||||
Sets the peppol registration to canceled
|
||||
- If the user is active on the SMP, we can't just cancel it.
|
||||
They have to request a migration key using the `button_migrate_peppol_registration` action
|
||||
- 'not_registered', 'rejected', 'canceled' proxy states mean that canceling the registration
|
||||
makes no sense, so we don't do it
|
||||
- Calls the IAP server first before setting the state as canceled on the client side,
|
||||
in case they've been activated on the IAP side in the meantime
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
if self.account_peppol_proxy_state == 'active':
|
||||
raise UserError(_("Can't cancel an active registration. Please request a migration instead."))
|
||||
|
||||
if self.account_peppol_proxy_state in {'not_registered', 'rejected', 'canceled'}:
|
||||
raise UserError(_(
|
||||
"Can't cancel registration with this status: %s", self.account_peppol_proxy_state
|
||||
))
|
||||
|
||||
self._call_peppol_proxy(
|
||||
endpoint='/api/peppol/1/cancel_peppol_registration',
|
||||
params={},
|
||||
)
|
||||
self.account_peppol_proxy_state = 'canceled'
|
||||
|
||||
def button_reopen_peppol_registration(self):
|
||||
"""
|
||||
Reopen a canceled or rejected peppol application.
|
||||
This means that the EDI user already exists in the DB and on the IAP proxy.
|
||||
So, reopening resets the user to the stage after validating (not_verified).
|
||||
The user then needs to verify their phone number, even if they verified before canceling.
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
if self.account_peppol_proxy_state in {'rejected', 'canceled'}:
|
||||
params = {
|
||||
'update_data': {
|
||||
'peppol_state': 'draft',
|
||||
}
|
||||
}
|
||||
|
||||
self._call_peppol_proxy(
|
||||
endpoint='/api/peppol/1/update_user',
|
||||
params=params,
|
||||
)
|
||||
except AccountEdiProxyError as e:
|
||||
raise UserError(e.message)
|
||||
if 'error' in response:
|
||||
raise UserError(response['error'])
|
||||
|
||||
self.company_id.account_peppol_proxy_state = 'pending'
|
||||
# we don't need to store the attachments once they've been sent to the proxy
|
||||
self.company_id.account_peppol_attachment_ids.unlink()
|
||||
self.account_peppol_proxy_state = 'not_verified'
|
||||
|
||||
def button_migrate_peppol_registration(self):
|
||||
"""
|
||||
If the user is active, they need to request a migration key, generated on the IAP server.
|
||||
The migration key is then displayed in Peppol settings.
|
||||
Currently, reopening after migrating away is not supported.
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
if self.account_peppol_proxy_state != 'active':
|
||||
raise UserError(_(
|
||||
"Can't migrate registration with this status: %s", self.account_peppol_proxy_state
|
||||
))
|
||||
|
||||
response = self._call_peppol_proxy(
|
||||
endpoint='/api/peppol/1/migrate_peppol_registration',
|
||||
params={},
|
||||
)
|
||||
self.account_peppol_proxy_state = 'canceled'
|
||||
self.account_peppol_migration_key = response['migration_key']
|
||||
|
||||
@@ -65,7 +65,9 @@ class ResPartner(models.Model):
|
||||
participant = f'{self.peppol_eas}:{self.peppol_endpoint}'.lower()
|
||||
hash_participant = md5(participant.lower().encode()).hexdigest()
|
||||
endpoint_participant = parse.quote_plus(f"iso6523-actorid-upis::{participant}")
|
||||
smp_url = f"http://B-{hash_participant}.iso6523-actorid-upis.edelivery.tech.ec.europa.eu/{endpoint_participant}"
|
||||
peppol_param = self.env['ir.config_parameter'].sudo().get_param('account_peppol.edi.mode', False)
|
||||
sml_zone = 'acc.edelivery' if peppol_param == 'test' else 'edelivery'
|
||||
smp_url = f"http://B-{hash_participant}.iso6523-actorid-upis.{sml_zone}.tech.ec.europa.eu/{endpoint_participant}"
|
||||
self.account_peppol_validity_last_check = fields.Date.context_today(self)
|
||||
try:
|
||||
response = requests.get(smp_url, timeout=TIMEOUT)
|
||||
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { escape } from "@web/core/utils/strings";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { pick } from "@web/core/utils/objects";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { standardWidgetProps } from "@web/views/widgets/standard_widget_props";
|
||||
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
|
||||
import { Component, markup } from "@odoo/owl";
|
||||
|
||||
|
||||
class PeppolSettingsButtons extends Component {
|
||||
static props = {
|
||||
...standardWidgetProps,
|
||||
};
|
||||
static template = "account_peppol.ActionButtons";
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
this.dialogService = useService("dialog");
|
||||
this.notification = useService("notification");
|
||||
}
|
||||
|
||||
get proxyState() {
|
||||
return this.props.record.data.account_peppol_proxy_state;
|
||||
}
|
||||
|
||||
async _callConfigMethod(methodName, save = false) {
|
||||
if (save) {
|
||||
await this._save();
|
||||
}
|
||||
this.env.onClickViewButton({
|
||||
clickParams: {
|
||||
name: methodName,
|
||||
type: "object",
|
||||
noSaveDialog: true,
|
||||
},
|
||||
getResParams: () =>
|
||||
pick(this.env.model.root, "context", "evalContext", "resModel", "resId", "resIds"),
|
||||
});
|
||||
}
|
||||
|
||||
async _save () {
|
||||
this.env.model.root.save({
|
||||
noReload: true,
|
||||
stayInEdition: true,
|
||||
useSaveErrorDialog: false,
|
||||
savePoint: true,
|
||||
});
|
||||
}
|
||||
|
||||
showConfirmation() {
|
||||
const message = this.env._t("This will migrate your Peppol registration away from Odoo. You will not be able to send or receive Peppol documents in Odoo anymore.");
|
||||
const confirmMessage = this.env._t("Are you sure you want to proceed?");
|
||||
this.dialogService.add(ConfirmationDialog, {
|
||||
body: markup(
|
||||
`<div class="text-danger">${escape(message)}</div>
|
||||
<div class="text-danger">${escape(confirmMessage)}</div>`
|
||||
),
|
||||
confirm: async () => {
|
||||
await this._callConfigMethod("button_migrate_peppol_registration");
|
||||
},
|
||||
cancel: () => { },
|
||||
});
|
||||
}
|
||||
|
||||
async updateDetails() {
|
||||
// avoid making users click save on the settings
|
||||
// and then clicking the update button
|
||||
// changes on both the client side and the iap side need to be saved within one method
|
||||
await this._callConfigMethod("button_update_peppol_user_data", true);
|
||||
this.notification.add(
|
||||
this.env._t("Contact details were updated."),
|
||||
{ type: "success" }
|
||||
);
|
||||
}
|
||||
|
||||
async checkCode() {
|
||||
// avoid making users click save on the settings
|
||||
// and then clicking the confirm button to check the code
|
||||
await this._callConfigMethod("button_check_peppol_verification_code", true);
|
||||
}
|
||||
|
||||
async createUser() {
|
||||
await this._callConfigMethod("button_create_peppol_proxy_user", true);
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("view_widgets").add("peppol_settings_buttons", {
|
||||
component: PeppolSettingsButtons,
|
||||
});
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<templates>
|
||||
<t t-name="account_peppol.ActionButtons" owl="1">
|
||||
<xpath expr="//div[hasclass('action_buttons')]" position="inside">
|
||||
<div class="d-flex" colspan="2">
|
||||
<div class="mt-3">
|
||||
<button type="button"
|
||||
class="btn btn-primary"
|
||||
t-on-click="createUser"
|
||||
t-if="proxyState === 'not_registered'">
|
||||
Validate registration
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<button type="button"
|
||||
class="btn btn-secondary me-1"
|
||||
t-on-click="showConfirmation"
|
||||
t-if="proxyState === 'active'">
|
||||
Migrate registration to another service
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<button type="button"
|
||||
class="btn btn-primary"
|
||||
t-on-click="updateDetails"
|
||||
t-if="['pending', 'manually_approved', 'active'].includes(proxyState)">
|
||||
Update contact details
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<button type="button"
|
||||
class="btn btn-primary"
|
||||
t-on-click="checkCode"
|
||||
t-if="proxyState === 'sent_verification'">
|
||||
Confirm
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { standardFieldProps } from "@web/views/fields/standard_field_props";
|
||||
|
||||
import { Component, onMounted, useRef } from "@odoo/owl";
|
||||
|
||||
class VerificationCodeWidget extends Component {
|
||||
static props = {
|
||||
...standardFieldProps,
|
||||
};
|
||||
static template = "account_peppol.VerificationCodeWidget";
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
this.inputs = [];
|
||||
for (let i = 0; i < 6; i++) {
|
||||
this.inputs.push(useRef(`input_${i}`));
|
||||
}
|
||||
|
||||
/*
|
||||
if the verification code was previously filled in and the user saved the page,
|
||||
pre-fill the input fields with the stored value.
|
||||
*/
|
||||
onMounted(async () => {
|
||||
const verificationCode = this.props.record.data.account_peppol_verification_code;
|
||||
for (let i = 0; i < this.inputs.length; i++) {
|
||||
this.inputs[i].el.value = verificationCode[i] || null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
overrides the default paste behaviour, so that if a value is pasted
|
||||
into one of the verification code fields, it's split between the input fields,
|
||||
so they can easily copy and paste the code they received via SMS.
|
||||
*/
|
||||
onPaste(ev) {
|
||||
if (!ev.clipboardData?.items) {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
|
||||
let pastedData = ev.clipboardData.getData('text').split('');
|
||||
let target = ev.target;
|
||||
for (let i = target.id; i < this.inputs.length; i++) {
|
||||
this.inputs[i].el.value = pastedData.shift() || null;
|
||||
}
|
||||
}
|
||||
|
||||
// switch focus to the next input box once they enter
|
||||
// one digit and switch focus to the previous input box
|
||||
// if they press backspace
|
||||
onKeyUp(ev) {
|
||||
if (ev.target.value.length === 1 && ev.target.id < 5) {
|
||||
ev.target.nextElementSibling.focus();
|
||||
} else if (ev.key == 'Backspace' && ev.target.value === "" && ev.target.id > 0) {
|
||||
ev.target.previousElementSibling.focus();
|
||||
}
|
||||
}
|
||||
|
||||
_save() {
|
||||
let verificationCode = [...this.inputs.map((i) => i.el.value)].join('');
|
||||
this.props.record.update({ account_peppol_verification_code: verificationCode });
|
||||
this.env.model.root.save({
|
||||
noReload: true,
|
||||
stayInEdition: true,
|
||||
useSaveErrorDialog: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("fields").add("verification_code", {
|
||||
component: VerificationCodeWidget,
|
||||
supportedTypes: ["char"],
|
||||
});
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates>
|
||||
|
||||
<t t-name="account_peppol.VerificationCodeWidget" owl="1">
|
||||
<div class="row w-50 mt-0 pt-0" t-on-focusout="_save">
|
||||
<t t-foreach="[...Array(6).keys()]" t-as="i" t-key="i">
|
||||
<input class="verification_code col border border-2 rounded m-1 p-0 text-center"
|
||||
type="text"
|
||||
maxlength="1"
|
||||
t-att-id="i"
|
||||
t-ref="input_{{i}}"
|
||||
t-on-keyup="onKeyUp"
|
||||
t-on-paste="onPaste"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -65,7 +65,7 @@ class TestPeppolMessage(TestAccountMoveSendCommon):
|
||||
'partner_id': cls.env.company.partner_id.id,
|
||||
})
|
||||
|
||||
def create_move(self, partner, account_revenue=False, account_expense=False):
|
||||
def create_move(self, partner):
|
||||
return self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'partner_id': partner.id,
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
from contextlib import contextmanager
|
||||
from freezegun import freeze_time
|
||||
from unittest.mock import Mock, patch
|
||||
from psycopg2 import IntegrityError
|
||||
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tests.common import tagged, TransactionCase
|
||||
from odoo.tools import file_open
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
ID_CLIENT = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
|
||||
FAKE_UUID = 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy'
|
||||
@@ -31,6 +32,10 @@ class TestPeppolParticipant(TransactionCase):
|
||||
elif url.endswith('/iap/account_edi/2/create_user'):
|
||||
response.json = lambda: {'result': {
|
||||
'id_client': ID_CLIENT, 'refresh_token': FAKE_UUID}}
|
||||
elif url.endswith('/api/peppol/1/send_verification_code')\
|
||||
or url.endswith('/api/peppol/1/update_user')\
|
||||
or url.endswith('/api/peppol/1/verify_phone_number'):
|
||||
response.json = lambda: {'result': {}}
|
||||
else:
|
||||
raise Exception(f'Unexpected request: {url}')
|
||||
|
||||
@@ -60,20 +65,19 @@ class TestPeppolParticipant(TransactionCase):
|
||||
'is_account_peppol_participant': True,
|
||||
'account_peppol_eas': '9925',
|
||||
'account_peppol_endpoint': '0000000000',
|
||||
'account_peppol_phone_number': '+32483123456',
|
||||
})
|
||||
company = self.env.company
|
||||
|
||||
with file_open(PDF_FILE_PATH, 'rb') as pdf_file:
|
||||
settings.account_peppol_attachment_ids = self.env['ir.attachment'].create({
|
||||
'mimetype': 'application/pdf',
|
||||
'name': 'peppol_id_test.pdf',
|
||||
'raw': pdf_file.read(),
|
||||
}).ids
|
||||
|
||||
settings.execute()
|
||||
|
||||
with self._patch_peppol_requests():
|
||||
settings.button_create_peppol_proxy_user()
|
||||
self.assertEqual(company.account_peppol_proxy_state, 'not_verified')
|
||||
settings.button_send_peppol_verification_code()
|
||||
self.assertEqual(company.account_peppol_proxy_state, 'sent_verification')
|
||||
settings.account_peppol_verification_code = '123456'
|
||||
settings.button_check_peppol_verification_code()
|
||||
self.assertEqual(company.account_peppol_proxy_state, 'pending')
|
||||
self.env['account_edi_proxy_client.user']._cron_peppol_get_participant_status()
|
||||
self.assertEqual(company.account_peppol_proxy_state, 'active')
|
||||
@@ -86,24 +90,19 @@ class TestPeppolParticipant(TransactionCase):
|
||||
'is_account_peppol_participant': True,
|
||||
'account_peppol_eas': '9925',
|
||||
'account_peppol_endpoint': '0000000000',
|
||||
'account_peppol_phone_number': '+32483123456',
|
||||
})
|
||||
company = self.env.company
|
||||
|
||||
with file_open(PDF_FILE_PATH, 'rb') as pdf_file:
|
||||
settings.account_peppol_attachment_ids = self.env['ir.attachment'].create({
|
||||
'mimetype': 'application/pdf',
|
||||
'name': 'peppol_id_test.pdf',
|
||||
'raw': pdf_file.read(),
|
||||
}).ids
|
||||
|
||||
settings.execute()
|
||||
|
||||
with self._patch_peppol_requests(reject=True):
|
||||
settings.button_create_peppol_proxy_user()
|
||||
self.assertEqual(company.account_peppol_proxy_state, 'pending')
|
||||
company.account_peppol_proxy_state = 'pending'
|
||||
self.env['account_edi_proxy_client.user']._cron_peppol_get_participant_status()
|
||||
self.assertEqual(company.account_peppol_proxy_state, 'rejected')
|
||||
|
||||
@mute_logger('odoo.sql_db')
|
||||
def test_create_duplicate_participant(self):
|
||||
# should not be possible to create a duplicate participant
|
||||
settings = self.env['res.config.settings'].create({})
|
||||
@@ -111,19 +110,13 @@ class TestPeppolParticipant(TransactionCase):
|
||||
'is_account_peppol_participant': True,
|
||||
'account_peppol_eas': '9925',
|
||||
'account_peppol_endpoint': '0000000000',
|
||||
'account_peppol_phone_number': '+32483123456',
|
||||
})
|
||||
|
||||
with file_open(PDF_FILE_PATH, 'rb') as pdf_file:
|
||||
settings.account_peppol_attachment_ids = self.env['ir.attachment'].create({
|
||||
'mimetype': 'application/pdf',
|
||||
'name': 'peppol_id_test.pdf',
|
||||
'raw': pdf_file.read(),
|
||||
}).ids
|
||||
|
||||
settings.execute()
|
||||
|
||||
with self._patch_peppol_requests():
|
||||
settings.button_create_peppol_proxy_user()
|
||||
self.assertEqual(settings.account_peppol_proxy_state, 'pending')
|
||||
with self.assertRaises(UserError), self.cr.savepoint():
|
||||
with self.assertRaises(IntegrityError), self.cr.savepoint():
|
||||
settings.account_peppol_proxy_state = 'not_registered'
|
||||
settings.button_create_peppol_proxy_user()
|
||||
|
||||
@@ -14,86 +14,185 @@
|
||||
<field name="account_peppol_proxy_state" invisible="1"/>
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="is_account_peppol_participant"
|
||||
attrs="{'invisible': [('account_peppol_proxy_state', 'not in', (False, 'not_registered'))]}"/>
|
||||
attrs="{'invisible': [('account_peppol_proxy_state', '!=', 'not_registered')]}"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<div attrs="{'invisible': [('account_peppol_proxy_state', '!=', 'not_registered')]}">
|
||||
<label for="is_account_peppol_participant" string='Use PEPPOL Invoicing'/>
|
||||
<span class="fa fa-lg fa-building-o" title="Values set here are company-specific."/>
|
||||
<div class="text-muted">
|
||||
Once you select this option and confirm, we will review your application.
|
||||
You will be able to start sending and receiving documents via PEPPOL once your participation is approved.
|
||||
We will notify you once we have reviewed your application.
|
||||
Our staff members will first review your request within two working days.
|
||||
</div>
|
||||
<div class="pt-3 pb-3"
|
||||
<div class="text-muted">
|
||||
Start sending and receiving documents via Peppol as soon as you receive our email notification.
|
||||
</div>
|
||||
<div class="alert alert-warning mt-3"
|
||||
role="alert"
|
||||
attrs="{'invisible': ['|',
|
||||
('is_account_peppol_participant', '=', False),
|
||||
('account_peppol_endpoint_warning', '=', False)
|
||||
]}">
|
||||
<field name="account_peppol_endpoint_warning"/>
|
||||
</div>
|
||||
<div class="pt-3"
|
||||
attrs="{'invisible': [('is_account_peppol_participant', '=', False)]}">
|
||||
<b>
|
||||
Please verify the EAS code and the Endpoint (Participant ID) below
|
||||
before confirming the registration!
|
||||
</b>
|
||||
<div class="row pt-3">
|
||||
<div class="row">
|
||||
<label string="Peppol EAS"
|
||||
for="account_peppol_eas"
|
||||
class="col-lg-3 o_light_label"/>
|
||||
for="account_peppol_eas"
|
||||
class="col-lg-3 o_light_label"/>
|
||||
<field name="account_peppol_eas"/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label string="Peppol Endpoint"
|
||||
for="account_peppol_eas"
|
||||
class="col-lg-3 o_light_label"/>
|
||||
for="account_peppol_eas"
|
||||
class="col-lg-3 o_light_label"/>
|
||||
<field name="account_peppol_endpoint"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-group"
|
||||
attrs="{'invisible': ['|',
|
||||
('is_account_peppol_participant', '=', False),
|
||||
('account_peppol_proxy_state', 'in', ('rejected', 'not_registered'))]}">
|
||||
<div class="row">
|
||||
<label string="Incoming Invoices Journal"
|
||||
for="account_peppol_purchase_journal_id"
|
||||
<div class="row"
|
||||
attrs="{'invisible': ['|',
|
||||
('is_account_peppol_participant', '=', False),
|
||||
('account_peppol_proxy_state', 'not in', ('not_registered', 'not_verified'))
|
||||
]}">
|
||||
<label string="Phone Number"
|
||||
for="account_peppol_phone_number"
|
||||
class="col-lg-3 o_light_label"/>
|
||||
<field name="account_peppol_purchase_journal_id"
|
||||
attrs="{'required': [
|
||||
('is_account_peppol_participant', '=', True),
|
||||
('account_peppol_proxy_state', 'not in', ('rejected', 'not_registered')),
|
||||
]}"/>
|
||||
</div>
|
||||
<field name="account_peppol_phone_number"
|
||||
attrs="{'required': [
|
||||
('is_account_peppol_participant', '=', True),
|
||||
('account_peppol_proxy_state', 'in', ('not_registered', 'not_verified')),
|
||||
]}"/>
|
||||
</div>
|
||||
<div class="row"
|
||||
attrs="{'invisible': ['|',
|
||||
('is_account_peppol_participant', '=', False),
|
||||
('account_peppol_proxy_state', 'in', ('rejected', 'canceled', 'sent_verification'))]}">
|
||||
<label string="Primary contact email"
|
||||
for="account_peppol_contact_email"
|
||||
class="col-lg-3 o_light_label"/>
|
||||
<field name="account_peppol_contact_email"
|
||||
attrs="{'required': [('is_account_peppol_participant', '=', True)]}"/>
|
||||
</div>
|
||||
<div class="content-group pt-3"
|
||||
attrs="{'invisible': ['|',
|
||||
('is_account_peppol_participant', '=', False),
|
||||
('account_peppol_proxy_state', '!=', 'not_registered')]}">
|
||||
attrs="{'invisible': ['|',
|
||||
('account_peppol_proxy_state', 'not in', ('not_registered')),
|
||||
('is_account_peppol_participant', '=', False)]}">
|
||||
<span>
|
||||
Upload a document to verify Company information.
|
||||
I want to migrate my Peppol connection to Odoo (optional):
|
||||
</span>
|
||||
<div class="row mt-3">
|
||||
<field name="account_peppol_attachment_ids"
|
||||
widget="many2many_binary"
|
||||
<label string="Migration key"
|
||||
for="account_peppol_migration_key"
|
||||
class="col-lg-3 o_light_label"/>
|
||||
<field name="account_peppol_migration_key"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3"
|
||||
attrs="{'invisible': [('is_account_peppol_participant', '=', False)]}">
|
||||
<div class="content-group mt-3"
|
||||
attrs="{'invisible': [
|
||||
('account_peppol_proxy_state', 'in', (
|
||||
'rejected', 'not_registered', 'canceled', 'not_verified', 'sent_verification'))
|
||||
]}">
|
||||
<div class="row">
|
||||
<label string="Incoming Invoices Journal"
|
||||
for="account_peppol_purchase_journal_id"
|
||||
class="col-lg-3 o_light_label"/>
|
||||
<field name="account_peppol_purchase_journal_id"
|
||||
attrs="{'required': [
|
||||
('is_account_peppol_participant', '=', True),
|
||||
('account_peppol_proxy_state', 'not in', (
|
||||
'rejected', 'not_registered', 'canceled', 'not_verified', 'sent_verification')),
|
||||
]}"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-group mt-3"
|
||||
attrs="{'invisible': [('account_peppol_proxy_state', '!=', 'sent_verification')]}">
|
||||
<span class="text-muted">
|
||||
We sent a verification code to
|
||||
<field name="account_peppol_phone_number"
|
||||
nolabel="1"
|
||||
attrs="{'readonly': [
|
||||
('is_account_peppol_participant', '=', True),
|
||||
('account_peppol_proxy_state', '=', 'sent_verification'),
|
||||
]}"/>.
|
||||
</span>
|
||||
<div class="row mt-1 ps-3">
|
||||
<field name="account_peppol_verification_code" widget="verification_code"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-3 pb-3"
|
||||
attrs="{'invisible': [('account_peppol_proxy_state', '=', 'not_registered')]}">
|
||||
Application status:
|
||||
<b>
|
||||
<field name="account_peppol_proxy_state"
|
||||
readonly="1"
|
||||
decoration-danger="account_peppol_proxy_state == 'rejected'"
|
||||
decoration-info="account_peppol_proxy_state == 'active'"/>
|
||||
</b>
|
||||
</div>
|
||||
<div attrs="{'invisible': [('account_peppol_proxy_state', '!=', 'rejected')]}">
|
||||
<p>
|
||||
The rejection reason has been sent to you via email.
|
||||
</p>
|
||||
<p>
|
||||
Please do not hesitate to contact our support if you need further assistance.
|
||||
</p>
|
||||
</div>
|
||||
<div attrs="{'invisible': ['|',
|
||||
('account_peppol_proxy_state', '!=', 'canceled'),
|
||||
('account_peppol_migration_key', '=', False)]}">
|
||||
Your migration key is
|
||||
<field name="account_peppol_migration_key"
|
||||
nolabel="1"
|
||||
string="Upload documents"/>
|
||||
attrs="{'readonly': [
|
||||
('account_peppol_proxy_state', '=', 'canceled'),
|
||||
('account_peppol_migration_key', '!=', False)
|
||||
]}"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-3 pb-3"
|
||||
attrs="{'invisible': [('account_peppol_proxy_state', 'in', (False, 'not_registered'))]}">
|
||||
Application status:
|
||||
<b>
|
||||
<field name="account_peppol_proxy_state"
|
||||
decoration-danger="account_peppol_proxy_state == 'rejected'"
|
||||
decoration-info="account_peppol_proxy_state == 'active'"/>
|
||||
</b>
|
||||
</div>
|
||||
<div class="mt-5"
|
||||
attrs="{'invisible': ['|',
|
||||
('is_account_peppol_participant', '=', False),
|
||||
('account_peppol_proxy_state', '!=', 'not_registered')]}">
|
||||
<div class="mb-3">
|
||||
By clicking the "Confirm" button, I accept that Odoo may process my e-invoices.
|
||||
<div attrs="{'invisible': [('account_peppol_proxy_state', '!=', 'active')]}">
|
||||
Your Peppol identification is
|
||||
<field name="account_peppol_edi_identification"
|
||||
nolabel="1"/>
|
||||
</div>
|
||||
<div class="mt-5"
|
||||
attrs="{'invisible': [('account_peppol_proxy_state', '!=', 'not_registered')]}">
|
||||
<div class="mb-3">
|
||||
I accept that Odoo may process my e-invoices.
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-1 action_buttons" colspan="3">
|
||||
<div class="mt-3"
|
||||
attrs="{'invisible': [('account_peppol_proxy_state', '!=', 'not_verified')]}">
|
||||
<button name="button_send_peppol_verification_code"
|
||||
type="object"
|
||||
string="Verify phone number"
|
||||
class="btn btn-primary"/>
|
||||
</div>
|
||||
<widget name="peppol_settings_buttons"
|
||||
attrs="{'invisible': [('account_peppol_proxy_state', 'not in', (
|
||||
'not_registered', 'sent_verification', 'pending', 'manually_approved', 'active'))
|
||||
]}"/>
|
||||
<div class="mt-3"
|
||||
attrs="{'invisible': [
|
||||
('account_peppol_proxy_state', 'in', (
|
||||
'not_registered', 'active', 'rejected', 'canceled'))]}">
|
||||
<button name="button_cancel_peppol_registration"
|
||||
type="object"
|
||||
string="Cancel registration"
|
||||
class="btn btn-secondary"/>
|
||||
</div>
|
||||
<div class="mt-3"
|
||||
attrs="{'invisible': ['|',
|
||||
('account_peppol_proxy_state', '!=', 'canceled'),
|
||||
('account_peppol_migration_key', '!=', False)]}">
|
||||
<button name="button_reopen_peppol_registration"
|
||||
type="object"
|
||||
string="Retry registration"
|
||||
class="btn btn-secondary"/>
|
||||
</div>
|
||||
</div>
|
||||
<button name="button_create_peppol_proxy_user"
|
||||
type="object"
|
||||
string="Confirm"
|
||||
class="btn btn-primary"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -146,13 +146,8 @@ class AccountMoveSend(models.Model):
|
||||
if not params['documents']:
|
||||
return
|
||||
|
||||
edi_user = self.env['account_edi_proxy_client.user'].search(
|
||||
[
|
||||
('company_id', '=', self.company_id.id),
|
||||
('proxy_type', '=', 'peppol'),
|
||||
],
|
||||
limit=1,
|
||||
)
|
||||
edi_user = self.company_id.account_edi_proxy_client_ids.filtered(
|
||||
lambda u: u.proxy_type == 'peppol')
|
||||
|
||||
try:
|
||||
response = edi_user._make_request(
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
<field name="model">account.move.send</field>
|
||||
<field name="inherit_id" ref="account.account_move_send_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='standard_options']" position="before">
|
||||
<xpath expr="//div[@name='warnings']" position="inside">
|
||||
<field name="enable_peppol" invisible="1"/>
|
||||
<div class="alert alert-warning text-center" role="alert" attrs="{
|
||||
<div class="alert alert-warning" role="alert" attrs="{
|
||||
'invisible': ['|',
|
||||
('checkbox_send_peppol', '!=', True),
|
||||
('peppol_warning', '=', False),
|
||||
|
||||
+2
-2
@@ -23,7 +23,7 @@
|
||||
</cac:AdditionalDocumentReference>
|
||||
<cac:AccountingSupplierParty>
|
||||
<cac:Party>
|
||||
<cbc:EndpointID schemeID="9925">BE0202239951</cbc:EndpointID>
|
||||
<cbc:EndpointID schemeID="0208">0202239951</cbc:EndpointID>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>partner_1</cbc:Name>
|
||||
</cac:PartyName>
|
||||
@@ -52,7 +52,7 @@
|
||||
</cac:AccountingSupplierParty>
|
||||
<cac:AccountingCustomerParty>
|
||||
<cac:Party>
|
||||
<cbc:EndpointID schemeID="9925">BE0477472701</cbc:EndpointID>
|
||||
<cbc:EndpointID schemeID="0208">0477472701</cbc:EndpointID>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>partner_2</cbc:Name>
|
||||
</cac:PartyName>
|
||||
|
||||
+2
-2
@@ -23,7 +23,7 @@
|
||||
</cac:AdditionalDocumentReference>
|
||||
<cac:AccountingSupplierParty>
|
||||
<cac:Party>
|
||||
<cbc:EndpointID schemeID="9925">BE0202239951</cbc:EndpointID>
|
||||
<cbc:EndpointID schemeID="0208">0202239951</cbc:EndpointID>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>partner_1</cbc:Name>
|
||||
</cac:PartyName>
|
||||
@@ -52,7 +52,7 @@
|
||||
</cac:AccountingSupplierParty>
|
||||
<cac:AccountingCustomerParty>
|
||||
<cac:Party>
|
||||
<cbc:EndpointID schemeID="9925">BE0477472701</cbc:EndpointID>
|
||||
<cbc:EndpointID schemeID="0208">0477472701</cbc:EndpointID>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>partner_2</cbc:Name>
|
||||
</cac:PartyName>
|
||||
|
||||
+2
-2
@@ -23,7 +23,7 @@
|
||||
</cac:AdditionalDocumentReference>
|
||||
<cac:AccountingSupplierParty>
|
||||
<cac:Party>
|
||||
<cbc:EndpointID schemeID="9925">BE0202239951</cbc:EndpointID>
|
||||
<cbc:EndpointID schemeID="0208">0202239951</cbc:EndpointID>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>partner_1</cbc:Name>
|
||||
</cac:PartyName>
|
||||
@@ -52,7 +52,7 @@
|
||||
</cac:AccountingSupplierParty>
|
||||
<cac:AccountingCustomerParty>
|
||||
<cac:Party>
|
||||
<cbc:EndpointID schemeID="9925">BE0477472701</cbc:EndpointID>
|
||||
<cbc:EndpointID schemeID="0208">0477472701</cbc:EndpointID>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>partner_2</cbc:Name>
|
||||
</cac:PartyName>
|
||||
|
||||
+2
-2
@@ -20,7 +20,7 @@
|
||||
</cac:AdditionalDocumentReference>
|
||||
<cac:AccountingSupplierParty>
|
||||
<cac:Party>
|
||||
<cbc:EndpointID schemeID="9925">BE0202239951</cbc:EndpointID>
|
||||
<cbc:EndpointID schemeID="0208">0202239951</cbc:EndpointID>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>partner_1</cbc:Name>
|
||||
</cac:PartyName>
|
||||
@@ -49,7 +49,7 @@
|
||||
</cac:AccountingSupplierParty>
|
||||
<cac:AccountingCustomerParty>
|
||||
<cac:Party>
|
||||
<cbc:EndpointID schemeID="9925">BE0477472701</cbc:EndpointID>
|
||||
<cbc:EndpointID schemeID="0208">0477472701</cbc:EndpointID>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>partner_2</cbc:Name>
|
||||
</cac:PartyName>
|
||||
|
||||
+4
-3
@@ -20,7 +20,7 @@
|
||||
</cac:AdditionalDocumentReference>
|
||||
<cac:AccountingSupplierParty>
|
||||
<cac:Party>
|
||||
<cbc:EndpointID schemeID="9925">BE0202239951</cbc:EndpointID>
|
||||
<cbc:EndpointID schemeID="0208">0202239951</cbc:EndpointID>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>partner_1</cbc:Name>
|
||||
</cac:PartyName>
|
||||
@@ -58,7 +58,7 @@
|
||||
<cbc:CityName>Ramillies</cbc:CityName>
|
||||
<cbc:PostalZone>1367</cbc:PostalZone>
|
||||
<cac:Country>
|
||||
<cbc:IdentificationCode>BE</cbc:IdentificationCode>
|
||||
<cbc:IdentificationCode>LU</cbc:IdentificationCode>
|
||||
</cac:Country>
|
||||
</cac:PostalAddress>
|
||||
<cac:PartyLegalEntity>
|
||||
@@ -70,13 +70,14 @@
|
||||
</cac:Party>
|
||||
</cac:AccountingCustomerParty>
|
||||
<cac:Delivery>
|
||||
<cbc:ActualDeliveryDate>2017-01-01</cbc:ActualDeliveryDate>
|
||||
<cac:DeliveryLocation>
|
||||
<cac:Address>
|
||||
<cbc:StreetName>Rue des Bourlottes 9</cbc:StreetName>
|
||||
<cbc:CityName>Ramillies</cbc:CityName>
|
||||
<cbc:PostalZone>1367</cbc:PostalZone>
|
||||
<cac:Country>
|
||||
<cbc:IdentificationCode>BE</cbc:IdentificationCode>
|
||||
<cbc:IdentificationCode>LU</cbc:IdentificationCode>
|
||||
</cac:Country>
|
||||
</cac:Address>
|
||||
</cac:DeliveryLocation>
|
||||
|
||||
+2
-2
@@ -25,7 +25,7 @@
|
||||
</cac:AdditionalDocumentReference>
|
||||
<cac:AccountingSupplierParty>
|
||||
<cac:Party>
|
||||
<cbc:EndpointID schemeID="9925">BE0202239951</cbc:EndpointID>
|
||||
<cbc:EndpointID schemeID="0208">0202239951</cbc:EndpointID>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>partner_1</cbc:Name>
|
||||
</cac:PartyName>
|
||||
@@ -54,7 +54,7 @@
|
||||
</cac:AccountingSupplierParty>
|
||||
<cac:AccountingCustomerParty>
|
||||
<cac:Party>
|
||||
<cbc:EndpointID schemeID="9925">BE0477472701</cbc:EndpointID>
|
||||
<cbc:EndpointID schemeID="0208">0477472701</cbc:EndpointID>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>partner_2</cbc:Name>
|
||||
</cac:PartyName>
|
||||
|
||||
+2
-2
@@ -19,7 +19,7 @@
|
||||
</cac:AdditionalDocumentReference>
|
||||
<cac:AccountingSupplierParty>
|
||||
<cac:Party>
|
||||
<cbc:EndpointID schemeID="9925">BE0202239951</cbc:EndpointID>
|
||||
<cbc:EndpointID schemeID="0208">0202239951</cbc:EndpointID>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>partner_1</cbc:Name>
|
||||
</cac:PartyName>
|
||||
@@ -48,7 +48,7 @@
|
||||
</cac:AccountingSupplierParty>
|
||||
<cac:AccountingCustomerParty>
|
||||
<cac:Party>
|
||||
<cbc:EndpointID schemeID="9925">BE0477472701</cbc:EndpointID>
|
||||
<cbc:EndpointID schemeID="0208">0477472701</cbc:EndpointID>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>partner_2</cbc:Name>
|
||||
</cac:PartyName>
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
</cac:OrderReference>
|
||||
<cac:AccountingSupplierParty>
|
||||
<cac:Party>
|
||||
<cbc:EndpointID schemeID="9925">BE0202239951</cbc:EndpointID>
|
||||
<cbc:EndpointID schemeID="0208">0202239951</cbc:EndpointID>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>partner_1</cbc:Name>
|
||||
</cac:PartyName>
|
||||
|
||||
@@ -218,10 +218,11 @@ class TestUBLBE(TestUBLCommon, TestAccountMoveSendCommon):
|
||||
`peppol_endpoint`).
|
||||
"""
|
||||
# Setup a public admin in Luxembourg
|
||||
self.partner_2.update({
|
||||
self.partner_2.write({
|
||||
'vat': None,
|
||||
'peppol_endpoint': '00005000041',
|
||||
'peppol_eas': '9938',
|
||||
'country_id': self.env.ref('base.lu').id,
|
||||
})
|
||||
|
||||
invoice = self._generate_move(
|
||||
|
||||
Reference in New Issue
Block a user