Public release from ruodoo-project: 19.0 - 2026-05-10 21:19:01 UTC

This commit is contained in:
CI Publish Bot
2026-05-10 21:19:11 +00:00
commit cbf9e6e6d6
1213 changed files with 183945 additions and 0 deletions

30
access_apps/README.rst Normal file
View File

@ -0,0 +1,30 @@
.. image:: https://itpp.dev/images/infinity-readme.png
:alt: Tested and maintained by IT Projects Labs
:target: https://itpp.dev
========================
Control access to Apps
========================
Allows to configure administrators which don't have access to Apps.
Adds **Apps access** selection in user's access rights tab. Two options are available:
* *Allow installing apps*
* *Allow installing apps only from settings*
Questions?
==========
To get an assistance on this module contact us by email :arrow_right: help@itpp.dev
Contributors
============
* `Ivan Yelizariev <https://it-projects.info/team/yelizariev>`__
* `Ildar Nasyrov <https://it-projects.info/team/iledarn>`__
Further information
===================
Tested on `Odoo 17.0 <https://github.com/odoo/odoo/commit/40b19d89846303016098840f4958fe7cc105067c>`_

2
access_apps/__init__.py Normal file
View File

@ -0,0 +1,2 @@
from . import models
from .hooks import uninstall_hook

View File

@ -0,0 +1,29 @@
# Copyright 2018,2021 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# Copyright 2018 Ildar Nasyrov <https://it-projects.info/team/iledarn>
# License MIT (https://opensource.org/licenses/MIT).
{
"name": """Control access to Apps""",
"summary": """You can configure administrators which don't have access to Apps""",
"category": "Extra Tools",
# "live_test_url": "",
"images": ["images/banner.png"],
"version": "19.0.2.0.0",
"application": False,
"author": "IT-Projects LLC, Ivan Yelizariev",
"support": "apps@itpp.dev",
"website": "https://twitter.com/OdooFree",
"license": "Other OSI approved licence", # MIT
# "price": 10.00,
"currency": "EUR",
"depends": ["access_restricted"],
"external_dependencies": {"python": [], "bin": []},
"data": ["security/access_apps_security.xml", "security/ir.model.access.csv"],
"demo": [],
"qweb": [],
"post_load": None,
"pre_init_hook": None,
"post_init_hook": None,
"uninstall_hook": "uninstall_hook",
"auto_install": False,
"installable": True,
}

View File

@ -0,0 +1,37 @@
`2.0.0`
-------
- **Fix:** restore original access rights on uninstallation
- **Fix:** error in Settings menu when some modules are installed
`1.3.3`
-------
- **Fix:** Grant `Allow installing apps` to Admin and System users (it was only System)
`1.3.2`
-------
- **Fix:** allow users in group `Allow install apps only from settings` also to uninstall from settings
- **Fix:** `Allow install apps only from settings` security group should imply `Administration: Settings` - no access to settings otherwise
`1.3.1`
-------
- **Fix:** `Access Error` when administrators without access to apps trying to open ``[[ Website Admin ]] >> Configuratiion >> Settings``
`1.3.0`
-------
- **Fix:** the "Allow install apps" group is now implies "Administration: Settings" ("base.group_system") since in Odoo 11.0 only group_system users can install apps
`1.2.0`
-------
- **Improvement:** rename "Show Apps Menu" to "Allow install apps"
- **New:** "Allow install apps only from settings"
- **Improvement:** group "Show Apps Menu" and "Allow install apps only from settings" under "Apps access" security category
`1.0.1`
-------
- **Fix:** updates for recent odoo 9.0
- **Improvement:** apps dashboard can be showed if user has access 'Show Apps Menu'

32
access_apps/doc/index.rst Normal file
View File

@ -0,0 +1,32 @@
========================
Control access to Apps
========================
Installation
============
* `Install <https://odoo-development.readthedocs.io/en/latest/odoo/usage/install-module.html>`__ this module in a usual way
Configuration
=============
After the installation of this module by default only *default admin* and *superuser* have access to installing modules.
To update the permission follow for other users follow the steps below.
* Open menu ``[[ Settings ]] >> Users & Companies >> Users``, select the user you want to grant the access to
* On ``Access Rights`` tab, ``Other`` settings group there is an ``Apps access`` security category
* Select ``Allow installing apps`` - to allow apps installation from everywhere
* Select ``Allow installing apps only from settings`` - allow apps installation only from other module's ``Configuration >> Settings`` menu. More precisely, it just hides ``[[ Apps ]]`` menu
* Select blank line - to restrict application installation
Usage
=====
* Be sure that you don't test the functionality under the ``Administrator`` (superuser with id=1) user - he is immune to any access restriction except of hiding menus or buttons in UI
* If you don't select anything in ``Apps access``: there is no ``[[ Apps ]]`` menu - even if your user is in ``Administration: Settings`` security group. Also note that you
have no ability to include yourself in groups allowing to installing apps - this is what ``access_restricted`` module does (``access_apps`` depends on it)
* If you have ``Allow installing apps`` selected: there is ``[[ Apps ]]`` menu
* If you have ``Allow installing apps only from settings``: from other module's ``Configuration >> Settings`` menu, e.g. from ``[[ Website ]] >> Configuration >> Settings`` see that
you have the ability to check the ``Digital Content`` checkbox that actually installs the ``website_sale_digital`` module after clicking on ``[Apply]`` button.

7
access_apps/hooks.py Normal file
View File

@ -0,0 +1,7 @@
# Copyright 2021,2023 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# License MIT (https://opensource.org/licenses/MIT).
def uninstall_hook(env):
access = env.ref(
"base.access_ir_module_module_group_user", raise_if_not_found=False
)
access.write({"active": True})

View File

@ -0,0 +1,35 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * access_apps
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 12.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: access_apps
#: model:res.groups,name:access_apps.group_allow_apps
msgid "Allow installing apps"
msgstr ""
#. module: access_apps
#: model:res.groups,name:access_apps.group_allow_apps_only_from_settings
msgid "Allow installing apps only from settings"
msgstr ""
#. module: access_apps
#: model:ir.module.category,name:access_apps.module_category_access_apps
msgid "Apps access"
msgstr ""
#. module: access_apps
#: model:ir.model,name:access_apps.model_res_config_settings
msgid "Config Settings"
msgstr ""

42
access_apps/i18n/de.po Normal file
View File

@ -0,0 +1,42 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * access_apps
#
# Translators:
# Sergej Briesin <20bs18@gmail.com>, 2018
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 11.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-04-21 00:17+0000\n"
"PO-Revision-Date: 2018-04-21 00:17+0000\n"
"Last-Translator: Sergej Briesin <20bs18@gmail.com>, 2018\n"
"Language-Team: German (https://www.transifex.com/it-projects-llc/teams/76080/"
"de/)\n"
"Language: de\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"
#. module: access_apps
#: model:res.groups,name:access_apps.group_allow_apps
#, fuzzy
msgid "Allow installing apps"
msgstr "Erlaube Apps zu installieren"
#. module: access_apps
#: model:res.groups,name:access_apps.group_allow_apps_only_from_settings
#, fuzzy
msgid "Allow installing apps only from settings"
msgstr "Erlaube Apps nur vom Einstellungen zu installieren"
#. module: access_apps
#: model:ir.module.category,name:access_apps.module_category_access_apps
msgid "Apps access"
msgstr "Apps Zugriff"
#. module: access_apps
#: model:ir.model,name:access_apps.model_res_config_settings
msgid "Config Settings"
msgstr ""

45
access_apps/i18n/es.po Normal file
View File

@ -0,0 +1,45 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * access_apps
#
# Translators:
# charles paul requena palomino <rcharles84@gmail.com>, 2018
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 10.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-04-26 19:17+0000\n"
"PO-Revision-Date: 2018-04-26 19:17+0000\n"
"Last-Translator: charles paul requena palomino <rcharles84@gmail.com>, 2018\n"
"Language-Team: Spanish (https://www.transifex.com/it-projects-llc/"
"teams/76080/es/)\n"
"Language: es\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"
#. module: access_apps
#: model:res.groups,name:access_apps.group_allow_apps
#, fuzzy
msgid "Allow installing apps"
msgstr "Permitir instalar aplicaciones solo desde la configuración"
#. module: access_apps
#: model:res.groups,name:access_apps.group_allow_apps_only_from_settings
#, fuzzy
msgid "Allow installing apps only from settings"
msgstr "Permitir instalar aplicaciones solo desde la configuración"
#. module: access_apps
#: model:ir.module.category,name:access_apps.module_category_access_apps
msgid "Apps access"
msgstr "Acceso a aplicaciones"
#. module: access_apps
#: model:ir.model,name:access_apps.model_res_config_settings
msgid "Config Settings"
msgstr ""
#~ msgid "Access to Modules"
#~ msgstr "Acceso a los módulos"

43
access_apps/i18n/es_CR.po Normal file
View File

@ -0,0 +1,43 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * access_apps
#
# Translators:
# Randall <randall_castro@me.com>, 2018
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 11.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-04-21 00:17+0000\n"
"PO-Revision-Date: 2017-11-28 13:02+0000\n"
"Last-Translator: Randall <randall_castro@me.com>, 2018\n"
"Language-Team: Spanish (Costa Rica) (https://www.transifex.com/it-projects-"
"llc/teams/76080/es_CR/)\n"
"Language: es_CR\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"
#. module: access_apps
#: model:res.groups,name:access_apps.group_allow_apps
#, fuzzy
msgid "Allow installing apps"
msgstr "Permitir instalar aplicaciones"
#. module: access_apps
#: model:res.groups,name:access_apps.group_allow_apps_only_from_settings
msgid "Allow installing apps only from settings"
msgstr "Permitir instalar aplicaciones sólo desde ajustes"
#. module: access_apps
#: model:ir.module.category,name:access_apps.module_category_access_apps
msgid "Apps access"
msgstr "Acceso aplicaciones"
#. module: access_apps
#: model:ir.model,name:access_apps.model_res_config_settings
#, fuzzy
msgid "Config Settings"
msgstr "res.config.settings"

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

View File

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

View File

