Initial commit: OCA Technical packages (595 packages)

This commit is contained in:
Ernad Husremovic 2025-08-29 15:43:03 +02:00
commit 2cc02aac6e
24950 changed files with 2318079 additions and 0 deletions

View file

@ -0,0 +1,106 @@
.. image:: https://odoo-community.org/readme-banner-image
:target: https://odoo-community.org/get-involved?utm_source=readme
:alt: Odoo Community Association
============
Partner Auth
============
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:33a8bc75dc8127331753aa9a54fe3a5b56f7d51a23cc7e9eb0000cc55f78c689
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github
:target: https://github.com/OCA/rest-framework/tree/16.0/auth_partner
:alt: OCA/rest-framework
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/rest-framework-16-0/rest-framework-16-0-auth_partner
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/rest-framework&target_branch=16.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
This module adds to the partners the ability to authenticate through directories.
This module does not implement any routing, it only provides the basic mechanisms in a directory for:
- Registering a partner and sending an welcome email (to validate email address): `_signup`
- Authenticating a partner: `_login`
- Validating a partner email using a token: `_validate_email`
- Impersonating: `_impersonate`, `_impersonating`
- Resetting the password with a unique token sent by mail: `_request_reset_password`, `_set_password`
- Sending an invite mail when registering a partner from odoo interface for the partner to enter a password: `_send_invite`, `_set_password`
For a routing implementation, see the `fastapi_auth_partner <../fastapi_auth_partner>`_ module.
**Table of contents**
.. contents::
:local:
Usage
=====
This module isn't meant to be used standalone but you can still see the directories and authenticable partners in:
Settings > Technical > Partner Authentication > Partner
and
Settings > Technical > Partner Authentication > Directory
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/rest-framework/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/rest-framework/issues/new?body=module:%20auth_partner%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* Akretion
Contributors
~~~~~~~~~~~~
* `Akretion <https://www.akretion.com>`_:
* Sébastien Beau
* Florian Mounier
Maintainers
~~~~~~~~~~~
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
This module is part of the `OCA/rest-framework <https://github.com/OCA/rest-framework/tree/16.0/auth_partner>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View file

@ -0,0 +1,2 @@
from . import models
from . import wizards

View file

@ -0,0 +1,38 @@
# Copyright 2024 Akretion (http://www.akretion.com).
# @author Sébastien BEAU <sebastien.beau@akretion.com>
# @author Florian Mounier <florian.mounier@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
"name": "Partner Auth",
"summary": "Implements the base features for a authenticable partner",
"version": "16.0.1.0.0",
"license": "AGPL-3",
"author": "Akretion,Odoo Community Association (OCA)",
"website": "https://github.com/OCA/rest-framework",
"depends": [
"auth_signup",
"mail",
"queue_job",
"server_environment",
],
"data": [
"security/res_group.xml",
"security/ir.model.access.csv",
"security/ir_rule.xml",
"data/email_data.xml",
"wizards/wizard_auth_partner_force_set_password_view.xml",
"wizards/wizard_auth_partner_reset_password_view.xml",
"views/auth_partner_view.xml",
"views/auth_directory_view.xml",
"views/res_partner_view.xml",
],
"demo": [
"demo/res_partner_demo.xml",
"demo/auth_directory_demo.xml",
"demo/auth_partner_demo.xml",
],
"external_dependencies": {
"python": ["itsdangerous", "pyjwt"],
},
}

View file

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo noupdate="1">
<record id="email_reset_password" model="mail.template">
<field name="name">Auth Directory: Reset Password</field>
<field name="email_from">noreply@example.org</field>
<field name="subject">Reset Password</field>
<field name="partner_to">{{object.partner_id.id}}</field>
<field name="model_id" ref="model_auth_partner" />
<field name="auto_delete" eval="True" />
<field name="lang">${object.partner_id.lang}</field>
<field name="body_html" type="html">
<div>
Hi <t t-out="object.partner_id.name or ''" />
Click on the following link to reset your password
<a
t-attf-href="https://example.org/password/reset?token={{ object.env.context['token']}}"
target="_blank"
style="color:#FFFFFF; text-decoration:none;"
>Reset Password</a>
</div>
</field>
</record>
<record id="email_set_password" model="mail.template">
<field name="name">Auth Directory: Set Password</field>
<field name="email_from">noreply@example.org</field>
<field name="subject">Welcome</field>
<field name="partner_to">{{object.partner_id.id}}</field>
<field name="model_id" ref="model_auth_partner" />
<field name="auto_delete" eval="True" />
<field name="lang">{{object.partner_id.lang}}</field>
<field name="body_html" type="html">
<div>
Hi <t t-out="object.partner_id.name or ''" />
Welcome, your account have been created
Click on the following link to set your password
<a
t-attf-href="https://example.org/password/reset?token={{ object.env.context['token']}}"
target="_blank"
style="color:#FFFFFF; text-decoration:none;"
>Set Password</a>
</div>
</field>
</record>
<record id="email_validate_email" model="mail.template">
<field name="name">Auth Directory: Validate Email</field>
<field name="email_from">noreply@example.org</field>
<field name="subject">Welcome</field>
<field name="partner_to">{{object.partner_id.id}}</field>
<field name="model_id" ref="model_auth_partner" />
<field name="auto_delete" eval="True" />
<field name="lang">{{object.partner_id.lang}}</field>
<field name="body_html" type="html">
<div>
Hi <t t-out="object.partner_id.name or ''" />
Welcome to the site, please click on the following link to verify your email
<a
t-attf-href="https://example.org/email/validate?token={{ object.env.context['token']}}"
target="_blank"
style="color:#FFFFFF; text-decoration:none;"
>Validate Email</a>
</div>
</field>
</record>
</odoo>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="demo_directory" model="auth.directory">
<field name="name">Demo Auth Directory</field>
<field name="reset_password_template_id" ref="email_reset_password" />
<field name="set_password_template_id" ref="email_set_password" />
<field name="validate_email_template_id" ref="email_validate_email" />
</record>
</odoo>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="auth_partner_demo" model="auth.partner">
<field name="partner_id" ref="res_partner_auth_demo" />
<field name="directory_id" ref="demo_directory" />
<field name="password">Super-secret$1</field>
</record>
</odoo>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="res_partner_auth_demo" model="res.partner">
<field name="name">Demo auth partner</field>
<field name="email">partner-auth@example.org</field>
</record>
</odoo>

View file

@ -0,0 +1,554 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * auth_partner
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \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: auth_partner
#: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__14-days
msgid "14 Days"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__2-days
msgid "2-days"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__6-hours
msgid "6 Hours"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__7-days
msgid "7 Days"
msgstr ""
#. module: auth_partner
#: model:mail.template,body_html:auth_partner.email_reset_password
msgid ""
"<div>\n"
" Hi <t t-out=\"object.partner_id.name or ''\"></t>\n"
" Click on the following link to reset your password\n"
" <a t-attf-href=\"https://example.org/password/reset?token={{ object.env.context['token']}}\" target=\"_blank\" style=\"color:#FFFFFF; text-decoration:none;\">Reset Password</a>\n"
" </div>\n"
" "
msgstr ""
#. module: auth_partner
#: model:mail.template,body_html:auth_partner.email_validate_email
msgid ""
"<div>\n"
" Hi <t t-out=\"object.partner_id.name or ''\"></t>\n"
" Welcome to the site, please click on the following link to verify your email\n"
" <a t-attf-href=\"https://example.org/email/validate?token={{ object.env.context['token']}}\" target=\"_blank\" style=\"color:#FFFFFF; text-decoration:none;\">Validate Email</a>\n"
" </div>\n"
" "
msgstr ""
#. module: auth_partner
#: model:mail.template,body_html:auth_partner.email_set_password
msgid ""
"<div>\n"
" Hi <t t-out=\"object.partner_id.name or ''\"></t>\n"
" Welcome, your account have been created\n"
" Click on the following link to set your password\n"
" <a t-attf-href=\"https://example.org/password/reset?token={{ object.env.context['token']}}\" target=\"_blank\" style=\"color:#FFFFFF; text-decoration:none;\">Set Password</a>\n"
" </div>\n"
" "
msgstr ""
#. module: auth_partner
#: model:res.groups,name:auth_partner.group_auth_partner_api
msgid "API Partner Auth Access"
msgstr ""
#. module: auth_partner
#: model:res.groups,name:auth_partner.group_auth_partner_manager
msgid "API Partner Auth Manager"
msgstr ""
#. module: auth_partner
#: model_terms:ir.ui.view,arch_db:auth_partner.auth_directory_view_form
#: model_terms:ir.ui.view,arch_db:auth_partner.view_partner_form
msgid "Account"
msgstr ""
#. module: auth_partner
#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_reset_password_view_form
msgid ""
"An email will be send with a token to each customer, you can specify the "
"date until the link is valid"
msgstr ""
#. module: auth_partner
#: model:ir.model,name:auth_partner.model_auth_directory
msgid "Auth Directory"
msgstr ""
#. module: auth_partner
#: model:mail.template,name:auth_partner.email_reset_password
msgid "Auth Directory: Reset Password"
msgstr ""
#. module: auth_partner
#: model:mail.template,name:auth_partner.email_set_password
msgid "Auth Directory: Set Password"
msgstr ""
#. module: auth_partner
#: model:mail.template,name:auth_partner.email_validate_email
msgid "Auth Directory: Validate Email"
msgstr ""
#. module: auth_partner
#: model:ir.model,name:auth_partner.model_auth_partner
#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_form
#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_search
msgid "Auth Partner"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_res_partner__auth_partner_count
#: model:ir.model.fields,field_description:auth_partner.field_res_users__auth_partner_count
msgid "Auth Partner Count"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__auth_partner_ids
msgid "Auth Partners"
msgstr ""
#. module: auth_partner
#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_force_set_password_view_form
#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_reset_password_view_form
msgid "Cancel"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__password_confirm
msgid "Confirm Password"
msgstr ""
#. module: auth_partner
#: model:ir.model,name:auth_partner.model_res_partner
msgid "Contact"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__count_partner
msgid "Count Partner"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__create_uid
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__create_uid
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__create_uid
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__create_uid
msgid "Created by"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__create_date
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__create_date
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__create_date
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__create_date
msgid "Created on"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__date_last_impersonation
msgid "Date Last Impersonation"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__date_last_request_reset_pwd
msgid "Date Last Request Reset Pwd"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__date_last_sucessfull_reset_pwd
msgid "Date Last Sucessfull Reset Pwd"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__date_validity
msgid "Date Validity"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,help:auth_partner.field_auth_partner__date_last_request_reset_pwd
msgid "Date of the last password reset request"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,help:auth_partner.field_auth_partner__date_last_impersonation
msgid "Date of the last sucessfull impersonation"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,help:auth_partner.field_auth_partner__date_last_sucessfull_reset_pwd
msgid "Date of the last sucessfull password reset"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__delay
msgid "Delay"
msgstr ""
#. module: auth_partner
#: model:ir.actions.act_window,name:auth_partner.auth_directory_action
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__directory_id
#: model:ir.ui.menu,name:auth_partner.auth_directory_menu
#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_search
msgid "Directory"
msgstr ""
#. module: auth_partner
#: model_terms:ir.ui.view,arch_db:auth_partner.auth_directory_view_form
#: model_terms:ir.ui.view,arch_db:auth_partner.auth_directory_view_search
msgid "Directory Auth"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__display_name
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__display_name
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__display_name
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__display_name
msgid "Display Name"
msgstr ""
#. module: auth_partner
#. odoo-python
#: code:addons/auth_partner/models/auth_partner.py:0
#, python-format
msgid ""
"Email address not validated. Validate your email address by clicking on the "
"link in the email sent to you or request a new password. "
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__encrypted_password
msgid "Encrypted Password"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__force_verified_email
msgid "Force Verified Email"
msgstr ""
#. module: auth_partner
#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_search
msgid "Group By"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__id
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__id
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__id
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__id
msgid "ID"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,help:auth_partner.field_auth_directory__force_verified_email
msgid "If checked, email must be verified to be able to log in"
msgstr ""
#. module: auth_partner
#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_form
msgid "Impersonate"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__impersonating_token_duration
msgid "Impersonating Token Duration"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__impersonating_user_ids
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__impersonating_user_ids
msgid "Impersonating Users"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,help:auth_partner.field_auth_directory__set_password_token_duration
msgid "In minute, default 1440 minutes => 24h"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,help:auth_partner.field_auth_directory__impersonating_token_duration
msgid "In seconds, default 60 seconds"
msgstr ""
#. module: auth_partner
#. odoo-python
#: code:addons/auth_partner/models/auth_partner.py:0
#, python-format
msgid "Invalid Login or Password"
msgstr ""
#. module: auth_partner
#. odoo-python
#: code:addons/auth_partner/models/auth_directory.py:0
#: code:addons/auth_partner/models/auth_directory.py:0
#: code:addons/auth_partner/models/auth_directory.py:0
#, python-format
msgid "Invalid Token"
msgstr ""
#. module: auth_partner
#. odoo-python
#: code:addons/auth_partner/models/auth_directory.py:0
#, python-format
msgid "Invalid token"
msgstr ""
#. module: auth_partner
#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_force_set_password_view_form
#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_reset_password_view_form
msgid "Label"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory____last_update
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner____last_update
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password____last_update
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password____last_update
msgid "Last Modified on"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__write_uid
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__write_uid
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__write_uid
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__write_uid
msgid "Last Updated by"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__write_date
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__write_date
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__write_date
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__write_date
msgid "Last Updated on"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__login
msgid "Login"
msgstr ""
#. module: auth_partner
#: model:ir.model.constraint,message:auth_partner.constraint_auth_partner_directory_login_uniq
msgid "Login must be uniq per directory !"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__template_id
msgid "Mail Template"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__reset_password_template_id
msgid "Mail Template Forget Password"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__set_password_template_id
msgid "Mail Template New Password"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__validate_email_template_id
msgid "Mail Template Validate Email"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__mail_verified
msgid "Mail Verified"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__manually
msgid "Manually"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__name
msgid "Name"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__nbr_pending_reset_sent
msgid "Nbr Pending Reset Sent"
msgstr ""
#. module: auth_partner
#. odoo-python
#: code:addons/auth_partner/wizards/wizard_auth_partner_force_set_password.py:0
#, python-format
msgid "No active_id in context"
msgstr ""
#. module: auth_partner
#. odoo-python
#: code:addons/auth_partner/models/auth_directory.py:0
#, python-format
msgid "No email template defined for %(template)s in %(directory)s"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,help:auth_partner.field_auth_partner__nbr_pending_reset_sent
msgid ""
"Number of pending reset sent from your customer.This field is usefull when "
"after a migration from an other system you ask all you customer to reset "
"their password and you senddifferent mail depending on the number of "
"reminder"
msgstr ""
#. module: auth_partner
#: model:ir.actions.act_window,name:auth_partner.auth_partner_action
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__partner_id
#: model:ir.ui.menu,name:auth_partner.auth_partner_menu
msgid "Partner"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_res_partner__auth_partner_ids
#: model:ir.model.fields,field_description:auth_partner.field_res_users__auth_partner_ids
msgid "Partner Auth"
msgstr ""
#. module: auth_partner
#: model:ir.ui.menu,name:auth_partner.auth
msgid "Partner Authentication"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__password
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__password
msgid "Password"
msgstr ""
#. module: auth_partner
#. odoo-python
#: code:addons/auth_partner/wizards/wizard_auth_partner_force_set_password.py:0
#, python-format
msgid "Password and Confirm Password must be the same"
msgstr ""
#. module: auth_partner
#: model_terms:ir.ui.view,arch_db:auth_partner.auth_directory_view_form
msgid "Regenerate secret key"
msgstr ""
#. module: auth_partner
#: model:mail.template,subject:auth_partner.email_reset_password
msgid "Reset Password"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__secret_key
msgid "Secret Key"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__secret_key_env_default
msgid "Secret Key Env Default"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__secret_key_env_is_editable
msgid "Secret Key Env Is Editable"
msgstr ""
#. module: auth_partner
#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_form
msgid "Send Invite"
msgstr ""
#. module: auth_partner
#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_reset_password_view_form
msgid "Send Reset Password"
msgstr ""
#. module: auth_partner
#: model:ir.actions.act_window,name:auth_partner.auth_partner_action_reset_password
msgid "Send Reset Password Instruction"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__server_env_defaults
msgid "Server Env Defaults"
msgstr ""
#. module: auth_partner
#: model:ir.actions.act_window,name:auth_partner.auth_partner_force_set_password_action
#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_form
#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_force_set_password_view_form
msgid "Set Password"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__set_password_token_duration
msgid "Set Password Token Duration"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,help:auth_partner.field_auth_partner__user_can_impersonate
msgid "Technical field to check if the user can impersonate"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,help:auth_partner.field_auth_directory__impersonating_user_ids
#: model:ir.model.fields,help:auth_partner.field_auth_partner__impersonating_user_ids
msgid "These odoo users can impersonate any partner of this directory"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,help:auth_partner.field_auth_partner__mail_verified
msgid ""
"This field is set to True when the user has clicked on the link sent by "
"email"
msgstr ""
#. module: auth_partner
#. odoo-python
#: code:addons/auth_partner/wizards/wizard_auth_partner_force_set_password.py:0
#, python-format
msgid "This wizard can only be used on auth.partner"
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__user_can_impersonate
msgid "User Can Impersonate"
msgstr ""
#. module: auth_partner
#: model:mail.template,subject:auth_partner.email_set_password
#: model:mail.template,subject:auth_partner.email_validate_email
msgid "Welcome"
msgstr ""
#. module: auth_partner
#: model:ir.model,name:auth_partner.model_wizard_auth_partner_force_set_password
#: model:ir.model,name:auth_partner.model_wizard_auth_partner_reset_password
msgid "Wizard Partner Auth Reset Password"
msgstr ""
#. module: auth_partner
#. odoo-python
#: code:addons/auth_partner/models/auth_partner.py:0
#, python-format
msgid "You are not allowed to impersonate this user"
msgstr ""

View file

@ -0,0 +1,554 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * auth_partner
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \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: auth_partner
#: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__14-days
msgid "14 Days"
msgstr "14 dana"
#. module: auth_partner
#: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__2-days
msgid "2-days"
msgstr "2 dana"
#. module: auth_partner
#: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__6-hours
msgid "6 Hours"
msgstr "6 sati"
#. module: auth_partner
#: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__7-days
msgid "7 Days"
msgstr "7 dana"
#. module: auth_partner
#: model:mail.template,body_html:auth_partner.email_reset_password
msgid ""
"<div>\n"
" Hi <t t-out=\"object.partner_id.name or ''\"></t>\n"
" Click on the following link to reset your password\n"
" <a t-attf-href=\"https://example.org/password/reset?token={{ object.env.context['token']}}\" target=\"_blank\" style=\"color:#FFFFFF; text-decoration:none;\">Reset Password</a>\n"
" </div>\n"
" "
msgstr ""
#. module: auth_partner
#: model:mail.template,body_html:auth_partner.email_validate_email
msgid ""
"<div>\n"
" Hi <t t-out=\"object.partner_id.name or ''\"></t>\n"
" Welcome to the site, please click on the following link to verify your email\n"
" <a t-attf-href=\"https://example.org/email/validate?token={{ object.env.context['token']}}\" target=\"_blank\" style=\"color:#FFFFFF; text-decoration:none;\">Validate Email</a>\n"
" </div>\n"
" "
msgstr ""
#. module: auth_partner
#: model:mail.template,body_html:auth_partner.email_set_password
msgid ""
"<div>\n"
" Hi <t t-out=\"object.partner_id.name or ''\"></t>\n"
" Welcome, your account have been created\n"
" Click on the following link to set your password\n"
" <a t-attf-href=\"https://example.org/password/reset?token={{ object.env.context['token']}}\" target=\"_blank\" style=\"color:#FFFFFF; text-decoration:none;\">Set Password</a>\n"
" </div>\n"
" "
msgstr ""
#. module: auth_partner
#: model:res.groups,name:auth_partner.group_auth_partner_api
msgid "API Partner Auth Access"
msgstr "API partner Auth pristup"
#. module: auth_partner
#: model:res.groups,name:auth_partner.group_auth_partner_manager
msgid "API Partner Auth Manager"
msgstr "API partner Auth menadžer"
#. module: auth_partner
#: model_terms:ir.ui.view,arch_db:auth_partner.auth_directory_view_form
#: model_terms:ir.ui.view,arch_db:auth_partner.view_partner_form
msgid "Account"
msgstr "Konto"
#. module: auth_partner
#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_reset_password_view_form
msgid ""
"An email will be send with a token to each customer, you can specify the "
"date until the link is valid"
msgstr ""
#. module: auth_partner
#: model:ir.model,name:auth_partner.model_auth_directory
msgid "Auth Directory"
msgstr "Auth direktorij"
#. module: auth_partner
#: model:mail.template,name:auth_partner.email_reset_password
msgid "Auth Directory: Reset Password"
msgstr "Auth direktorij: Resetuj lozinku"
#. module: auth_partner
#: model:mail.template,name:auth_partner.email_set_password
msgid "Auth Directory: Set Password"
msgstr "Auth direktorij: Postavite lozinku"
#. module: auth_partner
#: model:mail.template,name:auth_partner.email_validate_email
msgid "Auth Directory: Validate Email"
msgstr "Auth direktorij: Validiraj e-mail"
#. module: auth_partner
#: model:ir.model,name:auth_partner.model_auth_partner
#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_form
#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_search
msgid "Auth Partner"
msgstr "Auth partner"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_res_partner__auth_partner_count
#: model:ir.model.fields,field_description:auth_partner.field_res_users__auth_partner_count
msgid "Auth Partner Count"
msgstr "Broj Auth partnera"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__auth_partner_ids
msgid "Auth Partners"
msgstr "Auth partneri"
#. module: auth_partner
#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_force_set_password_view_form
#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_reset_password_view_form
msgid "Cancel"
msgstr "Otkaži"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__password_confirm
msgid "Confirm Password"
msgstr "Potvrdi šifru"
#. module: auth_partner
#: model:ir.model,name:auth_partner.model_res_partner
msgid "Contact"
msgstr "Kontakt"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__count_partner
msgid "Count Partner"
msgstr "Broj partnera"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__create_uid
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__create_uid
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__create_uid
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__create_uid
msgid "Created by"
msgstr "Kreirao"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__create_date
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__create_date
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__create_date
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__create_date
msgid "Created on"
msgstr "Kreirano"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__date_last_impersonation
msgid "Date Last Impersonation"
msgstr "Datum poslednjeg oponašanja"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__date_last_request_reset_pwd
msgid "Date Last Request Reset Pwd"
msgstr "Datum poslednjeg zahtjeva za resetovanje lozinke"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__date_last_sucessfull_reset_pwd
msgid "Date Last Sucessfull Reset Pwd"
msgstr "Datum poslednjeg uspješnog resetovanja lozinke"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__date_validity
msgid "Date Validity"
msgstr "Datum važenja"
#. module: auth_partner
#: model:ir.model.fields,help:auth_partner.field_auth_partner__date_last_request_reset_pwd
msgid "Date of the last password reset request"
msgstr "Datum poslednjeg zahtjeva za resetovanje lozinke"
#. module: auth_partner
#: model:ir.model.fields,help:auth_partner.field_auth_partner__date_last_impersonation
msgid "Date of the last sucessfull impersonation"
msgstr "Datum poslednjeg uspješnog oponašanja"
#. module: auth_partner
#: model:ir.model.fields,help:auth_partner.field_auth_partner__date_last_sucessfull_reset_pwd
msgid "Date of the last sucessfull password reset"
msgstr "Datum poslednjeg uspješnog resetovanja lozinke"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__delay
msgid "Delay"
msgstr "Kašnjenje"
#. module: auth_partner
#: model:ir.actions.act_window,name:auth_partner.auth_directory_action
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__directory_id
#: model:ir.ui.menu,name:auth_partner.auth_directory_menu
#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_search
msgid "Directory"
msgstr "Imenik"
#. module: auth_partner
#: model_terms:ir.ui.view,arch_db:auth_partner.auth_directory_view_form
#: model_terms:ir.ui.view,arch_db:auth_partner.auth_directory_view_search
msgid "Directory Auth"
msgstr "Directory Auth"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__display_name
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__display_name
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__display_name
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__display_name
msgid "Display Name"
msgstr "Prikazani naziv"
#. module: auth_partner
#. odoo-python
#: code:addons/auth_partner/models/auth_partner.py:0
#, python-format
msgid ""
"Email address not validated. Validate your email address by clicking on the "
"link in the email sent to you or request a new password. "
msgstr ""
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__encrypted_password
msgid "Encrypted Password"
msgstr "Kriptirana lozina"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__force_verified_email
msgid "Force Verified Email"
msgstr "Forsiraj verificiran e-mail"
#. module: auth_partner
#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_search
msgid "Group By"
msgstr "Grupiši po"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__id
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__id
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__id
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__id
msgid "ID"
msgstr "ID"
#. module: auth_partner
#: model:ir.model.fields,help:auth_partner.field_auth_directory__force_verified_email
msgid "If checked, email must be verified to be able to log in"
msgstr "Ako je označeno, e-mail mora biti verificiran da bi se mogao prijaviti"
#. module: auth_partner
#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_form
msgid "Impersonate"
msgstr "Oponašaj"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__impersonating_token_duration
msgid "Impersonating Token Duration"
msgstr "Trajanje oponašavajućeg tokena"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__impersonating_user_ids
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__impersonating_user_ids
msgid "Impersonating Users"
msgstr "Oponašavajući korisnici"
#. module: auth_partner
#: model:ir.model.fields,help:auth_partner.field_auth_directory__set_password_token_duration
msgid "In minute, default 1440 minutes => 24h"
msgstr "U minutama, default 1440 minuta => 24h"
#. module: auth_partner
#: model:ir.model.fields,help:auth_partner.field_auth_directory__impersonating_token_duration
msgid "In seconds, default 60 seconds"
msgstr "U sekundama, default 60 sekundi"
#. module: auth_partner
#. odoo-python
#: code:addons/auth_partner/models/auth_partner.py:0
#, python-format
msgid "Invalid Login or Password"
msgstr "Nevažeća prijava ili lozinka"
#. module: auth_partner
#. odoo-python
#: code:addons/auth_partner/models/auth_directory.py:0
#: code:addons/auth_partner/models/auth_directory.py:0
#: code:addons/auth_partner/models/auth_directory.py:0
#, python-format
msgid "Invalid Token"
msgstr "Nevažeći token"
#. module: auth_partner
#. odoo-python
#: code:addons/auth_partner/models/auth_directory.py:0
#, python-format
msgid "Invalid token"
msgstr "Nevažeći token"
#. module: auth_partner
#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_force_set_password_view_form
#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_reset_password_view_form
msgid "Label"
msgstr "Opis"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory____last_update
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner____last_update
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password____last_update
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password____last_update
msgid "Last Modified on"
msgstr "Zadnje mijenjano"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__write_uid
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__write_uid
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__write_uid
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__write_uid
msgid "Last Updated by"
msgstr "Zadnji ažurirao"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__write_date
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__write_date
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__write_date
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__write_date
msgid "Last Updated on"
msgstr "Zadnje ažurirano"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__login
msgid "Login"
msgstr "Prijava"
#. module: auth_partner
#: model:ir.model.constraint,message:auth_partner.constraint_auth_partner_directory_login_uniq
msgid "Login must be uniq per directory !"
msgstr "Prijava mora biti jedinstvena po direktoriju!"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__template_id
msgid "Mail Template"
msgstr "Predložak maila"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__reset_password_template_id
msgid "Mail Template Forget Password"
msgstr "Mail template za zaboravljenu lozinku"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__set_password_template_id
msgid "Mail Template New Password"
msgstr "Mail template nova lozinka"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__validate_email_template_id
msgid "Mail Template Validate Email"
msgstr "Mail template za validaciju e-maila"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__mail_verified
msgid "Mail Verified"
msgstr "E-mail verificiran"
#. module: auth_partner
#: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__manually
msgid "Manually"
msgstr "ručno"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__name
msgid "Name"
msgstr "Naziv:"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__nbr_pending_reset_sent
msgid "Nbr Pending Reset Sent"
msgstr "Broj čekajućih resetovanja poslanih"
#. module: auth_partner
#. odoo-python
#: code:addons/auth_partner/wizards/wizard_auth_partner_force_set_password.py:0
#, python-format
msgid "No active_id in context"
msgstr "Nema active_id u kontekstu"
#. module: auth_partner
#. odoo-python
#: code:addons/auth_partner/models/auth_directory.py:0
#, python-format
msgid "No email template defined for %(template)s in %(directory)s"
msgstr "Nema definirani e-mail template za %(template)s u %(directory)s"
#. module: auth_partner
#: model:ir.model.fields,help:auth_partner.field_auth_partner__nbr_pending_reset_sent
msgid ""
"Number of pending reset sent from your customer.This field is usefull when "
"after a migration from an other system you ask all you customer to reset "
"their password and you senddifferent mail depending on the number of "
"reminder"
msgstr ""
#. module: auth_partner
#: model:ir.actions.act_window,name:auth_partner.auth_partner_action
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__partner_id
#: model:ir.ui.menu,name:auth_partner.auth_partner_menu
msgid "Partner"
msgstr "Partner"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_res_partner__auth_partner_ids
#: model:ir.model.fields,field_description:auth_partner.field_res_users__auth_partner_ids
msgid "Partner Auth"
msgstr "Partner Auth"
#. module: auth_partner
#: model:ir.ui.menu,name:auth_partner.auth
msgid "Partner Authentication"
msgstr "Partner autentifikacija"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__password
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__password
msgid "Password"
msgstr "Zaporka"
#. module: auth_partner
#. odoo-python
#: code:addons/auth_partner/wizards/wizard_auth_partner_force_set_password.py:0
#, python-format
msgid "Password and Confirm Password must be the same"
msgstr "Lozinka i potvrda lozinke moraju biti iste"
#. module: auth_partner
#: model_terms:ir.ui.view,arch_db:auth_partner.auth_directory_view_form
msgid "Regenerate secret key"
msgstr "Generiši tajni ključ ponovo"
#. module: auth_partner
#: model:mail.template,subject:auth_partner.email_reset_password
msgid "Reset Password"
msgstr "Promijeni lozinku"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__secret_key
msgid "Secret Key"
msgstr "Tajni ključ"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__secret_key_env_default
msgid "Secret Key Env Default"
msgstr "Tajni ključ Env default"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__secret_key_env_is_editable
msgid "Secret Key Env Is Editable"
msgstr "Tajni ključ Env je editabilan"
#. module: auth_partner
#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_form
msgid "Send Invite"
msgstr "Pošalji pozivnicu"
#. module: auth_partner
#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_reset_password_view_form
msgid "Send Reset Password"
msgstr "Pošalji resetovanje lozinke"
#. module: auth_partner
#: model:ir.actions.act_window,name:auth_partner.auth_partner_action_reset_password
msgid "Send Reset Password Instruction"
msgstr "Pošalji instrukcije za resetovanje lozinke"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__server_env_defaults
msgid "Server Env Defaults"
msgstr "Server Env defaults"
#. module: auth_partner
#: model:ir.actions.act_window,name:auth_partner.auth_partner_force_set_password_action
#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_form
#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_force_set_password_view_form
msgid "Set Password"
msgstr "Postavi šifru"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__set_password_token_duration
msgid "Set Password Token Duration"
msgstr "Trajanje tokena postavljanja lozinke"
#. module: auth_partner
#: model:ir.model.fields,help:auth_partner.field_auth_partner__user_can_impersonate
msgid "Technical field to check if the user can impersonate"
msgstr "Tehničko polje za provjeru da li korisnik može oponašati"
#. module: auth_partner
#: model:ir.model.fields,help:auth_partner.field_auth_directory__impersonating_user_ids
#: model:ir.model.fields,help:auth_partner.field_auth_partner__impersonating_user_ids
msgid "These odoo users can impersonate any partner of this directory"
msgstr "Ovi odoo korisnici mogu oponašati bilo kojeg partnera ovog direktorija"
#. module: auth_partner
#: model:ir.model.fields,help:auth_partner.field_auth_partner__mail_verified
msgid ""
"This field is set to True when the user has clicked on the link sent by "
"email"
msgstr ""
#. module: auth_partner
#. odoo-python
#: code:addons/auth_partner/wizards/wizard_auth_partner_force_set_password.py:0
#, python-format
msgid "This wizard can only be used on auth.partner"
msgstr "Ovaj čarobnjak se može koristiti samo na auth.partner"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__user_can_impersonate
msgid "User Can Impersonate"
msgstr "Korisnik može oponašati"
#. module: auth_partner
#: model:mail.template,subject:auth_partner.email_set_password
#: model:mail.template,subject:auth_partner.email_validate_email
msgid "Welcome"
msgstr "Dobrodošli"
#. module: auth_partner
#: model:ir.model,name:auth_partner.model_wizard_auth_partner_force_set_password
#: model:ir.model,name:auth_partner.model_wizard_auth_partner_reset_password
msgid "Wizard Partner Auth Reset Password"
msgstr "Čarobnjak partner auth resetovanje lozinke"
#. module: auth_partner
#. odoo-python
#: code:addons/auth_partner/models/auth_partner.py:0
#, python-format
msgid "You are not allowed to impersonate this user"
msgstr "Nije vam dozvoljeno oponašati ovog korisnika"

View file

@ -0,0 +1,598 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * auth_partner
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-06-25 09:25+0000\n"
"Last-Translator: mymage <stefano.consolaro@mymage.it>\n"
"Language-Team: none\n"
"Language: it\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.10.4\n"
#. module: auth_partner
#: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__14-days
msgid "14 Days"
msgstr "14 Giorni"
#. module: auth_partner
#: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__2-days
msgid "2-days"
msgstr "2 Giorni"
#. module: auth_partner
#: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__6-hours
msgid "6 Hours"
msgstr "6 Ore"
#. module: auth_partner
#: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__7-days
msgid "7 Days"
msgstr "7 Giorni"
#. module: auth_partner
#: model:mail.template,body_html:auth_partner.email_reset_password
msgid ""
"<div>\n"
" Hi <t t-out=\"object.partner_id.name or ''\"></t>\n"
" Click on the following link to reset your password\n"
" <a t-attf-href=\"https://example.org/password/reset?token={{ object.env.context['token']}}\" target=\"_blank\" style=\"color:#FFFFFF; text-decoration:none;\">Reset Password</a>\n"
" </div>\n"
" "
msgstr ""
"<div>\n"
" Salve <t t-out=\"object.partner_id.name or ''\"></t>\n"
" fare clic sul seguente collegamento per resettare la passwrod\n"
" <a t-attf-href="
"\"https://example.org/password/reset?token={{ object.env.context['token']}}\""
" target=\"_blank\" style=\"color:#FFFFFF; text-decoration:none;\">Resetta "
"password</a>\n"
" </div>\n"
" "
#. module: auth_partner
#: model:mail.template,body_html:auth_partner.email_validate_email
msgid ""
"<div>\n"
" Hi <t t-out=\"object.partner_id.name or ''\"></t>\n"
" Welcome to the site, please click on the following link to verify your email\n"
" <a t-attf-href=\"https://example.org/email/validate?token={{ object.env.context['token']}}\" target=\"_blank\" style=\"color:#FFFFFF; text-decoration:none;\">Validate Email</a>\n"
" </div>\n"
" "
msgstr ""
"<div>\n"
" Salve <t t-out=\"object.partner_id.name or ''\"></t>\n"
" Benvenuto sul sito, clicca sul seguente collegamento per "
"verificare la tua e-mail\n"
" <a t-attf-href="
"\"https://example.org/email/validate?token={{ object.env.context['token']}}\""
" target=\"_blank\" style=\"color:#FFFFFF; text-decoration:none;\">Valida "
"e-mail</a>\n"
" </div>\n"
" "
#. module: auth_partner
#: model:mail.template,body_html:auth_partner.email_set_password
msgid ""
"<div>\n"
" Hi <t t-out=\"object.partner_id.name or ''\"></t>\n"
" Welcome, your account have been created\n"
" Click on the following link to set your password\n"
" <a t-attf-href=\"https://example.org/password/reset?token={{ object.env.context['token']}}\" target=\"_blank\" style=\"color:#FFFFFF; text-decoration:none;\">Set Password</a>\n"
" </div>\n"
" "
msgstr ""
"<div>\n"
" Salve <t t-out=\"object.partner_id.name or ''\"></t>\n"
" Benvenuto, il tuo account è stato creato\n"
" Clicca sul collegamento seguente per impostare la tua password\n"
" <a t-attf-href="
"\"https://example.org/password/reset?token={{ object.env.context['token']}}\""
" target=\"_blank\" style=\"color:#FFFFFF; text-decoration:none;\">Imposta "
"password</a>\n"
" </div>\n"
" "
#. module: auth_partner
#: model:res.groups,name:auth_partner.group_auth_partner_api
msgid "API Partner Auth Access"
msgstr "Autorizzazione accesso API del partner"
#. module: auth_partner
#: model:res.groups,name:auth_partner.group_auth_partner_manager
msgid "API Partner Auth Manager"
msgstr "Responsabile autorizzazione API del partner"
#. module: auth_partner
#: model_terms:ir.ui.view,arch_db:auth_partner.auth_directory_view_form
#: model_terms:ir.ui.view,arch_db:auth_partner.view_partner_form
msgid "Account"
msgstr "Conto"
#. module: auth_partner
#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_reset_password_view_form
msgid ""
"An email will be send with a token to each customer, you can specify the "
"date until the link is valid"
msgstr ""
"Verrà inviata una e-mail con un token ad ogni cliente, si può indicare la "
"data entro cui il collegamento è valido"
#. module: auth_partner
#: model:ir.model,name:auth_partner.model_auth_directory
msgid "Auth Directory"
msgstr "Cartella autorizzazione"
#. module: auth_partner
#: model:mail.template,name:auth_partner.email_reset_password
msgid "Auth Directory: Reset Password"
msgstr "Cartella autorizzazione: reimposta password"
#. module: auth_partner
#: model:mail.template,name:auth_partner.email_set_password
msgid "Auth Directory: Set Password"
msgstr "Cartella autorizzazione: imposta password"
#. module: auth_partner
#: model:mail.template,name:auth_partner.email_validate_email
msgid "Auth Directory: Validate Email"
msgstr "Cartella autorizzazione: valida e-mail"
#. module: auth_partner
#: model:ir.model,name:auth_partner.model_auth_partner
#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_form
#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_search
msgid "Auth Partner"
msgstr "Partner autorizzazione"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_res_partner__auth_partner_count
#: model:ir.model.fields,field_description:auth_partner.field_res_users__auth_partner_count
msgid "Auth Partner Count"
msgstr "Conteggio partner autorizzazione"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__auth_partner_ids
msgid "Auth Partners"
msgstr "Partner autorizzazione"
#. module: auth_partner
#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_force_set_password_view_form
#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_reset_password_view_form
msgid "Cancel"
msgstr "Annulla"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__password_confirm
msgid "Confirm Password"
msgstr "Conferma password"
#. module: auth_partner
#: model:ir.model,name:auth_partner.model_res_partner
msgid "Contact"
msgstr "Contatto"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__count_partner
msgid "Count Partner"
msgstr "Conteggio partner"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__create_uid
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__create_uid
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__create_uid
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__create_uid
msgid "Created by"
msgstr "Creato da"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__create_date
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__create_date
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__create_date
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__create_date
msgid "Created on"
msgstr "Creato il"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__date_last_impersonation
msgid "Date Last Impersonation"
msgstr "Data ultima imitazione"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__date_last_request_reset_pwd
msgid "Date Last Request Reset Pwd"
msgstr "Data ultima richiesta reimpostazione password"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__date_last_sucessfull_reset_pwd
msgid "Date Last Sucessfull Reset Pwd"
msgstr "Data ultima reimpostazione password riuscita"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__date_validity
msgid "Date Validity"
msgstr "Validità data"
#. module: auth_partner
#: model:ir.model.fields,help:auth_partner.field_auth_partner__date_last_request_reset_pwd
msgid "Date of the last password reset request"
msgstr "Data ultima richiesta reimpostazione password"
#. module: auth_partner
#: model:ir.model.fields,help:auth_partner.field_auth_partner__date_last_impersonation
msgid "Date of the last sucessfull impersonation"
msgstr "Data ultima imitazione riuscita"
#. module: auth_partner
#: model:ir.model.fields,help:auth_partner.field_auth_partner__date_last_sucessfull_reset_pwd
msgid "Date of the last sucessfull password reset"
msgstr "Data ultima reimpostazione password riuscita"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__delay
msgid "Delay"
msgstr "Ritardo"
#. module: auth_partner
#: model:ir.actions.act_window,name:auth_partner.auth_directory_action
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__directory_id
#: model:ir.ui.menu,name:auth_partner.auth_directory_menu
#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_search
msgid "Directory"
msgstr "Cartella"
#. module: auth_partner
#: model_terms:ir.ui.view,arch_db:auth_partner.auth_directory_view_form
#: model_terms:ir.ui.view,arch_db:auth_partner.auth_directory_view_search
msgid "Directory Auth"
msgstr "Autorizzazione cartella"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__display_name
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__display_name
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__display_name
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__display_name
msgid "Display Name"
msgstr "Nome visualizzato"
#. module: auth_partner
#. odoo-python
#: code:addons/auth_partner/models/auth_partner.py:0
#, python-format
msgid ""
"Email address not validated. Validate your email address by clicking on the "
"link in the email sent to you or request a new password. "
msgstr ""
"Indirizzo e-mail non validato. Validare il proprio indirizzo e-mail facendo "
"click sul collegamento nella e-mail inviata per la richiesta di una nuova "
"password. "
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__encrypted_password
msgid "Encrypted Password"
msgstr "Password criptata"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__force_verified_email
msgid "Force Verified Email"
msgstr "Forza e-mail verificata"
#. module: auth_partner
#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_search
msgid "Group By"
msgstr "Raggruppa per"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__id
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__id
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__id
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__id
msgid "ID"
msgstr "ID"
#. module: auth_partner
#: model:ir.model.fields,help:auth_partner.field_auth_directory__force_verified_email
msgid "If checked, email must be verified to be able to log in"
msgstr ""
"Se selezionata, l'e-mail deve essere verificata per consentire l'accesso"
#. module: auth_partner
#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_form
msgid "Impersonate"
msgstr "Imita"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__impersonating_token_duration
msgid "Impersonating Token Duration"
msgstr "Durata token imitazione"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__impersonating_user_ids
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__impersonating_user_ids
msgid "Impersonating Users"
msgstr "Utenti imitazione"
#. module: auth_partner
#: model:ir.model.fields,help:auth_partner.field_auth_directory__set_password_token_duration
msgid "In minute, default 1440 minutes => 24h"
msgstr "In minuti,predefinito 1440 minuti => 24 ore"
#. module: auth_partner
#: model:ir.model.fields,help:auth_partner.field_auth_directory__impersonating_token_duration
msgid "In seconds, default 60 seconds"
msgstr "In secondi, predefinito 60 secondi"
#. module: auth_partner
#. odoo-python
#: code:addons/auth_partner/models/auth_partner.py:0
#, python-format
msgid "Invalid Login or Password"
msgstr "Nome o password errati"
#. module: auth_partner
#. odoo-python
#: code:addons/auth_partner/models/auth_directory.py:0
#: code:addons/auth_partner/models/auth_directory.py:0
#: code:addons/auth_partner/models/auth_directory.py:0
#, python-format
msgid "Invalid Token"
msgstr "Token non valido"
#. module: auth_partner
#. odoo-python
#: code:addons/auth_partner/models/auth_directory.py:0
#, python-format
msgid "Invalid token"
msgstr "Token non valido"
#. module: auth_partner
#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_force_set_password_view_form
#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_reset_password_view_form
msgid "Label"
msgstr "Etichetta"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory____last_update
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner____last_update
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password____last_update
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password____last_update
msgid "Last Modified on"
msgstr "Ultima modifica il"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__write_uid
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__write_uid
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__write_uid
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__write_uid
msgid "Last Updated by"
msgstr "Ultimo aggiornamento di"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__write_date
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__write_date
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__write_date
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__write_date
msgid "Last Updated on"
msgstr "Ultimo aggiornamento il"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__login
msgid "Login"
msgstr "Login"
#. module: auth_partner
#: model:ir.model.constraint,message:auth_partner.constraint_auth_partner_directory_login_uniq
msgid "Login must be uniq per directory !"
msgstr "La login deve essere univoca per cartella!"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__template_id
msgid "Mail Template"
msgstr "Modello e-mail"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__reset_password_template_id
msgid "Mail Template Forget Password"
msgstr "Modello e-mail password dimenticata"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__set_password_template_id
msgid "Mail Template New Password"
msgstr "Modello e-mail nuova password"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__validate_email_template_id
msgid "Mail Template Validate Email"
msgstr "Modello e-mail validazione e-mail"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__mail_verified
msgid "Mail Verified"
msgstr "E-mail verificata"
#. module: auth_partner
#: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__manually
msgid "Manually"
msgstr "Manualmente"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__name
msgid "Name"
msgstr "Nome"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__nbr_pending_reset_sent
msgid "Nbr Pending Reset Sent"
msgstr "N° reset inviati in attesa"
#. module: auth_partner
#. odoo-python
#: code:addons/auth_partner/wizards/wizard_auth_partner_force_set_password.py:0
#, python-format
msgid "No active_id in context"
msgstr "Manca active_id nel context"
#. module: auth_partner
#. odoo-python
#: code:addons/auth_partner/models/auth_directory.py:0
#, python-format
msgid "No email template defined for %(template)s in %(directory)s"
msgstr "Modello e-mail non definito per %(template)s in %(directory)s"
#. module: auth_partner
#: model:ir.model.fields,help:auth_partner.field_auth_partner__nbr_pending_reset_sent
msgid ""
"Number of pending reset sent from your customer.This field is usefull when "
"after a migration from an other system you ask all you customer to reset "
"their password and you senddifferent mail depending on the number of "
"reminder"
msgstr ""
"Numero di reimpostazioni in sospeso inviate dal cliente. Questo campo è "
"utile quando, dopo una migrazione da un altro sistema, si chiede a tutti i "
"tuoi clienti di reimpostare la propria password e si inviano e-mail diverse "
"a seconda del numero di promemoria"
#. module: auth_partner
#: model:ir.actions.act_window,name:auth_partner.auth_partner_action
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__partner_id
#: model:ir.ui.menu,name:auth_partner.auth_partner_menu
msgid "Partner"
msgstr "Partner"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_res_partner__auth_partner_ids
#: model:ir.model.fields,field_description:auth_partner.field_res_users__auth_partner_ids
msgid "Partner Auth"
msgstr "Autorizzazione partner"
#. module: auth_partner
#: model:ir.ui.menu,name:auth_partner.auth
msgid "Partner Authentication"
msgstr "Autenticazione partner"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__password
#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__password
msgid "Password"
msgstr "Password"
#. module: auth_partner
#. odoo-python
#: code:addons/auth_partner/wizards/wizard_auth_partner_force_set_password.py:0
#, python-format
msgid "Password and Confirm Password must be the same"
msgstr "La password e la conferma devono essere uguali"
#. module: auth_partner
#: model_terms:ir.ui.view,arch_db:auth_partner.auth_directory_view_form
msgid "Regenerate secret key"
msgstr "Rigenera chiave segreta"
#. module: auth_partner
#: model:mail.template,subject:auth_partner.email_reset_password
msgid "Reset Password"
msgstr "Resetta password"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__secret_key
msgid "Secret Key"
msgstr "Chiave segreta"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__secret_key_env_default
msgid "Secret Key Env Default"
msgstr "Chiave segreta ambiente predefinita"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__secret_key_env_is_editable
msgid "Secret Key Env Is Editable"
msgstr "La chiave segreta è modificabile"
#. module: auth_partner
#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_form
msgid "Send Invite"
msgstr "Invia invito"
#. module: auth_partner
#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_reset_password_view_form
msgid "Send Reset Password"
msgstr "Invia reset password"
#. module: auth_partner
#: model:ir.actions.act_window,name:auth_partner.auth_partner_action_reset_password
msgid "Send Reset Password Instruction"
msgstr "Invia istruzioni reset password"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__server_env_defaults
msgid "Server Env Defaults"
msgstr "Predefiniti ambiente server"
#. module: auth_partner
#: model:ir.actions.act_window,name:auth_partner.auth_partner_force_set_password_action
#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_form
#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_force_set_password_view_form
msgid "Set Password"
msgstr "Imposta password"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__set_password_token_duration
msgid "Set Password Token Duration"
msgstr "Durata token impostazione password"
#. module: auth_partner
#: model:ir.model.fields,help:auth_partner.field_auth_partner__user_can_impersonate
msgid "Technical field to check if the user can impersonate"
msgstr "Campo tecnico per controllare se l'utente può imitare"
#. module: auth_partner
#: model:ir.model.fields,help:auth_partner.field_auth_directory__impersonating_user_ids
#: model:ir.model.fields,help:auth_partner.field_auth_partner__impersonating_user_ids
msgid "These odoo users can impersonate any partner of this directory"
msgstr "Questi utenti Odoo possono imitare qualsiasi partner in questa cartella"
#. module: auth_partner
#: model:ir.model.fields,help:auth_partner.field_auth_partner__mail_verified
msgid ""
"This field is set to True when the user has clicked on the link sent by "
"email"
msgstr ""
"Questo campo è impostato a true quando l'utente ha cliccato nel collegamento "
"inviato per e-mail"
#. module: auth_partner
#. odoo-python
#: code:addons/auth_partner/wizards/wizard_auth_partner_force_set_password.py:0
#, python-format
msgid "This wizard can only be used on auth.partner"
msgstr "Questa procedura guidata può essere usata solo su auth.partner"
#. module: auth_partner
#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__user_can_impersonate
msgid "User Can Impersonate"
msgstr "L'utente può imitare"
#. module: auth_partner
#: model:mail.template,subject:auth_partner.email_set_password
#: model:mail.template,subject:auth_partner.email_validate_email
msgid "Welcome"
msgstr "Benvenuto"
#. module: auth_partner
#: model:ir.model,name:auth_partner.model_wizard_auth_partner_force_set_password
#: model:ir.model,name:auth_partner.model_wizard_auth_partner_reset_password
msgid "Wizard Partner Auth Reset Password"
msgstr "Procedura guidata reset password autorizzazione partner"
#. module: auth_partner
#. odoo-python
#: code:addons/auth_partner/models/auth_partner.py:0
#, python-format
msgid "You are not allowed to impersonate this user"
msgstr "Non si è autorizzati a imitare questo utente"

View file

@ -0,0 +1,3 @@
from . import auth_directory
from . import auth_partner
from . import res_partner

View file

@ -0,0 +1,209 @@
# Copyright 2024 Akretion (http://www.akretion.com).
# @author Sébastien BEAU <sebastien.beau@akretion.com>
# @author Florian Mounier <florian.mounier@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from datetime import datetime, timezone
from secrets import token_urlsafe
import jwt
from odoo import _, fields, models
from odoo.exceptions import UserError
from odoo.addons.queue_job.delay import chain
class AuthDirectory(models.Model):
_name = "auth.directory"
_description = "Auth Directory"
_inherit = "server.env.mixin"
name = fields.Char(required=True)
auth_partner_ids = fields.One2many("auth.partner", "directory_id", "Auth Partners")
set_password_token_duration = fields.Integer(
default=1440, help="In minute, default 1440 minutes => 24h", required=True
)
impersonating_token_duration = fields.Integer(
default=60, help="In seconds, default 60 seconds", required=True
)
reset_password_template_id = fields.Many2one(
"mail.template",
"Mail Template Forget Password",
required=True,
default=lambda self: self.env.ref(
"auth_partner.email_reset_password",
raise_if_not_found=False,
),
)
set_password_template_id = fields.Many2one(
"mail.template",
"Mail Template New Password",
required=True,
default=lambda self: self.env.ref(
"auth_partner.email_set_password",
raise_if_not_found=False,
),
)
validate_email_template_id = fields.Many2one(
"mail.template",
"Mail Template Validate Email",
required=True,
default=lambda self: self.env.ref(
"auth_partner.email_validate_email",
raise_if_not_found=False,
),
)
secret_key = fields.Char(
groups="base.group_system",
required=True,
default=lambda self: self._generate_default_secret_key(),
)
count_partner = fields.Integer(compute="_compute_count_partner")
impersonating_user_ids = fields.Many2many(
"res.users",
"auth_directory_impersonating_user_rel",
"directory_id",
"user_id",
string="Impersonating Users",
help="These odoo users can impersonate any partner of this directory",
default=lambda self: (
self.env.ref("base.user_root") | self.env.ref("base.user_admin")
).ids,
groups="auth_partner.group_auth_partner_manager",
)
force_verified_email = fields.Boolean(
help="If checked, email must be verified to be able to log in"
)
def _generate_default_secret_key(self):
# generate random ~64 chars secret key
return token_urlsafe(64)
def action_regenerate_secret_key(self):
self.ensure_one()
self.secret_key = self._generate_default_secret_key()
def _compute_count_partner(self):
data = self.env["auth.partner"].read_group(
[
("directory_id", "in", self.ids),
],
["directory_id"],
groupby=["directory_id"],
lazy=False,
)
res = {item["directory_id"][0]: item["__count"] for item in data}
for record in self:
record.count_partner = res.get(record.id, 0)
def _get_template(self, type_or_template):
if isinstance(type_or_template, str):
return getattr(self, type_or_template + "_template_id", None)
return type_or_template
def _prepare_mail_context(self, context):
return context or {}
def _send_mail_background(
self, type_or_template, auth_partner, callback_job=None, **context
):
"""
Send an email asynchronously to the auth_partner
using the template defined in the directory
"""
self.ensure_one()
auth_partner.ensure_one()
# Load context synchronously
context = self._prepare_mail_context(context)
job = self.delayable()._send_mail_impl(
type_or_template, auth_partner, **context
)
if callback_job:
job = chain(job, callback_job)
return job.delay()
def _send_mail(self, type_or_template, auth_partner, **context):
"""Send an email to the auth_partner using the template defined in the directory"""
self.ensure_one()
auth_partner.ensure_one()
context = self._prepare_mail_context(context)
self._send_mail_impl(type_or_template, auth_partner, **context)
def _send_mail_impl(self, type_or_template, auth_partner, **context):
template = self.sudo()._get_template(type_or_template)
if not template:
raise UserError(
_("No email template defined for %(template)s in %(directory)s")
% {"template": type_or_template, "directory": self.name}
)
template.sudo().with_context(**context).send_mail(
auth_partner.id, force_send=True, raise_exception=True
)
return f"Mail {template.name} sent to {auth_partner.login}"
def _generate_token(self, action, auth_partner, expiration_delta, key_salt=""):
# We need to sudo here as secret_key is a protected field
self = self.sudo()
return jwt.encode(
{
"exp": datetime.now(tz=timezone.utc) + expiration_delta,
"aud": str(self.id),
"action": action,
"ap": auth_partner.id,
},
self.secret_key + key_salt,
algorithm="HS256",
)
def _decode_token(
self,
token,
action,
key_salt=None,
):
# We need to sudo here as secret_key is a protected field
self = self.sudo()
key = self.secret_key
if key_salt:
try:
obj = jwt.decode(
token, algorithms=["HS256"], options={"verify_signature": False}
)
except jwt.PyJWTError as e:
raise UserError(_("Invalid Token")) from e
probable_auth_partner = self.env["auth.partner"].browse(obj["ap"])
if not probable_auth_partner:
raise UserError(_("Invalid Token"))
key += key_salt(probable_auth_partner)
try:
obj = jwt.decode(
token,
key,
audience=str(self.id),
options={"require": ["exp", "aud", "ap", "action"]},
algorithms=["HS256"],
)
except jwt.PyJWTError as e:
raise UserError(_("Invalid Token")) from e
auth_partner = self.env["auth.partner"].browse(obj["ap"])
if (
obj["action"] != action
or not auth_partner
or auth_partner.directory_id != self
):
raise UserError(_("Invalid token"))
return auth_partner
@property
def _server_env_fields(self):
return {"secret_key": {}}

View file

@ -0,0 +1,310 @@
# Copyright 2024 Akretion (http://www.akretion.com).
# @author Sébastien BEAU <sebastien.beau@akretion.com>
# @author Florian Mounier <florian.mounier@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
from datetime import timedelta
import passlib
from odoo import _, api, fields, models
from odoo.exceptions import AccessDenied
# please read passlib great documentation
# https://passlib.readthedocs.io
# https://passlib.readthedocs.io/en/stable/narr/quickstart.html#choosing-a-hash
# be carefull odoo requirements use an old version of passlib
DEFAULT_CRYPT_CONTEXT = passlib.context.CryptContext(["pbkdf2_sha512"])
_logger = logging.getLogger(__name__)
class AuthPartner(models.Model):
_name = "auth.partner"
_description = "Auth Partner"
_rec_name = "login"
partner_id = fields.Many2one(
"res.partner", "Partner", required=True, ondelete="cascade", index=True
)
directory_id = fields.Many2one(
"auth.directory", "Directory", required=True, index=True
)
user_can_impersonate = fields.Boolean(
compute="_compute_user_can_impersonate",
help="Technical field to check if the user can impersonate",
)
impersonating_user_ids = fields.Many2many(
related="directory_id.impersonating_user_ids",
)
login = fields.Char(
compute="_compute_login",
store=True,
required=True,
index=True,
precompute=True,
)
password = fields.Char(compute="_compute_password", inverse="_inverse_password")
encrypted_password = fields.Char(index=True)
nbr_pending_reset_sent = fields.Integer(
index=True,
help=(
"Number of pending reset sent from your customer."
"This field is usefull when after a migration from an other system "
"you ask all you customer to reset their password and you send"
"different mail depending on the number of reminder"
),
)
date_last_request_reset_pwd = fields.Datetime(
help="Date of the last password reset request"
)
date_last_sucessfull_reset_pwd = fields.Datetime(
help="Date of the last sucessfull password reset"
)
date_last_impersonation = fields.Datetime(
help="Date of the last sucessfull impersonation"
)
mail_verified = fields.Boolean(
help="This field is set to True when the user has clicked on the link sent by email"
)
_sql_constraints = [
(
"directory_login_uniq",
"unique (directory_id, login)",
"Login must be uniq per directory !",
),
]
@api.depends("partner_id.email")
def _compute_login(self):
for record in self:
record.login = record.partner_id.email
def _crypt_context(self):
return DEFAULT_CRYPT_CONTEXT
def _check_no_empty(self, login, password):
# double check by security but calling this through a service should
# already have check this
if not (
isinstance(password, str) and password and isinstance(login, str) and login
):
_logger.warning("Invalid login/password for sign in")
raise AccessDenied()
def _get_hashed_password(self, directory, login):
self.flush()
self.env.cr.execute(
"""
SELECT id, COALESCE(encrypted_password, '')
FROM auth_partner
WHERE login=%s AND directory_id=%s""",
(login, directory.id),
)
hashed = self.env.cr.fetchone()
if hashed and hashed[1]:
# ensure that we have a auth.partner and this partner have a password set
return hashed
else:
raise AccessDenied()
def _compute_password(self):
for record in self:
record.password = ""
def _inverse_password(self):
for record in self:
ctx = record._crypt_context()
hash_ = getattr(ctx, "hash", ctx.encrypt)
record.encrypted_password = hash_(record.password)
record.password = ""
def _prepare_partner_auth_signup(self, directory, vals):
return {
"login": vals["login"].lower(),
"password": vals["password"],
"directory_id": directory.id,
}
def _prepare_partner_signup(self, directory, vals):
return {
"name": vals["name"],
"email": vals["login"].lower(),
"auth_partner_ids": [
(0, 0, self._prepare_partner_auth_signup(directory, vals))
],
}
@api.model
def _signup(self, directory, **kwargs):
partner = self.env["res.partner"].create(
[
self._prepare_partner_signup(directory, kwargs),
]
)
auth_partner = partner.auth_partner_ids
directory._send_mail_background(
"validate_email",
auth_partner,
token=auth_partner._generate_validate_email_token(),
)
return auth_partner
@api.model
def _login(self, directory, login, password, **kwargs):
self._check_no_empty(login, password)
login = login.lower()
try:
_id, hashed = self._get_hashed_password(directory, login)
valid, replacement = self._crypt_context().verify_and_update(
password, hashed
)
auth_partner = valid and self.browse(_id)
except AccessDenied:
# We do not want to leak information about the login,
# always raise the same exception
auth_partner = None
if not auth_partner or not auth_partner.partner_id.active:
raise AccessDenied(_("Invalid Login or Password"))
if directory.sudo().force_verified_email and not auth_partner.mail_verified:
raise AccessDenied(
_(
"Email address not validated. Validate your email address by "
"clicking on the link in the email sent to you or request a new "
"password. "
)
)
if replacement is not None:
auth_partner.encrypted_password = replacement
return auth_partner
@api.model
def _validate_email(self, directory, token):
auth_partner = directory._decode_token(token, "validate_email")
auth_partner.write({"mail_verified": True})
return auth_partner
def _get_impersonate_url(self, token, **kwargs):
# You should override this method according to the impersonation url
# your framework is using
base = self.env["ir.config_parameter"].sudo().get_param("web.base.url")
url = f"{base}/auth/impersonate/{token}"
return url
def _get_impersonate_action(self, token, **kwargs):
return {
"type": "ir.actions.act_url",
"url": self._get_impersonate_url(token, **kwargs),
"target": "new",
}
def impersonate(self):
self.ensure_one()
if self.env.user not in self.impersonating_user_ids:
raise AccessDenied(_("You are not allowed to impersonate this user"))
token = self._generate_impersonating_token()
return self._get_impersonate_action(token)
@api.depends_context("uid")
def _compute_user_can_impersonate(self):
for record in self:
record.user_can_impersonate = self.env.user in record.impersonating_user_ids
@api.model
def _impersonating(self, directory, token):
partner_auth = directory._decode_token(
token,
"impersonating",
key_salt=lambda auth_partner: (
auth_partner.date_last_impersonation.isoformat()
if auth_partner.date_last_impersonation
else "never"
),
)
partner_auth.date_last_impersonation = fields.Datetime.now()
return partner_auth
def _on_reset_password_sent(self):
self.ensure_one()
self.date_last_request_reset_pwd = fields.Datetime.now()
self.date_last_sucessfull_reset_pwd = None
self.nbr_pending_reset_sent += 1
def _send_invite(self):
self.ensure_one()
self.directory_id._send_mail_background(
"set_password",
self,
callback_job=self.delayable()._on_reset_password_sent(),
token=self._generate_set_password_token(),
)
def send_invite(self):
for rec in self:
rec._send_invite()
def _request_reset_password(self):
return self.directory_id._send_mail_background(
"reset_password",
self,
callback_job=self.delayable()._on_reset_password_sent(),
token=self._generate_set_password_token(),
)
def _set_password(self, directory, token, password):
auth_partner = directory._decode_token(
token,
"set_password",
# See `_generate_set_password_token` for the key_salt
key_salt=lambda auth_partner: auth_partner.encrypted_password or "empty",
)
auth_partner.write(
{
"password": password,
"mail_verified": True,
}
)
auth_partner.date_last_sucessfull_reset_pwd = fields.Datetime.now()
auth_partner.nbr_pending_reset_sent = 0
return auth_partner
def _generate_set_password_token(self, expiration_delta=None):
# Here we use the current encrypted_password as key_salt to ensure that
# the token will be used to reset the password only once.
return self.directory_id._generate_token(
"set_password",
self,
expiration_delta
or timedelta(minutes=self.directory_id.set_password_token_duration),
key_salt=self.encrypted_password or "empty",
)
def _generate_validate_email_token(self):
return self.directory_id._generate_token(
# 30 days seem to be a good value, no need for configuration
"validate_email",
self,
timedelta(days=30),
)
def _generate_impersonating_token(self):
return self.directory_id._generate_token(
"impersonating",
self,
timedelta(minutes=self.directory_id.impersonating_token_duration),
key_salt=(
self.date_last_impersonation.isoformat()
if self.date_last_impersonation
else "never"
),
)

View file

@ -0,0 +1,34 @@
# Copyright 2024 Akretion (http://www.akretion.com).
# @author Sébastien BEAU <sebastien.beau@akretion.com>
# @author Florian Mounier <florian.mounier@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class ResPartner(models.Model):
_inherit = "res.partner"
auth_partner_ids = fields.One2many("auth.partner", "partner_id", "Partner Auth")
auth_partner_count = fields.Integer(
compute="_compute_auth_partner_count", compute_sudo=True
)
def _compute_auth_partner_count(self):
data = self.env["auth.partner"].read_group(
[
("partner_id", "in", self.ids),
],
["partner_id"],
groupby=["partner_id"],
lazy=False,
)
res = {item["partner_id"][0]: item["__count"] for item in data}
for record in self:
record.auth_partner_count = res.get(record.id, 0)
def _get_auth_partner_for_directory(self, directory):
return self.sudo().auth_partner_ids.filtered(
lambda r: r.directory_id == directory
)

View file

@ -0,0 +1,4 @@
* `Akretion <https://www.akretion.com>`_:
* Sébastien Beau
* Florian Mounier

