diff --git a/partner_stage/README.rst b/partner_stage/README.rst
new file mode 100644
index 000000000..876fdb5f4
--- /dev/null
+++ b/partner_stage/README.rst
@@ -0,0 +1 @@
+Generated
diff --git a/partner_stage/__init__.py b/partner_stage/__init__.py
new file mode 100644
index 000000000..d71877a49
--- /dev/null
+++ b/partner_stage/__init__.py
@@ -0,0 +1,2 @@
+from . import models
+from .init_hook import post_init_hook
diff --git a/partner_stage/__manifest__.py b/partner_stage/__manifest__.py
new file mode 100644
index 000000000..0cb9ab87b
--- /dev/null
+++ b/partner_stage/__manifest__.py
@@ -0,0 +1,22 @@
+# Copyright 2021 Open Source Integrators
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
+
+{
+ "name": "Partner Stage",
+ "summary": "Add lifecycle Stages to Partners",
+ "author": "Open Source Integrators, Odoo Community Association (OCA)",
+ "website": "https://github.com/OCA/partner-contact",
+ "category": "Sales/CRM",
+ "version": "14.0.2.0.0",
+ "license": "AGPL-3",
+ "depends": ["contacts"],
+ "data": [
+ "security/ir.model.access.csv",
+ "data/partner_stage_data.xml",
+ "views/res_partner_stage_views.xml",
+ "views/res_partner_views.xml",
+ ],
+ "post_init_hook": "post_init_hook",
+ "installable": True,
+ "maintainers": ["dreispt"],
+}
diff --git a/partner_stage/data/partner_stage_data.xml b/partner_stage/data/partner_stage_data.xml
new file mode 100644
index 000000000..c483483f4
--- /dev/null
+++ b/partner_stage/data/partner_stage_data.xml
@@ -0,0 +1,20 @@
+
+
+
+ Draft
+ draft
+ 10
+
+
+ Active
+ confirmed
+ 20
+ True
+
+
+ Inactive
+ cancel
+ 30
+ True
+
+
diff --git a/partner_stage/init_hook.py b/partner_stage/init_hook.py
new file mode 100644
index 000000000..02f5b8c0c
--- /dev/null
+++ b/partner_stage/init_hook.py
@@ -0,0 +1,26 @@
+# Copyright 2021 Open Source Integrators
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
+
+import logging
+
+from odoo import SUPERUSER_ID, api
+
+_logger = logging.getLogger(__name__)
+
+
+def post_init_hook(cr, registry):
+ """Set default Stage on partners"""
+ env = api.Environment(cr, SUPERUSER_ID, {})
+ Partner = env["res.partner"]
+ default_stage = Partner._get_default_stage_id()
+ missing_stages = Partner.search([("stage_id", "=", False)])
+ if default_stage and missing_stages:
+ _logger.info("Init stage_id for %d partner records...", len(missing_stages))
+ cr.execute(
+ """
+ UPDATE res_partner
+ SET stage_id = %(id)s, state = %(state)s
+ WHERE stage_id IS NULL
+ """,
+ {"id": default_stage.id, "state": default_stage.state},
+ )
diff --git a/partner_stage/migrations/14.0.2.0.0/post-migration.py b/partner_stage/migrations/14.0.2.0.0/post-migration.py
new file mode 100644
index 000000000..b9412d654
--- /dev/null
+++ b/partner_stage/migrations/14.0.2.0.0/post-migration.py
@@ -0,0 +1,25 @@
+# Copyright 2021 Open Source Integrators
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
+
+import logging
+
+from odoo import SUPERUSER_ID, api
+
+_logger = logging.getLogger(__name__)
+
+
+def migrate(cr, version):
+ env = api.Environment(cr, SUPERUSER_ID, {})
+ stages = env["res.partner.stage"].search([])
+ for stage in stages:
+ _logger.info(
+ "Migrating old state %s to stage_id %s...", stage.state, stage.name
+ )
+ cr.execute(
+ """
+ UPDATE res_partner
+ SET stage_id = %(id)s, state = old_state
+ WHERE old_state = %(state)s
+ """,
+ {"id": stage.id, "state": stage.state},
+ )
diff --git a/partner_stage/migrations/14.0.2.0.0/pre-migrate.py b/partner_stage/migrations/14.0.2.0.0/pre-migrate.py
new file mode 100644
index 000000000..86426ed19
--- /dev/null
+++ b/partner_stage/migrations/14.0.2.0.0/pre-migrate.py
@@ -0,0 +1,11 @@
+# Copyright 2021 Open Source Integrators
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
+
+from odoo.tools.sql import column_exists, rename_column
+
+
+def migrate(cr, version):
+ if column_exists(cr, "res_partner", "state"):
+ if column_exists(cr, "res_partner", "old_state"):
+ cr.execute("ALTER TABLE res_partner DROP COLUMN old_state")
+ rename_column(cr, "res_partner", "state", "old_state")
diff --git a/partner_stage/models/__init__.py b/partner_stage/models/__init__.py
new file mode 100644
index 000000000..3f194dcab
--- /dev/null
+++ b/partner_stage/models/__init__.py
@@ -0,0 +1,2 @@
+from . import res_partner_stage
+from . import res_partner
diff --git a/partner_stage/models/res_partner.py b/partner_stage/models/res_partner.py
new file mode 100644
index 000000000..1525b3175
--- /dev/null
+++ b/partner_stage/models/res_partner.py
@@ -0,0 +1,28 @@
+# Copyright 2021 Open Source Integrators
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
+
+from odoo import api, fields, models
+
+
+class Partner(models.Model):
+
+ _inherit = "res.partner"
+
+ @api.model
+ def _get_default_stage_id(self):
+ return self.env["res.partner.stage"].search(
+ [("is_default", "=", True)], limit=1
+ )
+
+ @api.model
+ def _read_group_stage_id(self, states, domain, order):
+ return states.search([])
+
+ stage_id = fields.Many2one(
+ comodel_name="res.partner.stage",
+ group_expand="_read_group_stage_id",
+ default=_get_default_stage_id,
+ index=True,
+ tracking=True,
+ )
+ state = fields.Selection(related="stage_id.state", store=True, readonly=True)
diff --git a/partner_stage/models/res_partner_stage.py b/partner_stage/models/res_partner_stage.py
new file mode 100644
index 000000000..9740fde33
--- /dev/null
+++ b/partner_stage/models/res_partner_stage.py
@@ -0,0 +1,34 @@
+# Copyright 2021 Open Source Integrators
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
+
+from odoo import _, api, fields, models
+from odoo.exceptions import ValidationError
+
+
+class PartnerStage(models.Model):
+ _name = "res.partner.stage"
+ _description = "Contact Stage"
+ _order = "sequence, id"
+
+ name = fields.Char(required=True, translate=True)
+ code = fields.Char()
+ sequence = fields.Integer(help="Used to order the stages", default=10)
+ fold = fields.Boolean()
+ active = fields.Boolean(default=True)
+ description = fields.Text(translate=True)
+ is_default = fields.Boolean("Default state")
+
+ state = fields.Selection(
+ [("draft", "To Approve"), ("confirmed", "Approved"), ("cancel", "Archived")],
+ string="Related State",
+ default="confirmed",
+ )
+
+ _sql_constraints = [
+ ("res_partner_stage_code_unique", "UNIQUE(code)", "Stage Code must be unique.")
+ ]
+
+ @api.constrains("is_default")
+ def _check_default(self):
+ if self.search_count([("is_default", "=", True)]) > 1:
+ raise ValidationError(_("There should be only one default stage"))
diff --git a/partner_stage/readme/CONFIGURATION.rst b/partner_stage/readme/CONFIGURATION.rst
new file mode 100644
index 000000000..49ec1096d
--- /dev/null
+++ b/partner_stage/readme/CONFIGURATION.rst
@@ -0,0 +1,7 @@
+To create a new Stage:
+
+#. Navigate to *Contacts > Configuration > Contact Stages*.
+
+#. Create a new record, and set the name, code and a description.
+
+#. The stage order can be modified using teh handle widget, on the list view.
diff --git a/partner_stage/readme/CONTRIBUTORS.rst b/partner_stage/readme/CONTRIBUTORS.rst
new file mode 100644
index 000000000..837de373e
--- /dev/null
+++ b/partner_stage/readme/CONTRIBUTORS.rst
@@ -0,0 +1 @@
+* Daniel Reis
diff --git a/partner_stage/readme/DESCRIPTION.rst b/partner_stage/readme/DESCRIPTION.rst
new file mode 100644
index 000000000..b3a8f6aad
--- /dev/null
+++ b/partner_stage/readme/DESCRIPTION.rst
@@ -0,0 +1,2 @@
+Adds stages to Contacts allowing, for example, to setup a lifecycle workflow.
+The default stages are: Draft, Active and Inactive.
diff --git a/partner_stage/readme/USAGE.rst b/partner_stage/readme/USAGE.rst
new file mode 100644
index 000000000..9f8c81c97
--- /dev/null
+++ b/partner_stage/readme/USAGE.rst
@@ -0,0 +1,4 @@
+Open a Contact form to see the corresponding Stage.
+It is visible in the stages bar, at the top right are of the form.
+
+The contact stage can be changed clicking on the stages bar.
diff --git a/partner_stage/security/ir.model.access.csv b/partner_stage/security/ir.model.access.csv
new file mode 100644
index 000000000..a8c5a47e1
--- /dev/null
+++ b/partner_stage/security/ir.model.access.csv
@@ -0,0 +1,3 @@
+"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
+"access_partner_stage_edit","access_partner_stage_edit","model_res_partner_stage","base.group_system",1,1,1,1
+"access_partner_stage_read","access_partner_stage_read","model_res_partner_stage",,1,0,0,0
diff --git a/partner_stage/static/description/icon.png b/partner_stage/static/description/icon.png
new file mode 100644
index 000000000..3a0328b51
Binary files /dev/null and b/partner_stage/static/description/icon.png differ
diff --git a/partner_stage/tests/__init__.py b/partner_stage/tests/__init__.py
new file mode 100644
index 000000000..5f010342f
--- /dev/null
+++ b/partner_stage/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_partner_stage
diff --git a/partner_stage/tests/test_partner_stage.py b/partner_stage/tests/test_partner_stage.py
new file mode 100644
index 000000000..be95ef183
--- /dev/null
+++ b/partner_stage/tests/test_partner_stage.py
@@ -0,0 +1,24 @@
+# Copyright 2021 Open Source Integrators
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
+
+from odoo.exceptions import ValidationError
+from odoo.tests.common import SavepointCase
+
+
+class TestPartnerStage(SavepointCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.Stage = cls.env["res.partner.stage"]
+ cls.Partner = cls.env["res.partner"]
+
+ def test_01_partner_stage(self):
+ default_stage = self.env.ref("partner_stage.partner_stage_active")
+ new_partner = self.Partner.create({"name": "A Partner"})
+ self.assertTrue(new_partner.stage_id, default_stage)
+
+ def test_02_stage_default_constraint(self):
+ with self.assertRaises(ValidationError) as ctx:
+ self.Stage.create({"name": "Another Default Stage", "is_default": True})
+ err_msg = ctx.exception.args[0]
+ self.assertEqual("There should be only one default stage", err_msg)
diff --git a/partner_stage/views/res_partner_stage_views.xml b/partner_stage/views/res_partner_stage_views.xml
new file mode 100644
index 000000000..569c6fb8e
--- /dev/null
+++ b/partner_stage/views/res_partner_stage_views.xml
@@ -0,0 +1,57 @@
+
+
+
+
+ res.partner.stage
+
+
+
+
+
+
+ res.partner.stage
+
+
+
+
+
+
+
+
+
+
+
+
+ Stage
+ res.partner.stage
+ tree,form
+
+
+
+
+
diff --git a/partner_stage/views/res_partner_views.xml b/partner_stage/views/res_partner_views.xml
new file mode 100644
index 000000000..2a5687801
--- /dev/null
+++ b/partner_stage/views/res_partner_views.xml
@@ -0,0 +1,34 @@
+
+
+
+
+ res.partner
+
+
+
+
+
+
+
+
+
+ res.partner
+
+
+
+
+
+
+
+
+
diff --git a/setup/partner_stage/odoo/addons/partner_stage b/setup/partner_stage/odoo/addons/partner_stage
new file mode 120000
index 000000000..1550ef756
--- /dev/null
+++ b/setup/partner_stage/odoo/addons/partner_stage
@@ -0,0 +1 @@
+../../../../partner_stage
\ No newline at end of file
diff --git a/setup/partner_stage/setup.py b/setup/partner_stage/setup.py
new file mode 100644
index 000000000..28c57bb64
--- /dev/null
+++ b/setup/partner_stage/setup.py
@@ -0,0 +1,6 @@
+import setuptools
+
+setuptools.setup(
+ setup_requires=['setuptools-odoo'],
+ odoo_addon=True,
+)