diff --git a/partner_changeset/README.rst b/partner_changeset/README.rst new file mode 100644 index 000000000..8b9ab712b --- /dev/null +++ b/partner_changeset/README.rst @@ -0,0 +1,168 @@ +.. 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/9.0 + +Known issues / Roadmap +====================== + +* Only a subset of the type of fields is actually supported + +Bug Tracker +=========== + +Bugs are tracked on `GitHub 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 + +Credits +======= + +Images +------ + +* Odoo Community Association: `Icon `_. + +Contributors +------------ + +* Guewen Baconnier +* Denis Leemann + +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. diff --git a/partner_changeset/__init__.py b/partner_changeset/__init__.py new file mode 100644 index 000000000..a77a6fcbc --- /dev/null +++ b/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 diff --git a/partner_changeset/__openerp__.py b/partner_changeset/__openerp__.py new file mode 100644 index 000000000..45b977d58 --- /dev/null +++ b/partner_changeset/__openerp__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# © 2015-2016 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +{'name': 'Partner Changesets', + 'version': '9.0.1.0.0', + 'author': 'Camptocamp, Odoo Community Association (OCA)', + 'license': 'AGPL-3', + 'category': 'Sales Management', + 'depends': ['base', + 'sale', + ], + '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, + } diff --git a/partner_changeset/demo/changeset_field_rule.xml b/partner_changeset/demo/changeset_field_rule.xml new file mode 100644 index 000000000..6b69f84a4 --- /dev/null +++ b/partner_changeset/demo/changeset_field_rule.xml @@ -0,0 +1,49 @@ + + + + + + auto + + + + + validate + + + + + validate + + + + + validate + + + + + validate + + + + + never + + + + + validate + + + + + validate + + + + + validate + + + diff --git a/partner_changeset/i18n/es.po b/partner_changeset/i18n/es.po new file mode 100644 index 000000000..8dda7f44b --- /dev/null +++ b/partner_changeset/i18n/es.po @@ -0,0 +1,324 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * partner_changeset +# +# Translators: +# Oihane Crucelaegui , 2016 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-07-03 07:56+0000\n" +"PO-Revision-Date: 2016-07-03 07:56+0000\n" +"Last-Translator: Oihane Crucelaegui , 2016\n" +"Language-Team: Spanish (https://www.transifex.com/oca/teams/23907/es/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: es\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\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:partner_changeset.res_partner_view_buttons +#: field:res.partner,count_pending_changesets:0 +#: view:res.partner.changeset:partner_changeset.view_res_partner_changeset_form +#: 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 +#: field:changeset.field.rule,display_name:0 +#: field:res.partner.changeset,display_name:0 +#: field:res.partner.changeset.change,display_name:0 +msgid "Display Name" +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 +#: field:res.partner.changeset.change,field_type:0 +msgid "Field Type" +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 "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 "" + +#. module: partner_changeset +#: field:changeset.field.rule,__last_update:0 +#: field:res.partner.changeset,__last_update:0 +#: field:res.partner.changeset.change,__last_update:0 +msgid "Last Modified on" +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_display: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 "Empresa" + +#. 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 +#: field:res.partner.changeset.change,origin_value_boolean:0 +#: field:res.partner.changeset.change,origin_value_char:0 +#: field:res.partner.changeset.change,origin_value_date:0 +#: field:res.partner.changeset.change,origin_value_datetime:0 +#: field:res.partner.changeset.change,origin_value_display:0 +#: field:res.partner.changeset.change,origin_value_float:0 +#: field:res.partner.changeset.change,origin_value_integer:0 +#: field:res.partner.changeset.change,origin_value_reference:0 +#: field:res.partner.changeset.change,origin_value_text:0 +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 "Estado" + +#. 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:401 +#, 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:416 +#, python-format +msgid "This change has already be applied." +msgstr "" diff --git a/partner_changeset/i18n/fr.po b/partner_changeset/i18n/fr.po new file mode 100644 index 000000000..77ead0c56 --- /dev/null +++ b/partner_changeset/i18n/fr.po @@ -0,0 +1,333 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * partner_changeset +# +# Translators: +# OCA Transbot , 2016 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-07-03 00:45+0000\n" +"PO-Revision-Date: 2016-07-03 00:45+0000\n" +"Last-Translator: OCA Transbot , 2016\n" +"Language-Team: French (https://www.transifex.com/oca/teams/23907/fr/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: fr\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\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:partner_changeset.res_partner_view_buttons +#: field:res.partner,count_pending_changesets:0 +#: view:res.partner.changeset:partner_changeset.view_res_partner_changeset_form +#: 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 +#: field:changeset.field.rule,display_name:0 +#: field:res.partner.changeset,display_name:0 +#: field:res.partner.changeset.change,display_name:0 +msgid "Display Name" +msgstr "" + +#. 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 +#: field:res.partner.changeset.change,field_type:0 +msgid "Field Type" +msgstr "" + +#. 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,__last_update:0 +#: field:res.partner.changeset,__last_update:0 +#: field:res.partner.changeset.change,__last_update:0 +msgid "Last Modified on" +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_display: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 +#: field:res.partner.changeset.change,origin_value_boolean:0 +#: field:res.partner.changeset.change,origin_value_char:0 +#: field:res.partner.changeset.change,origin_value_date:0 +#: field:res.partner.changeset.change,origin_value_datetime:0 +#: field:res.partner.changeset.change,origin_value_display:0 +#: field:res.partner.changeset.change,origin_value_float:0 +#: field:res.partner.changeset.change,origin_value_integer:0 +#: field:res.partner.changeset.change,origin_value_reference:0 +#: field:res.partner.changeset.change,origin_value_text:0 +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:401 +#, 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:416 +#, python-format +msgid "This change has already be applied." +msgstr "The changement est déjà appliqué." diff --git a/partner_changeset/i18n/partner_changeset.pot b/partner_changeset/i18n/partner_changeset.pot new file mode 100644 index 000000000..419ae81c0 --- /dev/null +++ b/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 "" + diff --git a/partner_changeset/i18n/sl.po b/partner_changeset/i18n/sl.po new file mode 100644 index 000000000..951b03fa0 --- /dev/null +++ b/partner_changeset/i18n/sl.po @@ -0,0 +1,331 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * partner_changeset +# +# Translators: +# Matjaž Mozetič , 2016 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-07-03 07:56+0000\n" +"PO-Revision-Date: 2016-07-03 07:56+0000\n" +"Last-Translator: Matjaž Mozetič , 2016\n" +"Language-Team: Slovenian (https://www.transifex.com/oca/teams/23907/sl/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: sl\n" +"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);\n" + +#. module: partner_changeset +#: sql_constraint:changeset.field.rule:0 +msgid "A rule already exists for this field." +msgstr "Za to polje že obstaja pravilo." + +#. module: partner_changeset +#: selection:res.partner.changeset.change,state:0 +msgid "Accepted" +msgstr "Sprejeto" + +#. module: partner_changeset +#: field:changeset.field.rule,action:0 +msgid "Action" +msgstr "Ukrep" + +#. 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 "Uveljavi" + +#. module: partner_changeset +#: view:res.partner.changeset:partner_changeset.view_res_partner_changeset_form +msgid "Apply pending changes" +msgstr "Uveljavi čakajoče spremembe" + +#. 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 "" +"Samodejno: vedno uveljavi spremembo.\n" +"Overi: ročno uveljavi administrator.\n" +"Nikoli: sprememba ni nikoli uveljavljena." + +#. module: partner_changeset +#: view:res.partner:partner_changeset.res_partner_view_buttons +#: field:res.partner,count_pending_changesets:0 +#: view:res.partner.changeset:partner_changeset.view_res_partner_changeset_form +#: field:res.partner.changeset,change_ids:0 +msgid "Changes" +msgstr "Spremembe" + +#. module: partner_changeset +#: field:res.partner.changeset.change,changeset_id:0 +msgid "Changeset" +msgstr "Niz sprememb" + +#. module: partner_changeset +#: model:res.groups,name:partner_changeset.group_changeset_manager +msgid "Changeset Configuration" +msgstr "Nastavitve niza sprememb" + +#. module: partner_changeset +#: model:ir.model,name:partner_changeset.model_changeset_field_rule +msgid "Changeset Field Rules" +msgstr "Pravila polja niza sprememb" + +#. 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 "Pravila polj niza sprememb" + +#. module: partner_changeset +#: model:ir.ui.menu,name:partner_changeset.menu_res_partner_changeset +#: field:res.partner,changeset_ids:0 +msgid "Changesets" +msgstr "Nizi sprememb" + +#. module: partner_changeset +#: model:res.groups,name:partner_changeset.group_changeset_user +msgid "Changesets Validations " +msgstr "Overitve nizov sprememb" + +#. 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 "Ustvaril" + +#. 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 "Ustvarjeno" + +#. module: partner_changeset +#: field:res.partner.changeset,date:0 +msgid "Date" +msgstr "Datum" + +#. module: partner_changeset +#: field:changeset.field.rule,display_name:0 +#: field:res.partner.changeset,display_name:0 +#: field:res.partner.changeset.change,display_name:0 +msgid "Display Name" +msgstr "Prikazni naziv" + +#. module: partner_changeset +#: view:res.partner.changeset:partner_changeset.view_res_partner_changeset_search +#: selection:res.partner.changeset,state:0 +msgid "Done" +msgstr "Opravljeno" + +#. module: partner_changeset +#: field:changeset.field.rule,field_id:0 +#: field:res.partner.changeset.change,field_id:0 +msgid "Field" +msgstr "Polje" + +#. module: partner_changeset +#: model:ir.ui.menu,name:partner_changeset.menu_changeset_field_rule +msgid "Field Rules" +msgstr "Pravila polja" + +#. module: partner_changeset +#: field:res.partner.changeset.change,field_type:0 +msgid "Field Type" +msgstr "Tip polja" + +#. module: partner_changeset +#: view:res.partner.changeset:partner_changeset.view_res_partner_changeset_search +msgid "Group By" +msgstr "Združi po" + +#. 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 "" +"Če je izvorni model določen, se pravilo uveljavi le za spremembe iz tega izvora. Pravila brez izvornega modela so globalna in se uveljavijo na vseh ozadjih.\n" +"Pravila izvornega modela imajo prednost pred globalnimi pravili, a če polje nima pravila z izvornim modelom, se uporabi globalno pravilo." + +#. module: partner_changeset +#: field:changeset.field.rule,__last_update:0 +#: field:res.partner.changeset,__last_update:0 +#: field:res.partner.changeset.change,__last_update:0 +msgid "Last Modified on" +msgstr "Zadnjič spremenjeno" + +#. 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 "Zadnji posodobil" + +#. 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 "Zadnjič posodobljeno" + +#. 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_display: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 "Novo" + +#. module: partner_changeset +#: field:res.partner.changeset,note:0 +msgid "Note" +msgstr "Opomba" + +#. 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 "Staro" + +#. 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 "Partner" + +#. 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 "Niz sprememb partnerja" + +#. 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 "Sprememba niza sprememb partnerja" + +#. module: partner_changeset +#: model:ir.ui.menu,name:partner_changeset.menu_changeset +msgid "Partner Changesets" +msgstr "Nizi sprememb partnerja" + +#. 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 "Čakanje" + +#. module: partner_changeset +#: view:res.partner:partner_changeset.view_res_partner_filter +msgid "Pending Changesets" +msgstr "Čakajoči nizi sprememb" + +#. module: partner_changeset +#: view:res.partner.changeset:partner_changeset.view_res_partner_changeset_form +#: field:res.partner.changeset.change,origin_value_boolean:0 +#: field:res.partner.changeset.change,origin_value_char:0 +#: field:res.partner.changeset.change,origin_value_date:0 +#: field:res.partner.changeset.change,origin_value_datetime:0 +#: field:res.partner.changeset.change,origin_value_display:0 +#: field:res.partner.changeset.change,origin_value_float:0 +#: field:res.partner.changeset.change,origin_value_integer:0 +#: field:res.partner.changeset.change,origin_value_reference:0 +#: field:res.partner.changeset.change,origin_value_text:0 +msgid "Previous" +msgstr "Prejšnji" + +#. 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 "Zavrni" + +#. module: partner_changeset +#: view:res.partner.changeset:partner_changeset.view_res_partner_changeset_form +msgid "Reject pending changes" +msgstr "Zavrni čakajoče spremembe" + +#. module: partner_changeset +#: selection:res.partner.changeset.change,state:0 +msgid "Rejected" +msgstr "Zavrnjeno" + +#. module: partner_changeset +#: field:changeset.field.rule,source_model_id:0 +msgid "Source Model" +msgstr "Izvorni model" + +#. module: partner_changeset +#: field:res.partner.changeset,source:0 +msgid "Source of the change" +msgstr "Izvor spremembe" + +#. 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 "Stanje" + +#. module: partner_changeset +#: model:res.groups,comment:partner_changeset.group_changeset_user +msgid "The user will be able to apply or reject changesets." +msgstr "Uporabnik bo lahko uveljavljal ali zavračal nize sprememb." + +#. 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 "Uporabnik bo imel dostop do nastavitev pravil niza sprememb." + +#. module: partner_changeset +#: code:addons/partner_changeset/models/res_partner_changeset.py:401 +#, 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 "" +"Te spremembe ni mogoče uveljaviti, ker obstaja za istega partnerja prejšnji čakajoč niz sprememb.\n" +"Pred uveljavitvijo tega uveljavite vse predhodne nize sprememb." + +#. module: partner_changeset +#: code:addons/partner_changeset/models/res_partner_changeset.py:416 +#, python-format +msgid "This change has already be applied." +msgstr "Ta sprememba je že uveljavljena." diff --git a/partner_changeset/models/__init__.py b/partner_changeset/models/__init__.py new file mode 100644 index 000000000..5b7f50da6 --- /dev/null +++ b/partner_changeset/models/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# © 2015-2016 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 diff --git a/partner_changeset/models/changeset_field_rule.py b/partner_changeset/models/changeset_field_rule.py new file mode 100644 index 000000000..8b2b5d771 --- /dev/null +++ b/partner_changeset/models/changeset_field_rule.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +# © 2015-2016 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 diff --git a/partner_changeset/models/res_partner.py b/partner_changeset/models/res_partner.py new file mode 100644 index 000000000..fa6da68d3 --- /dev/null +++ b/partner_changeset/models/res_partner.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# © 2015-2016 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)] diff --git a/partner_changeset/models/res_partner_changeset.py b/partner_changeset/models/res_partner_changeset.py new file mode 100644 index 000000000..380096456 --- /dev/null +++ b/partner_changeset/models/res_partner_changeset.py @@ -0,0 +1,514 @@ +# -*- coding: utf-8 -*- +# © 2015-2016 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 diff --git a/partner_changeset/security/ir.model.access.csv b/partner_changeset/security/ir.model.access.csv new file mode 100644 index 000000000..7d1346de2 --- /dev/null +++ b/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 diff --git a/partner_changeset/security/security.xml b/partner_changeset/security/security.xml new file mode 100644 index 000000000..946052059 --- /dev/null +++ b/partner_changeset/security/security.xml @@ -0,0 +1,29 @@ + + + + + + Changeset Configuration + The user will have an access to the configuration of the changeset rules. + + + + Changesets Validations + The user will be able to apply or reject changesets. + + + + + + + + + + + + + + + + + diff --git a/partner_changeset/static/description/icon.png b/partner_changeset/static/description/icon.png new file mode 100644 index 000000000..3a0328b51 Binary files /dev/null and b/partner_changeset/static/description/icon.png differ diff --git a/partner_changeset/static/src/img/changeset.png b/partner_changeset/static/src/img/changeset.png new file mode 100644 index 000000000..c117da412 Binary files /dev/null and b/partner_changeset/static/src/img/changeset.png differ diff --git a/partner_changeset/static/src/img/rules.png b/partner_changeset/static/src/img/rules.png new file mode 100644 index 000000000..37b9626f0 Binary files /dev/null and b/partner_changeset/static/src/img/rules.png differ diff --git a/partner_changeset/tests/__init__.py b/partner_changeset/tests/__init__.py new file mode 100644 index 000000000..226578928 --- /dev/null +++ b/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 diff --git a/partner_changeset/tests/common.py b/partner_changeset/tests/common.py new file mode 100644 index 000000000..29ba88d2f --- /dev/null +++ b/partner_changeset/tests/common.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# © 2015-2016 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) diff --git a/partner_changeset/tests/test_changeset_field_rule.py b/partner_changeset/tests/test_changeset_field_rule.py new file mode 100644 index 000000000..cdbd51e6b --- /dev/null +++ b/partner_changeset/tests/test_changeset_field_rule.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +# © 2015-2016 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +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)) diff --git a/partner_changeset/tests/test_changeset_field_type.py b/partner_changeset/tests/test_changeset_field_type.py new file mode 100644 index 000000000..802675db7 --- /dev/null +++ b/partner_changeset/tests/test_changeset_field_type.py @@ -0,0 +1,281 @@ +# -*- coding: utf-8 -*- +# © 2015-2016 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) + + # Cannot do this while it is on the same transaction. The cache may not + # be updated + # 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) diff --git a/partner_changeset/tests/test_changeset_flow.py b/partner_changeset/tests/test_changeset_flow.py new file mode 100644 index 000000000..19287b5cb --- /dev/null +++ b/partner_changeset/tests/test_changeset_flow.py @@ -0,0 +1,246 @@ +# -*- coding: utf-8 -*- +# © 2015-2016 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) + self.assertEqual(self.partner.count_pending_changesets, 1) + 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) diff --git a/partner_changeset/tests/test_changeset_origin.py b/partner_changeset/tests/test_changeset_origin.py new file mode 100644 index 000000000..7347c108c --- /dev/null +++ b/partner_changeset/tests/test_changeset_origin.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +# © 2015-2016 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'}) + # depends cannot trigger all fileds from partner. In real use case, + # the user will probably be in different transaction, he will get the + # new value of the field + self.partner.invalidate_cache() + 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'}) + # depends cannot trigger all fileds from partner. In real use case, + # the user will probably be in different transaction, he will get the + # new value of the field + self.partner.invalidate_cache() + 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') diff --git a/partner_changeset/views/changeset_field_rule_views.xml b/partner_changeset/views/changeset_field_rule_views.xml new file mode 100644 index 000000000..11c3b9cac --- /dev/null +++ b/partner_changeset/views/changeset_field_rule_views.xml @@ -0,0 +1,61 @@ + + + + changeset.field.rule.tree + changeset.field.rule + + + + + + + + + + + changeset.field.rule.form + changeset.field.rule + +
+ + + + + + + +
+
+
+ + + changeset.field.rule.search + changeset.field.rule + + + + + + + + + + + Changeset Fields Rules + ir.actions.act_window + changeset.field.rule + form + tree,form + + + + +
diff --git a/partner_changeset/views/menu.xml b/partner_changeset/views/menu.xml new file mode 100644 index 000000000..cf3c23a12 --- /dev/null +++ b/partner_changeset/views/menu.xml @@ -0,0 +1,8 @@ + + + + diff --git a/partner_changeset/views/res_partner_changeset_views.xml b/partner_changeset/views/res_partner_changeset_views.xml new file mode 100644 index 000000000..fd573aad8 --- /dev/null +++ b/partner_changeset/views/res_partner_changeset_views.xml @@ -0,0 +1,157 @@ + + + + res.partner.changeset.tree + res.partner.changeset + + + + + + + + + + + res.partner.changeset.form + res.partner.changeset + +
+
+
+ + + + + + + + + + + + + + + + + + + +
+ + + res.partner.select + res.partner + + + + + + + + +