Browse Source

Rename addon according to the new term: partner_changeset

Add translations

Add test coverage for caching of changeset rules

Add unique constraint (model_id, field_id) on rules

Add rules in demo data

Put the security groups links in noupdate

Put keep the groups in a noupdate=0 section so the records in ir.model.data
will still be modifiable by other modules.

Store the source of a changeset

Differentiate rules according to their origin

Rules are applied also for manual edition

Action is required

Do not keep recordsets in ormcache

Because they would be unreadable as soon as the cursor is closed.
Instead, we keep only the id and the record is browsed for every new
environment.

Remove useless 'model_id' on changeset rules

model_id has been removed

Remove reference to the model_name on rules

Because the model has been removed earlier (dead code)

Fix issue when applying empty many2one

Do not create changesets on moved contacts

As we just created the contact with a 'copy' we don't want to have a
changeset for the initialization values

Update translations

Use a selection widget on source model

Adapt for inclusion in OCA

Rename 'Pending Changesets' to 'Changes'

It's shorter

Add screenshots

Do not create a changeset when both sides are empty

But have a different type (e.g. False and '')
pull/357/head
Guewen Baconnier 9 years ago
committed by Denis Leemann
parent
commit
6d7e49ae08
  1. 171
      partner_changeset/README.rst
  2. 4
      partner_changeset/__init__.py
  3. 23
      partner_changeset/__openerp__.py
  4. 51
      partner_changeset/demo/changeset_field_rule.xml
  5. 313
      partner_changeset/i18n/fr.po
  6. 289
      partner_changeset/i18n/partner_changeset.pot
  7. 7
      partner_changeset/models/__init__.py
  8. 144
      partner_changeset/models/changeset_field_rule.py
  9. 50
      partner_changeset/models/res_partner.py
  10. 514
      partner_changeset/models/res_partner_changeset.py
  11. 10
      partner_changeset/security/ir.model.access.csv
  12. 29
      partner_changeset/security/security.xml
  13. BIN
      partner_changeset/static/description/icon.png
  14. BIN
      partner_changeset/static/src/img/changeset.png
  15. BIN
      partner_changeset/static/src/img/rules.png
  16. 6
      partner_changeset/tests/__init__.py
  17. 77
      partner_changeset/tests/common.py
  18. 95
      partner_changeset/tests/test_changeset_field_rule.py
  19. 279
      partner_changeset/tests/test_changeset_field_type.py
  20. 246
      partner_changeset/tests/test_changeset_flow.py
  21. 110
      partner_changeset/tests/test_changeset_origin.py
  22. 63
      partner_changeset/views/changeset_field_rule_views.xml
  23. 10
      partner_changeset/views/menu.xml
  24. 159
      partner_changeset/views/res_partner_changeset_views.xml
  25. 41
      partner_changeset/views/res_partner_views.xml

171
partner_changeset/README.rst

@ -0,0 +1,171 @@
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
==================
Partner Changesets
==================
This module extends the functionality of partners. It allows to create
changesets that must be validated when a partner is modified instead of direct
modifications. Rules allow to configure which field must be validated.
Configuration
=============
Access Rights
-------------
The changesets rules must be edited by users with the group ``Changesets
Configuration``. The changesets can be applied or canceled only by users
with the group ``Changesets Validations``
Changesets Rules
----------------
The changesets rules can be configured in ``Sales > Configuration >
Partner Changesets > Fields Rules``. For each partner field, an
action can be defined:
* Auto: the changes made on this field are always applied
* Validate: the changes made on this field must be manually confirmed by
a 'Changesets User' user
* Never: the changes made on this field are always refused
In any case, all the changes made by the users are always applied
directly on the users, but a 'validated' changeset is created for the
history.
The supported fields are:
* Char
* Text
* Date
* Datetime
* Integer
* Float
* Boolean
* Many2one
Rules can be global (no source model) or configured by source model.
Rules by source model have the priority. If a field is not configured
for the source model, it will use the global rule (if existing).
If a field has no rule, it is written to the partner without changeset.
Usage
=====
General case
------------
The first step is to create the changeset rules, once that done, writes on
partners will be created as changesets.
Finding changesets
------------------
A menu lists all the changesets in ``Sales > Configuration > Partner
Changesets > Changesets``.
However, it is more convenient to access them directly from the
partners. Pending changesets can be accessed directly from the top right
of the partners' view. A new filter on the partners shows the partners
having at least one pending changeset.
Handling changesets
-------------------
A changeset shows the list of the changes made on a partner. Some of the
changes may be 'Pending', some 'Accepted' or 'Rejected' according to the
changeset rules. The only changes that need an action from the user are
'Pending' changes. When a change is accepted, the value is written on
the user.
The changes view shows the name of the partner's field, the Origin value
and the New value alongside the state of the change. By clicking on the
change in some cases a more detailed view is displayed, for instance,
links for relations can be clicked on.
A button on a changeset allows to apply or reject all the changes at
once.
Custom source rules in your addon
---------------------------------
Addons wanting to create changeset with their own rules should pass the
following keys in the context when they write on the partner:
* ``__changeset_rules_source_model``: name of the model which asks for
the change
* ``__changeset_rules_source_id``: id of the record which asks for the
change
Also, they should extend the selection in
``ChangesetFieldRule._domain_source_models`` to add their model (the
same that is passed in ``__changeset_rules_source_model``).
The source is used for the application of the rules, allowing to have a
different rule for a different source. It is also stored on the changeset for
information.
Screenshot:
-----------
* Configuration of rules
.. image:: partner_changeset/static/src/img/rules.png
* Changeset waiting for validation
.. image:: partner_changeset/static/src/img/changeset.png
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
:alt: Try me on Runbot
:target: https://runbot.odoo-community.org/runbot/134/8.0
Known issues / Roadmap
======================
* Only a subset of the type of fields is actually supported
Bug Tracker
===========
Bugs are tracked on `GitHub Issues
<https://github.com/OCA/partner-contact/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/
partner-contact/issues/new?body=module:%20
partner_changeset%0Aversion:%20
8.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Credits
=======
Images
------
* Odoo Community Association: `Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_.
Contributors
------------
* Guewen Baconnier <guewen.baconnier@camptocamp.com>
Maintainer
----------
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
This module is maintained by the OCA.
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.
To contribute to this module, please visit https://odoo-community.org.

4
partner_changeset/__init__.py

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from . import models

23
partner_changeset/__openerp__.py

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# © 2015 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
{'name': 'Partner Changesets',
'version': '8.0.1.0.0',
'author': 'Camptocamp, Odoo Community Association (OCA)',
'license': 'AGPL-3',
'category': 'Sales Management',
'depends': ['base',
],
'website': 'http://www.camptocamp.com',
'data': ['security/security.xml',
'security/ir.model.access.csv',
'views/menu.xml',
'views/res_partner_changeset_views.xml',
'views/changeset_field_rule_views.xml',
'views/res_partner_views.xml',
],
'demo': ['demo/changeset_field_rule.xml',
],
'installable': True,
}

51
partner_changeset/demo/changeset_field_rule.xml

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="1">
<record model="changeset.field.rule" id="changeset_field_rule_name">
<field name="field_id" ref="base.field_res_partner_name"/>
<field name="action">auto</field>
</record>
<record model="changeset.field.rule" id="changeset_field_rule_street">
<field name="field_id" ref="base.field_res_partner_street"/>
<field name="action">validate</field>
</record>
<record model="changeset.field.rule" id="changeset_field_rule_street2">
<field name="field_id" ref="base.field_res_partner_street2"/>
<field name="action">validate</field>
</record>
<record model="changeset.field.rule" id="changeset_field_rule_zip">
<field name="field_id" ref="base.field_res_partner_zip"/>
<field name="action">validate</field>
</record>
<record model="changeset.field.rule" id="changeset_field_rule_city">
<field name="field_id" ref="base.field_res_partner_city"/>
<field name="action">validate</field>
</record>
<record model="changeset.field.rule" id="changeset_field_rule_email">
<field name="field_id" ref="base.field_res_partner_email"/>
<field name="action">never</field>
</record>
<record model="changeset.field.rule" id="changeset_field_rule_ref">
<field name="field_id" ref="base.field_res_partner_ref"/>
<field name="action">validate</field>
</record>
<record model="changeset.field.rule" id="changeset_field_rule_country_id">
<field name="field_id" ref="base.field_res_partner_country_id"/>
<field name="action">validate</field>
</record>
<record model="changeset.field.rule" id="changeset_field_rule_credit_limit">
<field name="field_id" ref="base.field_res_partner_credit_limit"/>
<field name="action">validate</field>
</record>
</data>
</openerp>

313
partner_changeset/i18n/fr.po