@ -0,0 +1,70 @@
# Copyright 2018 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# Copyright 2018 Ildar Nasyrov <https://it-projects.info/team/iledarn>
# License MIT (https://opensource.org/licenses/MIT).
from odoo import api, models
class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"
@api.model
def _install_modules(self, modules):
if self.env.user.has_group(
"access_apps.group_allow_apps_only_from_settings"
):
self = self.sudo()
return super(ResConfigSettings, self)._install_modules(modules)
@api.model
def default_get(self, fields):
# We restricted any access to apps by default (`ir.module.module`) but in `website_sale` module configuration
# there is a field that gets its default value by searching in apps.
# Without this there is a possibility to encounter the `Access Error` when trying to open settings
# - e.g. when administrators without access to apps open ``[[ Website Admin ]] >> Configuration >> Settings``
# TODO: this solution may lead to unexpected result
# if some of default methods uses self self.env.user to compute default value
res = super(ResConfigSettings, self.sudo()).default_get(fields)
# modules: which modules are installed/to install
classified = self._get_classified_fields()
for module in classified["to_uninstall"]:
name = f"module_{module.name}"
res[name] = module.state in ("installed", "to install", "to upgrade")
if self._fields[name].type == "selection":
res[name] = str(int(res[name]))
return res
@api.model
def _get_classified_fields(self, fnames=None):
# classify mudules to install and uninstall independently
res = super(ResConfigSettings, self)._get_classified_fields(fnames=fnames)
to_uninstall = res["module"].filtered(
lambda m: not self[f"module_{m.name}"]
and m.state in ("installed", "to upgrade")
)
modules = res["module"] - to_uninstall
res["module"] = modules
res["to_uninstall"] = to_uninstall
return res
def execute(self):
# base `exectute` doesn't know about new classification - it only has a list of modules to install now
res = super(ResConfigSettings, self).execute()
# uninstall modules if needed and a user has access
to_uninstall = self._get_classified_fields()["to_uninstall"]
if to_uninstall and self.env.user.has_group(
"access_apps.group_allow_apps_only_from_settings"
):
to_uninstall_modules = self.env["ir.module.module"]
for module in to_uninstall:
to_uninstall_modules += module
to_uninstall_modules.sudo().button_immediate_uninstall()
return res

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2018,2021 Ivan Yelizariev <https://twitter.com/yelizariev>
Copyright 2018 Ildar Nasyrov <https://it-projects.info/team/iledarn>
License MIT (https://opensource.org/licenses/MIT).
-->
<odoo>
<record id="base.access_ir_module_module_group_user" model="ir.model.access">
<field name="active" eval="False" />
</record>
<record id="module_category_access_apps" model="ir.module.category">
<field name="name">Apps access</field>
<field name="sequence">18</field>
</record>
<record id="group_allow_apps_only_from_settings" model="res.groups">
<field name="name">Allow installing apps only from settings</field>
<field name="implied_ids" eval="[(4, ref('base.group_system'))]" />
</record>
<record id="group_allow_apps" model="res.groups">
<field name="name">Allow installing apps</field>
<field
name="user_ids"
eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"
/>
<field
name="implied_ids"
eval="[(4, ref('group_allow_apps_only_from_settings'))]"
/>
</record>
<record model="ir.ui.menu" id="base.menu_management">
<field name="group_ids" eval="[(6,0, [ref('access_apps.group_allow_apps')])]" />
</record>
</odoo>

View File

@ -0,0 +1,6 @@
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
"access_ir_module_category_group_user","ir_module_category group_user","base.model_ir_module_category","group_allow_apps",1,0,0,0
"access_ir_module_module_group_user","ir_module_module group_user","base.model_ir_module_module","group_allow_apps",1,1,1,1
"access_ir_module_module_dependency_group_allow_apps","ir_module_module_dependency group_allow_apps","base.model_ir_module_module_dependency","group_allow_apps",1,1,1,1
"access_ir_module_module_group_allow_apps_only_from_settings","ir_module_module_group_allow_apps_only_from_settings","base.model_ir_module_module","group_allow_apps_only_from_settings",1,1,0,0
"access_ir_module_module_all_can_read","ir_module_module_all_can_read","base.model_ir_module_module","base.group_system",1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_ir_module_category_group_user ir_module_category group_user base.model_ir_module_category group_allow_apps 1 0 0 0
3 access_ir_module_module_group_user ir_module_module group_user base.model_ir_module_module group_allow_apps 1 1 1 1
4 access_ir_module_module_dependency_group_allow_apps ir_module_module_dependency group_allow_apps base.model_ir_module_module_dependency group_allow_apps 1 1 1 1
5 access_ir_module_module_group_allow_apps_only_from_settings ir_module_module_group_allow_apps_only_from_settings base.model_ir_module_module group_allow_apps_only_from_settings 1 1 0 0
6 access_ir_module_module_all_can_read ir_module_module_all_can_read base.model_ir_module_module base.group_system 1 0 0 0

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

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

View File

@ -0,0 +1,64 @@
# Copyright 2024 DOB
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo.exceptions import AccessError
from odoo.tests import new_test_user
from odoo.tests.common import TransactionCase
class TestAccessApps(TransactionCase):
"""Tests for access_apps: access control to the Apps section.
Validates: Requirement 6.1
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env = cls.env(
context=dict(cls.env.context, tracking_disable=True, no_reset_password=True)
)
# Regular user without admin rights and without group_allow_apps
cls.regular_user = new_test_user(
cls.env,
name="Regular User",
login="test_regular_user_access_apps",
groups="base.group_user",
)
def test_non_admin_cannot_read_ir_module_module(self):
"""WHEN a user without admin rights tries to access the Apps section,
access_apps SHALL deny access.
Validates: Requirement 6.1
"""
# The module deactivates base.access_ir_module_module_group_user,
# so only users in group_allow_apps can read ir.module.module.
# A regular user (not in group_allow_apps) should get AccessError.
with self.assertRaises(AccessError):
self.env["ir.module.module"].with_user(self.regular_user).search([])
def test_admin_can_read_ir_module_module(self):
"""WHEN the admin user (in group_allow_apps) accesses the Apps section,
access_apps SHALL allow access.
Validates: Requirement 6.1 (positive case)
"""
# Admin is in group_allow_apps by default (see security XML)
admin_user = self.env.ref("base.user_admin")
modules = self.env["ir.module.module"].with_user(admin_user).search([], limit=1)
# Should not raise; result may be empty or non-empty
self.assertIsNotNone(modules)
def test_non_admin_not_in_group_allow_apps(self):
"""WHEN a regular user is not in group_allow_apps,
they SHALL NOT have access to ir.module.module.
Validates: Requirement 6.1
"""
group_allow_apps = self.env.ref("access_apps.group_allow_apps")
self.assertNotIn(
self.regular_user,
group_allow_apps.users,
"Regular user should not be in group_allow_apps",
)

View File

@ -0,0 +1,27 @@
.. image:: https://itpp.dev/images/infinity-readme.png
:alt: Tested and maintained by IT Projects Labs
:target: https://itpp.dev
Restricted administration rights
================================
The module makes impossible for administrator to set (and see) more access rights (groups) than he already has.
The only partial exception of this rule is made if you are already a member of the *Allow add implied groups from settings* security group.
Then you are allowed to escalate your privileges but just from ``Settings`` menus (by means of ``group_XXX`` boolean fields of ``res.config.settings`` models views).
This doesn't affect superuser, of course.
Typical usage of the module
===========================
The superuser creates an administrator user without access group "Show Apps Menu" (see **access_apps** module). Then the administrator has access to settings, but not able to install new apps (without this module he can add himself to "Show Apps Menu" and get access to apps).
Roadmap
=======
* Settings menu shows group fields are not updated without *Allow add implied groups from settings* (ok), but it shows the fields as not editable (not ok)
Further information
===================
Tested on `Odoo 17.0 <https://github.com/odoo/odoo/commit/40b19d89846303016098840f4958fe7cc105067c>`_

View File

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

View File

@ -0,0 +1,15 @@
{
"name": "Restricted administration rights",
"summary": "Apply strict restriction on what an admin can do",
"version": "19.0.1.3.5",
"author": "IT-Projects LLC, Ivan Yelizariev",
"category": "Extra Tools",
"images": ["images/banner.jpg"],
"support": "apps@itpp.dev",
"website": "https://twitter.com/OdooFree",
"license": "Other OSI approved licence", # MIT
"currency": "EUR",
"depends": ["ir_rule_protected"],
"data": ["security/access_restricted_security.xml"],
"installable": True,
}

View File

@ -0,0 +1,50 @@
`1.3.5`
-------
- **Fix:** Fixed possibility to write inappropriate groups directly via write method after some odoo updates https://github.com/odoo/odoo/commit/5f12e244f6e57b8edb56788147774150e2ae134d
`1.3.4`
-------
- **Fix:** Allow superuser to write groups via settings menu
`1.3.3`
-------
- **Fix:** If no permission to add groups then just ignore write operations to groups model records but apply any other valid settings. In other words - do not block rest of the settings from applying if there is only no permission to add groups
`1.3.2`
-------
- **Fix:** regardless of ``Allow add implied groups from settings`` always allow to uncheck **group_XXX** fields from settings menu. This makes possible for ``access_apps`` module to independently install apps from settings menu. Otherwise users of ``access_apps`` need always be in ``Allow add implied...`` to install from settings
`1.3.1`
-------
- **Fix:** a user, if he has ``Allow add implied groups from settings`` group access right, should be able to uncheck **group_XXX** fields from a settings menu to exit from implied groups (all other users that are in implying group also quit from the implied group)
`1.3.0`
-------
- [ADD] security group that allows increasing rights from settings menu (by checking ``res.config.settings`` 'group_XXX' boolean fields)
`1.2.0`
-------
- [REF] clean and simplify code
`1.1.0`
-------
- ADD: Make restricted groups readonly in Settigs pages (res.config.settings)
- ADD: don't restrict access to Technical Settings group
`1.0.1`
-------
- FIX: update to the latest odoo 9.0 version due to this comit from Mar 24, 2016 https://github.com/odoo/odoo/commit/40a299c580c4608edab8781fda4e66f39611543b
`1.0.0`
-------
- init version

View File

@ -0,0 +1,37 @@
==================================
Restricted administration rights
==================================
Installation
============
* `Install <https://odoo-development.readthedocs.io/en/latest/odoo/usage/install-module.html>`__ this module in a usual way
Configuration
=============
* `Log in as SUPERUSER <https://odoo-development.readthedocs.io/en/latest/odoo/usage/login-as-superuser.html>`__
* Navigate to menu ``[[ Settings ]] >> Users & Companies >> Users``
* In ``Access Rights`` tab you can select *Allow add implied groups from settings*:
it allows a user to configure groups via ``group_XXX`` fields from ``Settings`` menu.
Usage
=====
By default all users except a superuser restricted to escalate the privileges.
Let's take ``Sales (sale_management)`` module as an example.
Without this module installed:
* Say you have a user with administration rights ``Administration: Access Rights``. This user thus may increase his own rights in ``Application Accesses`` from ``Sales: User: Own Documents Only``
to ``Sales: Administrator``. Also he can open menu ``[[ Sales ]] >> Configuration >> Settings`` and select ``Customer Addresses`` there
and then click ``[Apply]`` button (adding ``group_sale_delivery_address``)
With this module installed:
* The user from previous example cannot increase his privileges. There is no ``Sales: Manager`` option for him, and also no ``Customer Addresses``
option in module configuration
* The only exception is done for users who are in special group *Allow add implied groups from settings*: if your user is included in this group by the superuser then you may select
``Customer Addresses`` from ``Sale`` module ``Configuration >> Settings`` menu

View File

@ -0,0 +1,94 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * access_restricted
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 12.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: access_restricted
#: code:addons/access_restricted/models/res_config_settings.py:48
#, python-format
msgid "\n"
"\n"
"You don't have access to change this settings, because you administration rights are restricted"
msgstr ""
#. module: access_restricted
#: model:ir.model,name:access_restricted.model_res_groups
msgid "Access Groups"
msgstr ""
#. module: access_restricted
#: model:res.groups,name:access_restricted.group_allow_add_implied_from_settings
msgid "Allow add implied groups from settings"
msgstr ""
#. module: access_restricted
#: model:ir.model,name:access_restricted.model_res_config_settings
msgid "Config Settings"
msgstr ""
#. module: access_restricted
#: model:ir.model.fields,field_description:access_restricted.field_test_config_settings__create_uid
msgid "Created by"
msgstr ""
#. module: access_restricted
#: model:ir.model.fields,field_description:access_restricted.field_test_config_settings__create_date
msgid "Created on"
msgstr ""
#. module: access_restricted
#: model:ir.model.fields,field_description:access_restricted.field_test_config_settings__display_name
msgid "Display Name"
msgstr ""
#. module: access_restricted
#: model:ir.model.fields,field_description:access_restricted.field_test_config_settings__group_private_addresses
msgid "Group Private Addresses"
msgstr ""
#. module: access_restricted
#: model:ir.model.fields,field_description:access_restricted.field_test_config_settings__id
msgid "ID"
msgstr ""
#. module: access_restricted
#: model:ir.model.fields,field_description:access_restricted.field_test_config_settings____last_update
msgid "Last Modified on"
msgstr ""
#. module: access_restricted
#: model:ir.model.fields,field_description:access_restricted.field_test_config_settings__write_uid
msgid "Last Updated by"
msgstr ""
#. module: access_restricted
#: model:ir.model.fields,field_description:access_restricted.field_test_config_settings__write_date
msgid "Last Updated on"
msgstr ""
#. module: access_restricted
#: model:ir.model,name:access_restricted.model_res_users
msgid "Users"
msgstr ""
#. module: access_restricted
#: code:addons/access_restricted/models/res_users.py:95
#, python-format
msgid "You cannot add groups to Implied groups, because you are not allowed to increase your rights. Please contact your system administrator."
msgstr ""
#. module: access_restricted
#: model:ir.model,name:access_restricted.model_test_config_settings
msgid "test.config.settings"
msgstr ""

View File

@ -0,0 +1,105 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * access_restricted
#
# Translators:
# Sergej Briesin <20bs18@gmail.com>, 2018
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 11.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-04-21 00:17+0000\n"
"PO-Revision-Date: 2018-04-21 00:17+0000\n"
"Last-Translator: Sergej Briesin <20bs18@gmail.com>, 2018\n"
"Language-Team: German (https://www.transifex.com/it-projects-llc/teams/76080/"
"de/)\n"
"Language: de\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"
#. module: access_restricted
#: code:addons/access_restricted/models/res_config_settings.py:48
#, python-format
msgid ""
"\n"
"\n"
"You don't have access to change this settings, because you administration "
"rights are restricted"
msgstr ""
#. module: access_restricted
#: model:ir.model,name:access_restricted.model_res_groups
msgid "Access Groups"
msgstr ""
#. module: access_restricted
#: model:res.groups,name:access_restricted.group_allow_add_implied_from_settings
msgid "Allow add implied groups from settings"
msgstr ""
#. module: access_restricted
#: model:ir.model,name:access_restricted.model_res_config_settings
msgid "Config Settings"
msgstr ""
#. module: access_restricted
#: model:ir.model.fields,field_description:access_restricted.field_test_config_settings__create_uid
msgid "Created by"
msgstr ""
#. module: access_restricted
#: model:ir.model.fields,field_description:access_restricted.field_test_config_settings__create_date
msgid "Created on"
msgstr ""
#. module: access_restricted
#: model:ir.model.fields,field_description:access_restricted.field_test_config_settings__display_name
msgid "Display Name"
msgstr ""
#. module: access_restricted
#: model:ir.model.fields,field_description:access_restricted.field_test_config_settings__group_private_addresses
msgid "Group Private Addresses"
msgstr ""
#. module: access_restricted
#: model:ir.model.fields,field_description:access_restricted.field_test_config_settings__id
msgid "ID"
msgstr ""
#. module: access_restricted
#: model:ir.model.fields,field_description:access_restricted.field_test_config_settings____last_update
msgid "Last Modified on"
msgstr ""
#. module: access_restricted
#: model:ir.model.fields,field_description:access_restricted.field_test_config_settings__write_uid
msgid "Last Updated by"
msgstr ""
#. module: access_restricted
#: model:ir.model.fields,field_description:access_restricted.field_test_config_settings__write_date
msgid "Last Updated on"
msgstr ""
#. module: access_restricted
#: code:addons/access_restricted/models/res_users.py:95
#, python-format
msgid ""
"The requested operation cannot be completed due to security restrictions. "
"Please contact your system administrator.\n"
"\n"
"(Document type: %s, Operation: %s)"
msgstr ""
#. module: access_restricted
#: model:ir.model,name:access_restricted.model_res_users
msgid "Users"
msgstr "Nutzer"
#. module: access_restricted
#: model:ir.model,name:access_restricted.model_test_config_settings
msgid "test.config.settings"
msgstr ""

View File

@ -0,0 +1,105 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * access_restricted
#
# Translators:
# charles paul requena palomino <rcharles84@gmail.com>, 2018
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 10.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-04-26 19:17+0000\n"
"PO-Revision-Date: 2018-04-26 19:17+0000\n"
"Last-Translator: charles paul requena palomino <rcharles84@gmail.com>, 2018\n"
"Language-Team: Spanish (https://www.transifex.com/it-projects-llc/"
"teams/76080/es/)\n"
"Language: es\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"
#. module: access_restricted
#: code:addons/access_restricted/models/res_config_settings.py:48
#, python-format
msgid ""
"\n"
"\n"
"You don't have access to change this settings, because you administration "
"rights are restricted"
msgstr ""
#. module: access_restricted
#: model:ir.model,name:access_restricted.model_res_groups
msgid "Access Groups"
msgstr "Grupos de acceso"
#. module: access_restricted
#: model:res.groups,name:access_restricted.group_allow_add_implied_from_settings
msgid "Allow add implied groups from settings"
msgstr ""
#. module: access_restricted
#: model:ir.model,name:access_restricted.model_res_config_settings
msgid "Config Settings"
msgstr ""
#. module: access_restricted
#: model:ir.model.fields,field_description:access_restricted.field_test_config_settings__create_uid
msgid "Created by"
msgstr "Creado por"
#. module: access_restricted
#: model:ir.model.fields,field_description:access_restricted.field_test_config_settings__create_date
msgid "Created on"
msgstr "Creado en"
#. module: access_restricted
#: model:ir.model.fields,field_description:access_restricted.field_test_config_settings__display_name
msgid "Display Name"
msgstr "Nombre para mostrar"
#. module: access_restricted
#: model:ir.model.fields,field_description:access_restricted.field_test_config_settings__group_private_addresses
msgid "Group Private Addresses"
msgstr ""
#. module: access_restricted
#: model:ir.model.fields,field_description:access_restricted.field_test_config_settings__id
msgid "ID"
msgstr "ID"
#. module: access_restricted
#: model:ir.model.fields,field_description:access_restricted.field_test_config_settings____last_update
msgid "Last Modified on"
msgstr "Última modificación en"
#. module: access_restricted
#: model:ir.model.fields,field_description:access_restricted.field_test_config_settings__write_uid
msgid "Last Updated by"
msgstr "Última actualización por"
#. module: access_restricted
#: model:ir.model.fields,field_description:access_restricted.field_test_config_settings__write_date
msgid "Last Updated on"
msgstr "Ultima actualizacion en"
#. module: access_restricted
#: code:addons/access_restricted/models/res_users.py:95
#, python-format
msgid ""
"The requested operation cannot be completed due to security restrictions. "
"Please contact your system administrator.\n"
"\n"
"(Document type: %s, Operation: %s)"
msgstr ""
#. module: access_restricted
#: model:ir.model,name:access_restricted.model_res_users
msgid "Users"
msgstr ""
#. module: access_restricted
#: model:ir.model,name:access_restricted.model_test_config_settings
msgid "test.config.settings"
msgstr ""

View File

@ -0,0 +1,114 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * access_restricted
#
# Translators:
# Randall <randall_castro@me.com>, 2018
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 11.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-04-21 00:17+0000\n"
"PO-Revision-Date: 2017-11-28 13:02+0000\n"
"Last-Translator: Randall <randall_castro@me.com>, 2018\n"
"Language-Team: Spanish (Costa Rica) (https://www.transifex.com/it-projects-"
"llc/teams/76080/es_CR/)\n"
"Language: es_CR\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"
#. module: access_restricted
#: code:addons/access_restricted/models/res_config_settings.py:48
#, python-format
msgid ""
"\n"
"\n"
"You don't have access to change this settings, because you administration "
"rights are restricted"
msgstr ""
"\n"
"\n"
"No tiene acceso para cambiar esta configuración, ya que los derechos de "
"administración están restringidos."
#. module: access_restricted
#: model:ir.model,name:access_restricted.model_res_groups
msgid "Access Groups"
msgstr "Grupos de Accesos"
#. module: access_restricted
#: model:res.groups,name:access_restricted.group_allow_add_implied_from_settings
msgid "Allow add implied groups from settings"
msgstr "Permitir agregar grupos implícitos desde la configuración"
#. module: access_restricted
#: model:ir.model,name:access_restricted.model_res_config_settings
#, fuzzy
msgid "Config Settings"
msgstr "res.config.settings"
#. module: access_restricted
#: model:ir.model.fields,field_description:access_restricted.field_test_config_settings__create_uid
msgid "Created by"
msgstr "Creado por"
#. module: access_restricted
#: model:ir.model.fields,field_description:access_restricted.field_test_config_settings__create_date
msgid "Created on"
msgstr "Creado en"
#. module: access_restricted
#: model:ir.model.fields,field_description:access_restricted.field_test_config_settings__display_name
msgid "Display Name"
msgstr "Mostrar Nombre"
#. module: access_restricted
#: model:ir.model.fields,field_description:access_restricted.field_test_config_settings__group_private_addresses
msgid "Group Private Addresses"
msgstr ""
#. module: access_restricted
#: model:ir.model.fields,field_description:access_restricted.field_test_config_settings__id
msgid "ID"
msgstr "ID"
#. module: access_restricted
#: model:ir.model.fields,field_description:access_restricted.field_test_config_settings____last_update
msgid "Last Modified on"
msgstr "Última modificación el"
#. module: access_restricted
#: model:ir.model.fields,field_description:access_restricted.field_test_config_settings__write_uid
msgid "Last Updated by"
msgstr "Última actualización por"
#. module: access_restricted
#: model:ir.model.fields,field_description:access_restricted.field_test_config_settings__write_date
msgid "Last Updated on"
msgstr "Última actualización en "
#. module: access_restricted
#: code:addons/access_restricted/models/res_users.py:95
#, python-format
msgid ""
"The requested operation cannot be completed due to security restrictions. "
"Please contact your system administrator.\n"
"\n"
"(Document type: %s, Operation: %s)"
msgstr ""
#. module: access_restricted
#: model:ir.model,name:access_restricted.model_res_users
msgid "Users"
msgstr "Usuarios"
#. module: access_restricted
#: model:ir.model,name:access_restricted.model_test_config_settings
msgid "test.config.settings"
msgstr "test.config.settings"
#~ msgid "Group User"
#~ msgstr "Grupos de Usuario"

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 KiB

View File

@ -0,0 +1,3 @@
from . import res_config_settings
from . import res_users
from . import test_config_settings

View File

@ -0,0 +1,71 @@
from odoo import SUPERUSER_ID, api, models
from odoo.tools import ustr
from odoo.tools.translate import _
class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"
@api.model
def _get_classified_fields(self, fnames=None):
uid = self.env.uid
classified = super()._get_classified_fields(fnames)
config = self.env.context.get("config")
is_execute_stage = config and isinstance(config, models.Model)
if uid == SUPERUSER_ID or is_execute_stage:
return classified
group = []
user = self.env.user
for name, groups, implied_group in classified["group"]:
if (
implied_group in user.group_ids
or user.has_group(
"access_restricted.group_allow_add_implied_from_settings"
)
):
group.append((name, groups, implied_group))
classified["group"] = group
return classified
@api.model
def fields_get(self, allfields=None, **kwargs):
uid = self.env.uid
fields = super().fields_get(allfields, **kwargs)
if uid == SUPERUSER_ID:
return fields
user = self.env.user
for name in fields:
if not name.startswith("group_"):
continue
f = self._fields[name]
if (
f.implied_group in user.group_ids
or user.has_group(
"access_restricted.group_allow_add_implied_from_settings"
)
):
continue
fields[name].update(
readonly=True,
help=ustr(fields[name].get("help", ""))
+ _(
"\n\nYou don't have access to change this settings, because your administration rights are restricted"
),
)
return fields
def execute(self):
return super(
ResConfigSettings, self.with_context({"config": self})
).execute()

View File

@ -0,0 +1,112 @@
from odoo import SUPERUSER_ID, _, api, models
from odoo.exceptions import AccessError
IR_CONFIG_NAME = "access_restricted.fields_view_get_uid"
class ResUsers(models.Model):
_inherit = "res.users"
@api.model
def get_views(self, views, options=None):
last_uid = self.env["ir.config_parameter"].sudo().get_param(IR_CONFIG_NAME)
if int(last_uid) != self.env.uid:
self.env["res.groups"]._update_user_groups_view()
return super().get_views(views, options)
def write(self, vals):
if 'groups_id' in vals:
self.env["ir.config_parameter"].sudo().set_param(IR_CONFIG_NAME, "0")
return super().write(vals)
class ResGroups(models.Model):
_inherit = "res.groups"
@api.model
def _update_user_groups_view(self):
real_uid = self.env.uid
self.env["ir.config_parameter"].sudo().set_param(IR_CONFIG_NAME, real_uid)
self.env.flush_all()
@api.model
def get_application_groups(self, domain=None):
if domain is None:
domain = []
domain.append(("share", "=", False))
real_uid = int(
self.env["ir.config_parameter"].sudo().get_param(IR_CONFIG_NAME, "0")
)
if real_uid and real_uid != SUPERUSER_ID:
group_no_one_id = self.env.ref("base.group_no_one").id
domain = domain + [
"|",
("user_ids", "in", [real_uid]),
("id", "=", group_no_one_id),
]
return self.sudo().search(domain)
def write(self, vals):
config = self.env.context.get("config")
# `isinstance` check is a non-xmplrpc proof.
if config and isinstance(config, models.Model):
implied_ids = vals.get("implied_ids")
classified_group = config._get_classified_fields()["group"]
# when `res.config.settings`'s `execute` method writes the `users` field to group,
# it is always to remove users and the `users` field is the only key in the write dict
users = vals.get("user_ids")
implied_group = implied_ids and implied_ids[0][1]
users_exclude_operation = (
users and len(vals) == 1 and all(u[0] == 3 for u in users)
)
# ``all(u[0] == 3 for u in users)`` is to be sure that all operations are for removing.
# `(3, id)` tuple removes the record from the set (the Many2many field `users`)
add_implied_group_operation = implied_group in [
group[2].id for group in classified_group
]
curr_user_allowed = self.env.user._is_superuser() or self.env.user.has_group(
"access_restricted.group_allow_add_implied_from_settings"
)
if (
users_exclude_operation
or add_implied_group_operation
and curr_user_allowed
):
self = self.with_user(SUPERUSER_ID)
else:
# do nothing with groups if there is no permission to add from settings
return
# in the https://github.com/odoo/odoo/commit/5f12e244f6e57b8edb56788147774150e2ae134d commit
# the method was refactored due to a higher performance.
# Super method lacks of orm part so as consequent ir rules are not checked and we check its conditions manually.
# We apply super method and check the difference of implied groups,
# if the not proper group was set error is raised
check_for_implied_ids = (
"implied_ids" in vals
and vals["implied_ids"]
and self.env.user.id != SUPERUSER_ID
)
if check_for_implied_ids:
implied_ids_before = self.mapped("implied_ids")
groups_before = self.env.user.group_ids
result = super(ResGroups, self).write(vals)
if check_for_implied_ids:
implied_ids_after = self.mapped("implied_ids")
group_no_one = self.env.ref("base.group_no_one")
implied_group_ids = implied_ids_after - implied_ids_before - group_no_one
# R1 <= R2 True if all records of R1 are also in R2
if not implied_group_ids <= groups_before:
raise AccessError(
_(
"You cannot add groups to Implied groups, because you are not allowed to increase your rights. "
"Please contact your system administrator."
)
)
return result

View File

@ -0,0 +1,13 @@
from odoo import fields, models
class TestConfigSettings(models.TransientModel):
_description = "Test config settings"
_inherit = ["res.config.settings"]
group_test_access_restricted = fields.Boolean(
group="base.group_system",
# random group for test purposes
implied_group="base.group_multi_currency",
)

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="group_allow_add_implied_from_settings" model="res.groups">
<field name="name">Allow add implied groups from settings</field>
</record>
<record id="not_admin_tenant_rule" model="ir.rule">
<field name="name">Only admin can edit admin</field>
<field name="model_id" ref="base.model_res_users" />
<field name="global" eval="1" />
<field name="domain_force">[('id', '!=', 1)]</field>
<field name="perm_read" eval="False" />
<field name="perm_write" eval="True" />
<field name="perm_create" eval="True" />
<field name="perm_unlink" eval="True" />
<field name="protected" eval="True" />
</record>
<record id="res_groups_restricted" model="ir.rule">
<field name="name">res.groups.restricted</field>
<field name="model_id" ref="base.model_res_groups" />
<field
name="domain_force"
eval="&quot;['|', ('user_ids','in', [user.id]), ('id', '=', '%s')]&quot; % ref('base.group_no_one')"
/>
<field name="perm_read" eval="False" />
<field name="perm_write" eval="True" />
<field name="perm_create" eval="False" />
<field name="perm_unlink" eval="True" />
<field name="protected" eval="True" />
</record>
</odoo>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@ -0,0 +1,3 @@
from . import test_fields_view_get
from . import test_fields_get
from . import test_allow_implied

View File

@ -0,0 +1,86 @@
from odoo.exceptions import AccessError
from odoo.tests.common import TransactionCase
GROUP = "base.group_multi_currency"
class TestAllowImplied(TransactionCase):
def _get_classified_groups(self, config):
groups = config._get_classified_fields()["group"]
return [g[0] for g in groups]
def setUp(self):
super().setUp()
self.group = self.env.ref(GROUP)
self.demo_user = self.env["res.users"].create({
"name": "Test Demo User",
"login": "test_demo_allow_implied",
"groups_id": [(6, 0, [self.env.ref("base.group_user").id])],
})
def test_base(self):
demo_user = self.demo_user
group_system = self.env.ref("base.group_system")
demo_user.write({"group_ids": [(3, self.group.id)]})
self.group.write({"users": [(3, demo_user.id)]})
self.assertFalse(self.env["res.users"].with_user(demo_user.id).has_group(GROUP))
demo_user.write({"group_ids": [(4, group_system.id)]})
test_config_settings = (
self.env["res.config.settings"]
.with_user(demo_user.id)
.create({"group_test_access_restricted": True})
)
self.assertFalse(self.env["res.users"].with_user(demo_user.id).has_group(GROUP))
# check that the field is readonly
self.assertTrue(
test_config_settings.fields_get()["group_test_access_restricted"][
"readonly"
]
)
# check that test group hasn't got appended to classified
self.assertNotIn(
"self.group", self._get_classified_groups(test_config_settings)
)
group_allow = self.env.ref(
"access_restricted.group_allow_add_implied_from_settings"
)
demo_user.write({"group_ids": [(4, group_allow.id)]})
# check that now the field is not readonly
self.assertFalse(
test_config_settings.fields_get()["group_test_access_restricted"][
"readonly"
]
)
# check that now the group is in classified
self.assertIn(
"group_test_access_restricted",
self._get_classified_groups(test_config_settings),
)
self.assertFalse(self.env["res.users"].with_user(demo_user.id).has_group(GROUP))
test_config_settings.with_user(demo_user.id).execute()
self.assertTrue(self.env["res.users"].with_user(demo_user.id).has_group(GROUP))
def test_assert_raises(self):
demo_user = self.demo_user
group_system = self.env.ref("base.group_system")
demo_user.write({"group_ids": [(3, self.group.id)]})
self.group.write({"users": [(3, demo_user.id)]})
self.assertFalse(self.env["res.users"].with_user(demo_user.id).has_group(GROUP))
demo_user.write({"group_ids": [(4, group_system.id)]})
self.assertFalse(self.env["res.users"].with_user(demo_user.id).has_group(GROUP))
# check that there is no access to put test group into implied_ids anyways
with self.assertRaises(AccessError):
group_system.with_user(demo_user.id).write(
{"implied_ids": [(4, self.group.id)]}
)

View File

@ -0,0 +1,39 @@
from odoo.tests.common import TransactionCase
def _sel_groups_field_name(group_ids):
"""Generate sel_groups field name from group ids (replaces removed name_selection_groups)."""
return 'sel_groups_' + '_'.join(str(i) for i in sorted(group_ids))
class TestFieldsGet(TransactionCase):
def test_base(self):
demo_user = self.env["res.users"].create({
"name": "Test Demo User Fields",
"login": "test_demo_fields_get",
"groups_id": [(6, 0, [self.env.ref("base.group_user").id])],
})
group_erp_manager = self.env.ref("base.group_erp_manager")
group_system = self.env.ref("base.group_system")
demo_user.write({"group_ids": [(3, group_system.id)]})
group_system.write({"user_ids": [(3, demo_user.id)]})
demo_user.write({"group_ids": [(4, group_erp_manager.id)]})
view_users_form = self.env.ref("base.view_users_form")
(
self.env["res.users"]
.with_user(demo_user)
.with_context({"uid": demo_user.id})
.get_views([[view_users_form.id, "form"]])
)
sel_groups = _sel_groups_field_name([group_erp_manager.id])
res = self.env["res.users"].with_user(demo_user).fields_get()
self.assertTrue(res.get(sel_groups))
demo_user.write({"groups_id": [(4, group_system.id)]})
sel_groups = _sel_groups_field_name([group_erp_manager.id, group_system.id])
res = self.env["res.users"].with_user(demo_user).fields_get()
self.assertTrue(res.get(sel_groups))

View File

@ -0,0 +1,71 @@
from odoo.tests.common import TransactionCase, tagged
IR_CONFIG_NAME = "access_restricted.fields_view_get_uid"
@tagged("post_install", "-at_install")
class TestFieldsViewGet(TransactionCase):
def clear_config(self):
self.env["ir.config_parameter"].search([("key", "=", IR_CONFIG_NAME)]).unlink()
def clear_access(self, user):
user.write(
{
"group_ids": [
(3, self.env.ref("base.group_erp_manager").id, 0),
(3, self.env.ref("base.group_system").id, 0),
]
}
)
def add_access(self, user, group_xmlid):
user.write({"group_ids": [(4, self.env.ref(group_xmlid).id, 0)]})
def _view_form(self, user, view_xmlid):
view_id = self.env.ref(view_xmlid).id
# context = {'lang': "en_US", 'tz': "Europe/Brussels", 'uid': user.id}
self.env["res.users"].with_user(user.id).get_view(view_id=view_id)
def view_preference_form(self, user):
self._view_form(user, "base.view_users_form_simple_modif")
def view_user_form(self, user):
self._view_form(user, "base.view_users_form")
def view_form_all(self, user):
self.view_preference_form(user)
self.clear_config()
self.view_user_form(user)
self.clear_config()
def view_form_mix(self, user1, user2):
self.view_preference_form(user1)
self.view_user_form(user2)
self.view_preference_form(user1)
self.view_preference_form(user2)
self.view_form_all(user1)
self.view_user_form(user2)
def test_base(self):
admin = self.env.ref("base.user_root")
demo = self.env.ref("base.user_demo")
# test for admin
self.view_form_all(admin)
# demo doesn't have admin rights
self.clear_access(demo)
self.view_preference_form(demo)
# demo has "Access Rights"
self.add_access(demo, "base.group_erp_manager")
self.view_form_all(demo)
# demo has "Settings"
self.add_access(demo, "base.group_system")
self.view_form_all(demo)
self.view_form_mix(admin, demo)

View File

@ -0,0 +1,18 @@
.. image:: https://itpp.dev/images/infinity-readme.png
:alt: Tested and maintained by IT Projects Labs
:target: https://itpp.dev
Show settings menu for non-admin
================================
Adds "Show Settings Menu" checkbox in user's access rights tab.
Uninstallation
==============
After uninstalling, you need to update ``base`` module to return restriction to ``Settings`` menu back.
Further information
===================
Tested on `Odoo 17.0 <https://github.com/odoo/odoo/commit/40b19d89846303016098840f4958fe7cc105067c>`_

View File

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

View File

@ -0,0 +1,15 @@
{
"name": "Show settings menu for non-admin",
"summary": """Allows to make Settings menu visible for non-admins""",
"version": "19.0.2.0.0",
"author": "IT-Projects LLC, Ivan Yelizariev",
"category": "Extra Tools",
"images": ["images/banner.png"],
"support": "apps@itpp.dev",
"website": "https://twitter.com/OdooFree",
"license": "Other OSI approved licence", # MIT
"depends": ["access_restricted"],
"data": ["security/access_settings_menu_security.xml"],
"demo": ["security/access_settings_menu_security_demo.xml"],
"installable": True,
}

View File

@ -0,0 +1,20 @@
`2.0.0`
-------
- Improvement: replace `access_apps` dependency with `access_restricted` since only the latter one is needed
`1.0.2`
-------
- FIX: make compatible with CI-tests of other modules
`1.0.1`
-------
- FIX issue occured when user has ``[x] Show Settings Menu`` access, but doesn't have administration rights
`1.0.0`
-------
- Init version

View File

@ -0,0 +1,33 @@
======================
Access settings menu
======================
Installation
============
* `Install <https://odoo-development.readthedocs.io/en/latest/odoo/usage/install-module.html>`__ this module in a usual way
Configuration
=============
* Open menu ``[[ Settings ]] >> Users & Companies >> Users``
* Open user form view (click on the line with the user)
* Click ``[Edit]``
* Select ``[Show Settings Menu]`` and click ``[Save]``
Usage
=====
Without this module installed:
* Non-admin user can't see the ``[[ Settings ]]`` menu.
With this module installed:
* If non-admin user has the ``[Show Settings Menu]`` right he can see the ``[[ Settings ]]`` menu.
Uninstallation
==============
After uninstalling, you need to update ``base`` module to return restriction to ``Settings`` menu back.

View File

@ -0,0 +1,25 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * access_settings_menu
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 12.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: access_settings_menu
#: model:res.groups,name:access_settings_menu.group_show_settings_menu
msgid "Show Settings Menu"
msgstr ""
#. module: access_settings_menu
#: model:ir.model,name:access_settings_menu.model_res_users
msgid "Users"
msgstr ""

View File

@ -0,0 +1,30 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * access_settings_menu
#
# Translators:
# Sergej Briesin <20bs18@gmail.com>, 2018
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 11.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-04-21 00:17+0000\n"
"PO-Revision-Date: 2018-04-21 00:17+0000\n"
"Last-Translator: Sergej Briesin <20bs18@gmail.com>, 2018\n"
"Language-Team: German (https://www.transifex.com/it-projects-llc/teams/76080/"
"de/)\n"
"Language: de\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"
#. module: access_settings_menu
#: model:res.groups,name:access_settings_menu.group_show_settings_menu
msgid "Show Settings Menu"
msgstr "Einstellungsmenü anzeigen"
#. module: access_settings_menu
#: model:ir.model,name:access_settings_menu.model_res_users
msgid "Users"
msgstr "Nutzer"

View File

@ -0,0 +1,31 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * access_settings_menu
#
# Translators:
# Randall <randall_castro@me.com>, 2018
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 11.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-04-21 00:17+0000\n"
"PO-Revision-Date: 2017-11-28 13:02+0000\n"
"Last-Translator: Randall <randall_castro@me.com>, 2018\n"
"Language-Team: Spanish (Costa Rica) (https://www.transifex.com/it-projects-"
"llc/teams/76080/es_CR/)\n"
"Language: es_CR\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"
#. module: access_settings_menu
#: model:res.groups,name:access_settings_menu.group_show_settings_menu
msgid "Show Settings Menu"
msgstr "Mostrar Menu de Ajustes"
#. module: access_settings_menu
#: model:ir.model,name:access_settings_menu.model_res_users
msgid "Users"
msgstr "Usuarios"

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

View File

@ -0,0 +1,10 @@
from odoo import api, models
class ResUsers(models.Model):
_inherit = "res.users"
@api.model
def fields_get(self, *args, **kwargs):
# switch to superuser to get access to virtual fields
return super(ResUsers, self.sudo()).fields_get(*args, **kwargs)

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!-- Show Settings Menu -->
<record id="group_show_settings_menu" model="res.groups">
<field name="name">Show Settings Menu</field>
<field name="user_ids" eval="[(4, ref('base.user_root'))]" />
</record>
<record model="ir.ui.menu" id="base.menu_administration">
<field
name="group_ids"
eval="[(6,0, [ref('access_settings_menu.group_show_settings_menu')])]"
/>
</record>
<record model="res.groups" id="base.group_erp_manager">
<field
name="implied_ids"
eval="[(4, ref('access_settings_menu.group_show_settings_menu'))]"
/>
</record>
</odoo>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2018 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
License MIT (https://opensource.org/licenses/MIT). -->
<odoo>
<!-- Grant access to Demo User to don't break CI of other modules -->
<record id="group_show_settings_menu" model="res.groups">
<field name="user_ids" eval="[(4, ref('base.user_demo'))]" />
</record>
</odoo>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

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

View File

@ -0,0 +1,20 @@
from odoo.tests import common
from odoo.addons.access_restricted.tests.test_fields_view_get import (
TestFieldsViewGet as TestFieldsViewGetBase,
)
@common.tagged("post_install", "-at_install")
class TestFieldsViewGet(TestFieldsViewGetBase):
def test_access_settings_menu(self):
admin = self.env.ref("base.user_root")
demo = self.env.ref("base.user_demo")
# demo doesn't have admin rights, but has "Show Settings Menu"
self.clear_access(demo)
self.add_access(demo, "access_settings_menu.group_show_settings_menu")
self.view_form_all(demo)
self.view_form_mix(admin, demo)
self.clear_config()
self.view_form_mix(demo, admin)

View File

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

View File

@ -0,0 +1,19 @@
{
'name': 'Account Bank Statement 1C Import',
'category': 'Accounting',
'version': '19.0.0.1',
'author': 'MK.Lab',
'website': 'https://inf-centre.ru/',
'depends': [
'account',
'l10n_ru_doc',
],
'data': [
'security/ir.model.access.csv',
'wizard/invoice_import_wizard.xml',
'views/account_journal_wizard_action.xml',
'views/account_journal_dashboard_inherit.xml',
'views/account_journal_settings.xml',
],
'installable': True,
}

View File

@ -0,0 +1,160 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * account_bank_statement_1c_import
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 17.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-18 22:00+0000\n"
"PO-Revision-Date: 2024-03-18 22:00+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: account_bank_statement_1c_import
#: model:ir.model,name:account_bank_statement_1c_import.model_account_bank_statement
msgid "Bank Statement"
msgstr "Банковская выписка"
#. module: account_bank_statement_1c_import
#: model_terms:ir.ui.view,arch_db:account_bank_statement_1c_import.view_invoice_import_wizard_form
msgid "Cancel"
msgstr "Отменить"
#. module: account_bank_statement_1c_import
#: model:ir.model.fields,field_description:account_bank_statement_1c_import.field_invoice_import_wizard__create_uid
msgid "Created by"
msgstr ""
#. module: account_bank_statement_1c_import
#: model:ir.model.fields,field_description:account_bank_statement_1c_import.field_invoice_import_wizard__create_date
msgid "Created on"
msgstr ""
#. module: account_bank_statement_1c_import
#: model:ir.model.fields,field_description:account_bank_statement_1c_import.field_account_bank_statement__date_from
msgid "Date From"
msgstr ""
#. module: account_bank_statement_1c_import
#: model:ir.model.fields,field_description:account_bank_statement_1c_import.field_invoice_import_wizard__display_name
msgid "Display Name"
msgstr ""
#. module: account_bank_statement_1c_import
#. odoo-python
#: code:addons/account_bank_statement_1c_import/wizard/invoice_import_wizard.py:0
#, python-format
msgid "Error Import: %s"
msgstr ""
#. module: account_bank_statement_1c_import
#: model:ir.model.fields,field_description:account_bank_statement_1c_import.field_invoice_import_wizard__file
msgid "File"
msgstr "Файл"
#. module: account_bank_statement_1c_import
#: model:ir.model.fields,field_description:account_bank_statement_1c_import.field_invoice_import_wizard__file_name
msgid "File Name"
msgstr ""
#. module: account_bank_statement_1c_import
#: model:ir.model.fields,field_description:account_bank_statement_1c_import.field_invoice_import_wizard__id
msgid "ID"
msgstr ""
#. module: account_bank_statement_1c_import
#: model_terms:ir.ui.view,arch_db:account_bank_statement_1c_import.view_invoice_import_wizard_form
msgid "Import"
msgstr "Импортировать"
#. module: account_bank_statement_1c_import
#: model:ir.actions.act_window,name:account_bank_statement_1c_import.action_invoice_import_wizard
#: model:ir.model,name:account_bank_statement_1c_import.model_invoice_import_wizard
#: model:ir.ui.menu,name:account_bank_statement_1c_import.menu_invoice_import
#: model_terms:ir.ui.view,arch_db:account_bank_statement_1c_import.account_journal_dashboard_inherit
#: model_terms:ir.ui.view,arch_db:account_bank_statement_1c_import.view_invoice_import_wizard_form
msgid "Import Invoice"
msgstr "Импортировать выписку"
#. module: account_bank_statement_1c_import
#: model:ir.actions.server,name:account_bank_statement_1c_import.action_import_invoice_wizard
msgid "Import Invoice Wizard"
msgstr "Импорт выписки"
#. module: account_bank_statement_1c_import
#: model:ir.model,name:account_bank_statement_1c_import.model_account_journal
msgid "Journal"
msgstr "Журнал"
#. module: account_bank_statement_1c_import
#: model:ir.model.fields,field_description:account_bank_statement_1c_import.field_invoice_import_wizard__write_uid
msgid "Last Updated by"
msgstr ""
#. module: account_bank_statement_1c_import
#: model:ir.model.fields,field_description:account_bank_statement_1c_import.field_invoice_import_wizard__write_date
msgid "Last Updated on"
msgstr ""
#. module: account_bank_statement_1c_import
#. odoo-python
#: code:addons/account_bank_statement_1c_import/wizard/invoice_import_wizard.py:0
#, python-format
msgid "Only TXT files are allowed."
msgstr "Разрешены только файлы TXT"
#. module: account_bank_statement_1c_import
#. odoo-python
#: code:addons/account_bank_statement_1c_import/wizard/invoice_import_wizard.py:0
#, python-format
msgid "Please upload the file."
msgstr "Пожалуйста, загрузите файл"
#. module: account_bank_statement_1c_import
#. odoo-python
#: code:addons/account_bank_statement_1c_import/wizard/invoice_import_wizard.py:0
#, python-format
msgid "Statement not found for journal {} on date {}."
msgstr "Не найдено выписки по журналу {} и дате {}"
#. module: account_bank_statement_1c_import
#. odoo-python
#: code:addons/account_bank_statement_1c_import/wizard/invoice_import_wizard.py:0
#, python-format
msgid "Suitable journal not found."
msgstr ""
"Подходящий журнал не найден. Проверьте стоит ли галочка 'Использовать в "
"банковской выписке' в журнале"
#. module: account_bank_statement_1c_import
#: model:ir.model.fields,field_description:account_bank_statement_1c_import.field_account_journal__use_in_bank_statement
msgid "Use in Bank Statement"
msgstr "Использовать в банковской выписке"
#. module: account_bank_statement_1c_import
#. odoo-python
#: code:addons/account_bank_statement_1c_import/wizard/invoice_import_wizard.py:0
#, python-format
msgid "inn your company not found."
msgstr ""
"ИНН вашей компании не найден в файле. Пожалуйста, проверьте корректность "
"файла."
#. module: account_bank_statement_1c_import
#. odoo-python
#: code:addons/account_bank_statement_1c_import/wizard/invoice_import_wizard.py:0
#, python-format
msgid "Выписка успешно импортирована."
msgstr ""
#. module: account_bank_statement_1c_import
#. odoo-python
#: code:addons/account_bank_statement_1c_import/wizard/invoice_import_wizard.py:0
#, python-format
msgid "Импорт завершен"
msgstr ""

View File

@ -0,0 +1,2 @@
from . import account_journal
from . import account_bank_statement

View File

@ -0,0 +1,6 @@
from odoo import models, fields
class AccountBankStatement(models.Model):
_inherit = 'account.bank.statement'
date_from = fields.Date(string="Date From")

View File

@ -0,0 +1,6 @@
from odoo import models, fields
class AccountJournal(models.Model):
_inherit = 'account.journal'
use_in_bank_statement = fields.Boolean(string="Use in Bank Statement")

View File

@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_invoice_import_wizard,access_invoice_import_wizard,model_invoice_import_wizard,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_invoice_import_wizard access_invoice_import_wizard model_invoice_import_wizard base.group_user 1 1 1 1

View File

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

View File

@ -0,0 +1,293 @@
"""
Tests for Parser_1C (account_bank_statement_1c_import).
Validates: Requirements 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.9
"""
import base64
from odoo.exceptions import UserError
from odoo.tests.common import TransactionCase
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
COMPANY_INN = "7700000001"
COMPANY_KPP = "770001001"
PARTNER_INN = "7700000002"
PARTNER_KPP = "770001002"
VALID_TXT_TEMPLATE = """\
1CClientBankExchange
ВерсияФормата=1.03
Кодировка=Windows
Отправитель=Бухгалтерия предприятия
ДатаСоздания=01.01.2024
ВремяСоздания=12:00:00
ДатаНачала=01.01.2024
ДатаКонца=31.01.2024
РасчСчет=40702810000000000001
СекцияРасчСчет
ДатаНачала=01.01.2024
ДатаКонца=31.01.2024
РасчСчет=40702810000000000001
НачальныйОстаток=10000.00
ВсегоПоступило=5000.00
ВсегоСписано=3000.00
КонечныйОстаток=12000.00
КонецРасчСчет
{transactions}КонецФайла
"""
TRANSACTION_PAYER = """\
СекцияДокумент=Платежное поручение
Номер=1
Дата=15.01.2024
Сумма=1000.00
ПлательщикСчет=40702810000000000001
ДатаСписано=15.01.2024
ПлательщикИНН={company_inn}
ПлательщикКПП={company_kpp}
Плательщик=ООО Наша Компания
Плательщик1=ООО Наша Компания
ПлательщикРасчСчет=40702810000000000001
ПлательщикБанк1=Банк Плательщика
ПлательщикБИК=044525001
ПлательщикКорсчет=30101810400000000001
ПолучательСчет=40702810000000000002
ПолучательИНН={partner_inn}
ПолучательКПП={partner_kpp}
Получатель=ООО Контрагент
Получатель1=ООО Контрагент
ПолучательРасчСчет=40702810000000000002
ПолучательБанк1=Банк Получателя
ПолучательБИК=044525002
ПолучательКорсчет=30101810400000000002
НазначениеПлатежа=Оплата по договору №1
КонецДокумента
"""
TRANSACTION_RECIPIENT = """\
СекцияДокумент=Платежное поручение
Номер=2
Дата=16.01.2024
Сумма=2000.00
ПлательщикСчет=40702810000000000002
ДатаПоступило=16.01.2024
ПлательщикИНН={partner_inn}
ПлательщикКПП={partner_kpp}
Плательщик=ООО Контрагент
Плательщик1=ООО Контрагент
ПлательщикРасчСчет=40702810000000000002
ПлательщикБанк1=Банк Плательщика
ПлательщикБИК=044525002
ПлательщикКорсчет=30101810400000000002
ПолучательСчет=40702810000000000001
ПолучательИНН={company_inn}
ПолучательКПП={company_kpp}
Получатель=ООО Наша Компания
Получатель1=ООО Наша Компания
ПолучательРасчСчет=40702810000000000001
ПолучательБанк1=Банк Получателя
ПолучательБИК=044525001
ПолучательКорсчет=30101810400000000001
НазначениеПлатежа=Поступление по договору №2
КонецДокумента
"""
TRANSACTION_UNKNOWN_INN = """\
СекцияДокумент=Платежное поручение
Номер=3
Дата=17.01.2024
Сумма=500.00
ПлательщикСчет=40702810000000000003
ДатаСписано=17.01.2024
ПлательщикИНН=9999999999
ПлательщикКПП=999999999
Плательщик=ООО Чужая Компания
Плательщик1=ООО Чужая Компания
ПлательщикРасчСчет=40702810000000000003
ПлательщикБанк1=Чужой Банк
ПлательщикБИК=044525003
ПлательщикКорсчет=30101810400000000003
ПолучательСчет=40702810000000000004
ПолучательИНН=8888888888
ПолучательКПП=888888888
Получатель=ООО Другая Компания
Получатель1=ООО Другая Компания
ПолучательРасчСчет=40702810000000000004
ПолучательБанк1=Другой Банк
ПолучательБИК=044525004
ПолучательКорсчет=30101810400000000004
НазначениеПлатежа=Транзакция без нашей компании
КонецДокумента
"""
def _encode(text):
"""Encode text to base64 as cp1251 (as the wizard expects)."""
return base64.b64encode(text.encode('cp1251'))
def _make_txt(transactions=""):
return VALID_TXT_TEMPLATE.format(transactions=transactions)
def _payer_tx():
return TRANSACTION_PAYER.format(
company_inn=COMPANY_INN, company_kpp=COMPANY_KPP,
partner_inn=PARTNER_INN, partner_kpp=PARTNER_KPP,
)
def _recipient_tx():
return TRANSACTION_RECIPIENT.format(
company_inn=COMPANY_INN, company_kpp=COMPANY_KPP,
partner_inn=PARTNER_INN, partner_kpp=PARTNER_KPP,
)
def _unknown_inn_tx():
return TRANSACTION_UNKNOWN_INN
# ---------------------------------------------------------------------------
# Fixtures / setUp helpers
# ---------------------------------------------------------------------------
class _Base(TransactionCase):
"""Common setUp: company VAT, bank journal with use_in_bank_statement."""
def setUp(self):
super().setUp()
# Set company VAT so the parser can match INN
self.env.company.write({'vat': COMPANY_INN})
# Create (or find) a bank journal with use_in_bank_statement=True
self.journal = self.env['account.journal'].search(
[('type', '=', 'bank'), ('use_in_bank_statement', '=', True)],
limit=1,
)
if not self.journal:
self.journal = self.env['account.journal'].create({
'name': 'Test Bank Journal',
'type': 'bank',
'code': 'TBNK',
'use_in_bank_statement': True,
})
def _wizard(self, file_content_bytes, file_name='statement.txt'):
"""Create an InvoiceImportWizard record."""
return self.env['invoice.import.wizard'].create({
'file': base64.b64encode(file_content_bytes),
'file_name': file_name,
})
# ---------------------------------------------------------------------------
# TestParser1CBasic
# ---------------------------------------------------------------------------
class TestParser1CBasic(_Base):
"""Validates: Requirements 2.1, 2.2, 2.3"""
def test_valid_txt_parses_without_exception(self):
"""Req 2.1 — correct TXT file is parsed without exceptions."""
content = _make_txt().encode('cp1251')
wizard = self._wizard(content, 'statement.txt')
# action_import_invoice should not raise
wizard.action_import_invoice()
def test_wrong_extension_raises_user_error(self):
"""Req 2.2 — non-.txt extension raises UserError."""
content = _make_txt().encode('cp1251')
wizard = self._wizard(content, 'statement.csv')
with self.assertRaises(UserError):
wizard.action_import_invoice()
def test_no_file_raises_user_error(self):
"""Req 2.3 — missing file raises UserError."""
wizard = self.env['invoice.import.wizard'].create({
'file': False,
'file_name': 'statement.txt',
})
with self.assertRaises(UserError):
wizard.action_import_invoice()
# ---------------------------------------------------------------------------
# TestParser1CLines
# ---------------------------------------------------------------------------
class TestParser1CLines(_Base):
"""Validates: Requirements 2.4, 2.5, 2.6"""
def _import_and_get_lines(self, transactions_text):
content = _make_txt(transactions_text).encode('cp1251')
wizard = self._wizard(content, 'statement.txt')
wizard.action_import_invoice()
return self.env['account.bank.statement.line'].search([], order='id desc')
def test_company_as_payer_creates_negative_amount(self):
"""Req 2.4 — company INN as payer → negative amount line."""
lines = self._import_and_get_lines(_payer_tx())
payer_lines = lines.filtered(lambda l: l.payment_ref == '1')
self.assertTrue(payer_lines, "Expected a statement line with payment_ref='1'")
self.assertLess(payer_lines[0].amount, 0, "Amount should be negative when company is payer")
def test_company_as_recipient_creates_positive_amount(self):
"""Req 2.5 — company INN as recipient → positive amount line."""
lines = self._import_and_get_lines(_recipient_tx())
recipient_lines = lines.filtered(lambda l: l.payment_ref == '2')
self.assertTrue(recipient_lines, "Expected a statement line with payment_ref='2'")
self.assertGreater(recipient_lines[0].amount, 0, "Amount should be positive when company is recipient")
def test_duplicate_line_is_skipped(self):
"""Req 2.6 — importing the same transaction twice does not create a duplicate."""
tx = _payer_tx()
content = _make_txt(tx).encode('cp1251')
# First import
self._wizard(content, 'statement.txt').action_import_invoice()
count_after_first = self.env['account.bank.statement.line'].search_count(
[('payment_ref', '=', '1')]
)
# Second import of the same file
self._wizard(content, 'statement.txt').action_import_invoice()
count_after_second = self.env['account.bank.statement.line'].search_count(
[('payment_ref', '=', '1')]
)
self.assertEqual(count_after_first, count_after_second,
"Duplicate transaction should be skipped on second import")
# ---------------------------------------------------------------------------
# TestParser1CPartner
# ---------------------------------------------------------------------------
class TestParser1CPartner(_Base):
"""Validates: Requirements 2.7, 2.9"""
def test_unknown_inn_kpp_creates_new_partner(self):
"""Req 2.7 — unknown INN/KPP → new partner of type 'company' is created."""
# Ensure partner does not exist
self.env['res.partner'].search([('vat', '=', PARTNER_INN)]).unlink()
content = _make_txt(_payer_tx()).encode('cp1251')
self._wizard(content, 'statement.txt').action_import_invoice()
partner = self.env['res.partner'].search([('vat', '=', PARTNER_INN)], limit=1)
self.assertTrue(partner, "A new partner should have been created")
self.assertEqual(partner.company_type, 'company',
"New partner should have company_type='company'")
def test_unknown_company_inn_in_transaction_raises_user_error(self):
"""Req 2.9 — company INN not found as payer or recipient → UserError."""
content = _make_txt(_unknown_inn_tx()).encode('cp1251')
wizard = self._wizard(content, 'statement.txt')
with self.assertRaises(UserError):
wizard.action_import_invoice()

View File

@ -0,0 +1,16 @@
<odoo>
<record id="account_journal_dashboard_inherit" model="ir.ui.view">
<field name="name">account.dashboard.inherit</field>
<field name="model">account.journal</field>
<field name="inherit_id" ref="account.account_journal_dashboard_kanban_view"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='bank_cash_buttons']" position="inside">
<button type="action"
name="%(account_bank_statement_1c_import.action_import_invoice_wizard)d"
class="btn btn-primary">
Импортировать выписку
</button>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,14 @@
<odoo>
<data>
<record id="view_account_journal_form_inherit" model="ir.ui.view">
<field name="name">account.journal.form.inherit</field>
<field name="model">account.journal</field>
<field name="inherit_id" ref="account.view_account_journal_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='type']" position="after">
<field name="use_in_bank_statement"/>
</xpath>
</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,17 @@
<odoo>
<record id="account_bank_statement_1c_import.action_import_invoice_wizard" model="ir.actions.server">
<field name="name">Import Invoice Wizard</field>
<field name="model_id" ref="model_invoice_import_wizard"/>
<field name="binding_model_id" ref="account.model_account_journal"/>
<field name="state">code</field>
<field name="code">
action = {
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'invoice.import.wizard',
'target': 'new',
'name': 'Import Invoice'
}
</field>
</record>
</odoo>

View File

@ -0,0 +1,7 @@
<odoo>
<menuitem id="menu_invoice_import"
name="Import Invoice"
action="action_invoice_import_wizard"
parent="account.menu_finance"
sequence="5"/>
</odoo>

View File

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

View File

@ -0,0 +1,202 @@
from odoo import models, fields, _
from odoo.exceptions import UserError
import base64
import dateutil.parser
# import logging
# _logger = logging.getLogger(__name__)
class InvoiceImportWizard(models.TransientModel):
_name = 'invoice.import.wizard'
_description = 'Import Invoice'
file = fields.Binary(string='File', required=True)
file_name = fields.Char(string='File Name')
def action_import_invoice(self):
# _logger.info("Начало импорта файла выписки")
if not self.file:
# _logger.error("Файл не загружен")
raise UserError(_("Please upload the file."))
if not self.file_name.endswith('.txt'):
# _logger.error("Неподдерживаемый формат файла: %s", self.file_name)
raise UserError(_("Only TXT files are allowed."))
file_content = base64.b64decode(self.file).decode('cp1251')
journal = self.env['account.journal'].search([('type', '=', 'bank'), ('use_in_bank_statement', '=', True)],
limit=1)
if not journal:
raise UserError(_("Suitable journal not found."))
try:
sections = file_content.split('СекцияДокумент')
invoice_sections = sections[0]
transaction_sections = sections[1:]
invoices = invoice_sections.split('СекцияРасчСчет')[1:]
for invoice in invoices:
statement_data = self.parse_statement_data(invoice)
existing_statement = self.env['account.bank.statement'].search([
('date_from', '=', dateutil.parser.parse(statement_data.get('ДатаНачала', '').strip(), dayfirst=True).date()),
('name', '=', statement_data.get('РасчСчет', '').strip()),
], limit=1)
if not existing_statement:
statement = self.create_statement(journal, statement_data)
# _logger.info("Выписка создана успешно: %s", statement.name)
self.create_statement_lines(journal, transaction_sections)
except Exception as e:
# _logger.error("Ошибка при импорте файла: %s", e)
raise UserError(_("Error Import: %s" % str(e)))
message_title = _("Импорт завершен")
message_content = _("Выписка успешно импортирована.")
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': message_title,
'message': message_content,
},
}
def parse_statement_data(self, section):
lines = section.strip().split('\n')
statement_data = {}
for line in lines:
if '=' in line:
key, value = line.split('=', 1)
statement_data[key] = value
return statement_data
def create_statement(self, journal, statement_data):
statement_vals = {
'journal_id': journal.id,
'name': statement_data.get('РасчСчет', '').strip(),
'balance_start': float(statement_data.get('НачальныйОстаток', '0.0').strip()),
'balance_end_real': float(statement_data.get('КонечныйОстаток', '0.0').strip()),
'date_from': dateutil.parser.parse(statement_data.get('ДатаНачала', '').strip(), dayfirst=True).date(),
}
return self.env['account.bank.statement'].sudo().create(statement_vals)
def create_statement_lines(self, journal, sections):
for section in sections:
if section.strip():
lines = section.strip().split('\n')
transaction = self.parse_transaction_data(lines)
if transaction:
# _logger.info("<==================================>")
# _logger.info("Создание строки выписки для транзакции №%s", transaction.get('Номер').strip())
self.create_line(journal, transaction)
# _logger.info("Строка выписки для транзакции №%s создана успешно", transaction.get('Номер').strip())
def parse_transaction_data(self, lines):
transaction = {}
for line in lines:
if '=' in line:
key, value = line.split('=', 1)
transaction[key] = value
return transaction
def create_line(self, journal, transaction):
my_company_inn = journal.company_id.vat
transaction_date = dateutil.parser.parse(transaction.get('Дата', '').strip(), dayfirst=True).date()
statement = self.env['account.bank.statement'].search([
('date_from', '=', transaction_date),
], limit=1)
if not statement:
raise UserError(_("Statement not found for journal {} on date {}.").format(journal.name, transaction_date))
amount = float(transaction.get('Сумма', '0.0').strip())
if transaction.get('ПлательщикИНН').strip() == my_company_inn:
amount = -amount
partner_inn = transaction.get('ПолучательИНН').strip()
partner_kpp = transaction.get('ПолучательКПП').strip()
partner_name = transaction.get('Получатель').strip()
partner_account = transaction.get('ПолучательСчет').strip()
bank_bik = transaction.get('ПолучательБИК').strip()
bank_account = transaction.get('ПолучательКорсчет').strip()
bank_name = transaction.get('ПолучательБанк1').strip()
elif transaction.get('ПолучательИНН').strip() == my_company_inn:
# Если наша компания - получатель, значит это доход
partner_inn = transaction.get('ПлательщикИНН').strip()
partner_kpp = transaction.get('ПлательщикКПП').strip()
partner_name = transaction.get('Плательщик').strip()
partner_account = transaction.get('ПлательщикСчет').strip()
bank_bik = transaction.get('ПлательщикБИК').strip()
bank_account = transaction.get('ПлательщикКорсчет').strip()
bank_name = transaction.get('ПлательщикБанк1').strip()
else:
error_message = "ИНН вашей компании не найден в транзакции. Запись не создана."
error_message += "Журнал: {}, Дата транзакции: {}. ".format(journal.name, transaction_date)
error_message += "Плательщик: {}, ИНН плательщика: {}. ".format(transaction.get('Плательщик1', 'не указан').strip(),
transaction.get('ПлательщикИНН',
'не указан').strip())
error_message += "Получатель: {}, ИНН получателя: {}. ".format(transaction.get('Получатель1', 'не указан').strip(),
transaction.get('ПолучательИНН',
'не указан').strip())
raise UserError(_(f"{error_message}"))
existing_line = self.env['account.bank.statement.line'].search([
('date', '=', transaction_date),
('amount', '=', amount),
('narration', '=', transaction.get('НазначениеПлатежа', '').strip()),
('payment_ref', '=', transaction.get('Номер').strip()),
], limit=1)
if existing_line:
# _logger.info("Строка выписки уже существует: %s", existing_line.id)
return None
partner = self.env['res.partner'].search([('vat', '=', partner_inn), ('kpp', '=', partner_kpp)], limit=1)
if not partner:
# _logger.info("Создание нового партнера: ИНН %s, КПП %s", partner_inn, partner_kpp)
partner = self.env['res.partner'].create({
'name': partner_name,
'vat': partner_inn,
'kpp': partner_kpp,
'company_type': 'company',
})
bank = self.env['res.bank'].search([('bic', '=', bank_bik), ('corr_acc', '=', bank_account)], limit=1)
if not bank:
# _logger.info("Создание нового банка: БИК %s", bank_bik)
bank = self.env['res.bank'].create({
'name': bank_name,
'bic': bank_bik,
'corr_acc': bank_account,
})
partner_bank_account = self.env['res.partner.bank'].search(
[('acc_number', '=', partner_account), ('partner_id', '=', partner.id)], limit=1)
if not partner_bank_account:
# _logger.info("Создание нового банковского счета для партнера %s", partner.name)
partner_bank_account = self.env['res.partner.bank'].create({
'acc_number': partner_account,
'bank_id': bank.id,
'partner_id': partner.id,
})
line_vals = {
'journal_id': journal.id,
'partner_bank_id': partner_bank_account.id,
'partner_id': partner.id,
'statement_id': statement.id,
'company_id': journal.company_id.id,
'date': transaction_date,
'amount': amount,
'narration': transaction.get('НазначениеПлатежа', '').strip(),
'payment_ref': transaction.get('Номер').strip(),
}
self.env['account.bank.statement.line'].sudo().create(line_vals)

View File

@ -0,0 +1,25 @@
<odoo>
<record id="view_invoice_import_wizard_form" model="ir.ui.view">
<field name="name">invoice.import.wizard.form</field>
<field name="model">invoice.import.wizard</field>
<field name="arch" type="xml">
<form string="Import Invoice">
<group>
<field name="file" filename="file_name"/>
<field name="file_name" invisible="1"/>
</group>
<footer>
<button string="Import" name="action_import_invoice" type="object" class="btn-primary"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="action_invoice_import_wizard" model="ir.actions.act_window">
<field name="name">Import Invoice</field>
<field name="res_model">invoice.import.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</odoo>

View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from . import models

View File

@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
{
'name': 'Account Demo Data Loader',
'version': '1.0',
'category': 'Technical',
'summary': 'Demo data for Account module',
'depends': ['account', 'demo_data_loader_base'],
'data': [],
'demo': [
'demo/demo_account.xml',
],
'installable': True,
'application': False,
'license': 'LGPL-3',
}

View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<!-- Демо контрагенты -->
<record id="demo_partner_invoice_1" model="res.partner">
<field name="name">Клиент Альфа</field>
<field name="email">alpha@demo.example.com</field>
<field name="phone">+1 555 400 1000</field>
<field name="customer_rank">1</field>
</record>
<record id="demo_partner_invoice_2" model="res.partner">
<field name="name">Клиент Бета</field>
<field name="email">beta@demo.example.com</field>
<field name="phone">+1 555 400 2000</field>
<field name="customer_rank">1</field>
</record>
<record id="demo_partner_vendor_1" model="res.partner">
<field name="name">Поставщик Гамма</field>
<field name="email">gamma@demo.example.com</field>
<field name="phone">+1 555 400 3000</field>
<field name="supplier_rank">1</field>
</record>
<!-- Демо счёт клиенту 1 -->
<record id="demo_account_move_invoice_1" model="account.move">
<field name="move_type">out_invoice</field>
<field name="partner_id" ref="demo_partner_invoice_1"/>
</record>
<record id="demo_account_move_invoice_1_line_1" model="account.move.line">
<field name="move_id" ref="demo_account_move_invoice_1"/>
<field name="name">Консалтинговые услуги</field>
<field name="quantity">10</field>
<field name="price_unit">150.0</field>
</record>
<!-- Демо счёт клиенту 2 -->
<record id="demo_account_move_invoice_2" model="account.move">
<field name="move_type">out_invoice</field>
<field name="partner_id" ref="demo_partner_invoice_2"/>
</record>
<record id="demo_account_move_invoice_2_line_1" model="account.move.line">
<field name="move_id" ref="demo_account_move_invoice_2"/>
<field name="name">Лицензия на ПО</field>
<field name="quantity">3</field>
<field name="price_unit">500.0</field>
</record>
<record id="demo_account_move_invoice_2_line_2" model="account.move.line">
<field name="move_id" ref="demo_account_move_invoice_2"/>
<field name="name">Годовая поддержка</field>
<field name="quantity">1</field>
<field name="price_unit">1200.0</field>
</record>
<!-- Демо счёт от поставщика -->
<record id="demo_account_move_bill_1" model="account.move">
<field name="move_type">in_invoice</field>
<field name="partner_id" ref="demo_partner_vendor_1"/>
</record>
<record id="demo_account_move_bill_1_line_1" model="account.move.line">
<field name="move_id" ref="demo_account_move_bill_1"/>
<field name="name">Канцелярские товары</field>
<field name="quantity">5</field>
<field name="price_unit">80.0</field>
</record>
</odoo>

View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from . import res_config_settings

View File

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
from odoo import models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'

View File

@ -0,0 +1,97 @@
============================
Шаблоны типовых операций
============================
.. |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/licence-LGPL--3-blue.png
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
|badge1| |badge2|
Модуль позволяет создавать шаблоны типовых финансовых операций и применять их
для создания проводок с заранее определённым распределением по счетам.
**Содержание**
.. contents::
:local:
Возможности
===========
* Создание многократно используемых шаблонов типовых операций
* Определение распределения по счетам с использованием процентов
* Применение шаблонов для создания проводок через визард
* Категоризация шаблонов с помощью признаков (тегов)
* Автоматическая проверка баланса шаблона (дебет = кредит)
* Поддержка двух типов строк:
* Продуктовая строка - заменяет счёт в существующих строках
* Строка оплаты - создаёт новые строки дебиторки/кредиторки
Настройка
=========
Для настройки модуля необходимо:
#. Перейти в *Бухгалтерия > Настройка > Шаблоны типовых операций*
#. Создать новый шаблон
#. Добавить строки шаблона с указанием:
* Счёт
* Сторона (Дебет/Кредит)
* Процент (0.01-100.00%)
* Тип строки:
* Продуктовая строка - заменяет счёт в существующих product-строках
* Строка оплаты - создаёт новые receivable/payable строки
#. При необходимости добавить признаки для категоризации
Модуль автоматически проверяет, что сумма процентов по дебету равна сумме процентов по кредиту.
Использование
=============
Для использования модуля:
#. Откройте любой документ, который наследует ``account.move.template.mixin``
#. Нажмите кнопку "Создать проводку по шаблону"
#. Выберите шаблон
#. Укажите базовую сумму
#. Нажмите "Создать проводку"
Модуль создаст новую проводку со счетами, распределёнными согласно процентам в шаблоне.
Отслеживание ошибок
===================
Ошибки отслеживаются на `GitHub Issues <https://github.com/OCA/account-financial-tools/issues>`_.
В случае проблем, пожалуйста, проверьте, не была ли ваша проблема уже зарегистрирована.
Авторы
======
Авторы
~~~~~~
* MK.Lab, RuOdoo
Участники
~~~~~~~~~
* MK.Lab, RuOdoo
Сопровождающие
~~~~~~~~~~~~~~
Модуль сопровождается MK.Lab, RuOdoo.
.. image:: https://ruodoo.ru/logo.png
:alt: MK.Lab, RuOdoo
:target: https://ruodoo.ru
Модуль является частью проекта расширений для бухгалтерии.

View File

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

View File

@ -0,0 +1,36 @@
{
'name': 'Accounting Journal Templates',
'version': '19.0.1.0.0',
'summary': 'Шаблоны типовых финансовых операций',
'description': """
Шаблоны типовых финансовых операций
====================================
Создание многократно используемых шаблонов типовых проводок с заранее
определённым распределением по счетам с использованием процентов.
Возможности:
------------
* Создание шаблонов с несколькими строками счетов
* Определение сторон (дебет/кредит) и процентов
* Автоматическая проверка баланса шаблона
* Применение шаблонов через визард для создания проводок
* Категоризация шаблонов с помощью признаков (тегов)
* Поддержка продуктовых строк и строк оплаты
""",
'author': 'MK.Lab, RuOdoo',
'website': 'https://ruodoo.ru',
'category': 'Accounting/Accounting',
'depends': ['account'],
'data': [
'security/ir.model.access.csv',
'data/demo_templates.xml',
'views/account_move_template_views.xml',
'views/account_move_template_wizard_views.xml',
],
'demo': [],
'installable': True,
'application': False,
'license': 'LGPL-3',
'maintainers': ['mklab', 'ruodoo'],
}

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="demo_template_tag_warehouse" model="account.move.template.tag">
<field name="name">Склад</field>
<field name="color">1</field>
</record>
<record id="demo_template_tag_sales" model="account.move.template.tag">
<field name="name">Продажи</field>
<field name="color">2</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Теги шаблонов -->
<record id="demo_tag_general" model="account.move.template.tag">
<field name="name">Общие</field>
<field name="color">3</field>
</record>
<record id="demo_tag_tax" model="account.move.template.tag">
<field name="name">Налоги</field>
<field name="color">4</field>
</record>
<!-- Шаблон типовой операции: Начисление НДС -->
<!-- Примечание: account_id должны быть заменены на реальные счета из вашего плана счетов -->
<!-- Шаблон намеренно не создаётся здесь, т.к. требует конкретных счетов из плана счетов -->
</data>
</odoo>

View File

@ -0,0 +1,3 @@
from . import account_move_template
from . import account_move_template_mixin
from . import account_move_template_wizard

View File

@ -0,0 +1,94 @@
from odoo import api, fields, models
from odoo.exceptions import ValidationError
class AccountMoveTemplateTag(models.Model):
_name = 'account.move.template.tag'
_description = 'Признак шаблона'
name = fields.Char(string='Название', required=True, translate=True)
color = fields.Integer(string='Цвет')
class AccountMoveTemplate(models.Model):
_name = 'account.move.template'
_description = 'Шаблон типовой операции'
name = fields.Char(string='Название', required=True)
description = fields.Text(string='Описание')
tag_ids = fields.Many2many(
comodel_name='account.move.template.tag',
string='Признаки',
)
line_ids = fields.One2many(
comodel_name='account.move.template.line',
inverse_name='template_id',
string='Строки шаблона',
)
@api.constrains('line_ids')
def _check_balance(self):
for template in self:
if not template.line_ids:
raise ValidationError(
'Шаблон должен содержать хотя бы одну строку.'
)
debit_sum = sum(
line.percent for line in template.line_ids if line.move_type == 'debit'
)
credit_sum = sum(
line.percent for line in template.line_ids if line.move_type == 'credit'
)
if round(debit_sum, 2) != round(credit_sum, 2):
diff = round(abs(debit_sum - credit_sum), 2)
raise ValidationError(
f'Сумма дебета ({debit_sum:.2f}%) не равна сумме кредита '
f'({credit_sum:.2f}%), разница: {diff:.2f}%'
)
class AccountMoveTemplateLine(models.Model):
_name = 'account.move.template.line'
_description = 'Строка шаблона типовой операции'
template_id = fields.Many2one(
comodel_name='account.move.template',
string='Шаблон',
required=True,
ondelete='cascade',
)
account_id = fields.Many2one(
comodel_name='account.account',
string='Счёт',
required=True,
ondelete='restrict',
)
move_type = fields.Selection(
selection=[('debit', 'Дебет'), ('credit', 'Кредит')],
string='Сторона',
required=True,
)
percent = fields.Float(
string='Процент',
required=True,
digits=(5, 2),
)
line_type = fields.Selection(
selection=[
('product', 'Продуктовая строка (заменяет счёт)'),
('payment', 'Строка оплаты (receivable/payable)')
],
string='Тип строки',
required=True,
default='product',
help='Продуктовая строка заменяет счёт в существующих product-строках инвойса. '
'Строка оплаты создаёт новую payment_term строку.'
)
@api.constrains('percent')
def _check_percent(self):
for line in self:
if line.percent < 0.01 or line.percent > 100.0:
raise ValidationError(
'Процент должен быть от 0.01 до 100.00.'
)

View File

@ -0,0 +1,57 @@
from odoo import api, fields, models
class AccountMoveTemplateMixin(models.AbstractModel):
_name = 'account.move.template.mixin'
_description = 'Миксин для создания проводок через шаблоны'
move_ids = fields.Many2many(
comodel_name='account.move',
string='Проводки',
)
move_count = fields.Integer(
compute='_compute_move_count',
string='Количество проводок',
)
@api.depends('move_ids')
def _compute_move_count(self):
for record in self:
record.move_count = len(record.move_ids)
def action_open_journal_wizard(self):
"""If move_ids is empty, open wizard. Otherwise delegate to action_view_moves."""
self.ensure_one()
if self.move_ids:
return self.action_view_moves()
amounts = self.get_move_line_amounts()
default_amount = amounts[0]['amount'] if amounts else 0.0
return {
'type': 'ir.actions.act_window',
'name': 'Создать проводку',
'res_model': 'account.move.template.wizard',
'view_mode': 'form',
'target': 'new',
'context': {
'default_res_model': self._name,
'default_res_id': self.id,
'default_amount': default_amount,
},
}
def action_view_moves(self):
"""Return action to view related account.move records."""
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': 'Проводки',
'res_model': 'account.move',
'view_mode': 'list,form',
'domain': [('id', 'in', self.move_ids.ids)],
}
def get_move_line_amounts(self):
"""Override in inheriting models to provide line amounts.
Returns list of dicts: [{'name': str, 'amount': float}, ...]
"""
return []

View File

@ -0,0 +1,103 @@
from odoo import api, fields, models
from odoo.exceptions import UserError
class AccountMoveTemplateWizardLine(models.TransientModel):
_name = 'account.move.template.wizard.line'
_description = 'Черновая строка визарда шаблона'
wizard_id = fields.Many2one(
comodel_name='account.move.template.wizard',
required=True,
ondelete='cascade',
)
account_id = fields.Many2one(
comodel_name='account.account',
string='Счёт',
required=True,
)
move_type = fields.Selection(
selection=[('debit', 'Дебет'), ('credit', 'Кредит')],
string='Сторона',
required=True,
)
amount = fields.Float(
string='Сумма',
digits=(16, 2),
)
class AccountMoveTemplateWizard(models.TransientModel):
_name = 'account.move.template.wizard'
_description = 'Визард создания проводки по шаблону'
res_model = fields.Char(string='Модель документа', required=True)
res_id = fields.Integer(string='ID документа', required=True)
template_id = fields.Many2one(
comodel_name='account.move.template',
string='Шаблон',
)
amount = fields.Float(
string='Базовая сумма',
digits=(16, 2),
)
draft_line_ids = fields.One2many(
comodel_name='account.move.template.wizard.line',
inverse_name='wizard_id',
string='Черновые строки',
)
@api.onchange('template_id', 'amount')
def _onchange_compute_draft_lines(self):
"""Recompute draft lines when template or amount changes."""
self.draft_line_ids = [(5, 0, 0)] # clear existing
if not self.template_id or not self.amount:
return
lines = []
for tpl_line in self.template_id.line_ids:
line_amount = self.amount * tpl_line.percent / 100.0
lines.append((0, 0, {
'account_id': tpl_line.account_id.id,
'move_type': tpl_line.move_type,
'amount': line_amount,
}))
self.draft_line_ids = lines
def action_post(self):
"""Create account.move from draft lines and link to source document."""
self.ensure_one()
if not self.template_id or not self.amount:
raise UserError('Необходимо выбрать шаблон и указать базовую сумму.')
try:
# Build move lines
move_line_vals = []
for draft_line in self.draft_line_ids:
if draft_line.move_type == 'debit':
debit = draft_line.amount
credit = 0.0
else:
debit = 0.0
credit = draft_line.amount
move_line_vals.append((0, 0, {
'account_id': draft_line.account_id.id,
'debit': debit,
'credit': credit,
'name': self.template_id.name,
}))
# Create the move
move = self.env['account.move'].create({
'move_type': 'entry',
'state': 'draft',
'line_ids': move_line_vals,
})
# Link move to source document via move_ids
source_record = self.env[self.res_model].browse(self.res_id)
source_record.move_ids = [(4, move.id)]
except Exception as e:
raise UserError(f'Ошибка при создании проводки: {str(e)}') from e
return {'type': 'ir.actions.act_window_close'}

View File

@ -0,0 +1,6 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_account_move_template_user,account.move.template user,model_account_move_template,account.group_account_user,1,1,1,1
access_account_move_template_line_user,account.move.template.line user,model_account_move_template_line,account.group_account_user,1,1,1,1
access_account_move_template_tag_user,account.move.template.tag user,model_account_move_template_tag,account.group_account_user,1,1,1,1
access_account_move_template_wizard_user,account.move.template.wizard user,model_account_move_template_wizard,account.group_account_user,1,1,1,1
access_account_move_template_wizard_line_user,account.move.template.wizard.line user,model_account_move_template_wizard_line,account.group_account_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_account_move_template_user account.move.template user model_account_move_template account.group_account_user 1 1 1 1
3 access_account_move_template_line_user account.move.template.line user model_account_move_template_line account.group_account_user 1 1 1 1
4 access_account_move_template_tag_user account.move.template.tag user model_account_move_template_tag account.group_account_user 1 1 1 1
5 access_account_move_template_wizard_user account.move.template.wizard user model_account_move_template_wizard account.group_account_user 1 1 1 1
6 access_account_move_template_wizard_line_user account.move.template.wizard.line user model_account_move_template_wizard_line account.group_account_user 1 1 1 1

View File

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

View File

@ -0,0 +1,200 @@
"""
Tests for Template_Engine (account_move_templates).
Validates: Requirements 3.1, 3.2, 3.3, 3.4, 3.5
"""
from odoo.exceptions import ValidationError
from odoo.tests.common import TransactionCase
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _get_account(env, account_type='asset_current'):
"""Return an existing account of the given type, or create one."""
account = env['account.account'].search(
[('account_type', '=', account_type)], limit=1
)
if not account:
account = env['account.account'].create({
'name': f'Test Account ({account_type})',
'code': f'TST{account_type[:4].upper()}',
'account_type': account_type,
})
return account
def _make_template(env, name='Test Template', lines=None):
"""Create an AccountMoveTemplate with the given lines.
lines: list of dicts with keys: account_id, move_type, percent, line_type
"""
vals = {'name': name}
if lines is not None:
vals['line_ids'] = [(0, 0, line) for line in lines]
return env['account.move.template'].create(vals)
# ---------------------------------------------------------------------------
# TestMoveTemplateValidation
# ---------------------------------------------------------------------------
class TestMoveTemplateValidation(TransactionCase):
"""Validates: Requirements 3.1, 3.2, 3.3, 3.4"""
def setUp(self):
super().setUp()
self.account_debit = _get_account(self.env, 'asset_current')
self.account_credit = _get_account(self.env, 'liability_current')
def _line(self, move_type, percent, account=None):
if account is None:
account = self.account_debit if move_type == 'debit' else self.account_credit
return {
'account_id': account.id,
'move_type': move_type,
'percent': percent,
'line_type': 'product',
}
def test_no_lines_raises_validation_error(self):
"""Req 3.1 — template without lines raises ValidationError."""
with self.assertRaises(ValidationError):
_make_template(self.env, name='Empty Template', lines=[])
def test_imbalanced_percentages_raises_validation_error(self):
"""Req 3.2 — debit sum != credit sum raises ValidationError."""
with self.assertRaises(ValidationError):
_make_template(self.env, name='Imbalanced', lines=[
self._line('debit', 60.0),
self._line('credit', 40.0),
])
def test_balanced_percentages_saves_without_error(self):
"""Req 3.3 — debit sum == credit sum saves without error."""
template = _make_template(self.env, name='Balanced', lines=[
self._line('debit', 100.0),
self._line('credit', 100.0),
])
self.assertTrue(template.id, "Template should be saved with a valid ID")
def test_percent_below_minimum_raises_validation_error(self):
"""Req 3.4 — percent < 0.01 raises ValidationError."""
with self.assertRaises(ValidationError):
_make_template(self.env, name='Low Percent', lines=[
self._line('debit', 0.001),
self._line('credit', 0.001),
])
def test_percent_above_maximum_raises_validation_error(self):
"""Req 3.4 — percent > 100.0 raises ValidationError."""
with self.assertRaises(ValidationError):
_make_template(self.env, name='High Percent', lines=[
self._line('debit', 101.0),
self._line('credit', 101.0),
])
# ---------------------------------------------------------------------------
# TestMoveTemplateWizard
# ---------------------------------------------------------------------------
class TestMoveTemplateWizard(TransactionCase):
"""Validates: Requirement 3.5"""
def setUp(self):
super().setUp()
self.account_debit = _get_account(self.env, 'asset_current')
self.account_credit = _get_account(self.env, 'liability_current')
# Create a balanced template: 100% debit / 100% credit
self.template = self.env['account.move.template'].create({
'name': 'Wizard Test Template',
'line_ids': [
(0, 0, {
'account_id': self.account_debit.id,
'move_type': 'debit',
'percent': 100.0,
'line_type': 'product',
}),
(0, 0, {
'account_id': self.account_credit.id,
'move_type': 'credit',
'percent': 100.0,
'line_type': 'product',
}),
],
})
# Create a source document that uses the mixin (account.move itself
# does not use the mixin, so we create a minimal move to act as source)
self.source_move = self.env['account.move'].create({
'move_type': 'entry',
'state': 'draft',
'line_ids': [
(0, 0, {
'account_id': self.account_debit.id,
'debit': 0.0,
'credit': 0.0,
'name': 'Source line',
}),
],
})
def test_apply_template_creates_draft_move_with_correct_distribution(self):
"""Req 3.5 — applying template via wizard creates a draft account.move
with correct account distribution."""
base_amount = 1000.0
wizard = self.env['account.move.template.wizard'].create({
'res_model': 'account.move',
'res_id': self.source_move.id,
'template_id': self.template.id,
'amount': base_amount,
})
# Trigger onchange to populate draft lines
wizard._onchange_compute_draft_lines()
# Verify draft lines were computed
self.assertEqual(len(wizard.draft_line_ids), 2,
"Wizard should have 2 draft lines (one debit, one credit)")
debit_line = wizard.draft_line_ids.filtered(lambda l: l.move_type == 'debit')
credit_line = wizard.draft_line_ids.filtered(lambda l: l.move_type == 'credit')
self.assertAlmostEqual(debit_line.amount, base_amount,
msg="Debit line amount should equal base_amount * 100%")
self.assertAlmostEqual(credit_line.amount, base_amount,
msg="Credit line amount should equal base_amount * 100%")
# Post the wizard — creates the account.move
wizard.action_post()
# Reload source move and check linked moves
self.source_move.invalidate_recordset()
linked_moves = self.source_move.move_ids
self.assertTrue(linked_moves, "A new account.move should be linked to the source")
new_move = linked_moves[-1]
self.assertEqual(new_move.state, 'draft',
"Created move should be in draft state")
move_lines = new_move.line_ids
debit_move_line = move_lines.filtered(lambda l: l.debit > 0)
credit_move_line = move_lines.filtered(lambda l: l.credit > 0)
self.assertTrue(debit_move_line, "Move should have a debit line")
self.assertTrue(credit_move_line, "Move should have a credit line")
self.assertAlmostEqual(debit_move_line[0].debit, base_amount,
msg="Debit amount on move line should match base_amount")
self.assertAlmostEqual(credit_move_line[0].credit, base_amount,
msg="Credit amount on move line should match base_amount")
self.assertEqual(debit_move_line[0].account_id, self.account_debit,
"Debit line should use the debit account from template")
self.assertEqual(credit_move_line[0].account_id, self.account_credit,
"Credit line should use the credit account from template")

View File

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- List view -->
<record id="view_account_move_template_tree" model="ir.ui.view">
<field name="name">account.move.template.list</field>
<field name="model">account.move.template</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
<field name="description"/>
<field name="tag_ids" widget="many2many_tags"/>
</list>
</field>
</record>
<!-- Form view -->
<record id="view_account_move_template_form" model="ir.ui.view">
<field name="name">account.move.template.form</field>
<field name="model">account.move.template</field>
<field name="arch" type="xml">
<form>
<sheet>
<div class="oe_title">
<label for="name"/>
<h1>
<field name="name" placeholder="Название шаблона"/>
</h1>
</div>
<group>
<field name="description"/>
<field name="tag_ids" widget="many2many_tags"/>
</group>
<notebook>
<page string="Строки шаблона" name="lines">
<field name="line_ids">
<list editable="bottom">
<field name="account_id"/>
<field name="move_type"/>
<field name="percent"/>
<field name="line_type"/>
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- Search view -->
<record id="view_account_move_template_search" model="ir.ui.view">
<field name="name">account.move.template.search</field>
<field name="model">account.move.template</field>
<field name="arch" type="xml">
<search>
<field name="name" string="Название"/>
<field name="tag_ids" string="Признак"/>
<filter name="group_by_tag" string="По признаку" context="{'group_by': 'tag_ids'}"/>
</search>
</field>
</record>
<!-- Action -->
<record id="action_account_move_template" model="ir.actions.act_window">
<field name="name">Шаблоны типовых операций</field>
<field name="res_model">account.move.template</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_account_move_template_search"/>
</record>
<!-- Menu item under Accounting → Configuration -->
<menuitem
id="menu_account_move_template"
name="Шаблоны типовых операций"
parent="account.menu_finance_configuration"
action="action_account_move_template"
sequence="100"/>
</data>
</odoo>

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="view_account_move_template_wizard_form" model="ir.ui.view">
<field name="name">account.move.template.wizard.form</field>
<field name="model">account.move.template.wizard</field>
<field name="arch" type="xml">
<form string="Создать проводку по шаблону">
<group>
<field name="template_id"/>
<field name="amount"/>
</group>
<field name="draft_line_ids" readonly="1">
<list>
<field name="account_id"/>
<field name="move_type"/>
<field name="amount"/>
</list>
</field>
<footer>
<button name="action_post" type="object" string="Провести"
class="btn-primary"
invisible="not template_id or amount &lt;= 0"/>
<button string="Отмена" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,103 @@
====================================================
Шаблоны типовых операций - Интеграция с инвойсами
====================================================
.. |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/licence-LGPL--3-blue.png
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
|badge1| |badge2|
Модуль расширяет функциональность шаблонов типовых операций для работы
с инвойсами клиентов и счетами поставщиков, автоматически применяя шаблоны
при проведении документов.
**Содержание**
.. contents::
:local:
Возможности
===========
* Автоматическое применение шаблонов при проведении инвойсов
* Замена стандартного распределения по счетам на распределение из шаблона
* Поддержка инвойсов клиентов и счетов поставщиков
* Настройка через параметры системы
* Сохранение строк инвойса с обновлением счетов в проводках
* Автоматическое создание строк оплаты с датами погашения
Настройка
=========
Для настройки модуля необходимо:
#. Перейти в *Бухгалтерия > Настройка > Параметры*
#. В разделе "Шаблоны типовых операций" включить опцию "Использовать шаблоны для инвойсов"
#. Сохранить настройки
Настройка шаблонов:
~~~~~~~~~~~~~~~~~~~
При создании шаблонов для инвойсов настройте типы строк:
* **Продуктовая строка**: Заменяет счёт в существующих продуктовых строках (счета доходов/расходов)
* **Строка оплаты**: Создаёт строки дебиторки/кредиторки с датами погашения
Использование
=============
После настройки модуль работает автоматически:
#. Создайте инвойс клиента или счёт поставщика
#. Добавьте строки инвойса как обычно
#. Выберите шаблон типовой операции в поле "Шаблон типовой операции"
#. Нажмите "Провести"
Модуль выполнит следующие действия:
* Удалит стандартные строки условий оплаты
* Обновит счета в продуктовых строках согласно шаблону (line_type='product')
* Создаст новые строки оплаты согласно шаблону (line_type='payment')
* Установит корректные даты погашения для счетов дебиторки/кредиторки
* Проведёт инвойс с распределением по счетам из шаблона
Известные проблемы / Планы развития
====================================
* В настоящее время поддерживаются только типы документов out_invoice и in_invoice
* Шаблон должен быть выбран до проведения документа
* В планах: Автоматический выбор шаблона на основе свойств инвойса
Отслеживание ошибок
===================
Ошибки отслеживаются на `GitHub Issues <https://github.com/OCA/account-financial-tools/issues>`_.
В случае проблем, пожалуйста, проверьте, не была ли ваша проблема уже зарегистрирована.
Авторы
======
Авторы
~~~~~~
* MK.Lab, RuOdoo
Участники
~~~~~~~~~
* MK.Lab, RuOdoo
Сопровождающие
~~~~~~~~~~~~~~
Модуль сопровождается MK.Lab, RuOdoo.
.. image:: https://ruodoo.ru/logo.png
:alt: MK.Lab, RuOdoo
:target: https://ruodoo.ru
Модуль является частью проекта расширений для бухгалтерии.

View File

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

View File

@ -0,0 +1,42 @@
{
'name': 'Accounting Journal Templates - Invoice',
'version': '19.0.1.0.0',
'summary': 'Интеграция шаблонов типовых операций с Invoice и Vendor Bill',
'description': """
Шаблоны типовых операций - Интеграция с инвойсами
==================================================
Автоматическое применение шаблонов типовых операций к инвойсам клиентов и счетам
поставщиков при проведении, заменяя стандартное распределение по счетам на
распределение из шаблона.
Возможности:
------------
* Автоматическое применение шаблонов при проведении инвойсов
* Замена счетов в продуктовых строках на счета из шаблона
* Создание строк условий оплаты с корректными датами погашения
* Поддержка инвойсов клиентов и счетов поставщиков
* Настройка через параметры системы
* Сохранение итоговых сумм инвойса
Технические детали:
-------------------
* Расширяет модель account.move
* Переопределяет метод action_post()
* Обновляет существующие продуктовые строки вместо создания дубликатов
* Создаёт строки payment_term для счетов дебиторки/кредиторки
* Поддерживает баланс и валидацию инвойса
""",
'author': 'MK.Lab, RuOdoo',
'website': 'https://ruodoo.ru',
'category': 'Accounting/Accounting',
'depends': ['account', 'account_move_templates'],
'data': [
'views/account_move_views.xml',
'views/res_config_settings_views.xml',
],
'installable': True,
'application': False,
'license': 'LGPL-3',
'maintainers': ['mklab', 'ruodoo'],
}

View File

@ -0,0 +1,3 @@
from . import account_move
from . import account_move_line
from . import res_config_settings

View File

@ -0,0 +1,151 @@
from odoo import api, fields, models
from odoo.exceptions import UserError
import logging
_logger = logging.getLogger(__name__)
class AccountMove(models.Model):
_inherit = ['account.move', 'account.move.template.mixin']
move_ids = fields.Many2many(
comodel_name='account.move',
relation='account_move_template_invoice_move_rel',
column1='document_id',
column2='move_id',
string='Проводки по шаблону',
)
journal_template_id = fields.Many2one(
comodel_name='account.move.template',
string='Шаблон типовой операции',
default=lambda self: self._default_journal_template(),
)
def _default_journal_template(self):
use = self.env['ir.config_parameter'].sudo().get_param(
'account_move_templates_invoice.use_journal_templates_for_invoices'
)
if use:
return self.env['account.move.template'].search([], limit=1, order='id asc')
return False
def get_move_line_amounts(self):
"""Return invoice total as the base amount for the wizard."""
self.ensure_one()
return [{'name': self.name or '/', 'amount': self.amount_total}]
def _replace_lines_from_template(self, move):
"""
Replace ALL lines (product + payment_term) with template lines
plus one explicit payment_term line with date_maturity.
We pass 'balance' explicitly in vals so _get_protected_vals protects
it from being overwritten by _sync_invoice.
"""
_logger.info("TEMPLATE DEBUG: _replace_lines_from_template START for move %s", move.id)
template = move.journal_template_id
_logger.info("TEMPLATE DEBUG: template = %s", template)
base_amount = move.amount_untaxed or move.amount_total
_logger.info("TEMPLATE DEBUG: base_amount = %s", base_amount)
# Determine receivable/payable account
partner = move.commercial_partner_id.with_company(move.company_id)
if move.is_sale_document(include_receipts=True):
term_account = partner.property_account_receivable_id
else:
term_account = partner.property_account_payable_id
_logger.info("TEMPLATE DEBUG: term_account = %s", term_account)
# due date: use invoice_date_due, invoice_date, or today
due_date = (
move.invoice_date_due
or move.invoice_date
or fields.Date.context_today(move)
)
_logger.info("TEMPLATE DEBUG: due_date = %s", due_date)
# Remove only payment_term lines, keep product lines from invoice_line_ids
_logger.info("TEMPLATE DEBUG: Removing existing payment_term lines")
lines_to_remove = move.line_ids.filtered(lambda l: l.display_type == 'payment_term')
_logger.info("TEMPLATE DEBUG: Lines to remove: %s", [(l.id, l.account_id.code, l.display_type) for l in lines_to_remove])
lines_to_remove.with_context(
dynamic_unlink=True,
check_move_validity=False,
skip_invoice_sync=True,
).unlink()
_logger.info("TEMPLATE DEBUG: payment_term lines removed")
AML = self.env['account.move.line']
ctx = dict(check_move_validity=False, skip_invoice_sync=True)
# 1. Process template lines based on line_type
_logger.info("TEMPLATE DEBUG: Processing template lines")
# Get existing product lines from invoice_line_ids
product_lines = move.line_ids.filtered(lambda l: l.display_type == 'product')
_logger.info("TEMPLATE DEBUG: Found %d product lines", len(product_lines))
# Separate template lines by type
template_product_lines = template.line_ids.filtered(lambda l: l.line_type == 'product')
template_payment_lines = template.line_ids.filtered(lambda l: l.line_type == 'payment')
_logger.info("TEMPLATE DEBUG: Template has %d product lines and %d payment lines",
len(template_product_lines), len(template_payment_lines))
# Update existing product lines with accounts from template (line_type='product')
for idx, product_line in enumerate(product_lines):
if idx < len(template_product_lines):
tpl_line = template_product_lines[idx]
_logger.info("TEMPLATE DEBUG: Updating product line %d: old account=%s, new account=%s",
product_line.id, product_line.account_id.code, tpl_line.account_id.code)
product_line.with_context(**ctx).write({'account_id': tpl_line.account_id.id})
# Create payment lines from template (line_type='payment')
_logger.info("TEMPLATE DEBUG: Creating payment lines from template")
for tpl_line in template_payment_lines:
line_amount = base_amount * tpl_line.percent / 100.0
balance = line_amount if tpl_line.move_type == 'debit' else -line_amount
_logger.info("TEMPLATE DEBUG: Creating payment line: account=%s, balance=%s",
tpl_line.account_id.id, balance)
vals = {
'move_id': move.id,
'account_id': tpl_line.account_id.id,
'name': template.name,
'balance': balance,
'amount_currency': balance,
'date_maturity': due_date,
'display_type': 'payment_term',
}
AML.with_context(**ctx).create(vals)
_logger.info("TEMPLATE DEBUG: Template lines processed")
_logger.info("TEMPLATE DEBUG: _replace_lines_from_template END")
def action_post(self):
_logger.info("TEMPLATE DEBUG: action_post called for moves: %s", self.ids)
use_templates = self.env['ir.config_parameter'].sudo().get_param(
'account_move_templates_invoice.use_journal_templates_for_invoices'
)
_logger.info("TEMPLATE DEBUG: use_templates = %s", use_templates)
if use_templates:
for move in self.filtered(
lambda m: m.move_type in ('out_invoice', 'in_invoice')
):
_logger.info("TEMPLATE DEBUG: Processing move %s, template: %s", move.id, move.journal_template_id)
if not move.journal_template_id:
raise UserError(
'Выберите шаблон типовой операции перед проведением.'
)
try:
_logger.info("TEMPLATE DEBUG: Calling _replace_lines_from_template")
self._replace_lines_from_template(move)
_logger.info("TEMPLATE DEBUG: _replace_lines_from_template completed")
except Exception as e:
_logger.error("TEMPLATE DEBUG: Error in _replace_lines_from_template: %s", e, exc_info=True)
raise
return super().action_post()

View File

@ -0,0 +1,7 @@
from odoo import models
class AccountMoveLine(models.Model):
_inherit = 'account.move.line'
# No overrides needed - we handle everything in action_post()

View File

@ -0,0 +1,10 @@
from odoo import fields, models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
use_journal_templates_for_invoices = fields.Boolean(
string='Использовать шаблоны типовых операций для счетов',
config_parameter='account_move_templates_invoice.use_journal_templates_for_invoices',
)

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="view_account_move_template_invoice_form" model="ir.ui.view">
<field name="name">account.move.template.invoice.form</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_move_form"/>
<field name="arch" type="xml">
<!-- Smart button: visible when move_count > 0 -->
<xpath expr="//div[@name='button_box']" position="inside">
<button type="object"
name="action_view_moves"
class="oe_stat_button"
icon="fa-book"
invisible="move_count == 0">
<field name="move_count" widget="statinfo" string="Проводки"/>
</button>
</xpath>
<!-- Template selector after invoice_date -->
<xpath expr="//field[@name='invoice_date']" position="after">
<field name="journal_template_id"
string="Шаблон типовой операции"
options="{'no_create': True}"/>
</xpath>
</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="res_config_settings_view_form_template_invoice" model="ir.ui.view">
<field name="name">res.config.settings.view.form.template.invoice</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="account.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//app[@name='account']" position="inside">
<block title="Шаблоны типовых операций" name="journal_templates_setting_container">
<setting help="Использовать шаблоны типовых операций вместо стандартных проводок для счетов">
<field name="use_journal_templates_for_invoices"/>
<label for="use_journal_templates_for_invoices"/>
</setting>
</block>
</xpath>
</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,307 @@
.. image:: https://odoo-community.org/readme-banner-image
:target: https://odoo-community.org/get-involved?utm_source=readme
:alt: Odoo Community Association
====================
Base Tier Validation
====================
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:3e44413fd72f8949deba313c3f31047cbf30ba3b17d78342590d2376001d905f
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Mature-brightgreen.png
:target: https://odoo-community.org/page/development-status
:alt: Mature
.. |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%2Fserver--ux-lightgray.png?logo=github
:target: https://github.com/OCA/server-ux/tree/18.0/base_tier_validation
:alt: OCA/server-ux
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/server-ux-18-0/server-ux-18-0-base_tier_validation
: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/server-ux&target_branch=18.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
Validating some operations is a common need across different areas in a
company and sometimes it also involves several people and stages in the
process. With this module you will be able to define your custom
validation workflows for any Odoo document.
This module does not provide a functionality by itself but an abstract
model to implement a validation process based on tiers on other models
(e.g. purchase orders, sales orders, budgets, expenses...).
**Note:** To be able to use this module in a new model you will need
some development.
See
`purchase_tier_validation <https://github.com/OCA/purchase-workflow>`__
as an example of implementation.
Additionally, if your state field is a (stored) computed field, you need
to set ``_tier_validation_state_field_is_computed`` to ``True`` in your
model Python file, and you will want to add the dependent fields of the
compute method in ``_get_after_validation_exceptions`` and
``_get_under_validation_exceptions``.
**Table of contents**
.. contents::
:local:
Configuration
=============
To configure this module, you need to:
1. Go to *Settings > Technical > Tier Validations > Tier Definition*.
2. Create as many tiers as you want for any model having tier validation
functionality.
**Note:**
- If check *Notify Reviewers on Creation*, all possible reviewers will
be notified by email when this definition is triggered.
- If check *Notify reviewers on reaching pending* if you want to send a
notification when pending status is reached. This is usefull in a
approve by sequence scenario to only notify reviewers when it is their
turn in the sequence.
- If check *Comment*, reviewers can comment after click Validate or
Reject.
- If check *Approve by sequence*, reviewers is forced to review by
specified sequence.
To configure Tier Validation Exceptions, you need to:
1. Go to *Settings > Technical > Tier Validations > Tier Validation
Exceptions*.
2. Create as many tiers validation exceptions as you want for any model
having tier validation functionality.
3. Add desired fields to be checked in *Fields*.
4. Add desired groups that can use this Exception in *Groups*.
5. You must check *Write under Validation*, *Write after Validation* or
both.
**Note:**
- If you don't create any exception, the Validated record will be
readonly and cannot be modified.
- If check *Write under Validation*, records will be able to be modified
only in the defined fields when the Validation process is ongoing.
- If check *Write after Validation*, records will be able to be modified
only in the defined fields when the Validation process is finished.
- If check *Write after Validation* and *Write under Validation*,
records will be able to be modified defined fields always.
Known issues / Roadmap
======================
This is the list of known issues for this module. Any proposal for
improvement will be very valuable.
- **Issue:**
When using approve_sequence option in any tier.definition there can be
inconsistencies in the systray notifications.
**Description:**
Field can_review in tier.review is used to filter out, in the systray
notifications, the reviews a user can approve. This can_review field
is updated **in the database** in method review_user_count, this can
make it very inconsistent for databases with a lot of users and
recurring updates that can change the expected behavior.
- **Migration to 15.0:**
The parameter \_tier_validation_manual_config will become False, on
14.0, the default value is True, as the change is applied after the
migration. In order to use the new behavior we need to modify the
value on our expected model.
Changelog
=========
17.0.1.0.0 (2024-01-10)
-----------------------
Migrated to Odoo 17. Merged module with tier_validation_waiting. To
support sending messages in a validation sequence when it is their turn
to validate.
14.0.1.0.0 (2020-11-19)
-----------------------
Migrated to Odoo 14.
13.0.1.2.2 (2020-08-30)
-----------------------
Fixes:
- When using approve_sequence option in any tier.definition there can be
inconsistencies in the systray notifications
- When using approve_sequence, still not approve only the needed
sequence, but also other sequence for the same approver
12.0.3.3.1 (2019-12-02)
-----------------------
Fixes:
- Show comment on Reviews Table.
- Edit notification with approve_sequence.
12.0.3.3.0 (2019-11-27)
-----------------------
New features:
- Add comment on Reviews Table.
- Approve by sequence.
12.0.3.2.1 (2019-11-26)
-----------------------
Fixes:
- Remove message_subscribe_users
12.0.3.2.0 (2019-11-25)
-----------------------
New features:
- Notify reviewers
12.0.3.1.0 (2019-07-08)
-----------------------
Fixes:
- Singleton error
12.0.3.0.0 (2019-12-02)
-----------------------
Fixes:
- Edit Reviews Table
12.0.2.1.0 (2019-05-29)
-----------------------
Fixes:
- Edit drop-down style width and position
12.0.2.0.0 (2019-05-28)
-----------------------
New features:
- Pass parameters as functions.
- Add Systray.
12.0.1.0.0 (2019-02-18)
-----------------------
Migrated to Odoo 12.
11.0.1.0.0 (2018-05-09)
-----------------------
Migrated to Odoo 11.
10.0.1.0.0 (2018-03-26)
-----------------------
Migrated to Odoo 10.
9.0.1.0.0 (2017-12-02)
----------------------
First version.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/server-ux/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/server-ux/issues/new?body=module:%20base_tier_validation%0Aversion:%2018.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
-------
* ForgeFlow
Contributors
------------
- Lois Rilo <lois.rilo@forgeflow.com>
- Naglis Jonaitis <naglis@versada.eu>
- Adrià Gil Sorribes <adria.gil@forgeflow.com>
- Pimolnat Suntian <pimolnats@ecosoft.co.th>
- Pedro Gonzalez <pedro.gonzalez@pesol.es>
- Kitti U. <kittiu@ecosoft.co.th>
- Saran Lim. <saranl@ecosoft.co.th>
- Carlos Lopez <celm1990@gmail.com>
- Javier Colmeiro <javier.colmeiro@braintec.com>
- bosd
- Evan Soh <evan.soh@omnisoftsolution.com>
- Manuel Regidor <manuel.regidor@sygel.es>
- Eduardo de Miguel <edu@moduon.team>
- `XCG Consulting <https://xcg-consulting.fr>`__:
- Houzéfa Abbasbhay
- Stefan Rijnhart <stefan@opener.amsterdam>
- Kevin Khao <kevinkhao@gmail.com>
- Do Anh Duy <duyda@trobz.com>
Other credits
-------------
The migration of this module from 17.0 to 18.0 was financially supported
by Camptocamp.
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.
.. |maintainer-LoisRForgeFlow| image:: https://github.com/LoisRForgeFlow.png?size=40px
:target: https://github.com/LoisRForgeFlow
:alt: LoisRForgeFlow
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
|maintainer-LoisRForgeFlow|
This module is part of the `OCA/server-ux <https://github.com/OCA/server-ux/tree/18.0/base_tier_validation>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

@ -0,0 +1,4 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import models
from . import wizard

View File

@ -0,0 +1,35 @@
# Copyright 2017-24 ForgeFlow S.L. (https://www.forgeflow.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
{
"name": "Base Tier Validation",
"summary": "Implement a validation process based on tiers.",
"version": "19.0.2025.12.03",
"development_status": "Mature",
"maintainers": ["LoisRForgeFlow"],
"category": "Tools",
"website": "https://github.com/OCA/server-ux",
"author": "ForgeFlow, Odoo Community Association (OCA)",
"license": "AGPL-3",
"application": False,
"installable": True,
"depends": ["mail"],
"data": [
"data/cron_data.xml",
"data/mail_data.xml",
"security/ir.model.access.csv",
"security/tier_validation_security.xml",
"views/res_config_settings_views.xml",
"views/tier_definition_view.xml",
"views/tier_review_view.xml",
"views/tier_validation_exception_view.xml",
"wizard/comment_wizard_view.xml",
"templates/tier_validation_templates.xml",
],
"demo": [],
"assets": {
"web.assets_backend": [
"base_tier_validation/static/src/components/**/*",
"base_tier_validation/static/src/js/**/*",
],
},
}

Some files were not shown because too many files have changed in this diff Show More