View file

@ -0,0 +1,12 @@
This module adds to the partners the ability to authenticate through directories.
This module does not implement any routing, it only provides the basic mechanisms in a directory for:
- Registering a partner and sending an welcome email (to validate email address): `_signup`
- Authenticating a partner: `_login`
- Validating a partner email using a token: `_validate_email`
- Impersonating: `_impersonate`, `_impersonating`
- Resetting the password with a unique token sent by mail: `_request_reset_password`, `_set_password`
- Sending an invite mail when registering a partner from odoo interface for the partner to enter a password: `_send_invite`, `_set_password`
For a routing implementation, see the `fastapi_auth_partner <../fastapi_auth_partner>`_ module.

View file

@ -0,0 +1,8 @@
This module isn't meant to be used standalone but you can still see the directories and authenticable partners in:
Settings > Technical > Partner Authentication > Partner
and
Settings > Technical > Partner Authentication > Directory

View file

@ -0,0 +1,8 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_auth_directory,auth_directory_system,model_auth_directory,base.group_system,1,1,1,1
access_auth_directory_read,auth_directory_manager,model_auth_directory,group_auth_partner_manager,1,0,0,0
access_auth_partner,auth_partner_manager,model_auth_partner,group_auth_partner_manager,1,1,1,1
api_access_auth_partner,auth_partner_api,model_auth_partner,group_auth_partner_api,1,1,0,0
api_access_res_partner,res_partner_api,base.model_res_partner,group_auth_partner_api,1,0,0,0
api_access_wizard_auth_partner_reset_password,wizard_auth_partner_reset_password,model_wizard_auth_partner_reset_password,group_auth_partner_manager,1,1,1,1
api_access_wizard_auth_partner_force_set_password,wizard_auth_partner_force_set_password,model_wizard_auth_partner_force_set_password,group_auth_partner_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_auth_directory auth_directory_system model_auth_directory base.group_system 1 1 1 1
3 access_auth_directory_read auth_directory_manager model_auth_directory group_auth_partner_manager 1 0 0 0
4 access_auth_partner auth_partner_manager model_auth_partner group_auth_partner_manager 1 1 1 1
5 api_access_auth_partner auth_partner_api model_auth_partner group_auth_partner_api 1 1 0 0
6 api_access_res_partner res_partner_api base.model_res_partner group_auth_partner_api 1 0 0 0
7 api_access_wizard_auth_partner_reset_password wizard_auth_partner_reset_password model_wizard_auth_partner_reset_password group_auth_partner_manager 1 1 1 1
8 api_access_wizard_auth_partner_force_set_password wizard_auth_partner_force_set_password model_wizard_auth_partner_force_set_password group_auth_partner_manager 1 1 1 1

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record model="ir.rule" id="res_partner_api_rule">
<field name="name">Auth API (res_partner)</field>
<field name="model_id" ref="base.model_res_partner" />
<field name="groups" eval="[(4, ref('group_auth_partner_api'))]" />
<field name="domain_force">[('id','=', authenticated_partner_id)]</field>
<field name="perm_read" eval="True" />
<field name="perm_create" eval="False" />
<field name="perm_write" eval="False" />
<field name="perm_unlink" eval="False" />
</record>
<record model="ir.rule" id="auth_partner_api_rule">
<field name="name">Auth API (auth_partner)</field>
<field name="model_id" ref="auth_partner.model_auth_partner" />
<field name="groups" eval="[(4, ref('group_auth_partner_api'))]" />
<field
name="domain_force"
>[('partner_id','=', authenticated_partner_id)]</field>
<field name="perm_read" eval="True" />
<field name="perm_create" eval="False" />
<field name="perm_write" eval="False" />
<field name="perm_unlink" eval="False" />
</record>
</odoo>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="group_auth_partner_manager" model="res.groups">
<field name="name">API Partner Auth Manager</field>
<field name="category_id" ref="base.module_category_hidden" />
<field
name="users"
eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"
/>
</record>
<record id="group_auth_partner_api" model="res.groups">
<field name="name">API Partner Auth Access</field>
<field name="category_id" ref="base.module_category_hidden" />
</record>
</odoo>

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -0,0 +1,453 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>README.rst</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document">
<a class="reference external image-reference" href="https://odoo-community.org/get-involved?utm_source=readme">
<img alt="Odoo Community Association" src="https://odoo-community.org/readme-banner-image" />
</a>
<div class="section" id="partner-auth">
<h1>Partner Auth</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:33a8bc75dc8127331753aa9a54fe3a5b56f7d51a23cc7e9eb0000cc55f78c689
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/license-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/rest-framework/tree/16.0/auth_partner"><img alt="OCA/rest-framework" src="https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/rest-framework-16-0/rest-framework-16-0-auth_partner"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/rest-framework&amp;target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module adds to the partners the ability to authenticate through directories.</p>
<p>This module does not implement any routing, it only provides the basic mechanisms in a directory for:</p>
<blockquote>
<ul class="simple">
<li>Registering a partner and sending an welcome email (to validate email address): <cite>_signup</cite></li>
<li>Authenticating a partner: <cite>_login</cite></li>
<li>Validating a partner email using a token: <cite>_validate_email</cite></li>
<li>Impersonating: <cite>_impersonate</cite>, <cite>_impersonating</cite></li>
<li>Resetting the password with a unique token sent by mail: <cite>_request_reset_password</cite>, <cite>_set_password</cite></li>
<li>Sending an invite mail when registering a partner from odoo interface for the partner to enter a password: <cite>_send_invite</cite>, <cite>_set_password</cite></li>
</ul>
</blockquote>
<p>For a routing implementation, see the <a class="reference external" href="../fastapi_auth_partner">fastapi_auth_partner</a> module.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#usage" id="toc-entry-1">Usage</a></li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-2">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-3">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-4">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-5">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-6">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="usage">
<h2><a class="toc-backref" href="#toc-entry-1">Usage</a></h2>
<p>This module isnt meant to be used standalone but you can still see the directories and authenticable partners in:</p>
<p>Settings &gt; Technical &gt; Partner Authentication &gt; Partner</p>
<p>and</p>
<p>Settings &gt; Technical &gt; Partner Authentication &gt; Directory</p>
</div>
<div class="section" id="bug-tracker">
<h2><a class="toc-backref" href="#toc-entry-2">Bug Tracker</a></h2>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/rest-framework/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/rest-framework/issues/new?body=module:%20auth_partner%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h2><a class="toc-backref" href="#toc-entry-3">Credits</a></h2>
<div class="section" id="authors">
<h3><a class="toc-backref" href="#toc-entry-4">Authors</a></h3>
<ul class="simple">
<li>Akretion</li>
</ul>
</div>
<div class="section" id="contributors">
<h3><a class="toc-backref" href="#toc-entry-5">Contributors</a></h3>
<ul class="simple">
<li><a class="reference external" href="https://www.akretion.com">Akretion</a>:<ul>
<li>Sébastien Beau</li>
<li>Florian Mounier</li>
</ul>
</li>
</ul>
</div>
<div class="section" id="maintainers">
<h3><a class="toc-backref" href="#toc-entry-6">Maintainers</a></h3>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org">
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
</a>
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/rest-framework/tree/16.0/auth_partner">OCA/rest-framework</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1 @@
from . import test_auth_partner