@ -0,0 +1,313 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * partner_changeset
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 8.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-11-25 08:31+0000\n"
"PO-Revision-Date: 2015-09-18 14:48+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: partner_changeset
#: sql_constraint:changeset.field.rule:0
msgid "A rule already exists for this field."
msgstr "Une règle existe déjà pour ce champ."
#. module: partner_changeset
#: selection:res.partner.changeset.change,state:0
msgid "Accepted"
msgstr "Accepté"
#. module: partner_changeset
#: field:changeset.field.rule,action:0
msgid "Action"
msgstr "Action"
#. module: partner_changeset
#: view:res.partner.changeset:partner_changeset.view_res_partner_changeset_form
#: view:res.partner.changeset.change:partner_changeset.view_res_partner_changeset_change_form
msgid "Apply"
msgstr "Appliquer"
#. module: partner_changeset
#: view:res.partner.changeset:partner_changeset.view_res_partner_changeset_form
msgid "Apply pending changes"
msgstr "Appliquer les modifications en attente"
#. module: partner_changeset
#: help:changeset.field.rule,action:0
msgid ""
"Auto: always apply a change.\n"
"Validate: manually applied by an administrator.\n"
"Never: change never applied."
msgstr ""
"Auto: toujours appliquer une modification.\n"
"Validation: modification manuellement appliquée par un administrateur.\n"
"Jamais: modification jamais appliquée."
#. module: partner_changeset
#: view:res.partner.changeset:partner_changeset.view_res_partner_changeset_form
#: view:res.partner:partner_changeset.res_partner_view_buttons
#: field:res.partner.changeset,change_ids:0
msgid "Changes"
msgstr "Modifications"
#. module: partner_changeset
#: field:res.partner.changeset.change,changeset_id:0
msgid "Changeset"
msgstr "Jeu de modifications"
#. module: partner_changeset
#: model:res.groups,name:partner_changeset.group_changeset_manager
msgid "Changeset Configuration"
msgstr "Configuration des modifications"
#. module: partner_changeset
#: model:ir.model,name:partner_changeset.model_changeset_field_rule
msgid "Changeset Field Rules"
msgstr "Règles de modifications par champ"
#. module: partner_changeset
#: view:changeset.field.rule:partner_changeset.view_changeset_field_rule_form
#: view:changeset.field.rule:partner_changeset.view_changeset_field_rule_search
#: view:changeset.field.rule:partner_changeset.view_changeset_field_rule_tree
#: model:ir.actions.act_window,name:partner_changeset.action_changeset_field_rule_view
msgid "Changeset Fields Rules"
msgstr "Règles de modifications par champ"
#. module: partner_changeset
#: model:ir.ui.menu,name:partner_changeset.menu_res_partner_changeset
#: field:res.partner,changeset_ids:0
msgid "Changesets"
msgstr "Modifications"
#. module: partner_changeset
#: model:res.groups,name:partner_changeset.group_changeset_user
msgid "Changesets Validations "
msgstr "Validations des modifications"
#. module: partner_changeset
#: field:changeset.field.rule,create_uid:0
#: field:res.partner.changeset,create_uid:0
#: field:res.partner.changeset.change,create_uid:0
msgid "Created by"
msgstr "Créé par"
#. module: partner_changeset
#: field:changeset.field.rule,create_date:0
#: field:res.partner.changeset,create_date:0
#: field:res.partner.changeset.change,create_date:0
msgid "Created on"
msgstr "Créé le"
#. module: partner_changeset
#: field:res.partner.changeset,date:0
msgid "Date"
msgstr "Date"
#. module: partner_changeset
#: view:res.partner.changeset:partner_changeset.view_res_partner_changeset_search
#: selection:res.partner.changeset,state:0
msgid "Done"
msgstr "Fait"
#. module: partner_changeset
#: field:changeset.field.rule,field_id:0
#: field:res.partner.changeset.change,field_id:0
msgid "Field"
msgstr "Champ"
#. module: partner_changeset
#: model:ir.ui.menu,name:partner_changeset.menu_changeset_field_rule
msgid "Field Rules"
msgstr "Règle par champ"
#. module: partner_changeset
#: view:res.partner.changeset:partner_changeset.view_res_partner_changeset_search
msgid "Group By"
msgstr "Grouper par"
#. module: partner_changeset
#: field:changeset.field.rule,id:0 field:res.partner.changeset,id:0
#: field:res.partner.changeset.change,id:0
msgid "ID"
msgstr "Id."
#. module: partner_changeset
#: help:changeset.field.rule,source_model_id:0
msgid ""
"If a source model is defined, the rule will be applied only when the change "
"is made from this origin. Rules without source model are global and applies "
"to all backends.\n"
"Rules with a source model have precedence over global rules, but if a field "
"has no rule with a source model, the global rule is used."
msgstr ""
"Si un modèle source est défini, la règle ne sera appliquée que lorsque le "
"changement vient de cette source. Les règles sans modèle source sont "
"globales et sont appliquées à toutes les sources.\n "
"Les règles avec une souce ont la précédence sur les règles globales. Si un "
"champ n'a pas de règle avec source, la règle globale est utilisée."
#. module: partner_changeset
#: field:changeset.field.rule,write_uid:0
#: field:res.partner.changeset,write_uid:0
#: field:res.partner.changeset.change,write_uid:0
msgid "Last Updated by"
msgstr ""
#. module: partner_changeset
#: field:changeset.field.rule,write_date:0
#: field:res.partner.changeset,write_date:0
#: field:res.partner.changeset.change,write_date:0
msgid "Last Updated on"
msgstr ""
#. module: partner_changeset
#: field:res.partner.changeset.change,new_value_boolean:0
#: field:res.partner.changeset.change,new_value_char:0
#: field:res.partner.changeset.change,new_value_date:0
#: field:res.partner.changeset.change,new_value_datetime:0
#: field:res.partner.changeset.change,new_value_float:0
#: field:res.partner.changeset.change,new_value_integer:0
#: field:res.partner.changeset.change,new_value_reference:0
#: field:res.partner.changeset.change,new_value_text:0
msgid "New"
msgstr "Nouveau"
#. module: partner_changeset
#: field:res.partner.changeset,note:0
msgid "Note"
msgstr "Note"
#. module: partner_changeset
#: field:res.partner.changeset.change,old_value_boolean:0
#: field:res.partner.changeset.change,old_value_char:0
#: field:res.partner.changeset.change,old_value_date:0
#: field:res.partner.changeset.change,old_value_datetime:0
#: field:res.partner.changeset.change,old_value_float:0
#: field:res.partner.changeset.change,old_value_integer:0
#: field:res.partner.changeset.change,old_value_reference:0
#: field:res.partner.changeset.change,old_value_text:0
msgid "Old"
msgstr "Ancien"
#. module: partner_changeset
#: model:ir.model,name:partner_changeset.model_res_partner
#: view:res.partner.changeset:partner_changeset.view_res_partner_changeset_search
#: field:res.partner.changeset,partner_id:0
msgid "Partner"
msgstr "Partenaire"
#. module: partner_changeset
#: model:ir.actions.act_window,name:partner_changeset.action_res_partner_changeset_view
#: model:ir.model,name:partner_changeset.model_res_partner_changeset
#: view:res.partner.changeset:partner_changeset.view_res_partner_changeset_form
#: view:res.partner.changeset:partner_changeset.view_res_partner_changeset_search
#: view:res.partner.changeset:partner_changeset.view_res_partner_changeset_tree
msgid "Partner Changeset"
msgstr "Modifications de partenaire"
#. module: partner_changeset
#: model:ir.model,name:partner_changeset.model_res_partner_changeset_change
#: view:res.partner.changeset:partner_changeset.view_res_partner_changeset_form
#: view:res.partner.changeset.change:partner_changeset.view_res_partner_changeset_change_form
msgid "Partner Changeset Change"
msgstr "Modification de partenaire"
#. module: partner_changeset
#: model:ir.ui.menu,name:partner_changeset.menu_changeset
msgid "Partner Changesets"
msgstr "Modifications de partenaire"
#. module: partner_changeset
#: view:res.partner.changeset:partner_changeset.view_res_partner_changeset_search
#: selection:res.partner.changeset,state:0
#: selection:res.partner.changeset.change,state:0
msgid "Pending"
msgstr "En attente"
#. module: partner_changeset
#: view:res.partner:partner_changeset.view_res_partner_filter
msgid "Pending Changesets"
msgstr "Modifications en attente"
#. module: partner_changeset
#: view:res.partner.changeset:partner_changeset.view_res_partner_changeset_form
msgid "Previous"
msgstr "Précédent"
#. module: partner_changeset
#: view:res.partner.changeset:partner_changeset.view_res_partner_changeset_form
#: view:res.partner.changeset.change:partner_changeset.view_res_partner_changeset_change_form
msgid "Reject"
msgstr "Rejeter"
#. module: partner_changeset
#: view:res.partner.changeset:partner_changeset.view_res_partner_changeset_form
msgid "Reject pending changes"
msgstr "Rejeter les modifications en attente"
#. module: partner_changeset
#: selection:res.partner.changeset.change,state:0
msgid "Rejected"
msgstr "Rejeté"
#. module: partner_changeset
#: field:changeset.field.rule,source_model_id:0
msgid "Source Model"
msgstr "Modèle source"
#. module: partner_changeset
#: field:res.partner.changeset,source:0
msgid "Source of the change"
msgstr "Source du changement"
#. module: partner_changeset
#: view:res.partner.changeset:partner_changeset.view_res_partner_changeset_search
#: field:res.partner.changeset,state:0
#: field:res.partner.changeset.change,state:0
msgid "State"
msgstr "État"
#. module: partner_changeset
#: model:res.groups,comment:partner_changeset.group_changeset_user
msgid "The user will be able to apply or reject changesets."
msgstr "L'utilisateur pourra appliquer ou rejeter les jeux de modifications."
#. module: partner_changeset
#: model:res.groups,comment:partner_changeset.group_changeset_manager
msgid ""
"The user will have an access to the configuration of the changeset rules."
msgstr ""
"L'utilisateur aura accès à la configuration des règles de jeux de "
"modifications."
#. module: partner_changeset
#: code:addons/partner_changeset/models/res_partner_changeset.py:418
#, python-format
msgid ""
"This change cannot be applied because a previous changeset for the same "
"partner is pending.\n"
"Apply all the anterior changesets before applying this one."
msgstr ""
"The changement ne peux pas être appliqué car un précédent jeu de "
"modification pour le même partenaire est toujours en attente.\n"
"Il est nécessaire d'appliquer tous les précédents jeux de modification avant "
"d'applique celui-ci."
#. module: partner_changeset
#: code:addons/partner_changeset/models/res_partner_changeset.py:433
#, python-format
msgid "This change has already be applied."
msgstr "The changement est déjà appliqué."
#~ msgid "Model"
#~ msgstr "Modèle"

289
partner_changeset/i18n/partner_changeset.pot

