diff --git a/.gitignore b/.gitignore index 6d19369b..ad5dd0ac 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ # runbot work files runbot/static/build runbot/static/repo +runbot/static/nginx diff --git a/biwizard/__init__.py b/biwizard/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/biwizard/__init__.py @@ -0,0 +1 @@ + diff --git a/biwizard/__manifest__.py b/biwizard/__manifest__.py new file mode 100644 index 00000000..2fdb7e2c --- /dev/null +++ b/biwizard/__manifest__.py @@ -0,0 +1,65 @@ +# -*- encoding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-TODAY Odoo S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +{ + 'name' : 'Odoo BI wizard', + 'version': '1.0', + 'summary': 'A small wizard to create custom BI views', + 'sequence': '19', + 'category': 'Tools', + 'complexity': 'medium', + 'description': + """ +Odoo BI wizard +============== + +Create new views for BI analysis. This module is importable through base_import_module +and then usable on odoo SaaS. + """, + 'data': [ + 'models/cube.xml', + 'models/model.xml', + 'models/link.xml', + 'models/field.xml', + 'models/computedfield.xml', + 'models/rule.xml', + 'models/cubeunion.xml', + 'models/ruleunion.xml', + 'models/filters.xml', + 'models/o2m.xml', + 'views/computedfields.xml', + 'views/cubes.xml', + 'views/rules.xml', + 'views/menu.xml', + 'views/cubemodel.xml', + 'views/cubelink.xml', + 'views/cubeunion.xml', + 'security/biwizard_groups.xml', + 'security/ir.model.access.csv', + ], + 'depends' : ['base_automation'], + 'js': ['static/src/js/*.js'], + 'css': ['static/src/css/*.css'], + 'qweb': ['static/src/xml/*.xml'], + 'installable': True, + 'auto_install': False, + 'application': True, +} diff --git a/biwizard/models/computedfield.xml b/biwizard/models/computedfield.xml new file mode 100644 index 00000000..c7634dfd --- /dev/null +++ b/biwizard/models/computedfield.xml @@ -0,0 +1,94 @@ + + + + + + BI wizard computed fields + x_biwizard.cubescomputedfields + BI cubes' computed fields + manual + + + + + x_cube_id + Cube + + x_biwizard.cubescomputedfields + many2one + x_biwizard.cubes + manual + + + x_name + Name + + x_biwizard.cubescomputedfields + char + manual + 1 + + + x_type + Type + + x_biwizard.cubescomputedfields + selection + [('float', 'float'), ('integer', 'integer'), ('char','char'), ('date', 'date'), ('datetime', 'datetime'), ('many2one', 'many2one')] + manual + 1 + + + x_formula + Formula + + x_biwizard.cubescomputedfields + text + manual + 1 + + + x_searchable + Searchable + + x_biwizard.cubescomputedfields + boolean + manual + + + x_groupable + Groupable + + x_biwizard.cubescomputedfields + boolean + manual + + + x_pivot + Pivot + + x_biwizard.cubescomputedfields + selection + [('row', 'row'), ('col', 'col'), ('measure', 'measure')] + manual + + + x_graph + Graph + + x_biwizard.cubescomputedfields + selection + [('row', 'row'), ('measure', 'measure')] + manual + + + x_m2omodel + Model + + x_biwizard.cubescomputedfields + char + manual + + + + diff --git a/biwizard/models/cube.xml b/biwizard/models/cube.xml new file mode 100644 index 00000000..5603f33d --- /dev/null +++ b/biwizard/models/cube.xml @@ -0,0 +1,397 @@ + + + + + BI wizard cubes + x_biwizard.cubes + BI custom cubes + manual + + + + + x_name + Name + + x_biwizard.cubes + char + manual + 1 + + + x_state + Status + + x_biwizard.cubes + selection + [('draft', 'Draft'), ('edited', 'Edited'), ('sync', 'Synchronized')] + manual + + + x_group_ids + Groups + + x_biwizard.cubes + many2many + res.groups + manual + + + x_materialized + Materialized view + + x_biwizard.cubes + boolean + manual + + + x_refresh_concurrent + Refresh concurrently (requires PG 9.4) + + x_biwizard.cubes + boolean + manual + + + update model for cube + + code + 5 + +def calc_field_name(fieldname, model, alias=''): + if alias: + if alias.startswith("x_"): + return alias + return "x_%s" % alias + return "x_%s_%s" % (model, fieldname) + +def update_cube(object): + query = 'create ' + if object.x_materialized: + query += 'materialized ' + query += 'view "x_biwizard_%s" as select ' % (object.x_name) + + #Fields + if object.x_fields_ids: + query = "%s%s" % (query, ",".join(['"%s"."%s" as "%s"' % (field.x_cubemodel_id.x_name, field.x_field_id.name, calc_field_name(field.x_field_id.name, field.x_cubemodel_id.x_name, field.x_alias)) for field in object.x_fields_ids])) + + if object.x_fields_ids and object.x_computedfields_ids: + query ="%s," % query + + #Computed fields + if object.x_computedfields_ids: + query = "%s%s" % (query, ",".join(['%s as "%s"' % (field.x_formula, calc_field_name(field.x_name, 'cptd')) for field in object.x_computedfields_ids])) + + #magic fields: ID + if object.x_fields_ids or object.x_computedfields_ids: + query ="%s," % query + query = "%s%s" % (query, "row_number() OVER () as id") + #magic fields: create/write user/date + mdls = env['x_biwizard.cubesmodel'].search([('id', 'in', object.x_models_ids.ids), ('x_magicfields', '=', True)]) + if not mdls: + mdls = env['x_biwizard.cubesmodel'].search([('id', 'in', object.x_models_ids.ids)]) + for fld in ('create_date', 'create_uid', 'write_date', 'write_uid'): + query='%s,"%s"."%s" as "%s"' % (query, mdls[0].x_name, fld, fld) + + #Joins + aliased = [] + if object.x_links_ids: + jointures = "" + for jointure in env['x_biwizard.cubeslinks'].search([('id', 'in', object.x_links_ids.ids)]).sorted(key=lambda r: r.x_sequence+1): + if jointures=="": + jointures = '"%s" as %s' % (jointure.x_cubemodel1_id.x_model_id.model.replace(".", "_"), jointure.x_cubemodel1_id.x_name) + aliased.append(jointure.x_cubemodel1_id.x_name) + if jointure.x_cubemodel2_id.x_name in aliased: + jointures = '%s %s %s' % (jointures, jointure.x_linktype or 'inner join', jointure.x_cubemodel2_id.x_name) + else: + jointures = '%s %s "%s" as %s' % (jointures, jointure.x_linktype or 'inner join', jointure.x_cubemodel2_id.x_model_id.model.replace(".", "_"), jointure.x_cubemodel2_id.x_name) + aliased.append(jointure.x_cubemodel2_id.x_name) + jointures = '%s on "%s"."%s"="%s"."%s"' % (jointures, jointure.x_cubemodel1_id.x_name, jointure.x_field1_id.name, jointure.x_cubemodel2_id.x_name, jointure.x_field2_id.name) + query = "%s from %s" % (query, jointures) + env.cr.execute(query) + +def check_cube_validity(cube): + if not cube.x_models_ids or len(cube.x_models_ids)<2: + raise Warning('Add at least two models') + if not cube.x_group_ids: + raise Warning('Add at least an access rule, nobody can access your cube') + models = [model for model in cube.x_models_ids.ids] + linked_models = [link.x_cubemodel1_id.id for link in cube.x_links_ids] + [link.x_cubemodel2_id.id for link in cube.x_links_ids] + unlinked_models = [mod.x_name for mod in env["x_biwizard.cubesmodel"].browse([model for model in models if model not in linked_models])] + if unlinked_models: + raise Warning("Unlinked model(s): %s" % ",".join(unlinked_models)) + +object=record +check_cube_validity(object) + +#Delete previously created model +model_ids = env["ir.model"].search([('name', '=', object.x_name)]) +if model_ids: + for model_id in model_ids: + env.cr.execute("delete from ir_model_fields where model_id = %s", (model_id.id,)) + env.cr.execute("delete from ir_model where id = %s", (model_id.id,)) + env.cr.execute("drop %s view IF EXISTS x_biwizard_%s" % ("MATERIALIZED" if object.x_materialized else "", object.x_name)) + env.cr.execute('commit') + irrule_ids = env["ir.model.access"].search([('model_id', 'in', model_ids.ids)]) + irrule_ids.unlink() + irrule_ids = env["ir.rule"].search([('model_id', 'in', model_ids.ids)]) + irrule_ids.unlink() + model_ids.unlink() + + +model_data = { + 'name': object.x_name, + 'model': 'x_biwizard.%s' % object.x_name.replace(" ", "_"), + 'info': 'Model for cube %s' % object.x_name, + 'state': 'manual', +} +model = env["ir.model"].with_context({'install_mode':1}).create(model_data) + +#Create fields +for field in object.x_fields_ids: + field_data = { + 'name': calc_field_name(field.x_field_id.name, field.x_cubemodel_id.x_name, field.x_alias), + 'model_id': model.id, + 'state': 'manual', + 'field_description': field.x_field_id.field_description, + 'ttype': field.x_field_id.ttype, + 'relation': field.x_field_id.relation, + 'relation_field': field.x_field_id.relation_field, + 'selection': field.x_field_id.selection, + 'domain': field.x_field_id.domain, + 'related': field.x_field_id.related, + 'on_delete': field.x_field_id.on_delete, + 'required': field.x_field_id.required, + 'readonly': field.x_field_id.readonly, + 'index': field.x_field_id.index, + 'translate': False, + } + newfield = env["ir.model.fields"].create(field_data) + +for field in object.x_computedfields_ids: + field_data = { + 'name': calc_field_name(field.x_name, 'cptd'), + 'model_id': model.id, + 'state': 'manual', + 'field_description': field.x_name, + 'ttype': field.x_type, + } + if field.x_type=="many2one": + field_data['relation'] = field.x_m2omodel + field_id = env["ir.model.fields"].create(field_data) + +#Rebuild xml view +parent = env.ref('biwizard.menu_biwizard_cubeslist') +menu_ids = env["ir.ui.menu"].search([('name', '=', "%s" % object.x_name), ('parent_id', '=', parent.id)]) +if menu_ids: + menu_ids.unlink() + +act_ids = env["ir.actions.act_window"].search([('res_model', '=', object.x_name)]) +if act_ids: + act_ids.unlink() + +view_ids = env["ir.ui.view"].search([('model', '=', 'x_biwizard.%s' % object.x_name), ('type', 'in', ('graph','pivot','search'))]) +if view_ids: + view_ids.unlink() + +viewarch = '<?xml version="1.0"?><graph string="Cube %s" type="bar" stacked="True">' % object.x_name +for field in object.x_fields_ids: + if field.x_graph=='row': + viewarch += ' <field name="%s"/>' % calc_field_name(field.x_field_id.name, field.x_cubemodel_id.x_name, field.x_alias) + elif field.x_graph=='measure': + viewarch += ' <field name="%s" type="measure"/>' % calc_field_name(field.x_field_id.name, field.x_cubemodel_id.x_name, field.x_alias) +for field in object.x_computedfields_ids: + if field.x_graph=='row': + viewarch += ' <field name="%s"/>' % calc_field_name(field.x_name, 'cptd') + elif field.x_graph=='measure': + viewarch += ' <field name="%s" type="measure"/>' % calc_field_name(field.x_name, 'cptd') + +viewarch += '</graph>' + +view_data = { + 'name': 'cube_%s_graph' % object.x_name, + 'type': 'graph', + 'model': "x_biwizard.%s" % object.x_name, + 'arch': viewarch, +} +view_id = env["ir.ui.view"].create(view_data) + +viewarch = '<?xml version="1.0"?><graph string="Cube %s" type="pivot" stacked="True">' % object.x_name +for field in object.x_fields_ids: + if field.x_pivot=='row': + viewarch += ' <field name="%s" type="row" />' % calc_field_name(field.x_field_id.name, field.x_cubemodel_id.x_name, field.x_alias) + elif field.x_pivot=='col': + viewarch += ' <field name="%s" type="col"/>' % calc_field_name(field.x_field_id.name, field.x_cubemodel_id.x_name, field.x_alias) + elif field.x_pivot=='measure': + viewarch += ' <field name="%s" type="measure"/>' % calc_field_name(field.x_field_id.name, field.x_cubemodel_id.x_name, field.x_alias) +for field in object.x_computedfields_ids: + if field.x_pivot=='row': + viewarch += ' <field name="%s" type="row" />' % calc_field_name(field.x_name, 'cptd') + elif field.x_pivot=='col': + viewarch += ' <field name="%s" type="col"/>' % calc_field_name(field.x_name, 'cptd') + elif field.x_pivot=='measure': + viewarch += ' <field name="%s" type="measure"/>' % calc_field_name(field.x_name, 'cptd') +viewarch += '</graph>' + +view_data = { + 'name': 'cube_%s_pivot' % object.x_name, + 'type': 'pivot', + 'model': "x_biwizard.%s" % object.x_name, + 'arch': viewarch, +} +view_id = env["ir.ui.view"].create(view_data) + +action_data = { + 'name': "opencube%s" % object.x_name, + 'res_model': "x_biwizard.%s" % object.x_name, + 'view_mode': 'pivot,graph', +} +win_action = env["ir.actions.act_window"].create(action_data) + +menu_data = { + 'name': "%s" % object.x_name, + 'parent_id': parent.id, + 'action': "ir.actions.act_window,%s" % win_action.id, +} +env["ir.ui.menu"].create(menu_data) + +#Update search view +searchview = ' <search string="%s">' % object.x_name +for field in object.x_fields_ids: + if field.x_groupable: + searchview += ' <group string="Group By">' + searchview += ' <filter name="group_by_%s" string="%s"' % (field.x_field_id.name, field.x_field_id.field_description if field.x_field_id.field_description else field.x_field_id.name) + searchview += ' context="{\'group_by\': \'%s\'}"/>' % calc_field_name(field.x_field_id.name, field.x_cubemodel_id.x_name, field.x_alias) + searchview += ' </group>' + if field.x_searchable: + searchview += ' <field name="%s"/>' % calc_field_name(field.x_field_id.name, field.x_cubemodel_id.x_name, field.x_alias) +for field in object.x_computedfields_ids: + if field.x_groupable: + searchview += ' <group string="Group By">' + searchview += ' <filter name="group_by_%s" string="%s"' % (field.x_name, field.x_name) + searchview += ' context="{\'group_by\': \'%s\'}"/>' % calc_field_name(field.x_name, 'cptd') + searchview += ' </group>' + if field.x_searchable: + searchview += ' <field name="%s"/>' % calc_field_name(field.x_name, 'cptd') +for flt in object.x_filter_ids: + searchview += ' <filter name="%s" string="%s" domain="%s"/>' % (flt.x_name, flt.x_string, flt.x_domain) + +searchview += ' </search>' + +view_data = { + 'name': 'cube_%s_search' % object.x_name, + 'type': 'search', + 'model': "x_biwizard.%s" % object.x_name, + 'arch': searchview, +} +view_id = env["ir.ui.view"].create(view_data) + +#Create rigths +for grp in object.x_group_ids: + env["ir.model.access"].create({ + 'model_id': model.id, + 'name': 'Cube access %s for %s' % (object.x_name, grp.name), + 'group_id': grp.id, + 'perm_read': True, + 'perm_create': False, + 'perm_write': False, + 'perm_unlink': False, + }) +for rule in object.x_rule_ids: + env['ir.rule'].create({ + 'model_id': model.id, + 'name': 'Cube access %s' % (object.x_name), + 'active': True, + 'domain_force': rule.x_domain, + 'perm_read': True, + 'perm_create': False, + 'perm_write': False, + 'perm_unlink': False, + 'groups': [(6,0,rule.x_group_ids.ids),], + }) + +#Drop the created postgresql table +#Replace it with a postgresql View +env.cr.execute('drop table x_biwizard_%s' % object.x_name) +update_cube(object) +object.with_context(action=True).write({'x_state': 'sync'}) + + + + + delete model for cube + + code + 5 + +model_names = [rec.model for rec in env["ir.model"].search([('name', '=', record.x_name)])] + +#Clean action +act_ids = env["ir.actions.act_window"].search([('res_model', '=', record.x_name)]) +if act_ids: + act_ids.unlink() + +#Clean menu +parent = env.ref('biwizard.menu_biwizard_cubeslist') +menu_ids = env["ir.ui.menu"].search([('name', '=', "%s" % record.x_name), ('parent_id', '=', parent.id)]) +if menu_ids: + menu_ids.unlink() + +#Clean views +view_ids = env["ir.ui.view"].search([('model', 'in', model_names)]) +if view_ids: + view_ids.unlink() + +#Clean model +model_ids = env["ir.model"].search([('name', '=', record.x_name)]) +if model_ids: + env.cr.execute("drop %s view IF EXISTS x_biwizard_%s" % ("MATERIALIZED" if record.x_materialized else "", record.x_name)) + irrule_ids = env["ir.model.access"].search([('model_id', 'in', model_ids.ids)]) + irrule_ids.unlink() + irrule_ids = env["ir.rule"].search([('model_id', 'in', model_ids.ids)]) + irrule_ids.unlink() + model_ids.unlink() + +record.write({'x_state': 'draft'}) + + + + + Set cube to Edited + + +if not record.x_state=='draft': + record.write({'x_state': 'edited'}) + + + + + Set state to edited + + on_write + + + + + Update one materialized views + + +if record.x_materialized: + env.cr.execute("refresh materialized view %s x_biwizard_%s" % ("CONCURRENTLY" if record.x_refresh_concurrent else '', record.x_name)) + + + + + Update materialized views + + +for cube in model.search([('x_materialized', '=', True)]): + env.cr.execute("refresh materialized view %s x_biwizard_%s" % ("CONCURRENTLY" if record.x_refresh_concurrent else '', record.x_name)) +for cube in env['x_biwizard.cubeunion'].search([('x_materialized', '=', True)]): + env.cr.execute("refresh materialized view %s x_biwizard_%s" % ("CONCURRENTLY" if record.x_refresh_concurrent else '', record.x_name)) + + + + + + diff --git a/biwizard/models/cubeunion.xml b/biwizard/models/cubeunion.xml new file mode 100644 index 00000000..463628a3 --- /dev/null +++ b/biwizard/models/cubeunion.xml @@ -0,0 +1,139 @@ + + + + + BI wizard cube union + x_biwizard.cubeunion + BI custom cube union + manual + + + + + + x_name + Name + + x_biwizard.cubeunion + char + manual + 1 + + + x_state + Status + + x_biwizard.cubeunion + selection + [('draft', 'Draft'), ('edited', 'Edited'), ('sync', 'Synchronized')] + manual + + + x_group_ids + Groups + + x_biwizard.cubeunion + many2many + res.groups + manual + + + x_materialized + Materialized view + + x_biwizard.cubeunion + boolean + manual + + + x_refresh_concurrent + Refresh concurrently (requires PG 9.4) + + x_biwizard.cubeunion + boolean + manual + + + x_model_ids + Models + + x_biwizard.cubeunion + many2many + ir.model + manual + + + update model for cube + + code + 5 + +action = True + + + + + delete model for cube union + + code + 5 + +#Clean action +act_ids = env["ir.actions.act_window"].search([('res_model', '=', object.x_name)]) +if act_ids: + act_ids.unlink() + +#Clean menu +parent = env.ref('biwizard.menu_biwizard_reporting') +menu_ids = env["ir.ui.menu"].search([('name', '=', "%s" % object.x_name), ('parent_id', '=', parent.id)]) +if menu_ids: + menu_ids.unlink() + +#Clean views +view_ids = env["ir.ui.view"].search([('model', '=', object.x_name)]) +if view_ids: + view_ids.unlink() + +#Clean model +model_ids = env["ir.model"].search([('name', '=', object.x_name)]) +if model_ids: + cr.execute("drop %s view IF EXISTS x_biwizard_%s" % ("MATERIALIZED" if object.x_materialized else "", object.x_name)) + irrule_ids = env["ir.model.access"].search([('model_id', 'in', model_ids.ids)]) + irrule_ids.unlink() + irrule_ids = env["ir.rule"].search([('model_id', 'in', model_ids.ids)]) + irrule_ids.unlink() + model_ids.unlink() + +object.write({'x_state': 'draft'}) + + + + + Set cube union to Edited + + +if not object.x_state=='draft': + object.write({'x_state': 'edited'}) + + + + + Set state to edited + + on_write + + + + + Update one materialized union view + + +if object.x_materialized: + cr.execute("refresh materialized view %s x_biwizard_%s" % ("CONCURRENTLY" if object.x_refresh_concurrent else '', object.x_name)) + + + + + + + diff --git a/biwizard/models/field.xml b/biwizard/models/field.xml new file mode 100644 index 00000000..cff46dba --- /dev/null +++ b/biwizard/models/field.xml @@ -0,0 +1,128 @@ + + + + + BI wizard fields + x_biwizard.cubesfields + BI cubes' fields + manual + + + + + + x_cube_id + Cube + + x_biwizard.cubesfields + many2one + x_biwizard.cubes + manual + + + x_cubemodel_id + Model + + x_biwizard.cubesfields + many2one + x_biwizard.cubesmodel + manual + + + x_field_id + Field + + x_biwizard.cubesfields + many2one + ir.model.fields + manual + + + x_searchable + Searchable + + x_biwizard.cubesfields + boolean + manual + + + x_groupable + Groupable + + x_biwizard.cubesfields + boolean + manual + + + x_pivot + Pivot + + x_biwizard.cubesfields + selection + [('row', 'row'), ('col', 'col'), ('measure', 'measure')] + manual + + + x_graph + Graph + + x_biwizard.cubesfields + selection + [('row', 'row'), ('measure', 'measure')] + manual + + + x_alias + Alias + + x_biwizard.cubesfields + char + manual + + + x_techname + Field name in db + + x_biwizard.cubesfields + char + x_field_id.name + manual + + + + Set cube_id in fields + + +if not object.x_cube_id and object.x_cubemodel_id: + object.write({'x_cube_id': object.x_cubemodel_id.x_cube_id.id}) + + + + + Set cube_id in links + + on_create_or_write + 5 + + + + + Set cube state to Edited + + +if not object.x_cube_id.x_state=='draft': + object.x_cube_id.write({'x_state': 'edited'}) + + + + + Set cube state to edited + + on_create_or_write + 20 + + + + + + diff --git a/biwizard/models/filters.xml b/biwizard/models/filters.xml new file mode 100644 index 00000000..68150d30 --- /dev/null +++ b/biwizard/models/filters.xml @@ -0,0 +1,48 @@ + + + + + BI wizard filters + x_biwizard.cubesfilters + BI cubes' filters + manual + + + + + + x_cube_id + Cube + + x_biwizard.cubesfilters + many2one + x_biwizard.cubes + manual + + + x_name + Name + + x_biwizard.cubesfilters + char + manual + + + x_string + String + + x_biwizard.cubesfilters + char + manual + + + x_domain + Domain + + x_biwizard.cubesfilters + char + manual + + + + diff --git a/biwizard/models/link.xml b/biwizard/models/link.xml new file mode 100644 index 00000000..43d977af --- /dev/null +++ b/biwizard/models/link.xml @@ -0,0 +1,143 @@ + + + + + + BI wizard links + x_biwizard.cubeslinks + BI cubes' links + manual + + + + + + x_cube_id + Cube + + x_biwizard.cubeslinks + many2one + x_biwizard.cubes + manual + + + x_sequence + Sequence + + x_biwizard.cubeslinks + integer + manual + + + x_cubemodel1_id + Source model + + x_biwizard.cubeslinks + many2one + x_biwizard.cubesmodel + manual + 1 + + + x_field1_id + Source field + + x_biwizard.cubeslinks + many2one + ir.model.fields + manual + 1 + + + x_field1_name + Source field db name + + x_biwizard.cubeslinks + char + x_field1_id.name + manual + 1 + + + x_cubemodel2_id + Destination model + + x_biwizard.cubeslinks + many2one + x_biwizard.cubesmodel + manual + 1 + + + x_field2_id + Destination field + + x_biwizard.cubeslinks + many2one + ir.model.fields + manual + 1 + + + x_field2_name + Destination field db name + + x_biwizard.cubeslinks + char + x_field2_id.name + manual + 1 + + + x_linktype + Link + + x_biwizard.cubeslinks + selection + [('inner join', 'Inner'), + ('left join', 'Left'), + ('right join', 'Right'), + ('full outer', 'Outer'), + ('cross join', 'Cross')] + + manual + 1 + + + + Set cube_id in links + + +if not object.x_cube_id and object.x_cubemodel1_id: + object.write({'x_cube_id': object.x_cubemodel1_id.x_cube_id.id}) + + + + + Set cube_id in links + + on_create_or_write + 5 + + + + + Set cube state to Edited + + +if not object.x_cube_id.x_state=='draft': + object.x_cube_id.write({'x_state': 'edited'}) + + + + + Set cube state to edited + + on_create_or_write + 20 + + + + + + diff --git a/biwizard/models/model.xml b/biwizard/models/model.xml new file mode 100644 index 00000000..b61dbfcb --- /dev/null +++ b/biwizard/models/model.xml @@ -0,0 +1,70 @@ + + + + + BI wizard models + x_biwizard.cubesmodel + BI cubes' models + manual + + + + + + x_cube_id + Cube + + x_biwizard.cubesmodel + many2one + x_biwizard.cubes + manual + + + x_model_id + Model + + x_biwizard.cubesmodel + many2one + ir.model + manual + 1 + + + x_name + Alias + + x_biwizard.cubesmodel + char + manual + 1 + + + x_magicfields + Use for magic fields + + x_biwizard.cubesmodel + boolean + manual + 1 + + + + + Set cube state to Edited + + +if not object.x_cube_id.x_state=='draft': + object.x_cube_id.write({'x_state': 'edited'}) + + + + + Set cube state to edited + + on_create_or_write + 20 + + + + + diff --git a/biwizard/models/o2m.xml b/biwizard/models/o2m.xml new file mode 100644 index 00000000..51407e7f --- /dev/null +++ b/biwizard/models/o2m.xml @@ -0,0 +1,120 @@ + + + + + x_models_ids + Models + + x_biwizard.cubes + one2many + x_biwizard.cubesmodel + x_cube_id + manual + + + x_links_ids + Links + + x_biwizard.cubes + one2many + x_biwizard.cubeslinks + x_cube_id + manual + + + x_fields_ids + Fields + + x_biwizard.cubes + one2many + x_biwizard.cubesfields + x_cube_id + manual + + + x_computedfields_ids + Computed fields + + x_biwizard.cubes + one2many + x_biwizard.cubescomputedfields + x_cube_id + manual + + + x_rule_ids + Record rules + + x_biwizard.cubes + one2many + x_biwizard.cubesrule + x_cube_id + manual + + + x_filter_ids + Filters + + x_biwizard.cubes + one2many + x_biwizard.cubesfilters + x_cube_id + manual + + + + + x_cubemodel_link_ids + Cube model links + + x_biwizard.cubesmodel + one2many + x_biwizard.cubeslinks + x_cubemodel1_id + manual + + + x_cubemodel_link_to_ids + Cube model links + + x_biwizard.cubesmodel + one2many + x_biwizard.cubeslinks + x_cubemodel2_id + manual + + + x_cubemodel_field_ids + Fields + + x_biwizard.cubesmodel + one2many + x_biwizard.cubesfields + x_cubemodel_id + manual + + + + + x_cubemodel_ids + Cube model + + ir.model + one2many + x_biwizard.cubesmodel + x_model_id + manual + + + + + x_rule_ids + Record rules + + x_biwizard.cubeunion + one2many + x_biwizard.cubeunionrule + x_cubeunion_id + manual + + diff --git a/biwizard/models/rule.xml b/biwizard/models/rule.xml new file mode 100644 index 00000000..bc08d67c --- /dev/null +++ b/biwizard/models/rule.xml @@ -0,0 +1,39 @@ + + + + + BI wizard rules + x_biwizard.cubesrule + BI cubes' rules + manual + + + + + x_cube_id + Cube + + x_biwizard.cubesrule + many2one + x_biwizard.cubes + manual + + + x_group_ids + Groups + + x_biwizard.cubesrule + many2many + res.groups + manual + + + x_domain + Domain + + x_biwizard.cubesrule + text + manual + + + diff --git a/biwizard/models/ruleunion.xml b/biwizard/models/ruleunion.xml new file mode 100644 index 00000000..62dbea61 --- /dev/null +++ b/biwizard/models/ruleunion.xml @@ -0,0 +1,41 @@ + + + + + BI wizard rules + x_biwizard.cubeunionrule + BI cube unions' rules + manual + + + + + + x_cubeunion_id + Cube + + x_biwizard.cubeunionrule + many2one + x_biwizard.cubeunion + manual + + + x_group_ids + Groups + + x_biwizard.cubeunionrule + many2many + res.groups + manual + + + x_domain + Domain + + x_biwizard.cubeunionrule + text + manual + + + + diff --git a/biwizard/security/biwizard_groups.xml b/biwizard/security/biwizard_groups.xml new file mode 100644 index 00000000..bee9e139 --- /dev/null +++ b/biwizard/security/biwizard_groups.xml @@ -0,0 +1,13 @@ + + + + + BI wizard / read + Read access to BI wizard + + + BI wizard / write + Write access to BI wizard + + + diff --git a/biwizard/security/ir.model.access.csv b/biwizard/security/ir.model.access.csv new file mode 100644 index 00000000..06f19b45 --- /dev/null +++ b/biwizard/security/ir.model.access.csv @@ -0,0 +1,28 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +biwizard_reader_cubes,Reader on cubes,biwizard.x_cubes,group_biwizard_reader,1,0,0,0 +biwizard_writer_cubes,Manager on cubes,biwizard.x_cubes,group_biwizard_writer,1,1,1,1 +biwizard_all_cubes,All on cubes,biwizard.x_cubes,,0,0,0,0 +biwizard_reader_cubesmodel,Reader on cubes,biwizard.x_cubesmodel,group_biwizard_reader,1,0,0,0 +biwizard_writer_cubesmodel,Manager on cubes,biwizard.x_cubesmodel,group_biwizard_writer,1,1,1,1 +biwizard_all_cubesmodel,All on cubes,biwizard.x_cubesmodel,,0,0,0,0 +biwizard_reader_cubeslinks,Reader on cubes links,biwizard.x_cubeslinks,group_biwizard_reader,1,0,0,0 +biwizard_writer_cubeslinks,Manager on cubes links,biwizard.x_cubeslinks,group_biwizard_writer,1,1,1,1 +biwizard_all_cubeslinks,All on cubes links,biwizard.x_cubeslinks,,0,0,0,0 +biwizard_reader_cubesfield,Reader on cubes,biwizard.x_cubesfield,group_biwizard_reader,1,0,0,0 +biwizard_writer_cubesfield,Manager on cubes,biwizard.x_cubesfield,group_biwizard_writer,1,1,1,1 +biwizard_all_cubesfield,All on cubes,biwizard.x_cubesfield,,0,0,0,0 +biwizard_reader_cubescomputedfield,Reader on cubes,biwizard.x_cubescomputedfield,group_biwizard_reader,1,0,0,0 +biwizard_writer_cubescomputedfield,Manager on cubes,biwizard.x_cubescomputedfield,group_biwizard_writer,1,1,1,1 +biwizard_all_cubescomputedfield,All on cubes,biwizard.x_cubescomputedfield,,0,0,0,0 +biwizard_reader_cubesacl,Reader on cubes acl,biwizard.x_cubesrule,group_biwizard_reader,1,0,0,0 +biwizard_writer_cubesacl,Manager on cubes acl,biwizard.x_cubesrule,group_biwizard_writer,1,1,1,1 +biwizard_all_cubesacl,All on cubes acl,biwizard.x_cubesrule,,0,0,0,0 +biwizard_reader_cubeunion,Reader on cube union,biwizard.x_cubeunion,group_biwizard_reader,1,0,0,0 +biwizard_writer_cubeunion,Manager on cube union,biwizard.x_cubeunion,group_biwizard_writer,1,1,1,1 +biwizard_all_cubeunion,All on cube union,biwizard.x_cubeunion,,0,0,0,0 +biwizard_reader_ruleunion,Reader on cube union rules,biwizard.x_cubeunionrule,group_biwizard_reader,1,0,0,0 +biwizard_writer_ruleunion,Manager on cube union rules,biwizard.x_cubeunionrule,group_biwizard_writer,1,1,1,1 +biwizard_all_ruleunion,All on cube union rules,biwizard.x_cubeunionrule,,0,0,0,0 +biwizard_reader_filter,Reader on cube filter rules,biwizard.x_cubesfilters,group_biwizard_reader,1,0,0,0 +biwizard_writer_filter,Manager on cube filter rules,biwizard.x_cubesfilters,group_biwizard_writer,1,1,1,1 +biwizard_all_filter,All on cube filter rules,biwizard.x_cubesfilters,,0,0,0,0 diff --git a/biwizard/views/computedfields.xml b/biwizard/views/computedfields.xml new file mode 100644 index 00000000..a2ab3e60 --- /dev/null +++ b/biwizard/views/computedfields.xml @@ -0,0 +1,34 @@ + + + + + + biwizard.view.computedfields.form + x_biwizard.cubescomputedfields + + +
+ + + + + + + + + + + + + + + + + + + +
+
+
+
+
diff --git a/biwizard/views/cubelink.xml b/biwizard/views/cubelink.xml new file mode 100644 index 00000000..164bf235 --- /dev/null +++ b/biwizard/views/cubelink.xml @@ -0,0 +1,25 @@ + + + + + + biwizard.view.cubeslinks.form + x_biwizard.cubeslinks + +
+ + + + + + + + + + + +
+
+
+
+
diff --git a/biwizard/views/cubemodel.xml b/biwizard/views/cubemodel.xml new file mode 100644 index 00000000..ad45feef --- /dev/null +++ b/biwizard/views/cubemodel.xml @@ -0,0 +1,33 @@ + + + + + + biwizard.view.cubemodel.form + x_biwizard.cubesmodel + +
+ + + + + + + + + + + + + + + + + + + +
+
+
+
+
diff --git a/biwizard/views/cubes.xml b/biwizard/views/cubes.xml new file mode 100644 index 00000000..b93deb97 --- /dev/null +++ b/biwizard/views/cubes.xml @@ -0,0 +1,141 @@ + + + + + + biwizard.view.cubes.form + x_biwizard.cubes + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + biwizard.view.cubes.tree + x_biwizard.cubes + + + + + + + + + + + + biwizard.view.cubes.diagram + x_biwizard.cubes + + + + + + + + + + + + + + + + + + + + + +
+
diff --git a/biwizard/views/cubeunion.xml b/biwizard/views/cubeunion.xml new file mode 100644 index 00000000..91efbd66 --- /dev/null +++ b/biwizard/views/cubeunion.xml @@ -0,0 +1,60 @@ + + + + + + biwizard.view.cubeunion.form + x_biwizard.cubeunion + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + biwizard.view.cubeunion.tree + x_biwizard.cubeunion + + + + + + + + +
+
diff --git a/biwizard/views/menu.xml b/biwizard/views/menu.xml new file mode 100644 index 00000000..ba881838 --- /dev/null +++ b/biwizard/views/menu.xml @@ -0,0 +1,37 @@ + + + + + + Cubes + x_biwizard.cubes + tree,form,diagram + +