View file

@ -0,0 +1,60 @@
# Copyright 2024 Akretion (http://www.akretion.com).
# @author Florian Mounier <florian.mounier@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from contextlib import contextmanager
from typing import Any
from odoo.tests.common import TransactionCase
from odoo.addons.mail.tests.common import MockEmail
class CommonTestAuthPartner(TransactionCase, MockEmail):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env = cls.env(context=dict(cls.env.context, queue_job__no_delay=True))
cls.partner = cls.env.ref("auth_partner.res_partner_auth_demo")
cls.other_partner = cls.partner.copy(
{"name": "Other Partner", "email": "other-partner-auth@example.org"}
)
cls.auth_partner = cls.partner.auth_partner_ids
cls.directory = cls.env.ref("auth_partner.demo_directory")
cls.directory.impersonating_user_ids = cls.env.ref("base.user_admin")
cls.other_auth_partner = cls.env["auth.partner"].create(
{
"login": cls.other_partner.email,
"password": "Super-secret3",
"directory_id": cls.directory.id,
"partner_id": cls.other_partner.id,
}
)
cls.other_directory = cls.directory.copy({"name": "Other Directory"})
@contextmanager
def new_mails(self):
mailmail = self.env["mail.mail"]
class MailsProxy(mailmail.__class__):
__slots__ = ["_prev", "__weakref__"]
def __init__(self):
object.__setattr__(self, "_prev", mailmail.search([]))
def __getattribute__(self, name: str) -> Any:
mails = mailmail.search([]) - object.__getattribute__(self, "_prev")
return object.__getattribute__(mails, name)
new_mails = MailsProxy()
with self.mock_mail_gateway():
yield new_mails
@contextmanager
def assert_no_new_mail(self):
with self.new_mails() as new_mails:
yield
self.assertFalse(new_mails)