@ -0,0 +1,289 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * partner_changeset
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 8.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-11-25 08:31+0000\n"
"PO-Revision-Date: 2015-11-25 08:31+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: partner_changeset
#: sql_constraint:changeset.field.rule:0
msgid "A rule already exists for this field."
msgstr ""
#. module: partner_changeset
#: selection:res.partner.changeset.change,state:0
msgid "Accepted"
msgstr ""
#. module: partner_changeset
#: field:changeset.field.rule,action:0
msgid "Action"
msgstr ""
#. module: partner_changeset
#: view:res.partner.changeset:partner_changeset.view_res_partner_changeset_form
#: view:res.partner.changeset.change:partner_changeset.view_res_partner_changeset_change_form
msgid "Apply"
msgstr ""
#. module: partner_changeset
#: view:res.partner.changeset:partner_changeset.view_res_partner_changeset_form
msgid "Apply pending changes"
msgstr ""
#. module: partner_changeset
#: help:changeset.field.rule,action:0
msgid "Auto: always apply a change.\n"
"Validate: manually applied by an administrator.\n"
"Never: change never applied."
msgstr ""
#. module: partner_changeset
#: view:res.partner.changeset:partner_changeset.view_res_partner_changeset_form
#: view:res.partner:partner_changeset.res_partner_view_buttons
#: field:res.partner.changeset,change_ids:0
msgid "Changes"
msgstr ""
#. module: partner_changeset
#: field:res.partner.changeset.change,changeset_id:0
msgid "Changeset"
msgstr ""
#. module: partner_changeset
#: model:res.groups,name:partner_changeset.group_changeset_manager
msgid "Changeset Configuration"
msgstr ""
#. module: partner_changeset
#: model:ir.model,name:partner_changeset.model_changeset_field_rule
msgid "Changeset Field Rules"
msgstr ""
#. module: partner_changeset
#: view:changeset.field.rule:partner_changeset.view_changeset_field_rule_form
#: view:changeset.field.rule:partner_changeset.view_changeset_field_rule_search
#: view:changeset.field.rule:partner_changeset.view_changeset_field_rule_tree
#: model:ir.actions.act_window,name:partner_changeset.action_changeset_field_rule_view
msgid "Changeset Fields Rules"
msgstr ""
#. module: partner_changeset
#: model:ir.ui.menu,name:partner_changeset.menu_res_partner_changeset
#: field:res.partner,changeset_ids:0
msgid "Changesets"
msgstr ""
#. module: partner_changeset
#: model:res.groups,name:partner_changeset.group_changeset_user
msgid "Changesets Validations "
msgstr ""
#. module: partner_changeset
#: field:changeset.field.rule,create_uid:0
#: field:res.partner.changeset,create_uid:0
#: field:res.partner.changeset.change,create_uid:0
msgid "Created by"
msgstr ""
#. module: partner_changeset
#: field:changeset.field.rule,create_date:0
#: field:res.partner.changeset,create_date:0
#: field:res.partner.changeset.change,create_date:0
msgid "Created on"
msgstr ""
#. module: partner_changeset
#: field:res.partner.changeset,date:0
msgid "Date"
msgstr ""
#. module: partner_changeset
#: view:res.partner.changeset:partner_changeset.view_res_partner_changeset_search
#: selection:res.partner.changeset,state:0
msgid "Done"
msgstr ""
#. module: partner_changeset
#: field:changeset.field.rule,field_id:0
#: field:res.partner.changeset.change,field_id:0
msgid "Field"
msgstr ""
#. module: partner_changeset
#: model:ir.ui.menu,name:partner_changeset.menu_changeset_field_rule
msgid "Field Rules"
msgstr ""
#. module: partner_changeset
#: view:res.partner.changeset:partner_changeset.view_res_partner_changeset_search
msgid "Group By"
msgstr ""
#. module: partner_changeset
#: field:changeset.field.rule,id:0
#: field:res.partner.changeset,id:0
#: field:res.partner.changeset.change,id:0
msgid "ID"
msgstr ""
#. module: partner_changeset
#: help:changeset.field.rule,source_model_id:0
msgid "If a source model is defined, the rule will be applied only when the change is made from this origin. Rules without source model are global and applies to all backends.\n"
"Rules with a source model have precedence over global rules, but if a field has no rule with a source model, the global rule is used."
msgstr ""
#. module: partner_changeset
#: field:changeset.field.rule,write_uid:0
#: field:res.partner.changeset,write_uid:0
#: field:res.partner.changeset.change,write_uid:0
msgid "Last Updated by"
msgstr ""
#. module: partner_changeset
#: field:changeset.field.rule,write_date:0
#: field:res.partner.changeset,write_date:0
#: field:res.partner.changeset.change,write_date:0
msgid "Last Updated on"
msgstr ""
#. module: partner_changeset
#: field:res.partner.changeset.change,new_value_boolean:0
#: field:res.partner.changeset.change,new_value_char:0
#: field:res.partner.changeset.change,new_value_date:0
#: field:res.partner.changeset.change,new_value_datetime:0
#: field:res.partner.changeset.change,new_value_float:0
#: field:res.partner.changeset.change,new_value_integer:0
#: field:res.partner.changeset.change,new_value_reference:0
#: field:res.partner.changeset.change,new_value_text:0
msgid "New"
msgstr ""
#. module: partner_changeset
#: field:res.partner.changeset,note:0
msgid "Note"
msgstr ""
#. module: partner_changeset
#: field:res.partner.changeset.change,old_value_boolean:0
#: field:res.partner.changeset.change,old_value_char:0
#: field:res.partner.changeset.change,old_value_date:0
#: field:res.partner.changeset.change,old_value_datetime:0
#: field:res.partner.changeset.change,old_value_float:0
#: field:res.partner.changeset.change,old_value_integer:0
#: field:res.partner.changeset.change,old_value_reference:0
#: field:res.partner.changeset.change,old_value_text:0
msgid "Old"
msgstr ""
#. module: partner_changeset
#: model:ir.model,name:partner_changeset.model_res_partner
#: view:res.partner.changeset:partner_changeset.view_res_partner_changeset_search
#: field:res.partner.changeset,partner_id:0
msgid "Partner"
msgstr ""
#. module: partner_changeset
#: model:ir.actions.act_window,name:partner_changeset.action_res_partner_changeset_view
#: model:ir.model,name:partner_changeset.model_res_partner_changeset
#: view:res.partner.changeset:partner_changeset.view_res_partner_changeset_form
#: view:res.partner.changeset:partner_changeset.view_res_partner_changeset_search
#: view:res.partner.changeset:partner_changeset.view_res_partner_changeset_tree
msgid "Partner Changeset"
msgstr ""
#. module: partner_changeset
#: model:ir.model,name:partner_changeset.model_res_partner_changeset_change
#: view:res.partner.changeset:partner_changeset.view_res_partner_changeset_form
#: view:res.partner.changeset.change:partner_changeset.view_res_partner_changeset_change_form
msgid "Partner Changeset Change"
msgstr ""
#. module: partner_changeset
#: model:ir.ui.menu,name:partner_changeset.menu_changeset
msgid "Partner Changesets"
msgstr ""
#. module: partner_changeset
#: view:res.partner.changeset:partner_changeset.view_res_partner_changeset_search
#: selection:res.partner.changeset,state:0
#: selection:res.partner.changeset.change,state:0
msgid "Pending"
msgstr ""
#. module: partner_changeset
#: view:res.partner:partner_changeset.view_res_partner_filter
msgid "Pending Changesets"
msgstr ""
#. module: partner_changeset
#: view:res.partner.changeset:partner_changeset.view_res_partner_changeset_form
msgid "Previous"
msgstr ""
#. module: partner_changeset
#: view:res.partner.changeset:partner_changeset.view_res_partner_changeset_form
#: view:res.partner.changeset.change:partner_changeset.view_res_partner_changeset_change_form
msgid "Reject"
msgstr ""
#. module: partner_changeset
#: view:res.partner.changeset:partner_changeset.view_res_partner_changeset_form
msgid "Reject pending changes"
msgstr ""
#. module: partner_changeset
#: selection:res.partner.changeset.change,state:0
msgid "Rejected"
msgstr ""
#. module: partner_changeset
#: field:changeset.field.rule,source_model_id:0
msgid "Source Model"
msgstr ""
#. module: partner_changeset
#: field:res.partner.changeset,source:0
msgid "Source of the change"
msgstr ""
#. module: partner_changeset
#: view:res.partner.changeset:partner_changeset.view_res_partner_changeset_search
#: field:res.partner.changeset,state:0
#: field:res.partner.changeset.change,state:0
msgid "State"
msgstr ""
#. module: partner_changeset
#: model:res.groups,comment:partner_changeset.group_changeset_user
msgid "The user will be able to apply or reject changesets."
msgstr ""
#. module: partner_changeset
#: model:res.groups,comment:partner_changeset.group_changeset_manager
msgid "The user will have an access to the configuration of the changeset rules."
msgstr ""
#. module: partner_changeset
#: code:addons/partner_changeset/models/res_partner_changeset.py:418
#, python-format
msgid "This change cannot be applied because a previous changeset for the same partner is pending.\n"
"Apply all the anterior changesets before applying this one."
msgstr ""
#. module: partner_changeset
#: code:addons/partner_changeset/models/res_partner_changeset.py:433
#, python-format
msgid "This change has already be applied."
msgstr ""

7
partner_changeset/models/__init__.py

@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
# © 2015 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from . import res_partner
from . import res_partner_changeset
from . import changeset_field_rule

144
partner_changeset/models/changeset_field_rule.py

