diff --git a/product_profile/README.rst b/product_profile/README.rst index d8b3863515dc..0732451c45d3 100644 --- a/product_profile/README.rst +++ b/product_profile/README.rst @@ -1,15 +1,34 @@ - .. image:: https://img.shields.io/badge/license-AGPL--3-blue.png - :target: https://www.gnu.org/licenses/agpl - :alt: License: AGPL-3 - =============== Product Profile =============== +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fproduct--attribute-lightgray.png?logo=github + :target: https://github.com/OCA/product-attribute/tree/12.0/product_profile + :alt: OCA/product-attribute +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/product-attribute-12-0/product-attribute-12-0-product_profile + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/135/12.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + This module provides easier products configuration (in one click). It allows to configure a product template with only one field. - .. figure:: static/img/field.png + .. figure:: https://raw.githubusercontent.com/OCA/product-attribute/12.0/product_profile/static/img/field.png :alt: profile field on product :width: 600 px @@ -26,23 +45,22 @@ Note: This module is meant to be used by skilled people in database fields creat Additional feature: a default value can be attached to a profile (see § Configuration, part 3) +**Table of contents** + +.. contents:: + :local: Configuration ============= -1. Create your own profile here: +1. Create your own profile here: Sales > Configuration > Product > Product Profiles - .. figure:: static/img/list.png + .. figure:: https://raw.githubusercontent.com/OCA/product-attribute/12.0/product_profile/static/img/list.png :alt: profile list :width: 600 px -2. To have more fields available to attach to this profile you must define - these fields in the model 'product.profile' in your own module - If the field name (and its type) is the same than those in 'product.template' - then values of these will be populated automatically - in 'product.template' - Example of fields declaration in your own module: +2. Extend "product.profile" model to add fields from product.template, either in normal mode or default mode (see note section below). These fields should be identical to their original fields **(especially "required" field attribute)**. .. code-block:: python @@ -64,9 +82,11 @@ Configuration string='Can be Purchased') available_in_pos = fields.Boolean() -3. Second behavior: you might want to add a default behavior to these fields: - in this case use prefix "profile_default\_" for your field name - in 'product.profile' model. +3. Insert data (xml or csv) and define values for each field defined above + for each configuration scenario + +Note : +You might want to declare profile fields as defaults. To do this, just prefix the field with "profile_default". .. code-block:: python @@ -82,16 +102,9 @@ Configuration "you to define the route of the product: " "whether it will be bought, manufactured, MTO/MTS,...") - In this case 'categ_id' field (from product.template) is populated - with 'profile_default_categ_id' value but can be updated manually - by the user. - Careful: each time you change profile, the default value is also populated - whatever the previous value. Custom value is only keep if don't - change the profile. - -4. Insert data (xml or csv) and define values for each field defined above - for each configuration scenario - +Default fields only influence the records the first time they are set. +- if the profile is modified, changes are not propagated to all the records that have this profile +- if the record previously had another profile, changing profile will not influence default values Usage ===== @@ -106,45 +119,51 @@ Install **Product Profile Example** module to see a use case in action. Profiles are also defined as search filter and group. +Known issues / Roadmap +====================== + +- More robust/less error-prone functionality for required fields +- More flexible/configurable behaviour for profile fields (instead of only default/nondefault fields) + 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 smash it by providing detailed and welcomed feedback. +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 `_. + +Do not contact contributors directly about support or help with technical issues. Credits ======= -Images ------- +Authors +~~~~~~~ -* Odoo Community Association: `Icon `_. +* Akretion Contributors ------------- +~~~~~~~~~~~~ * David BEAL * Sébastien BEAU * Abdessamad HILALI +* Kevin Khao -Iconography ------------ - -https://www.iconfinder.com/icondesigner +Maintainers +~~~~~~~~~~~ -Maintainer ----------- +This module is maintained by the OCA. .. 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. +This module is part of the `OCA/product-attribute `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/product_profile/__manifest__.py b/product_profile/__manifest__.py index cd49b3feb995..b9f79bb4c015 100644 --- a/product_profile/__manifest__.py +++ b/product_profile/__manifest__.py @@ -3,19 +3,18 @@ { "name": "Product Profile", - "version": "10.0.1.0.0", + "version": "12.0.1.0.0", "author": "Akretion, Odoo Community Association (OCA)", "summary": "Allow to configure a product in 1 click", "category": "product", - "depends": ["sale",], - "website": "http://www.akretion.com/", + "depends": ["sale_management"], + "website": "https://github.com/oca/product-attribute", "data": [ "security/group.xml", "views/product_view.xml", "views/config_view.xml", "security/ir.model.access.csv", ], - "demo": ["demo/product.profile.csv",], "installable": True, "license": "AGPL-3", } diff --git a/product_profile/demo/product.profile.csv b/product_profile/demo/product.profile.csv deleted file mode 100644 index ec68f02e2977..000000000000 --- a/product_profile/demo/product.profile.csv +++ /dev/null @@ -1,3 +0,0 @@ -"id","name","sequence","type","explanation" -"prd","Product",1,"consu","Physical Product" -"serv","Service",2,"service","Service Product" diff --git a/product_profile/models/__init__.py b/product_profile/models/__init__.py index fe88a6d40cfb..e26ccae9a39b 100644 --- a/product_profile/models/__init__.py +++ b/product_profile/models/__init__.py @@ -1,7 +1,6 @@ -# coding: utf-8 # © 2015 David BEAL @ Akretion # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from . import product from . import product_profile +from . import product from . import config diff --git a/product_profile/models/config.py b/product_profile/models/config.py index a727fc2c232b..9d0390d0d334 100644 --- a/product_profile/models/config.py +++ b/product_profile/models/config.py @@ -2,15 +2,15 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from odoo import fields, models -from .product import PROFILE_MENU +from .product_profile import PROFILE_MENU -class BaseConfigSettings(models.TransientModel): - _inherit = "base.config.settings" +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" group_product_profile = fields.Boolean( string="Display Product Profile fields", - implied_group="product_profile.group_product_profile", + implied_group="product_profile.group_product_profile_user", help="Display fields computed by product profile " "module.\nFor debugging purpose see menu\n%s" % PROFILE_MENU, ) diff --git a/product_profile/models/product_profile.py b/product_profile/models/product_profile.py index 8050044008dc..f3365dac214a 100644 --- a/product_profile/models/product_profile.py +++ b/product_profile/models/product_profile.py @@ -1,6 +1,7 @@ # © 2015 David BEAL @ Akretion # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from copy import deepcopy import logging from odoo import models, fields, api, _ from odoo.osv import orm @@ -42,6 +43,7 @@ def get_profile_fields_to_exclude(): "name", "explanation", "sequence", + "id", "display_name", "__last_update", ] @@ -50,6 +52,7 @@ def get_profile_fields_to_exclude(): class ProductProfile(models.Model): _name = "product.profile" _order = "sequence, name" + _description = "Product Profile" name = fields.Char( required=True, @@ -62,7 +65,6 @@ class ProductProfile(models.Model): ) explanation = fields.Text( required=True, - oldname="description", help="An explanation on the selected profile\n" "(not synchronized with product.template fields)", ) @@ -72,38 +74,41 @@ class ProductProfile(models.Model): help="See 'type' field in product.template", ) - @api.multi def write(self, vals): - """ Profile update can impact products: we take care - to propagate ad hoc changes """ - new_vals = vals.copy() + """ Profile update can impact products: we take care to propagate + ad hoc changes. If on the profile at least one field has changed, + re-apply its values on relevant products""" excludable_fields = get_profile_fields_to_exclude() + values_to_keep = deepcopy(vals) for key in vals: - if ( + discard_value = ( key.startswith(PROF_DEFAULT_STR) or key in excludable_fields - or self.check_useless_key_in_vals(new_vals, key) - ): - new_vals.pop(key) - # super call must be after check_useless_key_in_vals() call - # because we compare value before and after write - res = super(ProductProfile, self).write(new_vals) - if new_vals: - for rec in self: - products = self.env["product.product"].search( - [("profile_id", "=", rec.id)] - ) - if products: - _logger.info( - " >>> %s Products updating after updated '%s' pro" - "duct profile" % (len(products), rec.name) - ) - data = products._get_vals_from_profile( - {"profile_id": rec.id} - ) - products.write(data) + or self.check_useless_key_in_vals(vals, key) + ) + if discard_value: + values_to_keep.pop(key) + if values_to_keep: + self._refresh_products_vals() + res = super(ProductProfile, self).write(vals) return res + def _refresh_products_vals(self): + """Reapply profile values on products""" + for rec in self: + products = self.env["product.product"].search( + [("profile_id", "=", rec.id)] + ) + if products: + _logger.info( + " >>> %s Products updating after updated '%s' pro" + "duct profile" % (len(products), rec.name) + ) + data = products._get_vals_from_profile( + {"profile_id": rec.id}, ignore_defaults=True + ) + products.write(data) + @api.model def check_useless_key_in_vals(self, vals, key): """ If replacing values are the same than in db, we remove them. @@ -151,6 +156,7 @@ def fields_view_get( class ProductMixinProfile(models.AbstractModel): _name = "product.mixin.profile" + _description = "Product Profile Mixin" @api.model def _get_profile_fields(self): @@ -162,34 +168,44 @@ def _get_profile_fields(self): ] @api.model - def _get_vals_from_profile(self, values): + def _get_vals_from_profile(self, product_values, ignore_defaults=False): + res = {} profile_obj = self.env["product.profile"] fields = self._get_profile_fields() - vals = profile_obj.browse(values["profile_id"]).read(fields)[0] - vals.pop("id") - for field, value in vals.items(): - if value and profile_obj._fields[field].type == "many2one": + profile_vals = profile_obj.browse(product_values["profile_id"]).read( + fields + )[0] + profile_vals.pop("id") + profile_vals = self._reformat_relationals(profile_vals) + for key, val in profile_vals.items(): + if key[:LEN_DEF_STR] == PROF_DEFAULT_STR: + if not ignore_defaults: + destination_field = key[LEN_DEF_STR:] + res[destination_field] = val + else: + res[key] = val + return res + + def _reformat_relationals(self, profile_vals): + res = deepcopy(profile_vals) + profile_obj = self.env["product.profile"] + for key, value in profile_vals.items(): + if value and profile_obj._fields[key].type == "many2one": # m2o value is a tuple - vals[field] = value[0] - if profile_obj._fields[field].type == "many2many": - vals[field] = [(6, 0, value)] - if PROF_DEFAULT_STR == field[:LEN_DEF_STR]: - if field[LEN_DEF_STR:] not in values: - # we only put the default profile value - # if their is no matching in default data - vals[field[LEN_DEF_STR:]] = vals[field] - # prefixed fields must be removed from dict - # because they are in profile not in product - vals.pop(field) - return vals + res[key] = value[0] + if profile_obj._fields[key].type == "many2many": + res[key] = [(6, 0, value)] + return res @api.onchange("profile_id") def _onchange_from_profile(self): """ Update product fields with product.profile corresponding fields """ self.ensure_one() if self.profile_id: + ignore_defaults = True if self._origin.profile_id else False values = self._get_vals_from_profile( - {"profile_id": self.profile_id.id} + {"profile_id": self.profile_id.id}, + ignore_defaults=ignore_defaults, ) for field, value in values.items(): try: @@ -200,13 +216,22 @@ def _onchange_from_profile(self): @api.model def create(self, vals): if vals.get("profile_id"): - vals.update(self._get_vals_from_profile(vals)) + vals.update( + self._get_vals_from_profile(vals, ignore_defaults=False) + ) return super(ProductMixinProfile, self).create(vals) - @api.multi def write(self, vals): - if vals.get("profile_id"): - vals.update(self._get_vals_from_profile(vals)) + profile_changed = vals.get("profile_id") + if profile_changed: + recs_has_profile = self.filtered(lambda r: r.profile_id) + recs_no_profile = self - recs_has_profile + recs_has_profile.write( + self._get_vals_from_profile(vals, ignore_defaults=True) + ) + recs_no_profile.write( + self._get_vals_from_profile(vals, ignore_defaults=False) + ) return super(ProductMixinProfile, self).write(vals) @api.model @@ -220,7 +245,9 @@ def _get_default_profile_fields(self): @api.model def _customize_view(self, res, view_type): - profile_group = self.env.ref("product_profile.group_product_profile") + profile_group = self.env.ref( + "product_profile.group_product_profile_user" + ) users_in_profile_group = [user.id for user in profile_group.users] default_fields = self._get_default_profile_fields() if view_type == "form": diff --git a/product_profile/readme/CONFIGURE.rst b/product_profile/readme/CONFIGURE.rst new file mode 100644 index 000000000000..4500653d5e39 --- /dev/null +++ b/product_profile/readme/CONFIGURE.rst @@ -0,0 +1,52 @@ +1. Create your own profile here: + Sales > Configuration > Product > Product Profiles + + .. figure:: static/img/list.png + :alt: profile list + :width: 600 px + +2. Extend "product.profile" model to add fields from product.template, either in normal mode or default mode (see note section below). These fields should be identical to their original fields **(especially "required" field attribute)**. + + .. code-block:: python + + class ProductProfile(models.Model): + """ Require dependency on sale, purchase and point_of_sale modules + """ + + _inherit = 'product.profile' + + def _get_types(self): + return [('product', 'Stockable Product'), + ('consu', 'Consumable'), + ('service', 'Service')] + + sale_ok = fields.Boolean( + string='Can be Sold', + help="Specify if the product can be selected in a sales order line.") + purchase_ok = fields.Boolean( + string='Can be Purchased') + available_in_pos = fields.Boolean() + +3. Insert data (xml or csv) and define values for each field defined above + for each configuration scenario + +Note : +You might want to declare profile fields as defaults. To do this, just prefix the field with "profile_default". + + .. code-block:: python + + class ProductProfile(models.Model): + profile_default_categ_id = fields.Many2one( + 'product.category', + string='Default category') + profile_default_route_ids = fields.Many2many( + 'stock.location.route', + string=u'Default Routes', + domain="[('product_selectable', '=', True)]", + help="Depending on the modules installed, this will allow " + "you to define the route of the product: " + "whether it will be bought, manufactured, MTO/MTS,...") + +Default fields only influence the records the first time they are set. +- if the profile is modified, changes are not propagated to all the records that have this profile +- if the record previously had another profile, changing profile will not influence default values diff --git a/product_profile/readme/CONTRIBUTORS.rst b/product_profile/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000000..db75bf6df7ec --- /dev/null +++ b/product_profile/readme/CONTRIBUTORS.rst @@ -0,0 +1,4 @@ +* David BEAL +* Sébastien BEAU +* Abdessamad HILALI +* Kevin Khao diff --git a/product_profile/readme/DESCRIPTION.rst b/product_profile/readme/DESCRIPTION.rst new file mode 100644 index 000000000000..0b88f7293864 --- /dev/null +++ b/product_profile/readme/DESCRIPTION.rst @@ -0,0 +1,19 @@ +This module provides easier products configuration (in one click). +It allows to configure a product template with only one field. + + .. figure:: static/img/field.png + :alt: profile field on product + :width: 600 px + +**Main use case**: a lot of modules are installed (mrp, purchase, sale, pos) +and products configuration becomes harder for end users: too many fields to take care of. + +You are concerned that at any time a product might be not configured correctly: this module is your friend. + +Thanks to this module, a lot of complexity becomes hidden (default behavior) to the end user and usability is optimal. + +It eases as well the data migration by only specifying the profile field instead of all fields which depend on it. + +Note: This module is meant to be used by skilled people in database fields creation within the ERP framework. + +Additional feature: a default value can be attached to a profile (see § Configuration, part 3) diff --git a/product_profile/readme/ROADMAP.rst b/product_profile/readme/ROADMAP.rst new file mode 100644 index 000000000000..2eee1d9fa39c --- /dev/null +++ b/product_profile/readme/ROADMAP.rst @@ -0,0 +1,2 @@ +- More robust/less error-prone functionality for required fields +- More flexible/configurable behaviour for profile fields (instead of only default/nondefault fields) diff --git a/product_profile/readme/USAGE.rst b/product_profile/readme/USAGE.rst new file mode 100644 index 000000000000..a3503ab2943a --- /dev/null +++ b/product_profile/readme/USAGE.rst @@ -0,0 +1,9 @@ +Assign a value to the profile field in the product template form. +Then, all fields which depend on this profile will be set to the right value at once. + +If you deselect the profile value, all these fields keep the same value and you can change them manually +(back to standard behavior). + +Install **Product Profile Example** module to see a use case in action. + +Profiles are also defined as search filter and group. diff --git a/product_profile/security/group.xml b/product_profile/security/group.xml index 24731d85690e..ca21325397f9 100644 --- a/product_profile/security/group.xml +++ b/product_profile/security/group.xml @@ -1,9 +1,15 @@ - - Product Profile - + + Display Product Profile fields + + + + + Manage Product Profiles + + diff --git a/product_profile/security/ir.model.access.csv b/product_profile/security/ir.model.access.csv index a2878718f6b9..5a5b2f91ed60 100644 --- a/product_profile/security/ir.model.access.csv +++ b/product_profile/security/ir.model.access.csv @@ -1,2 +1,3 @@ "id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" -"access_product_brand_product_manager","product.profile","model_product_profile","base.group_user",1,0,0,0 +"access_product_profile_manager","product.profile","model_product_profile","product_profile.group_product_profile_manager",1,1,1,1 +"access_product_profile_select","product.profile","model_product_profile","base.group_user",1,0,0,0 diff --git a/product_profile/views/config_view.xml b/product_profile/views/config_view.xml index 1fac98d1c89a..f4108032719e 100644 --- a/product_profile/views/config_view.xml +++ b/product_profile/views/config_view.xml @@ -1,16 +1,26 @@ - - base.config.settings - - - - - - - - - + + res.config.settings + + + +
+
+ +
+
+
+
+
+
+
diff --git a/product_profile/views/product_view.xml b/product_profile/views/product_view.xml index 6fa69a5ce168..f16db36b9cda 100644 --- a/product_profile/views/product_view.xml +++ b/product_profile/views/product_view.xml @@ -1,51 +1,51 @@ - - product.template - - - - - - - - - + + product.template + + + + + + + + + + - - + - - product.template - - - - - - - + + product.template + + + + + + + - - product.profile - - - - - - - - - + + product.profile + + + + + + + + + - - Product Profiles - product.profile - tree,form - + + Product Profiles + product.profile + tree,form + - +