View file

@ -0,0 +1,357 @@
# Copyright 2024 Akretion (http://www.akretion.com).
# @author Florian Mounier <florian.mounier@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from contextlib import contextmanager
from datetime import datetime, timedelta
from freezegun import freeze_time
from odoo.exceptions import AccessDenied, UserError
from .common import CommonTestAuthPartner
class TestAuthPartner(CommonTestAuthPartner):
@contextmanager
def assert_no_new_mail(self):
with self.new_mails() as new_mails:
yield
self.assertFalse(new_mails)
def test_default_secret_key(self):
self.assertGreaterEqual(len(self.directory.secret_key), 64)
def test_login_ok(self):
with self.assert_no_new_mail():
auth_partner = self.env["auth.partner"]._login(
self.directory,
login="partner-auth@example.org",
password="Super-secret$1",
)
self.assertTrue(auth_partner)
def test_login_inactive_partner(self):
self.partner.active = False
with self.assertRaisesRegex(AccessDenied, "Invalid Login or Password"):
self.env["auth.partner"]._login(
self.directory,
login="partner-auth@example.org",
password="Super-secret$1",
)
def test_login_no_auth(self):
self.auth_partner.unlink()
with self.assertRaisesRegex(AccessDenied, "Invalid Login or Password"):
self.env["auth.partner"]._login(
self.directory,
login="partner-auth@example.org",
password="Super-secret$1",
)
def test_login_wrong_password(self):
with self.assertRaisesRegex(AccessDenied, "Invalid Login or Password"):
self.env["auth.partner"]._login(
self.directory, login="partner-auth@example.org", password="wrong"
)
def test_login_mail_not_verified(self):
self.directory.force_verified_email = True
with self.assertRaisesRegex(AccessDenied, "Email address not validated"):
self.env["auth.partner"]._login(
self.directory,
login="partner-auth@example.org",
password="Super-secret$1",
)
def test_login_wrong_login(self):
with self.assertRaisesRegex(AccessDenied, "Invalid Login or Password"):
self.env["auth.partner"]._login(
self.directory,
login="partner-auth@example.com",
password="Super-secret$1",
)
def test_login_wrong_directory(self):
with self.assertRaisesRegex(AccessDenied, "Invalid Login or Password"):
self.env["auth.partner"]._login(
self.other_directory,
login="partner-auth@example.com",
password="Super-secret$1",
)
def test_signup(self):
with self.new_mails() as new_mails:
new_auth_partner = self.env["auth.partner"]._signup(
self.directory,
name="New Partner",
login="new-partner-auth@example.org",
password="NewSecret",
)
self.assertTrue(new_auth_partner)
# Ensure we can't read the password
self.assertNotEqual(new_auth_partner.password, "NewSecret")
self.assertEqual(len(new_mails), 1)
self.assertEqual(new_mails.subject, "Welcome")
self.assertIn("Welcome to the site, please", new_mails.body)
auth_partner = self.env["auth.partner"]._login(
self.directory, login="new-partner-auth@example.org", password="NewSecret"
)
self.assertTrue(auth_partner)
self.assertEqual(auth_partner, new_auth_partner)
def test_signup_wrong_directory(self):
new_auth_partner = self.env["auth.partner"]._signup(
self.other_directory,
name="New Partner",
login="new-partner-auth@example.org",
password="NewSecret",
)
self.assertTrue(new_auth_partner)
with self.assertRaisesRegex(AccessDenied, "Invalid Login or Password"):
self.env["auth.partner"]._login(
self.directory,
login="new-partner-auth@example.org",
password="NewSecret",
)
def test_signup_same_login_other_directory(self):
new_auth_partner = self.env["auth.partner"]._signup(
self.directory,
name="New Partner",
login="new-partner-auth@example.org",
password="NewSecret",
)
self.assertTrue(new_auth_partner)
new_auth_partner_2 = self.env["auth.partner"]._signup(
self.other_directory,
name="New Partner",
login="new-partner-auth@example.org",
password="NewSecret2",
)
self.assertTrue(new_auth_partner_2)
self.assertNotEqual(new_auth_partner, new_auth_partner_2)
with self.assertRaisesRegex(AccessDenied, "Invalid Login or Password"):
self.env["auth.partner"]._login(
self.directory,
login="new-partner-auth@example.org",
password="NewSecret2",
)
with self.assertRaisesRegex(AccessDenied, "Invalid Login or Password"):
self.env["auth.partner"]._login(
self.other_directory,
login="new-partner-auth@example.org",
password="NewSecret",
)
def test_validate_email_ok(self):
self.assertFalse(self.auth_partner.mail_verified)
token = self.auth_partner._generate_validate_email_token()
self.auth_partner._validate_email(self.directory, token)
self.assertTrue(self.auth_partner.mail_verified)
def test_validate_email_required_login(self):
self.directory.force_verified_email = True
token = self.auth_partner._generate_validate_email_token()
self.auth_partner._validate_email(self.directory, token)
with self.assert_no_new_mail():
auth_partner = self.env["auth.partner"]._login(
self.directory,
login="partner-auth@example.org",
password="Super-secret$1",
)
self.assertTrue(auth_partner)
def test_validate_email_wrong_token(self):
self.assertFalse(self.auth_partner.mail_verified)
with self.assertRaisesRegex(UserError, "Invalid Token"):
self.auth_partner._validate_email(self.directory, "wrong")
self.assertFalse(self.auth_partner.mail_verified)
def test_validate_email_token(self):
with self.new_mails() as new_mails:
new_auth_partner = self.env["auth.partner"]._signup(
self.directory,
name="New Partner",
login="new-partner-auth@example.org",
password="NewSecret",
)
self.assertFalse(new_auth_partner.mail_verified)
token = new_mails.body.split("token=")[1].split('">')[0]
new_auth_partner._validate_email(self.directory, token)
self.assertTrue(new_auth_partner.mail_verified)
def test_impersonate_ok(self):
action = self.auth_partner.with_user(
self.env.ref("base.user_admin")
).impersonate()
token = action["url"].split("/")[-1]
auth_partner = self.env["auth.partner"]._impersonating(self.directory, token)
self.assertEqual(auth_partner, self.auth_partner)
def test_impersonate_once(self):
action = self.auth_partner.with_user(
self.env.ref("base.user_admin")
).impersonate()
token = action["url"].split("/")[-1]
self.env["auth.partner"]._impersonating(self.directory, token)
with self.assertRaisesRegex(UserError, "Invalid Token"):
self.env["auth.partner"]._impersonating(self.directory, token)
def test_impersonate_wrong_directory(self):
action = self.auth_partner.with_user(
self.env.ref("base.user_admin")
).impersonate()
token = action["url"].split("/")[-1]
with self.assertRaisesRegex(UserError, "Invalid Token"):
self.env["auth.partner"]._impersonating(self.other_directory, token)
def test_impersonate_wrong_user(self):
with self.assertRaisesRegex(AccessDenied, "not allowed to impersonate"):
self.auth_partner.with_user(self.env.ref("base.default_user")).impersonate()
def test_impersonate_not_expired_token(self):
self.directory.impersonating_token_duration = 100
action = self.auth_partner.with_user(
self.env.ref("base.user_admin")
).impersonate()
token = action["url"].split("/")[-1]
with freeze_time(datetime.now() + timedelta(hours=1)):
self.env["auth.partner"]._impersonating(self.directory, token)
def test_impersonate_expired_token(self):
self.directory.impersonating_token_duration = 100
action = self.auth_partner.with_user(
self.env.ref("base.user_admin")
).impersonate()
token = action["url"].split("/")[-1]
with (
freeze_time(datetime.now() + timedelta(hours=2)),
self.assertRaisesRegex(UserError, "Invalid Token"),
):
self.env["auth.partner"]._impersonating(self.directory, token)
def test_set_password_ok(self):
self.auth_partner._set_password(
self.directory,
self.auth_partner._generate_set_password_token(),
"ResetSecret",
)
auth_partner = self.env["auth.partner"]._login(
self.directory, login="partner-auth@example.org", password="ResetSecret"
)
self.assertTrue(auth_partner)
def test_set_password_wrong_token(self):
with self.assertRaisesRegex(UserError, "Invalid Token"):
self.auth_partner._set_password(self.directory, "wrong", "ResetSecret")
def test_set_password_once(self):
token = self.auth_partner._generate_set_password_token()
self.auth_partner._set_password(self.directory, token, "ResetSecret")
with self.assertRaisesRegex(UserError, "Invalid Token"):
self.auth_partner._set_password(self.directory, token, "ResetSecret")
def test_set_password_not_expired_token(self):
self.directory.set_password_token_duration = 100
token = self.auth_partner._generate_set_password_token()
with freeze_time(datetime.now() + timedelta(hours=1)):
self.auth_partner._set_password(self.directory, token, "ResetSecret")
auth_partner = self.env["auth.partner"]._login(
self.directory, login="partner-auth@example.org", password="ResetSecret"
)
self.assertTrue(auth_partner)
def test_set_password_expired_token(self):
self.directory.set_password_token_duration = 100
token = self.auth_partner._generate_set_password_token()
with (
freeze_time(datetime.now() + timedelta(hours=2)),
self.assertRaisesRegex(UserError, "Invalid Token"),
):
self.auth_partner._set_password(self.directory, token, "ResetSecret")
def test_reset_password_ok(self):
with self.new_mails() as new_mails:
self.auth_partner._request_reset_password()
self.assertEqual(len(new_mails), 1)
self.assertEqual(new_mails.subject, "Reset Password")
self.assertIn(
"Click on the following link to reset your password", new_mails.body
)
token = new_mails.body.split("token=")[1].split('">')[0]
self.auth_partner._set_password(self.directory, token, "ResetSecret")
auth_partner = self.env["auth.partner"]._login(
self.directory, login="partner-auth@example.org", password="ResetSecret"
)
self.assertTrue(auth_partner)
def test_reset_password_wrong_partner(self):
with self.new_mails() as new_mails:
self.auth_partner._request_reset_password()
self.assertEqual(len(new_mails), 1)
self.assertEqual(new_mails.subject, "Reset Password")
self.assertIn(
"Click on the following link to reset your password", new_mails.body
)
token = new_mails.body.split("token=")[1].split('">')[0]
# This should probably raise instead of reseting the auth_partner password
self.other_auth_partner._set_password(self.directory, token, "ResetSecret")
with self.assertRaisesRegex(AccessDenied, "Invalid Login or Password"):
self.env["auth.partner"]._login(
self.directory,
login="other-partner-auth@example.org",
password="ResetSecret",
)
def test_reset_password_once(self):
with self.new_mails() as new_mails:
self.auth_partner._request_reset_password()
self.assertEqual(len(new_mails), 1)
self.assertEqual(new_mails.subject, "Reset Password")
token = new_mails.body.split("token=")[1].split('">')[0]
self.auth_partner._set_password(self.directory, token, "ResetSecret")
with self.assertRaisesRegex(UserError, "Invalid Token"):
self.auth_partner._set_password(self.directory, token, "ResetSecret2")
def test_send_invite_set_password_ok(self):
with self.new_mails() as new_mails:
self.auth_partner._send_invite()
self.assertEqual(len(new_mails), 1)
self.assertEqual(new_mails.subject, "Welcome")
self.assertIn("your account have been created", new_mails.body)
token = new_mails.body.split("token=")[1].split('">')[0]
self.auth_partner._set_password(self.directory, token, "ResetSecret")
auth_partner = self.env["auth.partner"]._login(
self.directory, login="partner-auth@example.org", password="ResetSecret"
)
self.assertTrue(auth_partner)
def test_send_invite_set_password_once(self):
with self.new_mails() as new_mails:
self.auth_partner._send_invite()
self.assertEqual(len(new_mails), 1)
self.assertEqual(new_mails.subject, "Welcome")
token = new_mails.body.split("token=")[1].split('">')[0]
self.auth_partner._set_password(self.directory, token, "ResetSecret")
with self.assertRaisesRegex(UserError, "Invalid Token"):
self.auth_partner._set_password(self.directory, token, "ResetSecret2")