@ -0,0 +1,144 @@
# -*- coding: utf-8 -*-
# © 2015 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openerp import models, fields, api
from openerp.tools.cache import ormcache
class ChangesetFieldRule(models.Model):
_name = 'changeset.field.rule'
_description = 'Changeset Field Rules'
_rec_name = 'field_id'
field_id = fields.Many2one(
comodel_name='ir.model.fields',
string='Field',
domain="[('model_id.model', '=', 'res.partner'), "
" ('ttype', 'in', ('char', 'selection', 'date', 'datetime', "
" 'float', 'integer', 'text', 'boolean', "
" 'many2one')), "
" ('readonly', '=', False)]",
ondelete='cascade',
required=True,
)
action = fields.Selection(
selection='_selection_action',
string='Action',
required=True,
help="Auto: always apply a change.\n"
"Validate: manually applied by an administrator.\n"
"Never: change never applied.",
)
source_model_id = fields.Many2one(
comodel_name='ir.model',
string='Source Model',
ondelete='cascade',
domain=lambda self: [('id', 'in', self._domain_source_models().ids)],
help="If a source model is defined, the rule will be applied only "
"when the change is made from this origin. "
"Rules without source model are global and applies to all "
"backends.\n"
"Rules with a source model have precedence over global rules, "
"but if a field has no rule with a source model, the global rule "
"is used."
)
_sql_constraints = [
('model_field_uniq',
'unique (source_model_id, field_id)',
'A rule already exists for this field.'),
]
@api.model
def _domain_source_models(self):
""" Returns the models for which we can define rules.
Example for submodules (replace by the xmlid of the model):
::
models = super(ChangesetFieldRule, self)._domain_source_models()
return models | self.env.ref('base.model_res_users')
Rules without model are global and apply for all models.
"""
return self.env.ref('base.model_res_users')
@api.model
def _selection_action(self):
return [('auto', 'Auto'),
('validate', 'Validate'),
('never', 'Never'),
]
@ormcache(skiparg=1)
@api.model
def _get_rules(self, source_model_name):
""" Cache rules
Keep only the id of the rules, because if we keep the recordsets
in the ormcache, we won't be able to browse them once their
cursor is closed.
The public method ``get_rules`` return the rules with the recordsets
when called.
"""
model_rules = self.search(
['|', ('source_model_id.model', '=', source_model_name),
('source_model_id', '=', False)],
# using 'ASC' means that 'NULLS LAST' is the default
order='source_model_id ASC',
)
# model's rules have precedence over global ones so we iterate
# over rules which have a source model first, then we complete
# them with the global rules
result = {}
for rule in model_rules:
# we already have a model's rule
if result.get(rule.field_id.name):
continue
result[rule.field_id.name] = rule.id
return result
@api.model
def get_rules(self, source_model_name):
""" Return the rules for a model
When a model is specified, it will return the rules for this
model. Fields that have no rule for this model will use the
global rules (those without source).
The source model is the model which ask for a change, it will be
for instance ``res.users``, ``lefac.backend`` or
``magellan.backend``.
The second argument (``source_model_name``) is optional but
cannot be an optional keyword argument otherwise it would not be
in the key for the cache. The callers have to pass ``None`` if
they want only global rules.
"""
rules = {}
cached_rules = self._get_rules(source_model_name)
for field, rule_id in cached_rules.iteritems():
rules[field] = self.browse(rule_id)
return rules
@api.model
def create(self, vals):
record = super(ChangesetFieldRule, self).create(vals)
self.clear_caches()
return record
@api.multi
def write(self, vals):
result = super(ChangesetFieldRule, self).write(vals)
self.clear_caches()
return result
@api.multi
def unlink(self):
result = super(ChangesetFieldRule, self).unlink()
self.clear_caches()
return result

50
partner_changeset/models/res_partner.py

@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
# © 2015 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openerp import models, fields, api
class ResPartner(models.Model):
_inherit = 'res.partner'
changeset_ids = fields.One2many(comodel_name='res.partner.changeset',
inverse_name='partner_id',
string='Changesets',
readonly=True)
count_pending_changesets = fields.Integer(
string='Changes',
compute='_count_pending_changesets',
search='_search_count_pending_changesets')
@api.one
@api.depends('changeset_ids', 'changeset_ids.state')
def _count_pending_changesets(self):
changesets = self.changeset_ids.filtered(
lambda rev: rev.state == 'draft' and rev.partner_id == self
)
self.count_pending_changesets = len(changesets)
@api.multi
def write(self, values):
if self.env.context.get('__no_changeset'):
return super(ResPartner, self).write(values)
else:
changeset_model = self.env['res.partner.changeset']
for record in self:
local_values = changeset_model.add_changeset(record, values)
super(ResPartner, record).write(local_values)
return True
def _search_count_pending_changesets(self, operator, value):
if operator not in ('=', '!=', '<', '<=', '>', '>=', 'in', 'not in'):
return []
query = ("SELECT p.id "
"FROM res_partner p "
"INNER JOIN res_partner_changeset r ON r.partner_id = p.id "
"WHERE r.state = 'draft' "
"GROUP BY p.id "
"HAVING COUNT(r.id) %s %%s ") % operator
self.env.cr.execute(query, (value,))
ids = [row[0] for row in self.env.cr.fetchall()]
return [('id', 'in', ids)]

514
partner_changeset/models/res_partner_changeset.py

