[MIG] base_changeset: Migration to 16.0
Co-authored-by stefan@opener.amsterdam Co-authored-by shams.mukhibillaev@emesa.nl Co-authored-by remy@emesa.nlpull/2663/head
parent
cb6b3983a7
commit
0513667f18
|
@ -2,10 +2,13 @@
|
|||
Track record changesets
|
||||
=======================
|
||||
|
||||
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
..
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:97cd931d612b60483f97e88ae7a01f5dbb3d5bfba9ed8caed7a148c490c14369
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png
|
||||
:target: https://odoo-community.org/page/development-status
|
||||
|
@ -14,16 +17,16 @@ Track record changesets
|
|||
: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--tools-lightgray.png?logo=github
|
||||
:target: https://github.com/OCA/server-tools/tree/15.0/base_changeset
|
||||
:target: https://github.com/OCA/server-tools/tree/16.0/base_changeset
|
||||
:alt: OCA/server-tools
|
||||
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
|
||||
:target: https://translation.odoo-community.org/projects/server-tools-15-0/server-tools-15-0-base_changeset
|
||||
:target: https://translation.odoo-community.org/projects/server-tools-16-0/server-tools-16-0-base_changeset
|
||||
:alt: Translate me on Weblate
|
||||
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
|
||||
:target: https://runbot.odoo-community.org/runbot/149/15.0
|
||||
:alt: Try me on Runbot
|
||||
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
|
||||
:target: https://runboat.odoo-community.org/builds?repo=OCA/server-tools&target_branch=16.0
|
||||
:alt: Try me on Runboat
|
||||
|
||||
|badge1| |badge2| |badge3| |badge4| |badge5|
|
||||
|badge1| |badge2| |badge3| |badge4| |badge5|
|
||||
|
||||
This module extends the functionality of records. It allows to create
|
||||
changesets that must be validated when a record is modified instead of direct
|
||||
|
@ -67,7 +70,7 @@ Record Changesets > Fields Rules``.
|
|||
|
||||
* Configuration of rules
|
||||
|
||||
.. image:: https://raw.githubusercontent.com/OCA/server-tools/15.0/base_changeset/static/src/img/rules.png
|
||||
.. image:: https://raw.githubusercontent.com/OCA/server-tools/16.0/base_changeset/static/src/img/rules.png
|
||||
|
||||
For each record field, an action can be defined:
|
||||
|
||||
|
@ -118,7 +121,7 @@ Remove the "Pending" filter to show all the changesets.
|
|||
|
||||
* Changeset waiting for validation
|
||||
|
||||
.. image:: https://raw.githubusercontent.com/OCA/server-tools/15.0/base_changeset/static/src/img/changeset.png
|
||||
.. image:: https://raw.githubusercontent.com/OCA/server-tools/16.0/base_changeset/static/src/img/changeset.png
|
||||
|
||||
The changes view shows the name of the record's field, the Origin value
|
||||
and the New value alongside the state of the change. By clicking on the
|
||||
|
@ -138,13 +141,13 @@ number of pending changes next to it like this:
|
|||
|
||||
* Badge with the number of pending changes
|
||||
|
||||
.. image:: https://raw.githubusercontent.com/OCA/server-tools/15.0/base_changeset/static/src/img/badge.png
|
||||
.. image:: https://raw.githubusercontent.com/OCA/server-tools/16.0/base_changeset/static/src/img/badge.png
|
||||
|
||||
When you click on it:
|
||||
|
||||
* Clicking the badge: red button to reject, green one to apply
|
||||
|
||||
.. image:: https://raw.githubusercontent.com/OCA/server-tools/15.0/base_changeset/static/src/img/badge_click.png
|
||||
.. image:: https://raw.githubusercontent.com/OCA/server-tools/16.0/base_changeset/static/src/img/badge_click.png
|
||||
|
||||
Click the red button to reject the change, click the green one to apply it.
|
||||
|
||||
|
@ -182,14 +185,21 @@ Known issues / Roadmap
|
|||
|
||||
* Only a subset of the type of fields is actually supported
|
||||
* Multicompany not fully supported
|
||||
* The popover widget indicating the number of pending changes is not shown for
|
||||
fields without a label at the moment. The approach was already failing in 15.0
|
||||
(in the case of inline fields such as the partner address fields)
|
||||
and even in 14.0 (in the case of fields for which no value was set yet).
|
||||
Or, for a more flexible approach, implement a kind of view preprocessing that
|
||||
allows a developer to indicate where the widget needs to go (analogous to
|
||||
`<label for="field_name" />`).
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `GitHub Issues <https://github.com/OCA/server-tools/issues>`_.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us smashing it by providing a detailed and welcomed
|
||||
`feedback <https://github.com/OCA/server-tools/issues/new?body=module:%20base_changeset%0Aversion:%2015.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
|
||||
If you spotted it first, help us to smash it by providing a detailed and welcomed
|
||||
`feedback <https://github.com/OCA/server-tools/issues/new?body=module:%20base_changeset%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
|
||||
|
||||
Do not contact contributors directly about support or help with technical issues.
|
||||
|
||||
|
@ -211,6 +221,8 @@ Contributors
|
|||
* Dennis Sluijk <d.sluijk@onestein.nl>
|
||||
* Andrea Stirpe <a.stirpe@onestein.nl>
|
||||
* Holger Brunn <mail@hunki-enterprises.com>
|
||||
* Mark Schuit <mark@gig.solutions>
|
||||
* Stefan Rijnhart <stefan@opener.amsterdam>
|
||||
|
||||
Maintainers
|
||||
~~~~~~~~~~~
|
||||
|
@ -233,6 +245,6 @@ Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
|
|||
|
||||
|maintainer-astirpe|
|
||||
|
||||
This module is part of the `OCA/server-tools <https://github.com/OCA/server-tools/tree/15.0/base_changeset>`_ project on GitHub.
|
||||
This module is part of the `OCA/server-tools <https://github.com/OCA/server-tools/tree/16.0/base_changeset>`_ project on GitHub.
|
||||
|
||||
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
|
||||
|
|
|
@ -23,10 +23,10 @@
|
|||
],
|
||||
"assets": {
|
||||
"web.assets_backend": [
|
||||
"base_changeset/static/src/js/backend.js",
|
||||
"base_changeset/static/src/scss/backend.scss",
|
||||
"base_changeset/static/src/components/form_label.*",
|
||||
"base_changeset/static/src/components/changeset_popover.*",
|
||||
"base_changeset/static/src/components/record.esm.js",
|
||||
],
|
||||
"web.assets_qweb": ["base_changeset/static/src/xml/backend.xml"],
|
||||
},
|
||||
"demo": ["demo/changeset_field_rule.xml"],
|
||||
"installable": True,
|
||||
|
|
|
@ -32,8 +32,8 @@
|
|||
<field name="field_id" ref="base.field_res_partner__country_id" />
|
||||
<field name="action">auto</field>
|
||||
</record>
|
||||
<record model="changeset.field.rule" id="changeset_field_rule_credit_limit">
|
||||
<field name="field_id" ref="base.field_res_partner__credit_limit" />
|
||||
<record model="changeset.field.rule" id="changeset_field_rule_partner_latitude">
|
||||
<field name="field_id" ref="base.field_res_partner__partner_latitude" />
|
||||
<field name="action">auto</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
|
@ -68,7 +68,9 @@ class Base(models.AbstractModel):
|
|||
:args:
|
||||
:returns: list of models
|
||||
"""
|
||||
models = self.env["changeset.field.rule"].search([]).mapped("model_id.model")
|
||||
models = (
|
||||
self.env["changeset.field.rule"].sudo().search([]).mapped("model_id.model")
|
||||
)
|
||||
if config["test_enable"] and self.env.context.get("test_record_changeset"):
|
||||
if "res.partner" not in models:
|
||||
models += ["res.partner"] # Used in tests
|
||||
|
@ -144,16 +146,15 @@ class Base(models.AbstractModel):
|
|||
return res
|
||||
|
||||
@api.model
|
||||
def _fields_view_get(
|
||||
self, view_id=None, view_type="form", toolbar=False, submenu=False
|
||||
):
|
||||
res = super()._fields_view_get(
|
||||
view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu
|
||||
)
|
||||
to_track_changeset = self._name in self.models_to_track_changeset()
|
||||
can_see = len(self) == 1 and self.user_can_see_changeset
|
||||
button_label = _("Changes")
|
||||
if to_track_changeset and can_see and view_type == "form":
|
||||
def get_view(self, view_id=None, view_type="form", **options):
|
||||
"""Insert the pending changes smart button in the form view of tracked models."""
|
||||
res = super().get_view(view_id=view_id, view_type=view_type, **options)
|
||||
if (
|
||||
view_type == "form"
|
||||
and self._name in self.models_to_track_changeset()
|
||||
and self._user_can_see_changeset()
|
||||
):
|
||||
button_label = _("Changes")
|
||||
doc = etree.XML(res["arch"])
|
||||
for node in doc.xpath("//div[@name='button_box']"):
|
||||
xml_field = etree.Element(
|
||||
|
@ -179,10 +180,14 @@ class Base(models.AbstractModel):
|
|||
res["arch"] = etree.tostring(doc, encoding="unicode")
|
||||
return res
|
||||
|
||||
def _compute_user_can_see_changeset(self):
|
||||
is_superuser = self.env.is_superuser()
|
||||
has_changeset_group = self.user_has_groups(
|
||||
@api.model
|
||||
def _user_can_see_changeset(self):
|
||||
"""Return if the current user has changeset access"""
|
||||
return self.env.is_superuser() or self.user_has_groups(
|
||||
"base_changeset.group_changeset_user"
|
||||
)
|
||||
|
||||
def _compute_user_can_see_changeset(self):
|
||||
user_can_see_changeset = self._user_can_see_changeset()
|
||||
for rec in self:
|
||||
rec.user_can_see_changeset = is_superuser or has_changeset_group
|
||||
rec.user_can_see_changeset = user_can_see_changeset
|
||||
|
|
|
@ -110,7 +110,7 @@ class ChangesetFieldRule(models.Model):
|
|||
|
||||
"""
|
||||
domain = self._get_rules_search_domain(record_model_name, source_model_name)
|
||||
model_rules = self.search(
|
||||
model_rules = self.sudo().search(
|
||||
domain,
|
||||
# using 'ASC' means that 'NULLS LAST' is the default
|
||||
order="source_model_id ASC",
|
||||
|
@ -164,9 +164,9 @@ class ChangesetFieldRule(models.Model):
|
|||
self.expression, {"object": record, "user": self.env.user}
|
||||
)
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
record = super().create(vals)
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
record = super().create(vals_list)
|
||||
self.clear_caches()
|
||||
return record
|
||||
|
||||
|
|
|
@ -130,7 +130,11 @@ class RecordChangesetChange(models.Model):
|
|||
|
||||
@api.model
|
||||
def _reference_models(self):
|
||||
models = self.env["ir.model"].search([])
|
||||
"""Get all model names from ir.model.
|
||||
|
||||
Requires sudo, as ir.model is only readable for ERP managers.
|
||||
"""
|
||||
models = self.sudo().env["ir.model"].search([])
|
||||
return [(model.model, model.name) for model in models]
|
||||
|
||||
_suffix_to_types = {
|
||||
|
@ -188,7 +192,7 @@ class RecordChangesetChange(models.Model):
|
|||
@api.model
|
||||
def get_field_for_type(self, field, prefix):
|
||||
assert prefix in ("origin", "old", "new")
|
||||
field_type = self._type_to_suffix.get(field.ttype)
|
||||
field_type = self._type_to_suffix.get(field.sudo().ttype)
|
||||
if not field_type:
|
||||
raise NotImplementedError("field type %s is not supported" % field_type)
|
||||
return "{}_value_{}".format(prefix, field_type)
|
||||
|
@ -347,11 +351,12 @@ class RecordChangesetChange(models.Model):
|
|||
|
||||
:returns: dict of values, boolean
|
||||
"""
|
||||
new_field_name = self.get_field_for_type(rule.field_id, "new")
|
||||
field = rule.sudo().field_id
|
||||
new_field_name = self.get_field_for_type(field, "new")
|
||||
new_value = self._value_for_changeset(record, field_name, value=value)
|
||||
change = {
|
||||
new_field_name: new_value,
|
||||
"field_id": rule.field_id.id,
|
||||
"field_id": field.id,
|
||||
"rule_id": rule.id,
|
||||
}
|
||||
if rule.action == "auto":
|
||||
|
@ -368,7 +373,7 @@ class RecordChangesetChange(models.Model):
|
|||
# Normally the 'old' value is set when we use the 'apply'
|
||||
# button, but since we short circuit the 'apply', we
|
||||
# directly set the 'old' value here
|
||||
old_field_name = self.get_field_for_type(rule.field_id, "old")
|
||||
old_field_name = self.get_field_for_type(field, "old")
|
||||
# get values ready to write as expected by the changeset
|
||||
# (for instance, a many2one is written in a reference
|
||||
# field)
|
||||
|
@ -380,7 +385,13 @@ class RecordChangesetChange(models.Model):
|
|||
return change, pop_value
|
||||
|
||||
@api.model
|
||||
def get_fields_changeset_changes(self, model, res_id):
|
||||
def get_changeset_changes_by_field(self, model, res_id):
|
||||
"""Return changes grouped by field.
|
||||
|
||||
:returns: dictionary with field names as keys and lists of dictionaries
|
||||
describing changes as keys.
|
||||
:rtype: dict
|
||||
"""
|
||||
fields = [
|
||||
"new_value_display",
|
||||
"origin_value_display",
|
||||
|
@ -393,8 +404,14 @@ class RecordChangesetChange(models.Model):
|
|||
("changeset_id.res_id", "=", res_id),
|
||||
("state", "in", states),
|
||||
]
|
||||
return self.search_read(domain, fields)
|
||||
return {
|
||||
field_name: list(changes)
|
||||
for (field_name, changes) in groupby(
|
||||
self.search_read(domain, fields), lambda vals: vals["field_name"]
|
||||
)
|
||||
}
|
||||
|
||||
@api.depends_context("user")
|
||||
def _compute_user_can_validate_changeset(self):
|
||||
is_superuser = self.env.is_superuser()
|
||||
has_group = self.user_has_groups("base_changeset.group_changeset_user")
|
||||
|
|
|
@ -4,3 +4,5 @@
|
|||
* Dennis Sluijk <d.sluijk@onestein.nl>
|
||||
* Andrea Stirpe <a.stirpe@onestein.nl>
|
||||
* Holger Brunn <mail@hunki-enterprises.com>
|
||||
* Mark Schuit <mark@gig.solutions>
|
||||
* Stefan Rijnhart <stefan@opener.amsterdam>
|
||||
|
|
|
@ -1,2 +1,9 @@
|
|||
* Only a subset of the type of fields is actually supported
|
||||
* Multicompany not fully supported
|
||||
* The popover widget indicating the number of pending changes is not shown for
|
||||
fields without a label at the moment. The approach was already failing in 15.0
|
||||
(in the case of inline fields such as the partner address fields)
|
||||
and even in 14.0 (in the case of fields for which no value was set yet).
|
||||
Or, for a more flexible approach, implement a kind of view preprocessing that
|
||||
allows a developer to indicate where the widget needs to go (analogous to
|
||||
`<label for="field_name" />`).
|
||||
|
|
|
@ -16,8 +16,14 @@
|
|||
</data>
|
||||
<data noupdate="1">
|
||||
<record id="group_changeset_manager" model="res.groups">
|
||||
<field name="users" eval="[(4, ref('base.user_root'))]" />
|
||||
<field name="implied_ids" eval="[(4, ref('group_changeset_user'))]" />
|
||||
<field
|
||||
name="users"
|
||||
eval="[Command.link(ref('base.user_root')), Command.link(ref('base.user_admin'))]"
|
||||
/>
|
||||
<field
|
||||
name="implied_ids"
|
||||
eval="[Command.link(ref('group_changeset_user'))]"
|
||||
/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
|
@ -1,20 +1,19 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="generator" content="Docutils 0.15.1: http://docutils.sourceforge.net/" />
|
||||
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
|
||||
<title>Track record changesets</title>
|
||||
<style type="text/css">
|
||||
|
||||
/*
|
||||
:Author: David Goodger (goodger@python.org)
|
||||
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
|
||||
:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $
|
||||
:Copyright: This stylesheet has been placed in the public domain.
|
||||
|
||||
Default cascading style sheet for the HTML output of Docutils.
|
||||
|
||||
See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
|
||||
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
|
||||
customize this style sheet.
|
||||
*/
|
||||
|
||||
|
@ -366,8 +365,10 @@ ul.auto-toc {
|
|||
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:97cd931d612b60483f97e88ae7a01f5dbb3d5bfba9ed8caed7a148c490c14369
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
|
||||
<p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Alpha" src="https://img.shields.io/badge/maturity-Alpha-red.png" /></a> <a class="reference external" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/server-tools/tree/15.0/base_changeset"><img alt="OCA/server-tools" src="https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/server-tools-15-0/server-tools-15-0-base_changeset"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/149/15.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p>
|
||||
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Alpha" src="https://img.shields.io/badge/maturity-Alpha-red.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/server-tools/tree/16.0/base_changeset"><img alt="OCA/server-tools" src="https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/server-tools-16-0/server-tools-16-0-base_changeset"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/server-tools&target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
|
||||
<p>This module extends the functionality of records. It allows to create
|
||||
changesets that must be validated when a record is modified instead of direct
|
||||
modifications. Rules allow to configure which field must be validated.</p>
|
||||
|
@ -387,11 +388,11 @@ Only for development or testing purpose, do not use in production.
|
|||
<p><strong>Table of contents</strong></p>
|
||||
<div class="contents local topic" id="contents">
|
||||
<ul class="simple">
|
||||
<li><a class="reference internal" href="#configuration" id="id1">Configuration</a></li>
|
||||
<li><a class="reference internal" href="#configuration" id="toc-entry-1">Configuration</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="configuration">
|
||||
<h2><a class="toc-backref" href="#id1">Configuration</a></h2>
|
||||
<h2><a class="toc-backref" href="#toc-entry-1">Configuration</a></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="access-rights">
|
||||
|
@ -406,7 +407,7 @@ with the group <tt class="docutils literal">Changesets Validations</tt></p>
|
|||
Record Changesets > Fields Rules</tt>.</p>
|
||||
<ul>
|
||||
<li><p class="first">Configuration of rules</p>
|
||||
<img alt="https://raw.githubusercontent.com/OCA/server-tools/15.0/base_changeset/static/src/img/rules.png" src="https://raw.githubusercontent.com/OCA/server-tools/15.0/base_changeset/static/src/img/rules.png" />
|
||||
<img alt="https://raw.githubusercontent.com/OCA/server-tools/16.0/base_changeset/static/src/img/rules.png" src="https://raw.githubusercontent.com/OCA/server-tools/16.0/base_changeset/static/src/img/rules.png" />
|
||||
</li>
|
||||
</ul>
|
||||
<p>For each record field, an action can be defined:</p>
|
||||
|
@ -452,7 +453,7 @@ Changesets > Changesets</tt>.</p>
|
|||
Remove the “Pending” filter to show all the changesets.</p>
|
||||
<ul>
|
||||
<li><p class="first">Changeset waiting for validation</p>
|
||||
<img alt="https://raw.githubusercontent.com/OCA/server-tools/15.0/base_changeset/static/src/img/changeset.png" src="https://raw.githubusercontent.com/OCA/server-tools/15.0/base_changeset/static/src/img/changeset.png" />
|
||||
<img alt="https://raw.githubusercontent.com/OCA/server-tools/16.0/base_changeset/static/src/img/changeset.png" src="https://raw.githubusercontent.com/OCA/server-tools/16.0/base_changeset/static/src/img/changeset.png" />
|
||||
</li>
|
||||
</ul>
|
||||
<p>The changes view shows the name of the record’s field, the Origin value
|
||||
|
@ -470,13 +471,13 @@ records. When there is a pending change for a field you get a badge with the
|
|||
number of pending changes next to it like this:</p>
|
||||
<ul>
|
||||
<li><p class="first">Badge with the number of pending changes</p>
|
||||
<img alt="https://raw.githubusercontent.com/OCA/server-tools/15.0/base_changeset/static/src/img/badge.png" src="https://raw.githubusercontent.com/OCA/server-tools/15.0/base_changeset/static/src/img/badge.png" />
|
||||
<img alt="https://raw.githubusercontent.com/OCA/server-tools/16.0/base_changeset/static/src/img/badge.png" src="https://raw.githubusercontent.com/OCA/server-tools/16.0/base_changeset/static/src/img/badge.png" />
|
||||
</li>
|
||||
</ul>
|
||||
<p>When you click on it:</p>
|
||||
<ul>
|
||||
<li><p class="first">Clicking the badge: red button to reject, green one to apply</p>
|
||||
<img alt="https://raw.githubusercontent.com/OCA/server-tools/15.0/base_changeset/static/src/img/badge_click.png" src="https://raw.githubusercontent.com/OCA/server-tools/15.0/base_changeset/static/src/img/badge_click.png" />
|
||||
<img alt="https://raw.githubusercontent.com/OCA/server-tools/16.0/base_changeset/static/src/img/badge_click.png" src="https://raw.githubusercontent.com/OCA/server-tools/16.0/base_changeset/static/src/img/badge_click.png" />
|
||||
</li>
|
||||
</ul>
|
||||
<p>Click the red button to reject the change, click the green one to apply it.</p>
|
||||
|
@ -510,14 +511,21 @@ will fail.</p>
|
|||
<ul class="simple">
|
||||
<li>Only a subset of the type of fields is actually supported</li>
|
||||
<li>Multicompany not fully supported</li>
|
||||
<li>The popover widget indicating the number of pending changes is not shown for
|
||||
fields without a label at the moment. The approach was already failing in 15.0
|
||||
(in the case of inline fields such as the partner address fields)
|
||||
and even in 14.0 (in the case of fields for which no value was set yet).
|
||||
Or, for a more flexible approach, implement a kind of view preprocessing that
|
||||
allows a developer to indicate where the widget needs to go (analogous to
|
||||
<cite><label for=”field_name” /></cite>).</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="bug-tracker">
|
||||
<h2>Bug Tracker</h2>
|
||||
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/server-tools/issues">GitHub Issues</a>.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us smashing it by providing a detailed and welcomed
|
||||
<a class="reference external" href="https://github.com/OCA/server-tools/issues/new?body=module:%20base_changeset%0Aversion:%2015.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
|
||||
If you spotted it first, help us to smash it by providing a detailed and welcomed
|
||||
<a class="reference external" href="https://github.com/OCA/server-tools/issues/new?body=module:%20base_changeset%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
|
||||
<p>Do not contact contributors directly about support or help with technical issues.</p>
|
||||
</div>
|
||||
<div class="section" id="credits">
|
||||
|
@ -538,6 +546,8 @@ If you spotted it first, help us smashing it by providing a detailed and welcome
|
|||
<li>Dennis Sluijk <<a class="reference external" href="mailto:d.sluijk@onestein.nl">d.sluijk@onestein.nl</a>></li>
|
||||
<li>Andrea Stirpe <<a class="reference external" href="mailto:a.stirpe@onestein.nl">a.stirpe@onestein.nl</a>></li>
|
||||
<li>Holger Brunn <<a class="reference external" href="mailto:mail@hunki-enterprises.com">mail@hunki-enterprises.com</a>></li>
|
||||
<li>Mark Schuit <<a class="reference external" href="mailto:mark@gig.solutions">mark@gig.solutions</a>></li>
|
||||
<li>Stefan Rijnhart <<a class="reference external" href="mailto:stefan@opener.amsterdam">stefan@opener.amsterdam</a>></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="maintainers">
|
||||
|
@ -548,8 +558,8 @@ If you spotted it first, help us smashing it by providing a detailed and welcome
|
|||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.</p>
|
||||
<p>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainer</a>:</p>
|
||||
<p><a class="reference external" href="https://github.com/astirpe"><img alt="astirpe" src="https://github.com/astirpe.png?size=40px" /></a></p>
|
||||
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/server-tools/tree/15.0/base_changeset">OCA/server-tools</a> project on GitHub.</p>
|
||||
<p><a class="reference external image-reference" href="https://github.com/astirpe"><img alt="astirpe" src="https://github.com/astirpe.png?size=40px" /></a></p>
|
||||
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/server-tools/tree/16.0/base_changeset">OCA/server-tools</a> project on GitHub.</p>
|
||||
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import {Component} from "@odoo/owl";
|
||||
import {FormLabel} from "@web/views/form/form_label";
|
||||
import Popover from "web.Popover";
|
||||
|
||||
export class BaseChangesetPopover extends Popover {
|
||||
/*
|
||||
Call the ORM to accept the change and refresh the form view
|
||||
to update the field value.
|
||||
*/
|
||||
async applyChange(change_id) {
|
||||
await this.props.record.model.orm.call(
|
||||
"record.changeset.change",
|
||||
"apply",
|
||||
[[change_id]],
|
||||
{
|
||||
context: {set_change_by_ui: true},
|
||||
}
|
||||
);
|
||||
this._close();
|
||||
// Save the record first to prevent losing unsaved data on load.
|
||||
await this.props.record.save();
|
||||
await this.props.record.load();
|
||||
await this.props.record.model.notify();
|
||||
}
|
||||
/*
|
||||
Call the ORM to reject the change and only update the record's pending changes.
|
||||
*/
|
||||
async rejectChange(change_id) {
|
||||
await this.props.record.model.orm.call(
|
||||
"record.changeset.change",
|
||||
"cancel",
|
||||
[[change_id]],
|
||||
{
|
||||
context: {set_change_by_ui: true},
|
||||
}
|
||||
);
|
||||
this._close();
|
||||
this.props.record.changesetChanges =
|
||||
await this.props.record.fetchChangesetChanges();
|
||||
this.props.record.model.notify();
|
||||
}
|
||||
}
|
||||
BaseChangesetPopover.template = "base_changeset.ChangesetPopover";
|
||||
BaseChangesetPopover.props = ["fieldName", "popoverClass", "record", "title"];
|
||||
|
||||
export class BaseChangesetPopoverWrapper extends Component {}
|
||||
BaseChangesetPopoverWrapper.components = {BaseChangesetPopover};
|
||||
BaseChangesetPopoverWrapper.template = "base_changeset.ChangesetPopoverWrapper";
|
||||
|
||||
FormLabel.components = FormLabel.components || {};
|
||||
Object.assign(FormLabel.components, {BaseChangesetPopoverWrapper});
|
|
@ -0,0 +1,6 @@
|
|||
.o_changeset_popover {
|
||||
background-color: $o-view-background-color;
|
||||
}
|
||||
span.o_changeset_popover_wrapper > div {
|
||||
display: inline;
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="base_changeset.ChangesetPopoverWrapper" owl="1">
|
||||
<!--
|
||||
The popover button has to be set to inline display using a wrapper around
|
||||
the inherited Popover template. Otherwise, if instead we modify the top
|
||||
level div element of the Popover template, the component loses its `el`.
|
||||
-->
|
||||
<span class="o_changeset_popover_wrapper">
|
||||
<BaseChangesetPopover
|
||||
record="props.record"
|
||||
fieldName="props.fieldName"
|
||||
title="'Pending Changes'"
|
||||
popoverClass="'o_changeset_popover'"
|
||||
t-if="props.record.changesetChanges ? props.record.changesetChanges[props.fieldName] : 0"
|
||||
/>
|
||||
</span>
|
||||
</t>
|
||||
<t
|
||||
t-name="base_changeset.ChangesetPopover"
|
||||
owl="1"
|
||||
t-inherit="web.Popover"
|
||||
t-inherit-mode="primary"
|
||||
>
|
||||
<t t-portal="'body'" position="before">
|
||||
<a
|
||||
class="o_ChangesetPopoverView badge rounded-pill text-bg-warning mx-3 align-self-center"
|
||||
t-esc="props.record.changesetChanges[props.fieldName].length"
|
||||
role="button"
|
||||
/>
|
||||
</t>
|
||||
<t t-slot="opened" position="replace">
|
||||
<table class="pb-4">
|
||||
<tr
|
||||
t-foreach="props.record.changesetChanges[props.fieldName]"
|
||||
t-as="change"
|
||||
t-key="change.id"
|
||||
>
|
||||
<td>
|
||||
<t t-esc="change.origin_value_display" />
|
||||
</td>
|
||||
<td class="pl-2 pr-2">
|
||||
<i class="fa fa-arrow-right" />
|
||||
</td>
|
||||
<td>
|
||||
<t t-esc="change.new_value_display" />
|
||||
</td>
|
||||
<td class="pl-4" t-if="change.user_can_validate_changeset">
|
||||
<div class="btn-group">
|
||||
<button
|
||||
class="btn btn-danger base_changeset_reject btn-sm"
|
||||
t-attf-data-id="#{change.id}"
|
||||
t-on-click.synthetic="() => this.rejectChange(change.id)"
|
||||
>
|
||||
<i class="fa fa-times" />
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-success base_changeset_apply btn-sm"
|
||||
t-attf-data-id="#{change.id}"
|
||||
t-on-click.synthetic="() => this.applyChange(change.id)"
|
||||
>
|
||||
<i class="fa fa-check" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-inherit="web.FormLabel" t-inherit-mode="extension">
|
||||
<xpath expr="//label" position="inside">
|
||||
<BaseChangesetPopoverWrapper record="props.record" fieldName="props.id" />
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
|
@ -0,0 +1,31 @@
|
|||
/* @odoo-module */
|
||||
|
||||
import {Record} from "@web/views/basic_relational_model";
|
||||
import {patch} from "@web/core/utils/patch";
|
||||
|
||||
patch(Record.prototype, "base_changeset.Record", {
|
||||
/* Call the ORM to get this record's changeset changes */
|
||||
async fetchChangesetChanges() {
|
||||
return this.model.orm.call(
|
||||
"record.changeset.change",
|
||||
"get_changeset_changes_by_field",
|
||||
[this.resModel, this.resId]
|
||||
);
|
||||
},
|
||||
/* After loading the form's record data, fetch the changeset changes */
|
||||
async load() {
|
||||
await this._super(...arguments);
|
||||
if (this.__viewType === "form" && this.resId) {
|
||||
this.changesetChanges = await this.fetchChangesetChanges();
|
||||
}
|
||||
},
|
||||
/* Call the ORM to get this record's changeset changes after the form is modified */
|
||||
async save() {
|
||||
const isSaved = await this._super(...arguments);
|
||||
if (this.__viewType === "form" && this.resId) {
|
||||
this.changesetChanges = await this.fetchChangesetChanges();
|
||||
this.model.notify();
|
||||
}
|
||||
return isSaved;
|
||||
},
|
||||
});
|
|
@ -1,160 +0,0 @@
|
|||
odoo.define("base_changeset", function (require) {
|
||||
"use strict";
|
||||
|
||||
var FormRenderer = require("web.FormRenderer");
|
||||
var FormController = require("web.FormController");
|
||||
var BasicModel = require("web.BasicModel");
|
||||
var core = require("web.core");
|
||||
var qweb = core.qweb;
|
||||
|
||||
FormController.include({
|
||||
start: function () {
|
||||
return this._super
|
||||
.apply(this, arguments)
|
||||
.then(this._updateChangeset.bind(this));
|
||||
},
|
||||
|
||||
update: function () {
|
||||
var self = this;
|
||||
var res = this._super.apply(this, arguments);
|
||||
res.then(function () {
|
||||
self._updateChangeset();
|
||||
});
|
||||
return res;
|
||||
},
|
||||
|
||||
_updateChangeset: function () {
|
||||
var self = this;
|
||||
var state = this.model.get(this.handle);
|
||||
this.model
|
||||
.getChangeset(state.model, state.data.id)
|
||||
.then(function (changeset) {
|
||||
self.renderer.renderChangesetPopovers(changeset);
|
||||
});
|
||||
},
|
||||
|
||||
applyChange: function (id) {
|
||||
this.model.applyChange(id).then(this.reload.bind(this));
|
||||
},
|
||||
|
||||
rejectChange: function (id) {
|
||||
this.model.rejectChange(id).then(this.reload.bind(this));
|
||||
},
|
||||
});
|
||||
|
||||
FormRenderer.include({
|
||||
renderChangesetPopovers: function (changeset) {
|
||||
var self = this;
|
||||
_.each(changeset, function (changes, fieldName) {
|
||||
var labelId = self._getIDForLabel(fieldName);
|
||||
var $label = self.$el.find(_.str.sprintf('label[for="%s"]', labelId));
|
||||
if (!$label.length) {
|
||||
var widgets = _.filter(
|
||||
self.allFieldWidgets[self.state.id],
|
||||
function (widget) {
|
||||
return widget.name === fieldName;
|
||||
}
|
||||
);
|
||||
if (widgets.length === 1) {
|
||||
var widget = widgets[0];
|
||||
$label = widget.$el;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
self._renderChangesetPopover($label, changes);
|
||||
});
|
||||
},
|
||||
|
||||
_renderChangesetPopover: function ($el, changes) {
|
||||
var self = this;
|
||||
if (this.mode !== "readonly") {
|
||||
return;
|
||||
}
|
||||
var $button = $(
|
||||
qweb.render("ChangesetButton", {
|
||||
count: changes.length,
|
||||
})
|
||||
);
|
||||
|
||||
$el.append($button);
|
||||
|
||||
var options = {
|
||||
content: function () {
|
||||
var $content = $(
|
||||
qweb.render("ChangesetPopover", {
|
||||
changes: changes,
|
||||
})
|
||||
);
|
||||
$content.find(".base_changeset_apply").on("click", function () {
|
||||
self._applyClicked($(this));
|
||||
});
|
||||
$content.find(".base_changeset_reject").on("click", function () {
|
||||
self._rejectClicked($(this));
|
||||
});
|
||||
return $content;
|
||||
},
|
||||
html: true,
|
||||
placement: "bottom",
|
||||
title: "Pending Changes",
|
||||
trigger: "focus",
|
||||
delay: {show: 0, hide: 100},
|
||||
template: qweb.render("ChangesetTemplate"),
|
||||
};
|
||||
|
||||
$button.popover(options);
|
||||
},
|
||||
|
||||
_applyClicked: function ($el) {
|
||||
var id = parseInt($el.data("id"), 10);
|
||||
this.getParent().applyChange(id);
|
||||
},
|
||||
|
||||
_rejectClicked: function ($el) {
|
||||
var id = parseInt($el.data("id"), 10);
|
||||
this.getParent().rejectChange(id);
|
||||
},
|
||||
});
|
||||
|
||||
BasicModel.include({
|
||||
applyChange: function (id) {
|
||||
return this._rpc({
|
||||
model: "record.changeset.change",
|
||||
method: "apply",
|
||||
args: [[id]],
|
||||
context: _.extend({}, this.context, {set_change_by_ui: true}),
|
||||
});
|
||||
},
|
||||
|
||||
rejectChange: function (id) {
|
||||
return this._rpc({
|
||||
model: "record.changeset.change",
|
||||
method: "cancel",
|
||||
args: [[id]],
|
||||
context: _.extend({}, this.context, {set_change_by_ui: true}),
|
||||
});
|
||||
},
|
||||
|
||||
getChangeset: function (modelName, resId) {
|
||||
var self = this;
|
||||
return new Promise(function (resolve) {
|
||||
return self
|
||||
._rpc({
|
||||
model: "record.changeset.change",
|
||||
method: "get_fields_changeset_changes",
|
||||
args: [modelName, resId],
|
||||
})
|
||||
.then(function (changeset) {
|
||||
var res = {};
|
||||
_.each(changeset, function (changesetChange) {
|
||||
if (!_.contains(_.keys(res), changesetChange.field_name)) {
|
||||
res[changesetChange.field_name] = [];
|
||||
}
|
||||
res[changesetChange.field_name].push(changesetChange);
|
||||
});
|
||||
resolve(res);
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
|
@ -1,12 +0,0 @@
|
|||
.base_changeset_reject,
|
||||
.base_changeset_apply {
|
||||
width: 25px;
|
||||
}
|
||||
|
||||
.base_changeset_button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.base_changeset_popover {
|
||||
max-width: 100%;
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<templates>
|
||||
<t t-name="ChangesetPopover">
|
||||
<table class="pb-4">
|
||||
<tr t-foreach="changes" t-as="change">
|
||||
<td>
|
||||
<t t-esc="change.origin_value_display" />
|
||||
</td>
|
||||
<td class="pl-2 pr-2">
|
||||
<i class="fa fa-arrow-right" />
|
||||
</td>
|
||||
<td>
|
||||
<t t-esc="change.new_value_display" />
|
||||
</td>
|
||||
<td class="pl-4" t-if="change.user_can_validate_changeset">
|
||||
<div class="btn-group">
|
||||
<button
|
||||
class="btn btn-danger base_changeset_reject btn-sm"
|
||||
t-attf-data-id="#{change.id}"
|
||||
>
|
||||
<i class="fa fa-times" />
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-success base_changeset_apply btn-sm"
|
||||
t-attf-data-id="#{change.id}"
|
||||
>
|
||||
<i class="fa fa-check" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</t>
|
||||
<t t-name="ChangesetButton">
|
||||
<a
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label="Pending Changes"
|
||||
title="Pending Changes"
|
||||
data-toggle="tooltip"
|
||||
class="badge badge-warning badge-pill base_changeset_button ml-2"
|
||||
>
|
||||
<t t-esc="count" />
|
||||
</a>
|
||||
</t>
|
||||
<t t-name="ChangesetTemplate">
|
||||
<div class="popover base_changeset_popover" role="tooltip">
|
||||
<div class="arrow" />
|
||||
<h3 class="popover-header" />
|
||||
<div class="popover-body" />
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
|
@ -6,11 +6,12 @@ from odoo.tests import common
|
|||
|
||||
|
||||
class TestChangesetFieldRule(common.TransactionCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.company_model_id = self.env.ref("base.model_res_company").id
|
||||
self.field_name = self.env.ref("base.field_res_partner__name")
|
||||
self.field_street = self.env.ref("base.field_res_partner__street")
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.company_model_id = cls.env.ref("base.model_res_company").id
|
||||
cls.field_name = cls.env.ref("base.field_res_partner__name")
|
||||
cls.field_street = cls.env.ref("base.field_res_partner__street")
|
||||
|
||||
def test_get_rules(self):
|
||||
ChangesetFieldRule = self.env["changeset.field.rule"]
|
||||
|
|
|
@ -14,8 +14,9 @@ from .common import ChangesetTestCommon
|
|||
class TestChangesetFieldType(ChangesetTestCommon, TransactionCase):
|
||||
"""Check that changeset changes are stored expectingly to their types"""
|
||||
|
||||
def _setup_rules(self):
|
||||
ChangesetFieldRule = self.env["changeset.field.rule"]
|
||||
@classmethod
|
||||
def _setup_rules(cls):
|
||||
ChangesetFieldRule = cls.env["changeset.field.rule"]
|
||||
ChangesetFieldRule.search([]).unlink()
|
||||
fields = (
|
||||
("char", "ref"),
|
||||
|
@ -23,7 +24,7 @@ class TestChangesetFieldType(ChangesetTestCommon, TransactionCase):
|
|||
("boolean", "is_company"),
|
||||
("date", "date"),
|
||||
("integer", "color"),
|
||||
("float", "credit_limit"),
|
||||
("float", "partner_latitude"),
|
||||
("selection", "type"),
|
||||
("many2one", "country_id"),
|
||||
("many2many", "category_id"),
|
||||
|
@ -32,25 +33,26 @@ class TestChangesetFieldType(ChangesetTestCommon, TransactionCase):
|
|||
)
|
||||
for field_type, field in fields:
|
||||
attr_name = "field_%s" % field_type
|
||||
field_record = self.env["ir.model.fields"].search(
|
||||
field_record = cls.env["ir.model.fields"].search(
|
||||
[("model", "=", "res.partner"), ("name", "=", field)]
|
||||
)
|
||||
self.assertTrue(field_record, "Field %s not available" % field)
|
||||
cls.assertTrue(field_record, "Field %s not available" % field)
|
||||
# set attribute such as 'self.field_char' is a
|
||||
# ir.model.fields record of the field res_partner.ref
|
||||
setattr(self, attr_name, field_record)
|
||||
setattr(cls, attr_name, field_record)
|
||||
ChangesetFieldRule.create(
|
||||
{"field_id": field_record.id, "action": "validate"}
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self._setup_rules()
|
||||
self.partner = self.env["res.partner"].create(
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls._setup_rules()
|
||||
cls.partner = cls.env["res.partner"].create(
|
||||
{"name": "Original Name", "street": "Original Street"}
|
||||
)
|
||||
# Add context for this test for compatibility with other modules' tests
|
||||
self.partner = self.partner.with_context(test_record_changeset=True)
|
||||
cls.partner = cls.partner.with_context(test_record_changeset=True)
|
||||
|
||||
def test_new_changeset_char(self):
|
||||
"""Add a new changeset on a Char field"""
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from odoo import fields
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
@ -29,28 +31,51 @@ class TestChangesetFlow(ChangesetTestCommon, TransactionCase):
|
|||
becomes 'done'
|
||||
"""
|
||||
|
||||
def _setup_rules(self):
|
||||
ChangesetFieldRule = self.env["changeset.field.rule"]
|
||||
@classmethod
|
||||
def _setup_rules(cls):
|
||||
ChangesetFieldRule = cls.env["changeset.field.rule"]
|
||||
ChangesetFieldRule.search([]).unlink()
|
||||
self.field_name = self.env.ref("base.field_res_partner__name")
|
||||
self.field_street = self.env.ref("base.field_res_partner__street")
|
||||
self.field_street2 = self.env.ref("base.field_res_partner__street2")
|
||||
ChangesetFieldRule.create({"field_id": self.field_name.id, "action": "auto"})
|
||||
cls.field_name = cls.env.ref("base.field_res_partner__name")
|
||||
cls.field_street = cls.env.ref("base.field_res_partner__street")
|
||||
cls.field_street2 = cls.env.ref("base.field_res_partner__street2")
|
||||
ChangesetFieldRule.create({"field_id": cls.field_name.id, "action": "auto"})
|
||||
ChangesetFieldRule.create(
|
||||
{"field_id": self.field_street.id, "action": "validate"}
|
||||
)
|
||||
ChangesetFieldRule.create(
|
||||
{"field_id": self.field_street2.id, "action": "never"}
|
||||
{"field_id": cls.field_street.id, "action": "validate"}
|
||||
)
|
||||
ChangesetFieldRule.create({"field_id": cls.field_street2.id, "action": "never"})
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self._setup_rules()
|
||||
self.partner = self.env["res.partner"].create(
|
||||
{"name": "X", "street": "street X", "street2": "street2 X"}
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls._setup_rules()
|
||||
cls.demo_user = cls.env.ref("base.user_demo")
|
||||
cls.partner = (
|
||||
cls.env["res.partner"]
|
||||
.with_user(cls.demo_user)
|
||||
.create({"name": "X", "street": "street X", "street2": "street2 X"})
|
||||
)
|
||||
# Add context for this test for compatibility with other modules' tests
|
||||
self.partner = self.partner.with_context(test_record_changeset=True)
|
||||
cls.partner = cls.partner.with_context(test_record_changeset=True)
|
||||
|
||||
def test_get_view(self):
|
||||
"""For privileged users, the smart button is present on the form"""
|
||||
view = self.env.ref("base.view_partner_form")
|
||||
|
||||
def get_nodes(user):
|
||||
arch = etree.XML(
|
||||
self.env["res.partner"]
|
||||
.with_user(user)
|
||||
.get_view(view_id=view.id)["arch"]
|
||||
)
|
||||
return len(
|
||||
arch.xpath(
|
||||
"//div[@name='button_box']"
|
||||
"/button[@name='action_record_changeset_change_view']"
|
||||
)
|
||||
)
|
||||
|
||||
self.assertTrue(get_nodes(self.env.ref("base.user_admin")))
|
||||
self.assertFalse(get_nodes(self.env.ref("base.user_demo")))
|
||||
|
||||
def test_new_changeset(self):
|
||||
"""Add a new changeset on a partner
|
||||
|
@ -64,7 +89,7 @@ class TestChangesetFlow(ChangesetTestCommon, TransactionCase):
|
|||
self.assertEqual(self.partner.count_pending_changeset_changes, 1)
|
||||
self.assert_changeset(
|
||||
self.partner,
|
||||
self.env.user,
|
||||
self.demo_user,
|
||||
[
|
||||
(self.field_name, "X", "Y", "done"),
|
||||
(self.field_street, "street X", "street Y", "draft"),
|
||||
|
@ -74,6 +99,11 @@ class TestChangesetFlow(ChangesetTestCommon, TransactionCase):
|
|||
self.assertEqual(self.partner.name, "Y")
|
||||
self.assertEqual(self.partner.street, "street X")
|
||||
self.assertEqual(self.partner.street2, "street2 X")
|
||||
# Pending Changes widget can be rendered for the unprivileged user
|
||||
self.env.invalidate_all()
|
||||
self.env["record.changeset.change"].with_user(
|
||||
self.demo_user
|
||||
).get_changeset_changes_by_field(self.partner._name, self.partner.id)
|
||||
|
||||
def test_create_new_changeset(self):
|
||||
"""Create a new partner with a changeset"""
|
||||
|
@ -140,7 +170,7 @@ class TestChangesetFlow(ChangesetTestCommon, TransactionCase):
|
|||
self.assertEqual(self.partner.count_pending_changesets, 1)
|
||||
self.assert_changeset(
|
||||
self.partner,
|
||||
self.env.user,
|
||||
self.demo_user,
|
||||
[(self.field_street, "street X", False, "draft")],
|
||||
)
|
||||
|
||||
|
@ -168,7 +198,7 @@ class TestChangesetFlow(ChangesetTestCommon, TransactionCase):
|
|||
self.partner._compute_count_pending_changesets()
|
||||
self.assertEqual(self.partner.count_pending_changesets, 1)
|
||||
for change in changeset.change_ids:
|
||||
change.get_fields_changeset_changes(changeset.model, changeset.res_id)
|
||||
change.get_changeset_changes_by_field(changeset.model, changeset.res_id)
|
||||
changeset.change_ids.apply()
|
||||
self.partner._compute_changeset_ids()
|
||||
self.partner._compute_count_pending_changesets()
|
||||
|
@ -197,9 +227,11 @@ class TestChangesetFlow(ChangesetTestCommon, TransactionCase):
|
|||
self.assertEqual(self.partner.street, "street X")
|
||||
self.assertEqual(self.partner.changeset_ids.change_ids.state, "draft")
|
||||
|
||||
user = self.env.ref("base.user_demo")
|
||||
user.groups_id += self.env.ref("base_changeset.group_changeset_user")
|
||||
self.partner.changeset_ids.change_ids.with_user(user).apply()
|
||||
# Copy the user to have another user with similar rights, so that
|
||||
# self validation prevention doesn't kick in.
|
||||
other_demo_user = self.demo_user.copy()
|
||||
other_demo_user.groups_id += self.env.ref("base_changeset.group_changeset_user")
|
||||
self.partner.changeset_ids.change_ids.with_user(other_demo_user).apply()
|
||||
self.partner._compute_changeset_ids()
|
||||
self.partner._compute_count_pending_changesets()
|
||||
self.assertEqual(self.partner.count_pending_changesets, 0)
|
||||
|
@ -253,7 +285,7 @@ class TestChangesetFlow(ChangesetTestCommon, TransactionCase):
|
|||
self.partner._compute_count_pending_changesets()
|
||||
self.assertEqual(self.partner.count_pending_changesets, 1)
|
||||
for change in changeset.change_ids:
|
||||
change.get_fields_changeset_changes(changeset.model, changeset.res_id)
|
||||
change.get_changeset_changes_by_field(changeset.model, changeset.res_id)
|
||||
changeset.change_ids.apply()
|
||||
self.partner._compute_changeset_ids()
|
||||
self.partner._compute_count_pending_changesets()
|
||||
|
@ -272,7 +304,7 @@ class TestChangesetFlow(ChangesetTestCommon, TransactionCase):
|
|||
self.partner._compute_count_pending_changesets()
|
||||
self.assertEqual(self.partner.count_pending_changesets, 1)
|
||||
for change in changeset.change_ids:
|
||||
change.get_fields_changeset_changes(changeset.model, changeset.res_id)
|
||||
change.get_changeset_changes_by_field(changeset.model, changeset.res_id)
|
||||
changeset.change_ids.apply()
|
||||
self.partner._compute_changeset_ids()
|
||||
self.partner._compute_count_pending_changesets()
|
||||
|
@ -294,7 +326,7 @@ class TestChangesetFlow(ChangesetTestCommon, TransactionCase):
|
|||
self.assertEqual(self.partner.count_pending_changesets, 1)
|
||||
self.assertEqual(self.partner.count_pending_changeset_changes, 3)
|
||||
for change in changeset.change_ids:
|
||||
change.get_fields_changeset_changes(changeset.model, changeset.res_id)
|
||||
change.get_changeset_changes_by_field(changeset.model, changeset.res_id)
|
||||
changeset.apply()
|
||||
self.partner._compute_changeset_ids()
|
||||
self.partner._compute_count_pending_changesets()
|
||||
|
@ -381,7 +413,7 @@ class TestChangesetFlow(ChangesetTestCommon, TransactionCase):
|
|||
self.assertEqual(self.partner.count_pending_changesets, 1)
|
||||
self.assertEqual(self.partner.count_pending_changeset_changes, 3)
|
||||
for change in changeset.change_ids:
|
||||
change.get_fields_changeset_changes(changeset.model, changeset.res_id)
|
||||
change.get_changeset_changes_by_field(changeset.model, changeset.res_id)
|
||||
changeset2 = self._create_changeset(partner2, changes)
|
||||
partner2._compute_changeset_ids()
|
||||
partner2._compute_count_pending_changesets()
|
||||
|
@ -390,7 +422,7 @@ class TestChangesetFlow(ChangesetTestCommon, TransactionCase):
|
|||
self.assertEqual(partner2.count_pending_changesets, 1)
|
||||
self.assertEqual(partner2.count_pending_changeset_changes, 3)
|
||||
for change in changeset2.change_ids:
|
||||
change.get_fields_changeset_changes(changeset2.model, changeset2.res_id)
|
||||
change.get_changeset_changes_by_field(changeset2.model, changeset2.res_id)
|
||||
(changeset + changeset2).apply()
|
||||
self.assertEqual(self.partner.name, "Y")
|
||||
self.assertEqual(self.partner.street, "street Y")
|
||||
|
@ -406,7 +438,7 @@ class TestChangesetFlow(ChangesetTestCommon, TransactionCase):
|
|||
self.partner.write({"street": False})
|
||||
self.partner._compute_changeset_ids()
|
||||
changeset = self.partner.changeset_ids
|
||||
self.assertEqual(changeset.source, self.env.user)
|
||||
self.assertEqual(changeset.source, self.demo_user)
|
||||
|
||||
def test_new_changeset_source_other_model(self):
|
||||
"""Define source from another model"""
|
||||
|
@ -444,10 +476,10 @@ class TestChangesetFlow(ChangesetTestCommon, TransactionCase):
|
|||
]
|
||||
).expression = "object.street != 'street X'"
|
||||
self.partner.street = "street Y"
|
||||
self.partner.refresh()
|
||||
self.partner.invalidate_recordset()
|
||||
self.assertEqual(self.partner.street, "street Y")
|
||||
self.assertFalse(self.partner.changeset_ids)
|
||||
self.partner.street = "street Z"
|
||||
self.partner.refresh()
|
||||
self.partner.invalidate_recordset()
|
||||
self.assertTrue(self.partner.changeset_ids)
|
||||
self.assertEqual(self.partner.street, "street Y")
|
||||
|
|
|
@ -17,20 +17,20 @@ class TestChangesetOrigin(ChangesetTestCommon, TransactionCase):
|
|||
displays the 'old' value.
|
||||
"""
|
||||
|
||||
def _setup_rules(self):
|
||||
ChangesetFieldRule = self.env["changeset.field.rule"]
|
||||
@classmethod
|
||||
def _setup_rules(cls):
|
||||
ChangesetFieldRule = cls.env["changeset.field.rule"]
|
||||
ChangesetFieldRule.search([]).unlink()
|
||||
self.field_name = self.env.ref("base.field_res_partner__name")
|
||||
ChangesetFieldRule.create(
|
||||
{"field_id": self.field_name.id, "action": "validate"}
|
||||
)
|
||||
cls.field_name = cls.env.ref("base.field_res_partner__name")
|
||||
ChangesetFieldRule.create({"field_id": cls.field_name.id, "action": "validate"})
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self._setup_rules()
|
||||
self.partner = self.env["res.partner"].create({"name": "X"})
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls._setup_rules()
|
||||
cls.partner = cls.env["res.partner"].create({"name": "X"})
|
||||
# Add context for this test for compatibility with other modules' tests
|
||||
self.partner = self.partner.with_context(test_record_changeset=True)
|
||||
cls.partner = cls.partner.with_context(test_record_changeset=True)
|
||||
|
||||
def test_origin_value_of_change_with_apply(self):
|
||||
"""Origin field is read from the parter or 'old' - with apply
|
||||
|
|
|
@ -9,13 +9,14 @@ from .common import ChangesetTestCommon
|
|||
class TestChangesetFlow(ChangesetTestCommon, TransactionCase):
|
||||
"""Check that changesets don't leak information"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.env["changeset.field.rule"].search([]).unlink()
|
||||
self.rule = self.env["changeset.field.rule"].create(
|
||||
@classmethod
|
||||
def setUp(cls):
|
||||
super().setUpClass()
|
||||
cls.env["changeset.field.rule"].search([]).unlink()
|
||||
cls.rule = cls.env["changeset.field.rule"].create(
|
||||
{
|
||||
"model_id": self.env.ref("base.model_ir_config_parameter").id,
|
||||
"field_id": self.env.ref("base.field_ir_config_parameter__key").id,
|
||||
"model_id": cls.env.ref("base.model_ir_config_parameter").id,
|
||||
"field_id": cls.env.ref("base.field_ir_config_parameter__key").id,
|
||||
"action": "auto",
|
||||
}
|
||||
)
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="model_id" />
|
||||
<field name="field_id" />
|
||||
<field name="field_id" options="{'no_create': True}" />
|
||||
<field name="source_model_id" />
|
||||
<field name="expression" />
|
||||
<field name="validator_group_ids" />
|
||||
|
|
Loading…
Reference in New Issue