View file

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="auth_directory_view_tree" model="ir.ui.view">
<field name="model">auth.directory</field>
<field name="arch" type="xml">
<tree>
<field name="name" />
</tree>
</field>
</record>
<record id="auth_directory_view_form" model="ir.ui.view">
<field name="model">auth.directory</field>
<field name="arch" type="xml">
<form string="Directory Auth">
<header>
<button
name="action_regenerate_secret_key"
string="Regenerate secret key"
type="object"
groups="base.group_system"
/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<button
class="oe_stat_button"
name="%(auth_partner_action)d"
icon="fa-user"
type="action"
context="{'default_directory_id': active_id, 'search_default_directory_id': active_id}"
>
<field name="count_partner" />
Account
</button>
</div>
<div class="oe_title">
<h1>
<field name="name" />
</h1>
</div>
<group name="config">
<group name="main">
<field name="secret_key" />
<field name="reset_password_template_id" />
<field name="set_password_template_id" />
<field name="validate_email_template_id" />
<field name="set_password_token_duration" />
<field name="force_verified_email" />
</group>
<group
name="impersonate"
groups="auth_partner.group_auth_partner_manager"
>
<field
name="impersonating_user_ids"
widget="many2many_tags"
/>
<field name="impersonating_token_duration" />
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="auth_directory_view_search" model="ir.ui.view">
<field name="model">auth.directory</field>
<field name="arch" type="xml">
<search string="Directory Auth">
<field name="name" />
</search>
</field>
</record>
<record id="auth_directory_action" model="ir.actions.act_window">
<field name="name">Directory</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">auth.directory</field>
<field name="view_mode">tree,form</field>
<field name="search_view_id" ref="auth_directory_view_search" />
<field name="domain">[]</field>
<field name="context">{}</field>
</record>
<menuitem
id="auth_directory_menu"
parent="auth"
sequence="10"
action="auth_directory_action"
/>
</odoo>