@ -0,0 +1,514 @@
# -*- coding: utf-8 -*-
# © 2015 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from itertools import groupby
from lxml import etree
from operator import attrgetter
from openerp import models, fields, api, exceptions, _
from openerp.osv.orm import setup_modifiers
# sentinel object to be sure that no empty value was passed to
# ResPartnerChangesetChange._value_for_changeset
_NO_VALUE = object()
class ResPartnerChangeset(models.Model):
_name = 'res.partner.changeset'
_description = 'Partner Changeset'
_order = 'date desc'
_rec_name = 'date'
partner_id = fields.Many2one(comodel_name='res.partner',
string='Partner',
select=True,
required=True,
readonly=True,
ondelete='cascade')
change_ids = fields.One2many(comodel_name='res.partner.changeset.change',
inverse_name='changeset_id',
string='Changes',
readonly=True)
date = fields.Datetime(default=fields.Datetime.now,
select=True,
readonly=True)
state = fields.Selection(
compute='_compute_state',
selection=[('draft', 'Pending'),
('done', 'Done')],
string='State',
store=True,
)
note = fields.Text()
source = fields.Reference(
string='Source of the change',
selection='_reference_models',
readonly=True,
)
@api.model
def _reference_models(self):
models = self.env['ir.model'].search([])
return [(model.model, model.name) for model in models]
@api.one
@api.depends('change_ids', 'change_ids.state')
def _compute_state(self):
if all(change.state in ('done', 'cancel') for change
in self.mapped('change_ids')):
self.state = 'done'
else:
self.state = 'draft'
@api.multi
def apply(self):
self.mapped('change_ids').apply()
@api.multi
def cancel(self):
self.mapped('change_ids').cancel()
@api.multi
def add_changeset(self, record, values):
""" Add a changeset on a partner
By default, when a partner is modified by a user or by the
system, the the changeset will follow the rules configured for
the 'Users' / global rules.
A caller should pass the following keys in the context:
* ``__changeset_rules_source_model``: name of the model which
asks for the change
* ``__changeset_rules_source_id``: id of the record which asks
for the change
When the source model and id are not defined, the current user
is considered as the origin of the change.
Should be called before the execution of ``write`` on the record
so we can keep track of the existing value and also because the
returned values should be used for ``write`` as some of the
values may have been removed.
:param values: the values being written on the partner
:type values: dict
:returns: dict of values that should be wrote on the partner
(fields with a 'Validate' or 'Never' rule are excluded)
"""
record.ensure_one()
source_model = self.env.context.get('__changeset_rules_source_model')
source_id = self.env.context.get('__changeset_rules_source_id')
if not source_model:
# if the changes source is not defined, log the user who
# made the change
source_model = 'res.users'
if not source_id:
source_id = self.env.uid
if source_model and source_id:
source = '%s,%s' % (source_model, source_id)
else:
source = False
change_model = self.env['res.partner.changeset.change']
write_values = values.copy()
changes = []
rules = self.env['changeset.field.rule'].get_rules(
source_model_name=source_model,
)
for field in values:
rule = rules.get(field)
if not rule:
continue
if field in values:
if not change_model._has_field_changed(record, field,
values[field]):
continue
change, pop_value = change_model._prepare_changeset_change(
record, rule, field, values[field]
)
if pop_value:
write_values.pop(field)
changes.append(change)
if changes:
self.env['res.partner.changeset'].create({
'partner_id': record.id,
'change_ids': [(0, 0, vals) for vals in changes],
'date': fields.Datetime.now(),
'source': source,
})
return write_values
class ResPartnerChangesetChange(models.Model):
""" Store the change of one field for one changeset on one partner
This model is composed of 3 sets of fields:
* 'origin'
* 'old'
* 'new'
The 'new' fields contain the value that needs to be validated.
The 'old' field copies the actual value of the partner when the
change is either applied either canceled. This field is used as a storage
place but never shown by itself.
The 'origin' fields is a related field towards the actual values of
the partner until the change is either applied either canceled, past
that it shows the 'old' value.
The reason behind this is that the values may change on a partner between
the moment when the changeset is created and when it is applied.
On the views, we show the origin fields which represent the actual
partner values or the old values and we show the new fields.
The 'origin' and 'new_value_display' are displayed on
the tree view where we need a unique of field, the other fields are
displayed on the form view so we benefit from their widgets.
"""
_name = 'res.partner.changeset.change'
_description = 'Partner Changeset Change'
_rec_name = 'field_id'
changeset_id = fields.Many2one(comodel_name='res.partner.changeset',
required=True,
string='Changeset',
ondelete='cascade',
readonly=True)
field_id = fields.Many2one(comodel_name='ir.model.fields',
string='Field',
required=True,
readonly=True)
field_type = fields.Selection(related='field_id.ttype',
string='Field Type',
readonly=True)
origin_value_display = fields.Char(
string='Previous',
compute='_compute_value_display',
)
new_value_display = fields.Char(
string='New',
compute='_compute_value_display',
)
# Fields showing the origin partner's value or the 'old' value if
# the change is applied or canceled.
origin_value_char = fields.Char(compute='_compute_origin_values',
string='Previous',
readonly=True)
origin_value_date = fields.Date(compute='_compute_origin_values',
string='Previous',
readonly=True)
origin_value_datetime = fields.Datetime(compute='_compute_origin_values',
string='Previous',
readonly=True)
origin_value_float = fields.Float(compute='_compute_origin_values',
string='Previous',
readonly=True)
origin_value_integer = fields.Integer(compute='_compute_origin_values',
string='Previous',
readonly=True)
origin_value_text = fields.Text(compute='_compute_origin_values',
string='Previous',
readonly=True)
origin_value_boolean = fields.Boolean(compute='_compute_origin_values',
string='Previous',
readonly=True)
origin_value_reference = fields.Reference(
compute='_compute_origin_values',
string='Previous',
selection='_reference_models',
readonly=True,
)
# Fields storing the previous partner's values (saved when the
# changeset is applied)
old_value_char = fields.Char(string='Old',
readonly=True)
old_value_date = fields.Date(string='Old',
readonly=True)
old_value_datetime = fields.Datetime(string='Old',
readonly=True)
old_value_float = fields.Float(string='Old',
readonly=True)
old_value_integer = fields.Integer(string='Old',
readonly=True)
old_value_text = fields.Text(string='Old',
readonly=True)
old_value_boolean = fields.Boolean(string='Old',
readonly=True)
old_value_reference = fields.Reference(string='Old',
selection='_reference_models',
readonly=True)
# Fields storing the value applied on the partner
new_value_char = fields.Char(string='New',
readonly=True)
new_value_date = fields.Date(string='New',
readonly=True)
new_value_datetime = fields.Datetime(string='New',
readonly=True)
new_value_float = fields.Float(string='New',
readonly=True)
new_value_integer = fields.Integer(string='New',
readonly=True)
new_value_text = fields.Text(string='New',
readonly=True)
new_value_boolean = fields.Boolean(string='New',
readonly=True)
new_value_reference = fields.Reference(string='New',
selection='_reference_models',
readonly=True)
state = fields.Selection(
selection=[('draft', 'Pending'),
('done', 'Accepted'),
('cancel', 'Rejected'),
],
required=True,
default='draft',
readonly=True,
)
@api.model
def _reference_models(self):
models = self.env['ir.model'].search([])
return [(model.model, model.name) for model in models]
_suffix_to_types = {
'char': ('char', 'selection'),
'date': ('date',),
'datetime': ('datetime',),
'float': ('float',),
'integer': ('integer',),
'text': ('text',),
'boolean': ('boolean',),
'reference': ('many2one',),
}
_type_to_suffix = {ftype: suffix
for suffix, ftypes in _suffix_to_types.iteritems()
for ftype in ftypes}
_origin_value_fields = ['origin_value_%s' % suffix
for suffix in _suffix_to_types]
_old_value_fields = ['old_value_%s' % suffix
for suffix in _suffix_to_types]
_new_value_fields = ['new_value_%s' % suffix
for suffix in _suffix_to_types]
_value_fields = (_origin_value_fields +
_old_value_fields +
_new_value_fields)
@api.one
@api.depends('changeset_id.partner_id.*')
def _compute_origin_values(self):
field_name = self.get_field_for_type(self.field_id, 'origin')
if self.state == 'draft':
value = self.changeset_id.partner_id[self.field_id.name]
else:
old_field = self.get_field_for_type(self.field_id, 'old')
value = self[old_field]
setattr(self, field_name, value)
@api.one
@api.depends(lambda self: self._value_fields)
def _compute_value_display(self):
for prefix in ('origin', 'new'):
value = getattr(self, 'get_%s_value' % prefix)()
if self.field_id.ttype == 'many2one' and value:
value = value.display_name
setattr(self, '%s_value_display' % prefix, value)
@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)
if not field_type:
raise NotImplementedError(
'field type %s is not supported' % field_type
)
return '%s_value_%s' % (prefix, field_type)
@api.multi
def get_origin_value(self):
self.ensure_one()
field_name = self.get_field_for_type(self.field_id, 'origin')
return self[field_name]
@api.multi
def get_new_value(self):
self.ensure_one()
field_name = self.get_field_for_type(self.field_id, 'new')
return self[field_name]
@api.multi
def set_old_value(self):
""" Copy the value of the partner to the 'old' field """
for change in self:
# copy the existing partner's value for the history
old_value_for_write = self._value_for_changeset(
change.changeset_id.partner_id,
change.field_id.name
)
old_field_name = self.get_field_for_type(change.field_id, 'old')
change.write({old_field_name: old_value_for_write})
@api.multi
def apply(self):
""" Apply the change on the changeset's partner
It is optimized thus that it makes only one write on the partner
per changeset if many changes are applied at once.
"""
changes_ok = self.browse()
key = attrgetter('changeset_id')
for changeset, changes in groupby(self.sorted(key=key), key=key):
values = {}
partner = changeset.partner_id
for change in changes:
if change.state in ('cancel', 'done'):
continue
field = change.field_id
value_for_write = change._convert_value_for_write(
change.get_new_value()
)
values[field.name] = value_for_write
change.set_old_value()
changes_ok |= change
if not values:
continue
previous_changesets = self.env['res.partner.changeset'].search(
[('date', '<', changeset.date),
('state', '=', 'draft'),
('partner_id', '=', changeset.partner_id.id),
],
limit=1,
)
if previous_changesets:
raise exceptions.Warning(
_('This change cannot be applied because a previous '
'changeset for the same partner is pending.\n'
'Apply all the anterior changesets before applying '
'this one.')
)
partner.with_context(__no_changeset=True).write(values)
changes_ok.write({'state': 'done'})
@api.multi
def cancel(self):
""" Reject the change """
if any(change.state == 'done' for change in self):
raise exceptions.Warning(
_('This change has already be applied.')
)
self.set_old_value()
self.write({'state': 'cancel'})
@api.model
def _has_field_changed(self, record, field, value):
field_def = record._fields[field]
current_value = field_def.convert_to_write(record[field])
if not (current_value or value):
return False
return current_value != value
@api.multi
def _convert_value_for_write(self, value):
if not value:
return value
model = self.env[self.field_id.model_id.model]
model_field_def = model._fields[self.field_id.name]
return model_field_def.convert_to_write(value)
@api.model
def _value_for_changeset(self, record, field_name, value=_NO_VALUE):
""" Return a value from the record ready to write in a changeset field
:param record: modified record
:param field_name: name of the modified field
:param value: if no value is given, it is read from the record
"""
field_def = record._fields[field_name]
if value is _NO_VALUE:
# when the value is read from the record, we need to prepare
# it for the write (e.g. extract .id from a many2one record)
value = field_def.convert_to_write(record[field_name])
if field_def.type == 'many2one':
# store as 'reference'
comodel = field_def.comodel_name
return "%s,%s" % (comodel, value) if value else False
else:
return value
@api.multi
def _prepare_changeset_change(self, record, rule, field_name, value):
""" Prepare data for a changeset change
It returns a dict of the values to write on the changeset change
and a boolean that indicates if the value should be popped out
of the values to write on the model.
:returns: dict of values, boolean
"""
new_field_name = self.get_field_for_type(rule.field_id, 'new')
new_value = self._value_for_changeset(record, field_name, value=value)
change = {
new_field_name: new_value,
'field_id': rule.field_id.id,
}
if rule.action == 'auto':
change['state'] = 'done'
pop_value = False
elif rule.action == 'validate':
change['state'] = 'draft'
pop_value = True # change to apply manually
elif rule.action == 'never':
change['state'] = 'cancel'
pop_value = True # change never applied
if change['state'] in ('cancel', 'done'):
# 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')
# get values ready to write as expected by the changeset
# (for instance, a many2one is written in a reference
# field)
origin_value = self._value_for_changeset(record, field_name)
change[old_field_name] = origin_value
return change, pop_value
def fields_view_get(self, *args, **kwargs):
_super = super(ResPartnerChangesetChange, self)
result = _super.fields_view_get(*args, **kwargs)
if result['type'] != 'form':
return
doc = etree.XML(result['arch'])
for suffix, ftypes in self._suffix_to_types.iteritems():
for prefix in ('origin', 'old', 'new'):
field_name = '%s_value_%s' % (prefix, suffix)
field_nodes = doc.xpath("//field[@name='%s']" % field_name)
for node in field_nodes:
node.set(
'attrs',
"{'invisible': "
"[('field_type', 'not in', %s)]}" % (ftypes,)
)
setup_modifiers(node)
result['arch'] = etree.tostring(doc)
return result

10
partner_changeset/security/ir.model.access.csv

@ -0,0 +1,10 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_view_changeset_field_rule_partner_manager,changeset field rules for partner managers,model_changeset_field_rule,base.group_partner_manager,1,0,0,0
access_view_changeset_field_rule_user,changeset field rules for changeset users,model_changeset_field_rule,group_changeset_user,1,0,0,0
access_view_changeset_field_rule_manager,changeset field rules for changeset managers,model_changeset_field_rule,group_changeset_user,1,1,1,1
access_view_res_partner_changeset_partner_manager,changeset for partner managers,model_res_partner_changeset,base.group_partner_manager,1,0,1,0
access_view_res_partner_changeset_change_partner_manager,changeset change for partner managers,model_res_partner_changeset_change,base.group_partner_manager,1,0,1,0
access_view_res_partner_changeset_user,changeset for changeset users,model_res_partner_changeset,group_changeset_user,1,1,1,0
access_view_res_partner_changeset_change_user,changeset change for changeset users,model_res_partner_changeset_change,group_changeset_user,1,1,1,0
access_view_res_partner_changeset_manager,changeset for changeset managers,model_res_partner_changeset,group_changeset_manager,1,1,1,1
access_view_res_partner_changeset_change_manager,changeset change for changeset managers,model_res_partner_changeset_change,group_changeset_manager,1,1,1,1

29
partner_changeset/security/security.xml

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="group_changeset_manager" model="res.groups">
<field name="name">Changeset Configuration</field>
<field name="comment">The user will have an access to the configuration of the changeset rules.</field>
</record>
<record id="group_changeset_user" model="res.groups">
<field name="name">Changesets Validations </field>
<field name="comment">The user will be able to apply or reject changesets.</field>
</record>
</data>
<data noupdate="1">
<record id="group_changeset_manager" model="res.groups">
<field name="users" eval="[(4, ref('base.user_root'))]"/>
</record>
<record id="group_changeset_user" model="res.groups">
<field name="implied_ids" eval="[(4, ref('group_changeset_manager'))]"/>
<field name="users" eval="[(4, ref('base.user_root'))]"/>
</record>
</data>
</openerp>

BIN
partner_changeset/static/description/icon.png

After

Width: 128  |  Height: 128  |  Size: 9.2 KiB

BIN
partner_changeset/static/src/img/changeset.png

After

Width: 1252  |  Height: 577  |  Size: 85 KiB

BIN
partner_changeset/static/src/img/rules.png

After

Width: 996  |  Height: 557  |  Size: 60 KiB

6
partner_changeset/tests/__init__.py

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
from . import test_changeset_flow
from . import test_changeset_field_type
from . import test_changeset_origin
from . import test_changeset_field_rule

77
partner_changeset/tests/common.py

