[IMP] account_peppol: demo mode

This commit implements a demo mode in which a user can register to
peppol. The demo user performs no communication with the proxy server,
instead making and receiving only mocked calls and responses.

The @handle_demo decorator is added and utilised in order to avoid
adding the specific (and mostly irrelevant) branches of this of demo
functionality to the body of the code. When the edi_user has the
edi_mode 'demo' this decorator diverts the execution of the
function (excepting when running tests, whenupon it is ignored).

Specific logic is added to the res config settings in order to specify
which modes are availble to the current user based on the
"account_peppol.mode_constraint" config param, and the
"saas_trail.confirmation_token" config param. This allows us to default
users using runbot/demo-data/saas-trial databases to "demo" mode
registration.

task-id: 3497466
         3497725

closes odoo/odoo#136909

Signed-off-by: Laurent Smet (las) <las@odoo.com>
This commit is contained in:
Daniel Kosky (dako)
2023-10-17 00:35:05 +00:00
parent 06f756e993
commit 78192c45c0
21 changed files with 386 additions and 28 deletions
+1
View File
@@ -3,3 +3,4 @@
from . import models
from . import wizard
from . import tools
@@ -2,6 +2,10 @@
<odoo noupdate="1">
<record id="default_account_peppol_edi_mode" model="ir.config_parameter">
<field name="key">account_peppol.edi.mode</field>
<field name="value">test</field>
<field name="value">demo</field>
</record>
<record id="default_account_peppol_mode_constraint" model="ir.config_parameter">
<field name="key">account_peppol.mode_constraint</field>
<field name="value">demo</field>
</record>
</odoo>
@@ -4,8 +4,9 @@
import logging
from odoo import _, fields, models, modules, tools
from odoo.exceptions import UserError
from odoo.addons.account_edi_proxy_client.models.account_edi_proxy_user import AccountEdiProxyError
from odoo.addons.account_peppol.tools.demo_utils import handle_demo
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
@@ -20,6 +21,7 @@ class AccountEdiProxyClientUser(models.Model):
# HELPER METHODS
# -------------------------------------------------------------------------
@handle_demo
def _make_request(self, url, params=False):
# extends account_edi_proxy_client to update peppol_proxy_state
# of archived users
@@ -47,22 +49,10 @@ class AccountEdiProxyClientUser(models.Model):
urls['peppol'] = {
'prod': 'https://peppol.api.odoo.com',
'test': 'https://peppol.test.odoo.com',
'demo': 'demo',
}
return urls
def _get_server_url(self, proxy_type=None, edi_mode=None):
proxy_type = proxy_type or self.proxy_type
if not proxy_type == 'peppol':
return super()._get_server_url(proxy_type, edi_mode)
peppol_param = self.env['ir.config_parameter'].sudo().get_param(
'account_peppol.edi.mode', False
)
if peppol_param == 'test':
edi_mode = 'test'
return super()._get_server_url(proxy_type, edi_mode)
# -------------------------------------------------------------------------
# CRONS
# -------------------------------------------------------------------------
+7 -1
View File
@@ -1,7 +1,7 @@
# -*- coding:utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, _
from odoo import api, fields, models, _
from odoo.exceptions import UserError
@@ -21,6 +21,7 @@ class AccountMove(models.Model):
copy=False,
readonly=True,
)
peppol_is_demo_uuid = fields.Boolean(compute="_compute_peppol_is_demo_uuid")
def action_cancel_peppol_documents(self):
# if the peppol_move_state is processing/done
@@ -28,3 +29,8 @@ class AccountMove(models.Model):
if any(move.peppol_move_state in {'processing', 'done'} for move in self):
raise UserError(_("Cannot cancel an entry that has already been sent to PEPPOL"))
self.peppol_move_state = 'canceled'
@api.depends('peppol_message_uuid')
def _compute_peppol_is_demo_uuid(self):
for move in self:
move.peppol_is_demo_uuid = (move.peppol_message_uuid or '').startswith('demo_')
@@ -5,6 +5,11 @@ 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
from odoo.addons.account_peppol.tools.demo_utils import handle_demo
# at the moment, only European countries are accepted
ALLOWED_COUNTRIES = set(EAS_MAPPING.keys()) - {'AU', 'SG', 'NZ'}
class ResConfigSettings(models.TransientModel):
@@ -36,6 +41,17 @@ class ResConfigSettings(models.TransientModel):
has_peppol_participant = fields.Boolean(
compute='_compute_has_peppol_participant'
)
account_peppol_edi_mode = fields.Selection(
selection=[('demo', 'Demo'), ('test', 'Test'), ('prod', 'Live')],
compute='_compute_account_peppol_edi_mode',
inverse='_inverse_account_peppol_edi_mode',
readonly=False,
)
account_peppol_mode_constraint = fields.Selection(
selection=[('demo', 'Demo'), ('test', 'Test'), ('prod', 'Live')],
compute='_compute_account_peppol_mode_constraint',
help="Using the config params, this field specifies which edi modes may be selected from the UI"
)
# -------------------------------------------------------------------------
# HELPER METHODS
@@ -61,6 +77,26 @@ class ResConfigSettings(models.TransientModel):
# -------------------------------------------------------------------------
# COMPUTE METHODS
# -------------------------------------------------------------------------
@api.depends('is_account_peppol_eligible', 'account_peppol_edi_user')
def _compute_account_peppol_mode_constraint(self):
mode_constraint = self.env['ir.config_parameter'].sudo().get_param('account_peppol.mode_constraint')
trial_param = self.env['ir.config_parameter'].sudo().get_param('saas_trial.confirm_token')
self.account_peppol_mode_constraint = trial_param and 'demo' or mode_constraint or 'prod'
@api.depends('is_account_peppol_eligible', 'account_peppol_edi_user')
def _compute_account_peppol_edi_mode(self):
edi_mode = self.env['ir.config_parameter'].sudo().get_param('account_peppol.edi.mode')
for config in self:
if config.account_peppol_edi_user:
config.account_peppol_edi_mode = config.account_peppol_edi_user.edi_mode
else:
config.account_peppol_edi_mode = edi_mode or 'prod'
def _inverse_account_peppol_edi_mode(self):
for config in self:
if not config.account_peppol_edi_user and config.account_peppol_edi_mode:
self.env['ir.config_parameter'].sudo().set_param('account_peppol.edi.mode', config.account_peppol_edi_mode)
return
@api.depends("company_id.account_edi_proxy_client_ids")
def _compute_account_peppol_edi_user(self):
@@ -92,6 +128,7 @@ class ResConfigSettings(models.TransientModel):
# BUSINESS ACTIONS
# -------------------------------------------------------------------------
@handle_demo
def button_create_peppol_proxy_user(self):
"""
The first step of the Peppol onboarding.
@@ -109,7 +146,7 @@ class ResConfigSettings(models.TransientModel):
company = self.company_id
edi_proxy_client = self.env['account_edi_proxy_client.user']
edi_user = edi_proxy_client.sudo()._register_proxy_user(company, 'peppol', 'prod')
edi_user = edi_proxy_client.sudo()._register_proxy_user(company, 'peppol', self.account_peppol_edi_mode)
self.account_peppol_proxy_state = 'not_verified'
# if there is an error when activating the participant below,
@@ -144,6 +181,7 @@ class ResConfigSettings(models.TransientModel):
# but we need the field for future in case the user decided to migrate away from Odoo
self.account_peppol_migration_key = False
@handle_demo
def button_update_peppol_user_data(self):
"""
Action for the user to be able to update their contact details any time
@@ -245,6 +283,7 @@ class ResConfigSettings(models.TransientModel):
self.account_peppol_proxy_state = 'not_verified'
@handle_demo
def button_migrate_peppol_registration(self):
"""
If the user is active, they need to request a migration key, generated on the IAP server.
@@ -261,6 +300,7 @@ class ResConfigSettings(models.TransientModel):
response = self._call_peppol_proxy(endpoint='/api/peppol/1/migrate_peppol_registration')
self.account_peppol_migration_key = response['migration_key']
@handle_demo
def button_deregister_peppol_participant(self):
"""
Deregister the edi user from Peppol network
@@ -7,6 +7,7 @@ from hashlib import md5
from urllib import parse
from odoo import api, fields, models
from odoo.addons.account_peppol.tools.demo_utils import handle_demo
TIMEOUT = 10
@@ -51,6 +52,7 @@ class ResPartner(models.Model):
else:
partner.account_peppol_verification_label = 'not_valid'
@handle_demo
def button_account_peppol_check_partner_endpoint(self):
""" A basic check for whether a participant is reachable at the given
Peppol participant ID - peppol_eas:peppol_endpoint (ex: '9999:test')
@@ -0,0 +1,38 @@
/** @odoo-module **/
import { evaluateExpr } from "@web/core/py_js/py";
import { RadioField, radioField } from "@web/views/fields/radio/radio_field";
import { registry } from "@web/core/registry";
class PeppolRadioField extends RadioField {
static template = "account_peppol.PeppolRadioField";
static props = {
...RadioField.props,
hiddenItems: { type: String, optional: true },
readonlyItems: { type: String, optional: true },
};
setup() {
super.setup()
this.initialSelection = this.props.record.data[this.props.name];
this.readonlyItems = evaluateExpr(this.props.readonlyItems || "[]", this.props.record.evalContext);
this.hiddenItems = evaluateExpr(this.props.hiddenItems || "[]", this.props.record.evalContext).filter(item => (item != this.initialSelection));
}
get items() {
return super.items.filter(item => !(this.hiddenItems.includes(item[0])))
}
}
const peppolRadioField = {
...radioField,
component: PeppolRadioField,
extractProps: ({attrs, options}, dynamicInfo) => {
return {
hiddenItems: attrs.hidden_items || "[]",
readonlyItems: attrs.readonly_items || "[]",
...radioField.extractProps({attrs, options}, dynamicInfo),
}
},
}
registry.category("fields").add("account_peppol_radio_field", peppolRadioField);
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="account_peppol.PeppolRadioField" t-inherit-mode="primary" t-inherit="web.RadioField">
<xpath expr="//input" position="attributes">
<attribute name="t-att-disabled" add="|| this.readonlyItems.includes(item[0])" separator=" "/>
</xpath>
</t>
</templates>
@@ -31,6 +31,30 @@ class PeppolSettingsButtons extends Component {
return this.props.record.data.account_peppol_proxy_state === "active" && Boolean(this.props.record.data.account_peppol_migration_key);
}
get ediMode() {
return this.props.record.data.account_peppol_edi_mode;
}
get modeConstraint() {
return this.props.record.data.account_peppol_mode_constraint;
}
get createUserButtonLabel() {
const modes = {
demo: _t("Validate registration (Demo)"),
test: _t("Validate registration (Test)"),
prod: _t("Validate registration"),
}
return modes[this.ediMode] || _t("Validate registration");
}
get deregisterUserButtonLabel() {
const modes = {
demo: _t("Switch to Live"),
}
return this.modeConstraint !== "demo" && modes[this.ediMode] || _t("Deregister from Peppol");
}
async _callConfigMethod(methodName, save = false) {
if (save) {
await this._save();
@@ -74,10 +98,14 @@ class PeppolSettingsButtons extends Component {
}
deregister() {
this.showConfirmation(
"This will delete your Peppol registration.",
"button_deregister_peppol_participant"
)
if (this.ediMode === 'demo') {
this._callConfigMethod("button_deregister_peppol_participant");
} else {
this.showConfirmation(
"This will delete your Peppol registration.",
"button_deregister_peppol_participant"
)
}
}
async updateDetails() {
@@ -9,14 +9,14 @@
class="btn btn-primary"
t-on-click="createUser"
t-if="proxyState === 'not_registered'">
Validate registration
<t t-out="createUserButtonLabel"/>
</button>
</div>
<div class="mt-3">
<button type="button"
class="btn btn-secondary me-1"
t-on-click="migrate"
t-if="proxyState === 'active' and migrationPrepared === false">
t-if="proxyState === 'active' and migrationPrepared === false and ediMode!='demo'">
Migrate registration to another service
</button>
</div>
@@ -25,7 +25,7 @@
class="btn btn-secondary me-1"
t-on-click="deregister"
t-if="proxyState === 'active' and migrationPrepared === false">
Deregister from Peppol
<t t-out="deregisterUserButtonLabel"/>
</button>
</div>
<div class="mt-3">
@@ -24,6 +24,7 @@ class TestPeppolMessage(TestAccountMoveSendCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
cls.env['ir.config_parameter'].sudo().set_param('account_peppol.edi.mode', 'test')
cls.env.company.write({
'country_id': cls.env.ref('base.be').id,
@@ -18,6 +18,11 @@ PDF_FILE_PATH = 'account_peppol/tests/assets/peppol_identification_test.pdf'
@tagged('-at_install', 'post_install')
class TestPeppolParticipant(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env['ir.config_parameter'].sudo().set_param('account_peppol.edi.mode', 'test')
def _get_participant_vals(self):
return {
'is_account_peppol_participant': True,
+3
View File
@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import demo_utils
File diff suppressed because one or more lines are too long
+169
View File
@@ -0,0 +1,169 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from base64 import b64encode
from decorator import decorator
import uuid
from odoo import _, fields, modules, tools
from odoo.tools.misc import file_open
DEMO_BILL_PATH = 'account_peppol/tools/demo_bill'
DEMO_ENC_KEY = 'account_peppol/tools/enc_key'
DEMO_PRIVATE_KEY = 'account_peppol/tools/private_key.pem'
# -------------------------------------------------------------------------
# HELPERS
# -------------------------------------------------------------------------
def get_demo_vendor_bill(user):
return {
'direction': 'incoming',
'receiver': user.edi_identification,
'uuid': f'{user.company_id.id}_demo_vendor_bill',
'accounting_supplier_party': '0208:2718281828',
'state': 'done',
'filename': f'{user.company_id.id}_demo_vendor_bill',
'enc_key': file_open(DEMO_ENC_KEY, mode='rb').read(),
'document': file_open(DEMO_BILL_PATH, mode='rb').read(),
}
# -------------------------------------------------------------------------
# MOCKED FUNCTIONS
# -------------------------------------------------------------------------
def _mock_make_request(func, self, *args, **kwargs):
def _mock_get_all_documents(user, args, kwargs):
if not user.env['account.move'].search_count([
('peppol_message_uuid', '=', f'{user.company_id.id}_demo_vendor_bill')
]):
return {'messages': [get_demo_vendor_bill(user)]}
return {'messages': []}
def _mock_get_document(user, args, kwargs):
message_uuid = args[1]['message_uuids'][0]
if message_uuid.endswith('_demo_vendor_bill'):
return {message_uuid: get_demo_vendor_bill(user)}
return {message_uuid: {'state': 'done'}}
def _mock_send_document(user, args, kwargs):
# Trigger the reception of vendor bills
get_messages_cron = user.env['ir.cron'].sudo().env.ref(
'account_peppol.ir_cron_peppol_get_new_documents',
raise_if_not_found=False,
)
if get_messages_cron:
get_messages_cron._trigger()
return {
'messages': [{
'message_uuid': 'demo_%s' % uuid.uuid4(),
} for i in args[1]],
}
endpoint = args[0].split('/')[-1]
return {
'ack': lambda _user, _args, _kwargs: {},
'activate_participant': lambda _user, _args, _kwargs: {},
'get_all_documents': _mock_get_all_documents,
'get_document': _mock_get_document,
'participant_status': lambda _user, _args, _kwargs: {'peppol_state': 'active'},
'send_document': _mock_send_document,
}[endpoint](self, args, kwargs)
def _mock_button_verify_partner_endpoint(func, self, *args, **kwargs):
self.ensure_one()
self.account_peppol_validity_last_check = fields.Date.today()
self.account_peppol_is_endpoint_valid = True
def _mock_user_creation(func, self, *args, **kwargs):
func(self, *args, **kwargs)
self.write({
'account_peppol_proxy_state': 'active',
})
self.account_peppol_edi_user.write({
'private_key': b64encode(file_open(DEMO_PRIVATE_KEY, 'rb').read()),
})
def _mock_deregister_participant(func, self, *args, **kwargs):
# Set documents sent in demo to a state where they can be re-sent
demo_moves = self.env['account.move'].search([
('company_id', '=', self.company_id.id),
('peppol_message_uuid', '=like', 'demo_%'),
])
demo_moves.write({
'peppol_message_uuid': None,
'peppol_move_state': None,
})
demo_moves.message_main_attachment_id.unlink()
demo_moves.ubl_cii_xml_id.unlink()
log_message = _('The peppol status of the documents has been reset when switching from Demo to Live.')
demo_moves._message_log_batch(bodies=dict((move.id, log_message) for move in demo_moves))
# also unlink the demo vendor bill
self.env['account.move'].search([
('company_id', '=', self.company_id.id),
('peppol_message_uuid', '=', f'{self.company_id.id}_demo_vendor_bill'),
]).unlink()
mode_constraint = self.env['ir.config_parameter'].get_param('account_peppol.mode_constraint')
self.account_peppol_edi_user.unlink()
self.account_peppol_proxy_state = 'not_registered'
self.account_peppol_edi_mode = mode_constraint
def _mock_update_user_data(func, self, *args, **kwargs):
pass
def _mock_migrate_participant(func, self, *args, **kwargs):
self.account_peppol_migration_key = 'I9cz9yw*ruDM%4VSj94s'
_demo_behaviour = {
'_make_request': _mock_make_request,
'button_account_peppol_check_partner_endpoint': _mock_button_verify_partner_endpoint,
'button_create_peppol_proxy_user': _mock_user_creation,
'button_deregister_peppol_participant': _mock_deregister_participant,
'button_migrate_peppol_registration': _mock_migrate_participant,
'button_update_peppol_user_data': _mock_update_user_data,
}
# -------------------------------------------------------------------------
# DECORATORS
# -------------------------------------------------------------------------
@decorator
def handle_demo(func, self, *args, **kwargs):
""" This decorator is used on methods that should be mocked in demo mode.
First handle the decision: "Are we in demo mode?", and conditionally decide which function to
execute. Whether we are in demo mode depends on the edi_mode of the EDI user, but the EDI user
is accessible in different ways depending on the model the function is called from and in some
contexts it might not yet exist, in which case demo mode should instead depend on the content
of the "account_peppol.edi.mode" config param.
"""
def get_demo_mode_account_edi_proxy_client_user(self, args, kwargs):
if self.id:
return self.edi_mode == 'demo'
return self.env['ir.config_parameter'].get_param('account_peppol.edi.mode') == 'demo'
def get_demo_mode_res_config_settings(self, args, kwargs):
if self.account_peppol_edi_user:
return self.account_peppol_edi_user.edi_mode == 'demo'
return self.account_peppol_edi_mode == 'demo'
def get_demo_mode_res_partner(self, args, kwargs):
peppol_user = self.env.company.account_edi_proxy_client_ids.filtered(lambda user: user.proxy_type == 'peppol')
if peppol_user:
return peppol_user.edi_mode == 'demo'
return self.env['ir.config_parameter'].get_param('account_peppol.edi.mode') == 'demo'
get_demo_mode = {
'account_edi_proxy_client.user': get_demo_mode_account_edi_proxy_client_user,
'res.config.settings': get_demo_mode_res_config_settings,
'res.partner': get_demo_mode_res_partner,
}
demo_mode = get_demo_mode.get(self._name) and get_demo_mode[self._name](self, args, kwargs) or False
if not demo_mode or tools.config['test_enable'] or modules.module.current_test:
return func(self, *args, **kwargs)
return _demo_behaviour[func.__name__](func, self, *args, **kwargs)
+1
View File
@@ -0,0 +1 @@
HjlTVuGbHXeCBWAhlIDfR3xZcCtykbMmvRoeGLARDWN+AhOdTffFb0hR+3GkMpwCvTshTkIx4nJwdFyIx/g3OuKpGPQJLi1d/4IhwYyhhSFAuHJRWE8rm2Z++aZ4Qd69h4bQjr5ymy/icqiYZ6Atyu1EXfunZBk5LsctoTp99htyFD4EzX6IH+9sBzvb32tkdU7p4LBYalTonJr8cK7IzTHyoDhg7UIwArztHwCrnf2SMLDJB/BdiihN0alf+vNN8/FOhGmJY3qH2nqrDTKk96dgUoapNi6RRWrE6rzmcBL2Lr/dBYfDXqJKqC178+m89ezaPySSToj8MNKNpqXFUw==
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCj4tVufRBH9j1Q
1cag4xHHgYhNOWSTPtkZbB6ZVfxng/MLGgUF+InNRn2ZWFKpTsQucSa1JKWaYWt+
gmq+7NMUu1smgdhyZxbGXubVEm8bp1AQO7432V4pOU06NW8i6YqRb79ru+wY32RN
yWU1GnDyoGFfndM94ewlAkP9THwg6RHl5uYPcy+4PLRixhnMi+CoPhJ53yA8stHZ
rsTKpYXvMLgbOVjI5+7mRq4ixlbmtIwifXdv0yExcj6Q0VJ+iq8aRxcxIRlNV6/6
ifp7+cZUNpw/KzWVOQ+DcMgzMir7nn2YatLSPTcqv7YjQqpS/D2ZUu8S/SNxmQ50
nF4/xoknAgMBAAECggEAen9gfReqBb/+kN9ZeoR/k5o0oVRW4uDFMYVpUb+9zDoK
fq/SNWZSykb4NpiYIMkpRnV5M0jTJ5PN31/oHhGyrPpl5WCEwu5fTaM98uG2fvsO
kzO0uNYW1cVo/itWiMf7tT3L3OE4VlcUCDiTF6BN8G0Em43CiazG79rDqx9yYL3j
Vnj/NLiY0OnxXL6SkjxZHvjXPl8jjTrf8oZl+twpwskiC5cqIpfsTGSsDq5WS2rh
eYdGhH4EcUAfK5PHTTIwBGFReekrTXv1PXm3EK36T8himQVX5sSd2F79QsVXsnfM
PR5W85W7MhEKYD4mZSQsKThj+CCE8G7U5uJ2Ah+LYQKBgQDOPNQYdfrY2Ivcy1X7
umVniEyoJ5fs3GLRwXTH+nPNxIa5/YtzLqd17GU/Ec6nyEDGcDqEpuKfopcEEqqw
w6Z9JPudOPJoVRugTHkpAzv/A9Sq5IC3P2SJUDV11u80x9qe3j5oKj/woEE2HsUI
uCn8HF9NUO7r8G/eg5+8vFcHiQKBgQDLbfnt2xFRFgF06+E2GIK/GAKbkJHhYO1L
2obZQnGj7+irMlt+uaDbHk+jFCHk+q3xd48x+o1aliNwXSDQ3QJfxCSIfcP9xG2W
3l6kCO5irwgt589E5rEUEegQM2F6oS3OtcUr9zjeW1rDMjHHLD87Tb9rB2GAweQf
yLqsoZovLwKBgERjX2GNHdVyWU6qDqUetimSxPityG8+1XYA1JzLrEL7fEGIlgln
2xf7f8dePEze1rv20zDRtiyBWdp75iYfesHc1aLZE2kNb8/EDBlRfT+fIZJZm2Uo
nEn8Uv30e/Xgn9o2kDMyb2l3eqhbo7K0fxeewOt+fvu2CyKaOwn22lUhAoGBAKQz
YfAmykR8EbLxjnhesnJSjBBLUiTsWr3GZuBI7HdZ96Dv5cBVT0xum/NTFcTAvtRQ
IApEZgJ/e51/3jQYoIjyRlbRxPg5rAeB+DxJZTnMdDqxiLDh0H8VsQ4amw0juljG
iZ9iTsnUTV+PTXSp92QD7oUSkRYf6uXo3Rzo2A5LAoGAI4o8zbghCrqJvkuk0hfh
UP5X/eu06EMYVLamZit64kJMdumKcJuVDo4jyz+DYhz6T3bMPbAzRBqsZA4M8l73
XWp9z1bKS/AHsZSXLLRKk1C/OR7z5zLz/j8BgbqKpjNtRCp1DFu1IgPC2+3rVr89
mQa1nuu+xYOkgxcs7F5qM7g=
-----END PRIVATE KEY-----
@@ -21,7 +21,9 @@
class="d-flex"
invisible="not peppol_move_state or state == 'draft'">
<field name="peppol_is_demo_uuid" invisible="1"/>
<field name="peppol_move_state" class="oe_inline"/>
<span class="mx-1" invisible="not peppol_is_demo_uuid"> (Demo)</span>
<span class="text-muted mx-3"
invisible="peppol_move_state != 'to_send'">
The invoice will be sent automatically to PEPPOL
@@ -99,9 +99,12 @@
Application status:
<b>
<field name="account_peppol_proxy_state"
class="oe_inline"
readonly="1"
decoration-danger="account_peppol_proxy_state == 'rejected'"
decoration-info="account_peppol_proxy_state == 'active'"/>
<span class="text-info" invisible="not account_peppol_edi_mode == 'demo'"> (Demo)</span>
<span class="text-info" invisible="not account_peppol_edi_mode == 'test'"> (Test)</span>
</b>
</div>
<div invisible="account_peppol_proxy_state != 'rejected'">
@@ -123,10 +126,23 @@
<field name="account_peppol_edi_identification"
nolabel="1"/>
</div>
<div class="mt-5"
invisible="account_peppol_proxy_state != 'not_registered'">
<div class="mb-3">
I accept that Odoo may process my e-invoices.
<div class="mt-4" invisible="account_peppol_proxy_state != 'not_registered'">
<field name="account_peppol_mode_constraint" invisible="1"/>
<field name="account_peppol_edi_mode"
widget="account_peppol_radio_field"
class="o_field_radio"
readonly_items="account_peppol_mode_constraint != 'prod' and ['prod'] or []"
hidden_items="account_peppol_mode_constraint != 'test' and ['test'] or []"
options="{'horizontal': true }"/>
<div class="mb-3" invisible="not account_peppol_edi_mode == 'prod'">
By clicking the button below I accept that Odoo may process my e-invoices.
</div>
<div class="mb-3" invisible="not account_peppol_edi_mode == 'test'">
Test mode allows registration of the user on the test Peppol network.
By clicking the button below I accept that Odoo may process my e-invoices.
</div>
<div class="mb-3" invisible="not account_peppol_edi_mode == 'demo'">
In demo mode sending and receiving invoices is simulated. There will be no communication with the Peppol network.
</div>
</div>
<div class="d-flex gap-1 action_buttons" colspan="3">
@@ -21,6 +21,7 @@ class AccountMoveSend(models.Model):
string="Warning",
compute="_compute_peppol_warning",
)
account_peppol_edi_mode_info = fields.Char(compute='_compute_account_peppol_edi_mode_info')
# -------------------------------------------------------------------------
# COMPUTE METHODS
@@ -71,6 +72,19 @@ class AccountMoveSend(models.Model):
)
)
@api.depends('company_id.account_edi_proxy_client_ids.edi_mode')
def _compute_account_peppol_edi_mode_info(self):
mode_strings = {
'test': _('Test'),
'demo': _('Demo'),
}
for wizard in self:
edi_user = wizard.company_id.account_edi_proxy_client_ids.filtered(
lambda usr: usr.proxy_type == 'peppol'
)
mode = mode_strings.get(edi_user.edi_mode)
wizard.account_peppol_edi_mode_info = f' ({mode})' if mode else ''
# -------------------------------------------------------------------------
# ATTACHMENTS
# -------------------------------------------------------------------------
@@ -19,6 +19,7 @@
<b>
<label for="checkbox_send_peppol"/>
</b>
<field name="account_peppol_edi_mode_info" class="oe_inline o_form_label"/>
</div>
</xpath>
</field>