View file

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="auth_partner_view_tree" model="ir.ui.view">
<field name="model">auth.partner</field>
<field name="arch" type="xml">
<tree>
<field name="directory_id" />
<field name="partner_id" />
<field name="login" />
<field name="mail_verified" />
<field name="nbr_pending_reset_sent" />
<field name="date_last_request_reset_pwd" />
<field name="date_last_sucessfull_reset_pwd" />
</tree>
</field>
</record>
<record id="auth_partner_view_form" model="ir.ui.view">
<field name="model">auth.partner</field>
<field name="arch" type="xml">
<form string="Auth Partner">
<header>
<button name="send_invite" type="object" string="Send Invite" />
<button
name="impersonate"
type="object"
string="Impersonate"
class="btn-info"
attrs="{'invisible': [('user_can_impersonate', '=', False)]}"
/>
<button
name="%(auth_partner_force_set_password_action)d"
type="action"
string="Set Password"
class="btn-secondary"
groups="auth_partner.group_auth_partner_manager"
/>
</header>
<sheet>
<div class="oe_title">
<h1>
<field name="user_can_impersonate" invisible="1" />
<field name="partner_id" />
</h1>
</div>
<group>
<field name="login" />
<field name="directory_id" />
</group>
<group>
<field name="mail_verified" />
</group>
</sheet>
</form>
</field>
</record>
<record id="auth_partner_view_search" model="ir.ui.view">
<field name="model">auth.partner</field>
<field name="arch" type="xml">
<search string="Auth Partner">
<field name="directory_id" />
<field name="partner_id" />
<group expand="0" string="Group By">
<filter
string="Directory"
name="directory"
domain="[]"
context="{'group_by':'directory_id'}"
/>
</group>
</search>
</field>
</record>
<record id="auth_partner_action" model="ir.actions.act_window">
<field name="name">Partner</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">auth.partner</field>
<field name="view_mode">tree,form</field>
<field name="search_view_id" ref="auth_partner_view_search" />
<field name="domain">[]</field>
<field name="context">{}</field>
</record>
<menuitem
id="auth"
parent="base.menu_custom"
sequence="100"
name="Partner Authentication"
/>
<menuitem
id="auth_partner_menu"
parent="auth"
sequence="10"
action="auth_partner_action"
/>
</odoo>

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="view_partner_form" model="ir.ui.view">
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form" />
<field name="arch" type="xml">
<div name="button_box" position="inside">
<button
class="oe_stat_button"
name="%(auth_partner_action)d"
icon="fa-user"
type="action"
groups="auth_partner.group_auth_partner_manager"
context="{'search_default_partner_id': active_id}"
>
<field name="auth_partner_count" />
Account
</button>
</div>
</field>
</record>
</odoo>