@ -0,0 +1,77 @@
# -*- coding: utf-8 -*-
# © 2015 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
class ChangesetMixin(object):
def assert_changeset(self, partner, expected_source, expected_changes):
""" Check if a changeset has been created according to expected values
The partner should have no prior changeset than the one created in the
test (so it has exactly 1 changeset).
The expected changes are tuples with (field, origin_value,
new_value, state)
:param partner: record of partner having a changeset
:param expected_changes: contains tuples with the changes
:type expected_changes: list(tuple))
"""
changeset = self.env['res.partner.changeset'].search(
[('partner_id', '=', partner.id)],
)
self.assertEqual(len(changeset), 1,
"1 changeset expected, got %s" % (changeset,))
self.assertEqual(changeset.source, expected_source)
changes = changeset.change_ids
missing = []
for expected_change in expected_changes:
for change in changes:
if (change.field_id,
change.get_origin_value(),
change.get_new_value(),
change.state) == expected_change:
changes -= change
break
else:
missing.append(expected_change)
message = u''
for field, origin_value, new_value, state in missing:
message += ("- field: '%s', origin_value: '%s', "
"new_value: '%s', state: '%s'\n" %
(field.name, origin_value, new_value, state))
for change in changes:
message += ("+ field: '%s', origin_value: '%s', "
"new_value: '%s', state: '%s'\n" %
(change.field_id.name,
change.get_origin_value(),
change.get_new_value(),
change.state))
if message:
raise AssertionError('Changes do not match\n\n:%s' % message)
def _create_changeset(self, partner, changes):
""" Create a changeset and its associated changes
:param partner: 'res.partner' record
:param changes: list of changes [(field, new value, state)]
:returns: 'res.partner.changeset' record
"""
ChangesetChange = self.env['res.partner.changeset.change']
get_field = ChangesetChange.get_field_for_type
change_values = []
for field, value, state in changes:
change = {
'field_id': field.id,
# write in the field of the appropriate type for the
# origin field (char, many2one, ...)
get_field(field, 'new'): value,
'state': state,
}
change_values.append((0, 0, change))
values = {
'partner_id': partner.id,
'change_ids': change_values,
}
return self.env['res.partner.changeset'].create(values)

95
partner_changeset/tests/test_changeset_field_rule.py

@ -0,0 +1,95 @@
# -*- coding: utf-8 -*-
#
#
# Authors: Guewen Baconnier
# Copyright 2015 Camptocamp SA
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#
from openerp.tests import common
class TestChangesetFieldRule(common.TransactionCase):
def setUp(self):
super(TestChangesetFieldRule, self).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')
def test_get_rules(self):
ChangesetFieldRule = self.env['changeset.field.rule']
ChangesetFieldRule.search([]).unlink()
rule1 = ChangesetFieldRule.create({
'field_id': self.field_name.id,
'action': 'validate',
})
rule2 = ChangesetFieldRule.create({
'field_id': self.field_street.id,
'action': 'never',
})
get_rules = ChangesetFieldRule.get_rules(None)
self.assertEqual(get_rules, {'name': rule1, 'street': rule2})
def test_get_rules_source(self):
ChangesetFieldRule = self.env['changeset.field.rule']
ChangesetFieldRule.search([]).unlink()
rule1 = ChangesetFieldRule.create({
'field_id': self.field_name.id,
'action': 'validate',
})
rule2 = ChangesetFieldRule.create({
'field_id': self.field_street.id,
'action': 'never',
})
rule3 = ChangesetFieldRule.create({
'source_model_id': self.company_model_id,
'field_id': self.field_street.id,
'action': 'never',
})
model = ChangesetFieldRule
rules = model.get_rules(None)
self.assertEqual(rules, {u'name': rule1, u'street': rule2})
rules = model.get_rules('res.company')
self.assertEqual(rules, {u'name': rule1, u'street': rule3})
def test_get_rules_cache(self):
ChangesetFieldRule = self.env['changeset.field.rule']
ChangesetFieldRule.search([]).unlink()
rule = ChangesetFieldRule.create({
'field_id': self.field_name.id,
'action': 'validate',
})
self.assertEqual(
ChangesetFieldRule.get_rules(None)['name'].action,
'validate',
)
# Write on cursor to bypass the cache invalidation for the
# matter of the test
self.env.cr.execute("UPDATE changeset_field_rule "
"SET action = 'never' "
"WHERE id = %s", (rule.id,))
self.assertEqual(
ChangesetFieldRule.get_rules(None)['name'].action,
'validate',
)
rule.action = 'auto'
self.assertEqual(
ChangesetFieldRule.get_rules(None)['name'].action,
'auto',
)
rule.unlink()
self.assertFalse(ChangesetFieldRule.get_rules(None))

279
partner_changeset/tests/test_changeset_field_type.py