Create your first cube

+
+
+ + + + Cube unions + x_biwizard.cubeunion + tree,form + +

Create your first cube union

+
+
+ + + + + + + + + + + +
+
diff --git a/biwizard/views/rules.xml b/biwizard/views/rules.xml new file mode 100644 index 00000000..2a372a42 --- /dev/null +++ b/biwizard/views/rules.xml @@ -0,0 +1,21 @@ + + + + + + biwizard.view.cubesrule.form + x_biwizard.cubesrule + + +
+ + + + + + +
+
+
+
+
diff --git a/plugin_outlook/__init__.py b/crm_profiling/__init__.py similarity index 94% rename from plugin_outlook/__init__.py rename to crm_profiling/__init__.py index ccae4d7e..16035e48 100644 --- a/plugin_outlook/__init__.py +++ b/crm_profiling/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- ############################################################################## -# +# # OpenERP, Open Source Management Solution # Copyright (C) 2004-2010 Tiny SPRL (). # @@ -15,10 +15,10 @@ # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . +# along with this program. If not, see . # ############################################################################## -import plugin_outlook - - +import crm_profiling +import wizard # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: + diff --git a/plugin/__openerp__.py b/crm_profiling/__openerp__.py similarity index 50% rename from plugin/__openerp__.py rename to crm_profiling/__openerp__.py index 7af31450..8a44ee06 100644 --- a/plugin/__openerp__.py +++ b/crm_profiling/__openerp__.py @@ -21,21 +21,32 @@ { - 'name': 'CRM Plugins', - 'version': '1.0', - 'category': 'Hidden/Dependency', + 'name': 'Customer Profiling', + 'version': '1.3', + 'category': 'Marketing', 'description': """ -The common interface for plug-in. -================================= -""", +This module allows users to perform segmentation within partners. +================================================================= + +It uses the profiles criteria from the earlier segmentation module and improve it. +Thanks to the new concept of questionnaire. You can now regroup questions into a +questionnaire and directly use it on a partner. + +It also has been merged with the earlier CRM & SRM segmentation tool because they +were overlapping. + + **Note:** this module is not compatible with the module segmentation, since it's the same which has been renamed. + """, 'author': 'OpenERP SA', - 'website': 'http://www.openerp.com', - 'depends': ['base'], - 'data': [], - 'demo': [], - 'test': [], + 'website': 'https://www.odoo.com/page/crm', + 'depends': ['base', 'crm'], + 'data': ['security/ir.model.access.csv', 'wizard/open_questionnaire_view.xml', 'crm_profiling_view.xml'], + 'demo': ['crm_profiling_demo.xml'], + 'test': [ + #'test/process/profiling.yml', #TODO:It's not debuging because problem to write data for open.questionnaire from partner section. + ], 'installable': True, 'auto_install': False, - 'images': [], + 'images': ['images/profiling_questionnaires.jpeg','images/profiling_questions.jpeg'], } # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/crm_profiling/crm_profiling.py b/crm_profiling/crm_profiling.py new file mode 100644 index 00000000..a8866e76 --- /dev/null +++ b/crm_profiling/crm_profiling.py @@ -0,0 +1,293 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2010 Tiny SPRL (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp.osv import fields,osv +from openerp.osv import orm + +from openerp.tools.translate import _ + +def _get_answers(cr, uid, ids): + """ + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param ids: List of crm profiling’s IDs """ + + query = """ + select distinct(answer) + from profile_question_yes_rel + where profile IN %s""" + + cr.execute(query, (tuple(ids),)) + ans_yes = [x[0] for x in cr.fetchall()] + + query = """ + select distinct(answer) + from profile_question_no_rel + where profile IN %s""" + + cr.execute(query, (tuple(ids),)) + ans_no = [x[0] for x in cr.fetchall()] + + return [ans_yes, ans_no] + + +def _get_parents(cr, uid, ids): + """ + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param ids: List of crm profiling’s IDs + @return: Get parents's Id """ + + ids_to_check = ids + cr.execute(""" + select distinct(parent_id) + from crm_segmentation + where parent_id is not null + and id IN %s""",(tuple(ids),)) + + parent_ids = [x[0] for x in cr.fetchall()] + + trigger = False + for x in parent_ids: + if x not in ids_to_check: + ids_to_check.append(x) + trigger = True + + if trigger: + ids_to_check = _get_parents(cr, uid, ids_to_check) + + return ids_to_check + + +def test_prof(cr, uid, seg_id, pid, answers_ids=None): + + """ return True if the partner pid fetch the segmentation rule seg_id + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param seg_id: Segmentaion's ID + @param pid: partner's ID + @param answers_ids: Answers's IDs + """ + + ids_to_check = _get_parents(cr, uid, [seg_id]) + [yes_answers, no_answers] = _get_answers(cr, uid, ids_to_check) + temp = True + for y_ans in yes_answers: + if y_ans not in answers_ids: + temp = False + break + if temp: + for ans in answers_ids: + if ans in no_answers: + temp = False + break + if temp: + return True + return False + + +def _recompute_categ(self, cr, uid, pid, answers_ids): + """ Recompute category + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param pid: partner's ID + @param answers_ids: Answers's IDs + """ + + ok = [] + cr.execute(''' + select r.category_id + from res_partner_res_partner_category_rel r left join crm_segmentation s on (r.category_id = s.categ_id) + where r.partner_id = %s and (s.exclusif = false or s.exclusif is null) + ''', (pid,)) + for x in cr.fetchall(): + ok.append(x[0]) + + query = ''' + select id, categ_id + from crm_segmentation + where profiling_active = true''' + if ok != []: + query = query +''' and categ_id not in(%s)'''% ','.join([str(i) for i in ok ]) + query = query + ''' order by id ''' + + cr.execute(query) + segm_cat_ids = cr.fetchall() + + for (segm_id, cat_id) in segm_cat_ids: + if test_prof(cr, uid, segm_id, pid, answers_ids): + ok.append(cat_id) + return ok + + +class question(osv.osv): + """ Question """ + + _name="crm_profiling.question" + _description= "Question" + + _columns={ + 'name': fields.char("Question", required=True), + 'answers_ids': fields.one2many("crm_profiling.answer", "question_id", "Available Answers", copy=True), + } + + + +class questionnaire(osv.osv): + """ Questionnaire """ + + _name="crm_profiling.questionnaire" + _description= "Questionnaire" + + _columns = { + 'name': fields.char("Questionnaire", required=True), + 'description':fields.text("Description", required=True), + 'questions_ids': fields.many2many('crm_profiling.question','profile_questionnaire_quest_rel',\ + 'questionnaire', 'question', "Questions"), + } + + + +class answer(osv.osv): + _name="crm_profiling.answer" + _description="Answer" + _columns={ + "name": fields.char("Answer", required=True), + "question_id": fields.many2one('crm_profiling.question',"Question"), + } + + +class partner(osv.osv): + _inherit="res.partner" + _columns={ + "answers_ids": fields.many2many("crm_profiling.answer","partner_question_rel",\ + "partner","answer","Answers"), + } + + def _questionnaire_compute(self, cr, uid, answers, context=None): + """ + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param data: Get Data + @param context: A standard dictionary for contextual values """ + partner_id = context.get('active_id') + query = "select answer from partner_question_rel where partner=%s" + cr.execute(query, (partner_id,)) + for x in cr.fetchall(): + answers.append(x[0]) + self.write(cr, uid, [partner_id], {'answers_ids': [[6, 0, answers]]}, context=context) + return {} + + + def write(self, cr, uid, ids, vals, context=None): + """ + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param ids: List of crm profiling’s IDs + @param context: A standard dictionary for contextual values """ + + if 'answers_ids' in vals: + vals['category_id']=[[6, 0, _recompute_categ(self, cr, uid, ids[0], vals['answers_ids'][0][2])]] + + return super(partner, self).write(cr, uid, ids, vals, context=context) + + + +class crm_segmentation(osv.osv): + """ CRM Segmentation """ + + _inherit="crm.segmentation" + _columns={ + "answer_yes": fields.many2many("crm_profiling.answer","profile_question_yes_rel",\ + "profile","answer","Included Answers"), + "answer_no": fields.many2many("crm_profiling.answer","profile_question_no_rel",\ + "profile","answer","Excluded Answers"), + 'parent_id': fields.many2one('crm.segmentation', 'Parent Profile'), + 'child_ids': fields.one2many('crm.segmentation', 'parent_id', 'Child Profiles'), + 'profiling_active': fields.boolean('Use The Profiling Rules', help='Check\ + this box if you want to use this tab as part of the \ + segmentation rule. If not checked, the criteria beneath will be ignored') + } + + _constraints = [ + (osv.osv._check_recursion, 'Error ! You cannot create recursive profiles.', ['parent_id']) + ] + + def process_continue(self, cr, uid, ids, start=False): + """ + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param ids: List of crm segmentation’s IDs """ + + partner_obj = self.pool.get('res.partner') + categs = self.read(cr,uid,ids,['categ_id','exclusif','partner_id', \ + 'sales_purchase_active', 'profiling_active']) + for categ in categs: + if start: + if categ['exclusif']: + cr.execute('delete from res_partner_res_partner_category_rel where \ + category_id=%s', (categ['categ_id'][0],)) + partner_obj.invalidate_cache(cr, uid, ['category_id']) + + id = categ['id'] + + cr.execute('select id from res_partner order by id ') + partners = [x[0] for x in cr.fetchall()] + + if categ['sales_purchase_active']: + to_remove_list=[] + cr.execute('select id from crm_segmentation_line where segmentation_id=%s', (id,)) + line_ids = [x[0] for x in cr.fetchall()] + + for pid in partners: + if (not self.pool.get('crm.segmentation.line').test(cr, uid, line_ids, pid)): + to_remove_list.append(pid) + for pid in to_remove_list: + partners.remove(pid) + + if categ['profiling_active']: + to_remove_list = [] + for pid in partners: + + cr.execute('select distinct(answer) from partner_question_rel where partner=%s',(pid,)) + answers_ids = [x[0] for x in cr.fetchall()] + + if (not test_prof(cr, uid, id, pid, answers_ids)): + to_remove_list.append(pid) + for pid in to_remove_list: + partners.remove(pid) + + for partner in partner_obj.browse(cr, uid, partners): + category_ids = [categ_id.id for categ_id in partner.category_id] + if categ['categ_id'][0] not in category_ids: + cr.execute('insert into res_partner_res_partner_category_rel (category_id,partner_id) values (%s,%s)', (categ['categ_id'][0],partner.id)) + partner_obj.invalidate_cache(cr, uid, ['category_id'], [partner.id]) + + self.write(cr, uid, [id], {'state':'not running', 'partner_id':0}) + return True + + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: + diff --git a/crm_profiling/crm_profiling_demo.xml b/crm_profiling/crm_profiling_demo.xml new file mode 100644 index 00000000..ec93e6f1 --- /dev/null +++ b/crm_profiling/crm_profiling_demo.xml @@ -0,0 +1,175 @@ + + + + + + + + + Activity sector? + + + Number of employees? + + + Partner level? + + + Odoo partner? + + + + + + Base questionnaire + First questionnaire. + + + + + + + Services + + + + Telecom + + + + IT + + + + 1 to 50 + + + + 51 to 100 + + + + more than 100 + + + + ready + + + + silver + + + + gold + + + + yes + + + + no + + + + + + + + Telecom sector + + + + + + + + Odoo partners + + + True + + + Ready partners + + + + True + + + Silver partners + + + + True + + + Gold partners + + + + True + True + + + Service partners + + + True + + + Telecom partners + + + True + + + IT partners + + + True + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crm_profiling/crm_profiling_view.xml b/crm_profiling/crm_profiling_view.xml new file mode 100644 index 00000000..286296ee --- /dev/null +++ b/crm_profiling/crm_profiling_view.xml @@ -0,0 +1,200 @@ + + + + + + Questionnaires + crm_profiling.questionnaire + form + tree,form + You can create specific topic-related questionnaires to guide your team(s) in the sales cycle by helping them to ask the right questions. The segmentation tool allows you to automatically assign a partner to a category according to his answers to the different questionnaires. + + + + + + Questions + crm_profiling.question + form + tree,form + + + + + + + + Questionnaires + crm_profiling.questionnaire + + + + + + + + + + + + + Questionnaires + crm_profiling.questionnaire + +
+ + + + + + + + + +
+
+
+ + + + + Answers + crm_profiling.answer + + + + + + + + + + + + Answers + crm_profiling.answer + +
+ + + + +
+
+
+ + + + + Questions + crm_profiling.question + + + + + + + + + + + + Questions + crm_profiling.question + +
+ + + + + + + + + + + + + +
+ +
+
+ + + + res.partner.profile.form + res.partner + + + + + + + + +