View file

@ -0,0 +1,2 @@
from . import wizard_auth_partner_reset_password
from . import wizard_auth_partner_force_set_password

View file

@ -0,0 +1,37 @@
# Copyright 2025 Akretion (http://www.akretion.com).
# @author Florian Mounier <florian.mounier@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError
class WizardAuthPartnerForceSetPassword(models.TransientModel):
_name = "wizard.auth.partner.force.set.password"
_description = "Wizard Partner Auth Reset Password"
password = fields.Char(required=True)
password_confirm = fields.Char(string="Confirm Password", required=True)
@api.constrains("password", "password_confirm")
def _check_password(self):
for wizard in self:
if wizard.password != wizard.password_confirm:
raise ValidationError(
_("Password and Confirm Password must be the same")
)
def action_force_set_password(self):
self.ensure_one()
if self.env.context.get("active_model") != "auth.partner":
raise UserError(_("This wizard can only be used on auth.partner"))
auth_partner_id = self.env.context.get("active_id")
if not auth_partner_id:
raise UserError(_("No active_id in context"))
auth_partner = self.env["auth.partner"].browse(auth_partner_id)
auth_partner.write({"password": self.password})
return {"type": "ir.actions.act_window_close"}

View file

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright 2025 Akretion (http://www.akretion.com).
@author Florian Mounier <florian.mounier@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="wizard_auth_partner_force_set_password_view_form" model="ir.ui.view">
<field name="model">wizard.auth.partner.force.set.password</field>
<field name="arch" type="xml">
<form string="Label">
<group>
<field name="password" password="True" />
<field name="password_confirm" password="True" />
</group>
<footer>
<button
name="action_force_set_password"
string="Set Password"
type="object"
class="oe_highlight"
/>
<button string="Cancel" class="oe_link" special="cancel" />
</footer>
</form>
</field>
</record>
<record id="auth_partner_force_set_password_action" model="ir.actions.act_window">
<field name="name">Set Password</field>
<field name="res_model">wizard.auth.partner.force.set.password</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</odoo>

View file

@ -0,0 +1,59 @@
# Copyright 2024 Akretion (https://www.akretion.com).
# @author Sébastien BEAU <sebastien.beau@akretion.com>
# @author Florian Mounier <florian.mounier@akretion.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from datetime import datetime, timedelta
from odoo import api, fields, models
class WizardAuthPartnerResetPassword(models.TransientModel):
_name = "wizard.auth.partner.reset.password"
_description = "Wizard Partner Auth Reset Password"
delay = fields.Selection(
[
("manually", "Manually"),
("6-hours", "6 Hours"),
("2-days", "2-days"),
("7-days", "7 Days"),
("14-days", "14 Days"),
],
default="6-hours",
required=True,
)
template_id = fields.Many2one(
"mail.template",
"Mail Template",
required=True,
domain=[("model_id", "=", "auth.partner")],
)
date_validity = fields.Datetime(
compute="_compute_date_validity", store=True, readonly=False
)
@api.depends("delay")
def _compute_date_validity(self):
for record in self:
if record.delay != "manually":
duration, key = record.delay.split("-")
record.date_validity = datetime.now() + timedelta(
**{key: float(duration)}
)
def action_reset_password(self):
expiration_delta = None
if self.delay != "manually":
duration, key = self.delay.split("-")
expiration_delta = timedelta(**{key: float(duration)})
for auth_partner in self.env["auth.partner"].browse(
self._context["active_ids"]
):
auth_partner.directory_id._send_mail_background(
self.template_id,
auth_partner,
callback_job=auth_partner.delayable()._on_reset_password_sent(),
token=auth_partner._generate_set_password_token(expiration_delta),
)

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="wizard_auth_partner_reset_password_view_form" model="ir.ui.view">
<field name="model">wizard.auth.partner.reset.password</field>
<field name="arch" type="xml">
<form string="Label">
An email will be send with a token to each customer, you can specify the date until the link is valid
<group>
<field name="template_id" />
<field name="delay" />
<field
name="date_validity"
attrs="{'readonly': [('delay', '!=', 'manually')],
'required': [('delay', '=', 'manually')]}"
/>
</group>
<footer>
<button
name="action_reset_password"
string="Send Reset Password"
type="object"
class="oe_highlight"
/>
<button string="Cancel" class="oe_link" special="cancel" />
</footer>
</form>
</field>
</record>
<record id="auth_partner_action_reset_password" model="ir.actions.act_window">
<field name="name">Send Reset Password Instruction</field>
<field name="res_model">wizard.auth.partner.reset.password</field>
<field name="type">ir.actions.act_window</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="binding_model_id" ref="model_auth_partner" />
<field name="groups_id" eval="[(4, ref('group_auth_partner_manager'))]" />
</record>
</odoo>