@ -0,0 +1,279 @@
# -*- coding: utf-8 -*-
# © 2015 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openerp.tests import common
from .common import ChangesetMixin
class TestChangesetFieldType(ChangesetMixin, common.TransactionCase):
""" Check that changeset changes are stored expectingly to their types """
def _setup_rules(self):
ChangesetFieldRule = self.env['changeset.field.rule']
ChangesetFieldRule.search([]).unlink()
fields = (('char', 'ref'),
('text', 'comment'),
('boolean', 'customer'),
('date', 'date'),
('integer', 'color'),
('float', 'credit_limit'),
('selection', 'type'),
('many2one', 'country_id'),
('many2many', 'category_id'),
('one2many', 'user_ids'),
('binary', 'image'),
)
for field_type, field in fields:
attr_name = 'field_%s' % field_type
field_record = self.env['ir.model.fields'].search([
('model', '=', 'res.partner'),
('name', '=', 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)
ChangesetFieldRule.create({
'field_id': field_record.id,
'action': 'validate',
})
def setUp(self):
super(TestChangesetFieldType, self).setUp()
self._setup_rules()
self.partner = self.env['res.partner'].create({
'name': 'Original Name',
'street': 'Original Street',
})
def test_new_changeset_char(self):
""" Add a new changeset on a Char field """
self.partner.write({
self.field_char.name: 'New value',
})
self.assert_changeset(
self.partner,
self.env.user,
[(self.field_char, self.partner[self.field_char.name],
'New value', 'draft'),
]
)
def test_new_changeset_text(self):
""" Add a new changeset on a Text field """
self.partner.write({
self.field_text.name: 'New comment\non 2 lines',
})
self.assert_changeset(
self.partner,
self.env.user,
[(self.field_text, self.partner[self.field_text.name],
'New comment\non 2 lines', 'draft'),
]
)
def test_new_changeset_boolean(self):
""" Add a new changeset on a Boolean field """
# ensure the changeset has to change the value
self.partner.with_context(__no_changeset=True).write({
self.field_boolean.name: False,
})
self.partner.write({
self.field_boolean.name: True,
})
self.assert_changeset(
self.partner,
self.env.user,
[(self.field_boolean, self.partner[self.field_boolean.name],
True, 'draft'),
]
)
def test_new_changeset_date(self):
""" Add a new changeset on a Date field """
self.partner.write({
self.field_date.name: '2015-09-15',
})
self.assert_changeset(
self.partner,
self.env.user,
[(self.field_date, self.partner[self.field_date.name],
'2015-09-15', 'draft'),
]
)
def test_new_changeset_integer(self):
""" Add a new changeset on a Integer field """
self.partner.write({
self.field_integer.name: 42,
})
self.assert_changeset(
self.partner,
self.env.user,
[(self.field_integer, self.partner[self.field_integer.name],
42, 'draft'),
]
)
def test_new_changeset_float(self):
""" Add a new changeset on a Float field """
self.partner.write({
self.field_float.name: 3.1415,
})
self.assert_changeset(
self.partner,
self.env.user,
[(self.field_float, self.partner[self.field_float.name],
3.1415, 'draft'),
]
)
def test_new_changeset_selection(self):
""" Add a new changeset on a Selection field """
self.partner.write({
self.field_selection.name: 'delivery',
})
self.assert_changeset(
self.partner,
self.env.user,
[(self.field_selection, self.partner[self.field_selection.name],
'delivery', 'draft'),
]
)
def test_new_changeset_many2one(self):
""" Add a new changeset on a Many2one field """
self.partner.with_context(__no_changeset=True).write({
self.field_many2one.name: self.env.ref('base.fr').id,
})
self.partner.write({
self.field_many2one.name: self.env.ref('base.ch').id,
})
self.assert_changeset(
self.partner,
self.env.user,
[(self.field_many2one, self.partner[self.field_many2one.name],
self.env.ref('base.ch'), 'draft'),
]
)
def test_new_changeset_many2many(self):
""" Add a new changeset on a Many2many field is not supported """
with self.assertRaises(NotImplementedError):
self.partner.write({
self.field_many2many.name: [self.env.ref('base.ch').id],
})
def test_new_changeset_one2many(self):
""" Add a new changeset on a One2many field is not supported """
with self.assertRaises(NotImplementedError):
self.partner.write({
self.field_one2many.name: [self.env.ref('base.user_root').id],
})
def test_new_changeset_binary(self):
""" Add a new changeset on a Binary field is not supported """
with self.assertRaises(NotImplementedError):
self.partner.write({
self.field_binary.name: 'xyz',
})
def test_apply_char(self):
""" Apply a change on a Char field """
changes = [(self.field_char, 'New Ref', 'draft')]
changeset = self._create_changeset(self.partner, changes)
changeset.change_ids.apply()
self.assertEqual(self.partner[self.field_char.name], 'New Ref')
def test_apply_text(self):
""" Apply a change on a Text field """
changes = [(self.field_text, 'New comment\non 2 lines', 'draft')]
changeset = self._create_changeset(self.partner, changes)
changeset.change_ids.apply()
self.assertEqual(self.partner[self.field_text.name],
'New comment\non 2 lines')
def test_apply_boolean(self):
""" Apply a change on a Boolean field """
# ensure the changeset has to change the value
self.partner.write({self.field_boolean.name: False})
changes = [(self.field_boolean, True, 'draft')]
changeset = self._create_changeset(self.partner, changes)
changeset.change_ids.apply()
self.assertEqual(self.partner[self.field_boolean.name], True)
changes = [(self.field_boolean, False, 'draft')]
changeset = self._create_changeset(self.partner, changes)
changeset.change_ids.apply()
self.assertEqual(self.partner[self.field_boolean.name], False)
def test_apply_date(self):
""" Apply a change on a Date field """
changes = [(self.field_date, '2015-09-15', 'draft')]
changeset = self._create_changeset(self.partner, changes)
changeset.change_ids.apply()
self.assertAlmostEqual(self.partner[self.field_date.name],
'2015-09-15')
def test_apply_integer(self):
""" Apply a change on a Integer field """
changes = [(self.field_integer, 42, 'draft')]
changeset = self._create_changeset(self.partner, changes)
changeset.change_ids.apply()
self.assertAlmostEqual(self.partner[self.field_integer.name], 42)
def test_apply_float(self):
""" Apply a change on a Float field """
changes = [(self.field_float, 52.47, 'draft')]
changeset = self._create_changeset(self.partner, changes)
changeset.change_ids.apply()
self.assertAlmostEqual(self.partner[self.field_float.name], 52.47)
def test_apply_selection(self):
""" Apply a change on a Selection field """
changes = [(self.field_selection, 'delivery', 'draft')]
changeset = self._create_changeset(self.partner, changes)
changeset.change_ids.apply()
self.assertAlmostEqual(self.partner[self.field_selection.name],
'delivery')
def test_apply_many2one(self):
""" Apply a change on a Many2one field """
self.partner.with_context(__no_changeset=True).write({
self.field_many2one.name: self.env.ref('base.fr').id,
})
changes = [(self.field_many2one,
'res.country,%d' % self.env.ref('base.ch').id,
'draft')]
changeset = self._create_changeset(self.partner, changes)
changeset.change_ids.apply()
self.assertEqual(self.partner[self.field_many2one.name],
self.env.ref('base.ch'))
def test_apply_many2many(self):
""" Apply a change on a Many2many field is not supported """
changes = [(self.field_many2many,
self.env.ref('base.ch').id,
'draft')]
with self.assertRaises(NotImplementedError):
self._create_changeset(self.partner, changes)
def test_apply_one2many(self):
""" Apply a change on a One2many field is not supported """
changes = [(self.field_one2many,
[self.env.ref('base.user_root').id,
self.env.ref('base.user_demo').id,
],
'draft')]
with self.assertRaises(NotImplementedError):
self._create_changeset(self.partner, changes)
def test_apply_binary(self):
""" Apply a change on a Binary field is not supported """
changes = [(self.field_one2many, '', 'draft')]
with self.assertRaises(NotImplementedError):
self._create_changeset(self.partner, changes)

246
partner_changeset/tests/test_changeset_flow.py

@ -0,0 +1,246 @@
# -*- coding: utf-8 -*-
# © 2015 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from datetime import datetime, timedelta
from openerp import fields, exceptions
from openerp.tests import common
from .common import ChangesetMixin
class TestChangesetFlow(ChangesetMixin, common.TransactionCase):
""" Check how changeset are generated and applied based on the rules.
We do not really care about the types of the fields in this test
suite, so we only use 'char' fields. We have to ensure that the
general changeset flows work as expected, that is:
* create a changeset when a manual/system write is made on partner
* create a changeset according to the changeset rules when a source model
is specified
* apply a changeset change writes the value on the partner
* apply a whole changeset writes all the changes' values on the partner
* changes in state 'cancel' or 'done' do not write on the partner
* when all the changes are either 'cancel' or 'done', the changeset
becomes 'done'
"""
def _setup_rules(self):
ChangesetFieldRule = self.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',
})
ChangesetFieldRule.create({
'field_id': self.field_street.id,
'action': 'validate',
})
ChangesetFieldRule.create({
'field_id': self.field_street2.id,
'action': 'never',
})
def setUp(self):
super(TestChangesetFlow, self).setUp()
self._setup_rules()
self.partner = self.env['res.partner'].create({
'name': 'X',
'street': 'street X',
'street2': 'street2 X',
})
def test_new_changeset(self):
""" Add a new changeset on a partner
A new changeset is created when we write on a partner
"""
self.partner.write({
'name': 'Y',
'street': 'street Y',
'street2': 'street2 Y',
})
self.assert_changeset(
self.partner,
self.env.user,
[(self.field_name, 'X', 'Y', 'done'),
(self.field_street, 'street X', 'street Y', 'draft'),
(self.field_street2, 'street2 X', 'street2 Y', 'cancel'),
],
)
self.assertEqual(self.partner.name, 'Y')
self.assertEqual(self.partner.street, 'street X')
self.assertEqual(self.partner.street2, 'street2 X')
def test_new_changeset_empty_value(self):
""" Create a changeset change that empty a value """
self.partner.write({
'street': False,
})
self.assert_changeset(
self.partner,
self.env.user,
[(self.field_street, 'street X', False, 'draft')]
)
def test_no_changeset_empty_value_both_sides(self):
""" No changeset created when both sides have an empty value """
# we have to ensure that even if we write '' to a False field, we won't
# write a changeset
self.partner.with_context(__no_changeset=True).write({
'street': False,
})
self.partner.write({
'street': '',
})
self.assertFalse(self.partner.changeset_ids)
def test_apply_change(self):
""" Apply a changeset change on a partner """
changes = [
(self.field_name, 'Y', 'draft'),
]
changeset = self._create_changeset(self.partner, changes)
changeset.change_ids.apply()
self.assertEqual(self.partner.name, 'Y')
self.assertEqual(changeset.change_ids.state, 'done')
def test_apply_done_change(self):
""" Done changes do not apply (already applied) """
changes = [
(self.field_name, 'Y', 'done'),
]
changeset = self._create_changeset(self.partner, changes)
changeset.change_ids.apply()
self.assertEqual(self.partner.name, 'X')
def test_apply_cancel_change(self):
""" Cancel changes do not apply """
changes = [
(self.field_name, 'Y', 'cancel'),
]
changeset = self._create_changeset(self.partner, changes)
changeset.change_ids.apply()
self.assertEqual(self.partner.name, 'X')
def test_apply_empty_value(self):
""" Apply a change that empty a value """
changes = [
(self.field_street, False, 'draft'),
]
changeset = self._create_changeset(self.partner, changes)
changeset.change_ids.apply()
self.assertFalse(self.partner.street)
def test_apply_change_loop(self):
""" Test @api.multi on the changes """
changes = [
(self.field_name, 'Y', 'draft'),
(self.field_street, 'street Y', 'draft'),
(self.field_street2, 'street2 Y', 'draft'),
]
changeset = self._create_changeset(self.partner, changes)
changeset.change_ids.apply()
self.assertEqual(self.partner.name, 'Y')
self.assertEqual(self.partner.street, 'street Y')
self.assertEqual(self.partner.street2, 'street2 Y')
def test_apply(self):
""" Apply a full changeset on a partner """
changes = [
(self.field_name, 'Y', 'draft'),
(self.field_street, 'street Y', 'draft'),
(self.field_street2, 'street2 Y', 'draft'),
]
changeset = self._create_changeset(self.partner, changes)
changeset.apply()
self.assertEqual(self.partner.name, 'Y')
self.assertEqual(self.partner.street, 'street Y')
self.assertEqual(self.partner.street2, 'street2 Y')
def test_changeset_state_on_done(self):
""" Check that changeset state becomes done when changes are done """
changes = [(self.field_name, 'Y', 'draft')]
changeset = self._create_changeset(self.partner, changes)
self.assertEqual(changeset.state, 'draft')
changeset.change_ids.apply()
self.assertEqual(changeset.state, 'done')
def test_changeset_state_on_cancel(self):
""" Check that rev. state becomes done when changes are canceled """
changes = [(self.field_name, 'Y', 'draft')]
changeset = self._create_changeset(self.partner, changes)
self.assertEqual(changeset.state, 'draft')
changeset.change_ids.cancel()
self.assertEqual(changeset.state, 'done')
def test_changeset_state(self):
""" Check that changeset state becomes done with multiple changes """
changes = [
(self.field_name, 'Y', 'draft'),
(self.field_street, 'street Y', 'draft'),
(self.field_street2, 'street2 Y', 'draft'),
]
changeset = self._create_changeset(self.partner, changes)
self.assertEqual(changeset.state, 'draft')
changeset.apply()
self.assertEqual(changeset.state, 'done')
def test_apply_changeset_with_other_pending(self):
""" Error when applying when previous pending changesets exist """
changes = [(self.field_name, 'Y', 'draft')]
old_changeset = self._create_changeset(self.partner, changes)
# if the date is the same, both changeset can be applied
to_string = fields.Datetime.to_string
old_changeset.date = to_string(datetime.now() - timedelta(days=1))
changes = [(self.field_name, 'Z', 'draft')]
changeset = self._create_changeset(self.partner, changes)
with self.assertRaises(exceptions.Warning):
changeset.change_ids.apply()
def test_apply_different_changesets(self):
""" Apply different changesets at once """
partner2 = self.env['res.partner'].create({'name': 'P2'})
changes = [
(self.field_name, 'Y', 'draft'),
(self.field_street, 'street Y', 'draft'),
(self.field_street2, 'street2 Y', 'draft'),
]
changeset = self._create_changeset(self.partner, changes)
changeset2 = self._create_changeset(partner2, changes)
self.assertEqual(changeset.state, 'draft')
self.assertEqual(changeset2.state, 'draft')
(changeset + changeset2).apply()
self.assertEqual(self.partner.name, 'Y')
self.assertEqual(self.partner.street, 'street Y')
self.assertEqual(self.partner.street2, 'street2 Y')
self.assertEqual(partner2.name, 'Y')
self.assertEqual(partner2.street, 'street Y')
self.assertEqual(partner2.street2, 'street2 Y')
self.assertEqual(changeset.state, 'done')
self.assertEqual(changeset2.state, 'done')
def test_new_changeset_source(self):
""" Source is the user who made the change """
self.partner.write({
'street': False,
})
changeset = self.partner.changeset_ids
self.assertEqual(changeset.source, self.env.user)
def test_new_changeset_source_other_model(self):
""" Define source from another model """
company = self.env.ref('base.main_company')
keys = {
'__changeset_rules_source_model': 'res.company',
'__changeset_rules_source_id': company.id,
}
self.partner.with_context(**keys).write({
'street': False,
})
changeset = self.partner.changeset_ids
self.assertEqual(changeset.source, company)

110
partner_changeset/tests/test_changeset_origin.py

@ -0,0 +1,110 @@
# -*- coding: utf-8 -*-
# © 2015 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openerp.tests import common
from .common import ChangesetMixin
class TestChangesetOrigin(ChangesetMixin, common.TransactionCase):
""" Check that origin - old fields are stored as expected.
'origin' fields dynamically read fields from the partner when the state
of the change is 'draft'. Once a change becomes 'done' or 'cancel', the
'old' field copies the value from the partner and then the 'origin' field
displays the 'old' value.
"""
def _setup_rules(self):
ChangesetFieldRule = self.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',
})
def setUp(self):
super(TestChangesetOrigin, self).setUp()
self._setup_rules()
self.partner = self.env['res.partner'].create({
'name': 'X',
})
def test_origin_value_of_change_with_apply(self):
""" Origin field is read from the parter or 'old' - with apply
According to the state of the change.
"""
self.partner.write({
'name': 'Y',
})
changeset = self.partner.changeset_ids
change = changeset.change_ids
self.assertEqual(self.partner.name, 'X')
self.assertEqual(change.origin_value_char, 'X')
self.assertEqual(change.origin_value_display, 'X')
self.partner.with_context(__no_changeset=True).write({'name': 'A'})
self.assertEqual(change.origin_value_char, 'A')
self.assertEqual(change.origin_value_display, 'A')
change.apply()
self.assertEqual(change.origin_value_char, 'A')
self.assertEqual(change.origin_value_display, 'A')
self.partner.with_context(__no_changeset=True).write({'name': 'B'})
self.assertEqual(change.origin_value_char, 'A')
self.assertEqual(change.origin_value_display, 'A')
def test_origin_value_of_change_with_cancel(self):
""" Origin field is read from the parter or 'old' - with cancel
According to the state of the change.
"""
self.partner.write({
'name': 'Y',
})
changeset = self.partner.changeset_ids
change = changeset.change_ids
self.assertEqual(self.partner.name, 'X')
self.assertEqual(change.origin_value_char, 'X')
self.assertEqual(change.origin_value_display, 'X')
self.partner.with_context(__no_changeset=True).write({'name': 'A'})
self.assertEqual(change.origin_value_char, 'A')
self.assertEqual(change.origin_value_display, 'A')
change.cancel()
self.assertEqual(change.origin_value_char, 'A')
self.assertEqual(change.origin_value_display, 'A')
self.partner.with_context(__no_changeset=True).write({'name': 'B'})
self.assertEqual(change.origin_value_char, 'A')
self.assertEqual(change.origin_value_display, 'A')
def test_old_field_of_change_with_apply(self):
""" Old field is stored when the change is applied """
self.partner.write({
'name': 'Y',
})
changeset = self.partner.changeset_ids
change = changeset.change_ids
self.assertEqual(self.partner.name, 'X')
self.assertFalse(change.old_value_char)
self.partner.with_context(__no_changeset=True).write({'name': 'A'})
self.assertFalse(change.old_value_char)
change.apply()
self.assertEqual(change.old_value_char, 'A')
self.partner.with_context(__no_changeset=True).write({'name': 'B'})
self.assertEqual(change.old_value_char, 'A')
def test_old_field_of_change_with_cancel(self):
""" Old field is stored when the change is canceled """
self.partner.write({
'name': 'Y',
})
changeset = self.partner.changeset_ids
change = changeset.change_ids
self.assertEqual(self.partner.name, 'X')
self.assertFalse(change.old_value_char)
self.partner.with_context(__no_changeset=True).write({'name': 'A'})
self.assertFalse(change.old_value_char)
change.cancel()
self.assertEqual(change.old_value_char, 'A')
self.partner.with_context(__no_changeset=True).write({'name': 'B'})
self.assertEqual(change.old_value_char, 'A')

63
partner_changeset/views/changeset_field_rule_views.xml

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="0">
<record id="view_changeset_field_rule_tree" model="ir.ui.view">
<field name="name">changeset.field.rule.tree</field>
<field name="model">changeset.field.rule</field>
<field name="arch" type="xml">
<tree string="Changeset Fields Rules">
<field name="field_id"/>
<field name="source_model_id"/>
<field name="action"/>
</tree>
</field>
</record>
<record id="view_changeset_field_rule_form" model="ir.ui.view">
<field name="name">changeset.field.rule.form</field>
<field name="model">changeset.field.rule</field>
<field name="arch" type="xml">
<form string="Changeset Fields Rules">
<sheet string="Changeset Fields Rules">
<group>
<field name="field_id"
options="{'no_create_edit': True, 'no_open': True}"
/>
<field name="action"/>
<field name="source_model_id"
widget="selection" />
</group>
</sheet>
</form>
</field>
</record>
<record id="view_changeset_field_rule_search" model="ir.ui.view">
<field name="name">changeset.field.rule.search</field>
<field name="model">changeset.field.rule</field>
<field name="arch" type="xml">
<search string="Changeset Fields Rules">
<field name="field_id"/>
<field name="source_model_id"/>
<field name="action"/>
</search>
</field>
</record>
<record model="ir.actions.act_window" id="action_changeset_field_rule_view">
<field name="name">Changeset Fields Rules</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">changeset.field.rule</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="search_view_id" ref="view_changeset_field_rule_search"/>
</record>
<menuitem id="menu_changeset_field_rule"
parent="menu_changeset"
name="Field Rules"
groups="group_changeset_manager"
sequence="20"
action="action_changeset_field_rule_view"/>
</data>
</openerp>

10
partner_changeset/views/menu.xml

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="0">
<menuitem id="menu_changeset"
name="Partner Changesets"
groups="group_changeset_user"
parent="base.menu_base_config"
sequence="20"/>
</data>
</openerp>

159
partner_changeset/views/res_partner_changeset_views.xml

@ -0,0 +1,159 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="0">
<record id="view_res_partner_changeset_tree" model="ir.ui.view">
<field name="name">res.partner.changeset.tree</field>
<field name="model">res.partner.changeset</field>
<field name="arch" type="xml">
<tree string="Partner Changeset" delete="false" create="false">
<field name="partner_id"/>
<field name="date"/>
<field name="state"/>
</tree>
</field>
</record>
<record id="view_res_partner_changeset_form" model="ir.ui.view">
<field name="name">res.partner.changeset.form</field>
<field name="model">res.partner.changeset</field>
<field name="arch" type="xml">
<form string="Partner Changeset" delete="false" create="false">
<header>
<button name="apply"
string="Apply pending changes" type="object"
class="oe_highlight"
states="draft"/>
<button name="cancel"
string="Reject pending changes" type="object"
class="oe_highlight"
states="draft"/>
<field name="state" widget="statusbar"
statusbar_visible="draft,done"/>
</header>
<sheet string="Partner Changeset">
<group>
<field name="partner_id"/>
<field name="source"/>
<field name="date"/>
</group>
<group string="Changes">
<field name="change_ids" nolabel="1">
<tree string="Partner Changeset Change">
<field name="field_id" context="{'no_open': true}"/>
<field name="field_type" invisible="1"/>
<field name="origin_value_display" string="Previous"/>
<field name="new_value_display"/>
<field name="state"/>
<button name="apply"
string="Apply" type="object"
icon="gtk-apply"
states="draft"/>
<button name="cancel"
string="Reject" type="object"
icon="gtk-close"
states="draft"/>
</tree>
</field>
</group>
<group>
<field name="note"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_res_partner_changeset_change_form" model="ir.ui.view">
<field name="name">res.partner.changeset.change.form</field>
<field name="model">res.partner.changeset.change</field>
<field name="arch" type="xml">
<form string="Partner Changeset Change" delete="false" create="false">
<header>
<button name="apply"
string="Apply" type="object"
class="oe_highlight"
states="draft"/>
<button name="cancel"
string="Reject" type="object"
class="oe_highlight"
states="draft"/>
<field name="state" widget="statusbar"
statusbar_visible="draft,done"/>
</header>
<sheet>
<group>
<field name="field_id" options="{'no_open': true}"/>
<field name="field_type" invisible="1"/>
<!-- attrs are added in fields_view_get -->
<field name="origin_value_char"/>
<field name="new_value_char"/>
<field name="origin_value_date"/>
<field name="new_value_date"/>
<field name="origin_value_datetime"/>
<field name="new_value_datetime"/>
<field name="origin_value_float"/>
<field name="new_value_float"/>
<field name="origin_value_integer"/>
<field name="new_value_integer"/>
<field name="origin_value_text"/>
<field name="new_value_text"/>
<field name="origin_value_boolean"/>
<field name="new_value_boolean"/>
<field name="origin_value_reference"/>
<field name="new_value_reference"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_res_partner_changeset_search" model="ir.ui.view">
<field name="name">res.partner.changeset.search</field>
<field name="model">res.partner.changeset</field>
<field name="arch" type="xml">
<search string="Partner Changeset">
<field name="partner_id"/>
<filter string="Pending" name="draft"
domain="[('state','=','draft')]"/>
<filter string="Done" name="done"
domain="[('state','=','done')]"/>
<group expand="0" string="Group By">
<filter string="Partner"
name="groupby_partner_id"
context="{'group_by': 'partner_id'}"/>
<filter string="State"
name="groupby_state"
context="{'group_by': 'state'}"/>
</group>
</search>
</field>
</record>
<record model="ir.actions.act_window" id="action_res_partner_changeset_view">
<field name="name">Partner Changeset</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">res.partner.changeset</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="context">{'search_default_draft': 1}</field>
<field name="search_view_id" ref="view_res_partner_changeset_search"/>
</record>
<menuitem id="menu_res_partner_changeset"
parent="menu_changeset"
sequence="20"
name="Changesets"
action="action_res_partner_changeset_view"/>
</data>
</openerp>

41
partner_changeset/views/res_partner_views.xml

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="0">
<record id="res_partner_view_buttons" model="ir.ui.view">
<field name="name">res.partner.view.buttons</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form" />
<field name="priority" eval="18"/>
<field name="groups_id" eval="[(4, ref('partner_changeset.group_changeset_user'))]"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='buttons']" position="inside">
<button class="oe_inline oe_stat_button"
type="action"
name="%(partner_changeset.action_res_partner_changeset_view)d"
context="{'search_default_draft': 1, 'search_default_partner_id': active_id}"
icon="fa-code-fork">
<field string="Changes"
name="count_pending_changesets"
widget="statinfo"/>
</button>
</xpath>
</field>
</record>
<record id="view_res_partner_filter" model="ir.ui.view">
<field name="name">res.partner.select</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_res_partner_filter" />
<field name="arch" type="xml">
<filter name="customer" position="after">
<separator/>
<filter string="Pending Changesets"
name="pending_changesets"
domain="[('count_pending_changesets', '>', 0)]"/>
</filter>
</field>
</record>
</data>
</openerp>
Loading…
Cancel
Save