diff --git a/res_project/README.rst b/res_project/README.rst new file mode 100644 index 00000000..c5a09f16 --- /dev/null +++ b/res_project/README.rst @@ -0,0 +1,121 @@ +================== +Project Management +================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:04b18a5acdb42879701da5bafdcc0bd18504edaa08eedf7924375aa61e1a15e6 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |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-ecosoft--odoo%2Fbudgeting-lightgray.png?logo=github + :target: https://github.com/ecosoft-odoo/budgeting/tree/18.0/res_project + :alt: ecosoft-odoo/budgeting + +|badge1| |badge2| |badge3| + +This module is one of the master data used in all Project sections as +information + +This module adds the possibility of defining a sequence for the +project's reference. The reference is then set as the default when +creating a new project, using the defined sequence. + +NOTE: Before installing this module, you should check the following +information + +- For projects that have a parent project, the code will be overwritten + with the code of the parent project and followed by a sub-code, such + as PJ00001-1 + +- For main projects, if the code already exists, it will not be changed. + However, if the code does not exist, a new code will be automatically + assigned + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +To configure this module, following access right must be set. + +- Go to Settings > Users & Companies > Users + +- Select User that you have to see Project + +- Select access right on Res Project Field + + - Project User + - Project Manager + +To configure sequence, following access right must be set. + +- You can change the default sequence (PJ00001) by the one of your + choice going to *Settings > Technical > Sequences & Identifiers > + Sequences*, and editing the record name ``Res Project Sequence``. + +**Note:** + +- Project User: Users will be able to see all project documents but + cannot create documents. +- Project Manager: Users will be able to see all project documents, + create documents and configuration. +- You will only have access to that section if your section has + ``Technical features`` permission check marked. + +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 to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Ecosoft + +Contributors +------------ + +- ``Ecosoft ``\ \_\_: + + - Pimolnat Suntian pimolnats@ecosoft.co.th + - Saran Lim. saranl@ecosoft.co.th + +Maintainers +----------- + +.. |maintainer-Saran440| image:: https://github.com/Saran440.png?size=40px + :target: https://github.com/Saran440 + :alt: Saran440 + +Current maintainer: + +|maintainer-Saran440| + +This module is part of the `ecosoft-odoo/budgeting `_ project on GitHub. + +You are welcome to contribute. diff --git a/res_project/__init__.py b/res_project/__init__.py new file mode 100644 index 00000000..a9b75564 --- /dev/null +++ b/res_project/__init__.py @@ -0,0 +1,5 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import models +from . import wizard +from .hooks import assign_new_sequences diff --git a/res_project/__manifest__.py b/res_project/__manifest__.py new file mode 100644 index 00000000..a95e459b --- /dev/null +++ b/res_project/__manifest__.py @@ -0,0 +1,27 @@ +# Copyright 2020 Ecosoft Co., Ltd. (http://ecosoft.co.th) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +{ + "name": "Project Management", + "summary": "New menu Projects management", + "version": "18.0.1.0.0", + "license": "AGPL-3", + "category": "Project", + "website": "https://github.com/ecosoft-odoo/budgeting", + "author": "Ecosoft, Odoo Community Association (OCA)", + "depends": ["hr", "mail"], + "data": [ + "security/res_project_security_groups.xml", + "security/ir.model.access.csv", + "data/res_project_cron.xml", + "data/res_project_data.xml", + "views/res_project_menuitem.xml", + "views/res_project_views.xml", + "views/res_project_split.xml", + "views/hr_department_views.xml", + "views/hr_employee_views.xml", + "wizard/split_project_wizard_view.xml", + ], + "maintainers": ["Saran440"], + "development_status": "Alpha", +} diff --git a/res_project/data/res_project_cron.xml b/res_project/data/res_project_cron.xml new file mode 100644 index 00000000..77674e4e --- /dev/null +++ b/res_project/data/res_project_cron.xml @@ -0,0 +1,19 @@ + + + + Res Project: Automatically change state expiration date + + code + model.action_auto_expired() + + 1 + days + + + + diff --git a/res_project/data/res_project_data.xml b/res_project/data/res_project_data.xml new file mode 100644 index 00000000..19bcfa33 --- /dev/null +++ b/res_project/data/res_project_data.xml @@ -0,0 +1,9 @@ + + + + Res Project Sequence + res.project + + PJ + + diff --git a/res_project/hooks.py b/res_project/hooks.py new file mode 100644 index 00000000..35b42387 --- /dev/null +++ b/res_project/hooks.py @@ -0,0 +1,46 @@ +# Copyright 2021 Ecosoft Co., Ltd. (http://ecosoft.co.th) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import SUPERUSER_ID, _, api +from odoo.exceptions import UserError + + +def generate_code_from_parent(parent_project): + """generate code for project from its parent's code""" + next_split = parent_project.next_split + code = f"{parent_project.code}-{next_split}" + parent_project.write({"next_split": next_split + 1}) + return code + + +def check_recursive_project(project): + """Check if project has a recursive parent""" + parent_project = project + project_list = [] + while parent_project.parent_project_id: + if project.id in project_list: + raise UserError( + _( + "Please check 'Parent Project' in project '{}' " + "must not be recursive." + ).format(project.name) + ) + project_list.append(parent_project.parent_project_id.id) + parent_project = parent_project.parent_project_id + + +def assign_new_sequences(cr, registry): + """Assign new sequences to projects""" + env = api.Environment(cr, SUPERUSER_ID, {}) + project_obj = env["res.project"] + sequence_obj = env["ir.sequence"] + projects = project_obj.with_context(active_test=False).search([], order="id") + for project in projects: + check_recursive_project(project) + parent_project = project.parent_project_id + # Skip it, if project is parent and has code already + if parent_project and parent_project.code != "/": + code = generate_code_from_parent(parent_project) + project.write({"code": code}) + elif project.code == "/": + project.write({"code": sequence_obj.next_by_code("res.project")}) diff --git a/res_project/models/__init__.py b/res_project/models/__init__.py new file mode 100644 index 00000000..e0dcdc9f --- /dev/null +++ b/res_project/models/__init__.py @@ -0,0 +1,6 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import hr_department +from . import hr_employee_base +from . import res_project_plan +from . import res_project diff --git a/res_project/models/hr_department.py b/res_project/models/hr_department.py new file mode 100644 index 00000000..f99aef49 --- /dev/null +++ b/res_project/models/hr_department.py @@ -0,0 +1,16 @@ +# Copyright 2020 Ecosoft Co., Ltd. (http://ecosoft.co.th) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class Department(models.Model): + _inherit = "hr.department" + + project_ids = fields.One2many( + comodel_name="res.project", + inverse_name="department_id", + copy=False, + help="Project to which this department is linked " + "for structure organization.", + ) diff --git a/res_project/models/hr_employee_base.py b/res_project/models/hr_employee_base.py new file mode 100644 index 00000000..879a357d --- /dev/null +++ b/res_project/models/hr_employee_base.py @@ -0,0 +1,15 @@ +# Copyright 2021 Ecosoft Co., Ltd. (http://ecosoft.co.th) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import fields, models + + +class HrEmployeeBase(models.AbstractModel): + _inherit = "hr.employee.base" + + project_ids = fields.Many2many( + comodel_name="res.project", + relation="project_employee_rel", + column1="employee_id", + column2="project_id", + string="Project", + ) diff --git a/res_project/models/res_project.py b/res_project/models/res_project.py new file mode 100644 index 00000000..99fe059d --- /dev/null +++ b/res_project/models/res_project.py @@ -0,0 +1,235 @@ +# Copyright 2020 Ecosoft Co., Ltd. (http://ecosoft.co.th) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import _, api, fields, models +from odoo.exceptions import UserError + + +class ResProject(models.Model): + _name = "res.project" + _description = "Project Management" + _inherit = "mail.thread" + _check_company_auto = True + _rec_name = "code" + + name = fields.Char( + required=True, + tracking=True, + ) + code = fields.Char( + tracking=True, + ) + parent_project_id = fields.Many2one( + comodel_name="res.project", + string="Parent", + tracking=True, + ) + parent_project_name = fields.Char( + compute="_compute_parent_project_name", + string="Parent Project", + store=True, + readonly=False, + tracking=True, + ) + child_ids = fields.One2many( + comodel_name="res.project", + inverse_name="parent_project_id", + string="Child Projects", + check_company=True, + ) + active = fields.Boolean( + default=True, + tracking=True, + help="If the active field is set to False, " + "it will allow you to hide the project without removing it.", + ) + description = fields.Html(copy=False) + company_id = fields.Many2one( + comodel_name="res.company", + string="Company", + required=True, + readonly=True, + default=lambda self: self.env.company, + ) + currency_id = fields.Many2one( + comodel_name="res.currency", + string="Currency", + required=True, + related="company_id.currency_id", + ) + project_manager_id = fields.Many2one( + comodel_name="hr.employee", + string="Project Manager", + tracking=True, + ) + date_from = fields.Date( + required=True, + string="Project Start", + tracking=True, + ) + date_to = fields.Date( + required=True, + string="Project End", + tracking=True, + ) + department_id = fields.Many2one( + comodel_name="hr.department", + required=True, + ) + member_ids = fields.Many2many( + comodel_name="hr.employee.public", + relation="project_employee_rel", + column1="project_id", + column2="employee_id", + string="Member", + ) + plan_amount = fields.Monetary( + compute="_compute_plan_amount", + currency_field="currency_id", + help="Total Plan Amount for this project", + ) + project_plan_ids = fields.One2many( + comodel_name="res.project.plan", + inverse_name="project_id", + ) + + state = fields.Selection( + [ + ("draft", "Draft"), + ("confirm", "Confirmed"), + ("close", "Closed"), + ("cancel", "Cancelled"), + ], + string="Status", + required=True, + readonly=True, + copy=False, + tracking=True, + default="draft", + ) + amount = fields.Monetary() + code = fields.Char(required=True, default="/", readonly=True, copy=False) + next_split = fields.Integer(string="Next split code", default=1) + + _sql_constraints = [("unique_name", "UNIQUE(name)", "name must be unique")] + + @api.depends("parent_project_id", "name") + def _compute_parent_project_name(self): + for rec in self: + rec.parent_project_name = ( + rec.parent_project_id and rec.parent_project_id.name or rec.name + ) + + @api.depends("project_plan_ids") + def _compute_plan_amount(self): + for rec in self: + rec.plan_amount = sum(rec.project_plan_ids.mapped("amount")) + + @api.model + def create(self, vals): + """ + Sequence will run 2 method + - Split project: use the same code parent project and add subcode + Example: Project A has code A00001. + when split project A, it will A00001-1, next split is A00001-2 + - Create new project: use new sequence + - Has code already: skip it + """ + + if not vals.get("parent_project_id", False): + vals["parent_project_name"] = vals["name"] + + if vals.get("code", "/") == "/": + split_project = self._context.get("split_project", False) + parent_project_id = vals.get( + "parent_project_id", False + ) or self._context.get("parent_project_id", False) + import_file = self._context.get("import_file", False) + # Split project or import with parent + if split_project or (import_file and parent_project_id): + parent_project = self.env["res.project"].browse(parent_project_id) + if parent_project: + next_split = parent_project.next_split + code = f"{parent_project.code}-{next_split}" + parent_project.write({"next_split": next_split + 1}) + else: + code = self.env["ir.sequence"].next_by_code("res.project") + vals["code"] = code + + return super().create(vals) + + @api.model + def name_search(self, name, args=None, operator="ilike", limit=100): + args = args or [] + domain = [] + if name: + domain = ["|", ("code", operator, name), ("name", operator, name)] + projects = self.search(domain + args, limit=limit) + return projects.name_get() + + def name_get(self): + res = [] + for project in self: + name = project.name + if project.code: + name = f"[{project.code}] {name}" + res.append((project.id, name)) + return res + + def copy(self, default=None): + self.ensure_one() + default = dict(default or {}, name=_("%s (copy)") % self.name) + return super().copy(default) + + def action_split_project(self): + project = self.browse(self.env.context["active_ids"]) + if len(project) != 1: + raise UserError(_("Please select one project.")) + wizard = self.env.ref("res_project.split_project_wizard_form") + return { + "name": _("Split Project"), + "type": "ir.actions.act_window", + "view_mode": "form", + "res_model": "split.project.wizard", + "views": [(wizard.id, "form")], + "view_id": wizard.id, + "target": "new", + "context": { + "default_parent_project_id": project.parent_project_id.id or project.id, + "default_parent_project_name": project.parent_project_name, + "default_date_from": project.date_from, + "default_date_to": project.date_to, + "default_project_manager_id": project.project_manager_id.id, + "default_department_id": project.department_id.id, + "default_member_ids": [(6, 0, project.member_ids.ids)], + }, + } + + def action_confirm(self): + return self.write({"state": "confirm"}) + + def action_close_project(self): + return self.write({"state": "close"}) + + def action_draft(self): + return self.write({"state": "draft"}) + + def action_cancel(self): + return self.write({"state": "cancel"}) + + def _get_domain_project_expired(self): + date = self._context.get("force_project_date") or fields.Date.context_today( + self + ) + domain = [("date_to", "<", date), ("state", "=", "confirm")] + return domain + + def action_auto_expired(self): + """Close a project automatically when the specified conditions are met""" + domain = self._get_domain_project_expired() + project_expired = self.search(domain) + return project_expired.action_close_project() + + @api.onchange("project_manager_id") + def _onchange_department_id(self): + for rec in self: + rec.department_id = rec.project_manager_id.department_id or False diff --git a/res_project/models/res_project_plan.py b/res_project/models/res_project_plan.py new file mode 100644 index 00000000..d94bd57b --- /dev/null +++ b/res_project/models/res_project_plan.py @@ -0,0 +1,23 @@ +# Copyright 2021 Ecosoft Co., Ltd. (http://ecosoft.co.th) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResProjectPlan(models.Model): + _name = "res.project.plan" + _description = "Project Plan" + + project_id = fields.Many2one( + comodel_name="res.project", + required=True, + index=True, + ondelete="cascade", + ) + currency_id = fields.Many2one( + comodel_name="res.currency", + related="project_id.currency_id", + ) + date_from = fields.Date(required=True) + date_to = fields.Date(required=True) + amount = fields.Monetary() diff --git a/res_project/pyproject.toml b/res_project/pyproject.toml new file mode 100644 index 00000000..4231d0cc --- /dev/null +++ b/res_project/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/res_project/readme/CONFIGURE.md b/res_project/readme/CONFIGURE.md new file mode 100644 index 00000000..dbaaf3c8 --- /dev/null +++ b/res_project/readme/CONFIGURE.md @@ -0,0 +1,24 @@ +To configure this module, following access right must be set. + +- Go to Settings \> Users & Companies \> Users + +- Select User that you have to see Project + +- Select access right on Res Project Field + - Project User + - Project Manager + + +To configure sequence, following access right must be set. +- You can change the default sequence (PJ00001) by the one of your choice + going to *Settings > Technical > Sequences & Identifiers > Sequences*, and + editing the record name `Res Project Sequence`. + +**Note:** + +- Project User: Users will be able to see all project documents but + cannot create documents. +- Project Manager: Users will be able to see all project documents, + create documents and configuration. +- You will only have access to that section if your section has `Technical features` + permission check marked. diff --git a/res_project/readme/CONTRIBUTORS.md b/res_project/readme/CONTRIBUTORS.md new file mode 100644 index 00000000..7fd34cbf --- /dev/null +++ b/res_project/readme/CONTRIBUTORS.md @@ -0,0 +1,4 @@ +* `Ecosoft `__: + + * Pimolnat Suntian + * Saran Lim. \ No newline at end of file diff --git a/res_project/readme/DESCRIPTION.md b/res_project/readme/DESCRIPTION.md new file mode 100644 index 00000000..16985523 --- /dev/null +++ b/res_project/readme/DESCRIPTION.md @@ -0,0 +1,12 @@ +This module is one of the master data used in all Project sections as +information + +This module adds the possibility of defining a sequence for the project's reference. +The reference is then set as the default when creating a new project, using the defined sequence. + +NOTE: +Before installing this module, you should check the following information + +* For projects that have a parent project, the code will be overwritten with the code of the parent project and followed by a sub-code, such as PJ00001-1 + +* For main projects, if the code already exists, it will not be changed. However, if the code does not exist, a new code will be automatically assigned \ No newline at end of file diff --git a/res_project/security/ir.model.access.csv b/res_project/security/ir.model.access.csv new file mode 100644 index 00000000..88bc7d13 --- /dev/null +++ b/res_project/security/ir.model.access.csv @@ -0,0 +1,7 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_res_project_user,access_res_project_user,model_res_project,res_project.group_res_project_user,1,0,0,0 +access_res_project_manager,access_res_project_manager,model_res_project,res_project.group_res_project_manager,1,1,1,1 +access_res_project_plan_user,access_res_project_plan_user,model_res_project_plan,res_project.group_res_project_user,1,0,0,0 +access_res_project_plan_manager,access_res_project_plan_manager,model_res_project_plan,res_project.group_res_project_manager,1,1,1,1 +access_split_project_wizard,access_split_project_wizard,model_split_project_wizard,res_project.group_res_project_manager,1,1,1,1 +access_split_project_wizard_line,access_split_project_wizard_line,model_split_project_wizard_line,res_project.group_res_project_manager,1,1,1,1 diff --git a/res_project/security/res_project_security_groups.xml b/res_project/security/res_project_security_groups.xml new file mode 100644 index 00000000..21848829 --- /dev/null +++ b/res_project/security/res_project_security_groups.xml @@ -0,0 +1,26 @@ + + + + + Res Project + Helps you handle your project needs. + 10 + + + Project User + + + + Project Manager + + + + + diff --git a/res_project/static/description/icon.png b/res_project/static/description/icon.png new file mode 100644 index 00000000..1dcc49c2 Binary files /dev/null and b/res_project/static/description/icon.png differ diff --git a/res_project/static/description/index.html b/res_project/static/description/index.html new file mode 100644 index 00000000..b80e48df --- /dev/null +++ b/res_project/static/description/index.html @@ -0,0 +1,471 @@ + + + + + +Project Management + + + +
+

Project Management

+ + +

Alpha License: AGPL-3 ecosoft-odoo/budgeting

+

This module is one of the master data used in all Project sections as +information

+

This module adds the possibility of defining a sequence for the +project’s reference. The reference is then set as the default when +creating a new project, using the defined sequence.

+

NOTE: Before installing this module, you should check the following +information

+
    +
  • For projects that have a parent project, the code will be overwritten +with the code of the parent project and followed by a sub-code, such +as PJ00001-1
  • +
  • For main projects, if the code already exists, it will not be changed. +However, if the code does not exist, a new code will be automatically +assigned
  • +
+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

Table of contents

+ +
+

Configuration

+

To configure this module, following access right must be set.

+
    +
  • Go to Settings > Users & Companies > Users
  • +
  • Select User that you have to see Project
  • +
  • Select access right on Res Project Field
      +
    • Project User
    • +
    • Project Manager
    • +
    +
  • +
+

To configure sequence, following access right must be set.

+
    +
  • You can change the default sequence (PJ00001) by the one of your +choice going to Settings > Technical > Sequences & Identifiers > +Sequences, and editing the record name Res Project Sequence.
  • +
+

Note:

+
    +
  • Project User: Users will be able to see all project documents but +cannot create documents.
  • +
  • Project Manager: Users will be able to see all project documents, +create documents and configuration.
  • +
  • You will only have access to that section if your section has +Technical features permission check marked.
  • +
+
+
+

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 to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Ecosoft
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

Current maintainer:

+

Saran440

+

This module is part of the ecosoft-odoo/budgeting project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/res_project/tests/__init__.py b/res_project/tests/__init__.py new file mode 100644 index 00000000..20e9253d --- /dev/null +++ b/res_project/tests/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import common +from . import test_res_project diff --git a/res_project/tests/common.py b/res_project/tests/common.py new file mode 100644 index 00000000..f8ab8fa9 --- /dev/null +++ b/res_project/tests/common.py @@ -0,0 +1,48 @@ +# Copyright 2020 Ecosoft Co., Ltd. (http://ecosoft.co.th) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.tests import TransactionCase + + +class ResProjectCommon(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.ResProject = cls.env["res.project"] + cls.ProjectWizard = cls.env["split.project.wizard"] + cls.dep_admin = cls.env.ref("hr.dep_administration") + cls.dep_sales = cls.env.ref("hr.dep_sales") + cls.employee_sale = cls.env.ref("hr.employee_lur") + + def _create_res_project( + self, + name, + department_id, + date_from, + date_to, + code="/", + parent_project_id=False, + ): + return self.ResProject.create( + { + "name": name, + "code": code, + "parent_project_id": parent_project_id, + "department_id": department_id, + "date_from": date_from, + "date_to": date_to, + } + ) + + def _create_project_wizard(self, project, new_name=False): + return self.ProjectWizard.create( + { + "parent_project_id": project.parent_project_id.id or project.id, + "parent_project_name": project.parent_project_name, + "date_from": project.date_from, + "date_to": project.date_to, + "project_manager_id": project.project_manager_id.id, + "department_id": project.department_id.id, + "line_ids": [(0, 0, {"project_name": new_name})] if new_name else [], + } + ) diff --git a/res_project/tests/test_res_project.py b/res_project/tests/test_res_project.py new file mode 100644 index 00000000..36f06db7 --- /dev/null +++ b/res_project/tests/test_res_project.py @@ -0,0 +1,122 @@ +# Copyright 2020 Ecosoft Co., Ltd. (http://ecosoft.co.th) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from datetime import datetime, timedelta + +from freezegun import freeze_time + +from odoo.exceptions import UserError +from odoo.tests import Form, tagged + +from .common import ResProjectCommon + + +@tagged("post_install", "-at_install") +class ResProject(ResProjectCommon): + @classmethod + @freeze_time("2001-02-01") + def setUpClass(cls): + super().setUpClass() + cls.today = datetime.today() + cls.project1 = cls._create_res_project( + cls, + "Test Project1", + cls.dep_admin.id, + cls.today, + cls.today, + ) + + @freeze_time("2001-02-01") + def test_01_project_standard(self): + """Test normally process project""" + self.assertEqual(self.project1.name, "Test Project1") + self.assertEqual(self.project1.parent_project_name, "Test Project1") + self.assertTrue(self.project1.code) + # Change name project, parent project should change it too + self.project1.name = "Test new project" + self.assertEqual(self.project1.name, "Test new project") + self.assertEqual(self.project1.parent_project_name, "Test new project") + # Duplicate project, it should have (copy) at last + project_copy = self.project1.copy() + self.assertEqual(project_copy.name, "Test new project (copy)") + self.assertEqual(project_copy.parent_project_name, "Test new project (copy)") + # Check department should be change following project manager + self.assertEqual(self.project1.department_id, self.dep_admin) + with Form(self.project1) as p: + p.project_manager_id = self.employee_sale + p.save() + self.assertEqual(self.project1.department_id, self.dep_sales) + # Check plan amount when add it in project + self.assertFalse(self.project1.plan_amount) + self.project1.project_plan_ids.create( + { + "project_id": self.project1.id, + "date_from": self.today, + "date_to": self.today, + "amount": 100.0, + } + ) + self.assertEqual(self.project1.plan_amount, 100.0) + self.project1.action_cancel() + self.assertEqual(self.project1.state, "cancel") + self.project1.action_draft() + self.assertEqual(self.project1.state, "draft") + self.project1.action_confirm() + self.assertEqual(self.project1.state, "confirm") + # Check auto close project, if today is more than date to + self.project1.action_auto_expired() + self.assertEqual(self.project1.state, "confirm") + yesterday = self.today - timedelta(days=1) + self.project1.date_to = yesterday + self.project1.action_auto_expired() + self.assertEqual(self.project1.state, "close") + # Check name search with code + res = self.project1.name_search("NON_EXIST_CODE") + self.assertFalse(res) + self.project1.code = "C_TEST000001" + res = self.project1.name_search("0001") + self.assertTrue(res) + + @freeze_time("2001-02-01") + def test_02_split_project(self): + """add code project and search""" + self.project2 = self.project1.copy() + projects = self.project1 + self.project2 + # Not allow split multi project + with self.assertRaises(UserError): + self.ResProject.with_context(active_ids=projects.ids).action_split_project() + split_project_wizard = self.ResProject.with_context( + active_ids=self.project1.ids + ).action_split_project() + self.assertEqual(split_project_wizard["res_model"], "split.project.wizard") + # Create new wizard for split project + self.assertTrue(self.project1.active) + project_wizard = self._create_project_wizard(self.project1) + with self.assertRaises(UserError): + project_wizard.split_project() + project_wizard = self._create_project_wizard(self.project1, "new split1") + new_project_list = project_wizard.split_project() + new_project = self.ResProject.browse(new_project_list["domain"][0][2]) + # Parent project will archive when split project + self.assertFalse(self.project1.active) + self.assertEqual(new_project.name, "new split1") + self.assertEqual(new_project.parent_project_id, self.project1) + self.assertEqual(new_project.parent_project_name, self.project1.name) + + @freeze_time("2001-02-01") + def test_03_project_sequence(self): + # Check code in project + today = datetime.today() + project = self._create_res_project( + "Test Project Sequence", + self.dep_admin.id, + today, + today, + code="/", + ) + self.assertNotEqual(project.code, "/") + # Check code in split project + project_wizard = self._create_project_wizard(project, "new split1") + new_project_list = project_wizard.split_project() + new_project = self.ResProject.browse(new_project_list["domain"][0][2]) + self.assertEqual(new_project.code, f"{project.code}-1") diff --git a/res_project/views/hr_department_views.xml b/res_project/views/hr_department_views.xml new file mode 100644 index 00000000..90b86997 --- /dev/null +++ b/res_project/views/hr_department_views.xml @@ -0,0 +1,17 @@ + + + + hr.department.form + hr.department + + + + + + + + diff --git a/res_project/views/hr_employee_views.xml b/res_project/views/hr_employee_views.xml new file mode 100644 index 00000000..2d38f5b6 --- /dev/null +++ b/res_project/views/hr_employee_views.xml @@ -0,0 +1,15 @@ + + + + hr.employee.form + hr.employee + + + + + + + + + + diff --git a/res_project/views/res_project_menuitem.xml b/res_project/views/res_project_menuitem.xml new file mode 100644 index 00000000..1a6fd3ce --- /dev/null +++ b/res_project/views/res_project_menuitem.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/res_project/views/res_project_split.xml b/res_project/views/res_project_split.xml new file mode 100644 index 00000000..ccc22c7b --- /dev/null +++ b/res_project/views/res_project_split.xml @@ -0,0 +1,11 @@ + + + + Split Project + + + form + code + action = model.action_split_project() + + diff --git a/res_project/views/res_project_views.xml b/res_project/views/res_project_views.xml new file mode 100644 index 00000000..eba808c7 --- /dev/null +++ b/res_project/views/res_project_views.xml @@ -0,0 +1,247 @@ + + + + res.project.list + res.project + + + + + + + + + + + + + + + res.project.form + res.project + +
+
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + res.project.select + res.project + + + + + + + + + + + + + + + + + + + + + + + + + + + + Projects + res.project + list,form + + +

+ No projects found. Let's create one! +

+
+
+ + Confirm Project + + + code + list + + records.action_confirm() + + +
diff --git a/res_project/wizard/__init__.py b/res_project/wizard/__init__.py new file mode 100644 index 00000000..0955a992 --- /dev/null +++ b/res_project/wizard/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import split_project_wizard diff --git a/res_project/wizard/split_project_wizard.py b/res_project/wizard/split_project_wizard.py new file mode 100644 index 00000000..bb6579c6 --- /dev/null +++ b/res_project/wizard/split_project_wizard.py @@ -0,0 +1,98 @@ +# Copyright 2021 Ecosoft Co., Ltd. (http://ecosoft.co.th) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, fields, models +from odoo.exceptions import UserError + + +class SplitProjectWizard(models.TransientModel): + _name = "split.project.wizard" + _description = "Split Project Wizard" + + parent_project_id = fields.Many2one( + comodel_name="res.project", + string="Parent", + readonly=True, + required=True, + ) + parent_project_name = fields.Char( + string="Parent Project", + readonly=True, + required=True, + ) + project_manager_id = fields.Many2one( + comodel_name="hr.employee", + string="Project Manager", + readonly=True, + ) + department_id = fields.Many2one( + comodel_name="hr.department", + string="Department", + readonly=True, + required=True, + ) + date_from = fields.Date( + string="Project Start", + readonly=True, + required=True, + ) + date_to = fields.Date( + string="Project End", + readonly=True, + required=True, + ) + member_ids = fields.Many2many( + comodel_name="hr.employee", + string="Member", + readonly=True, + ) + line_ids = fields.One2many( + comodel_name="split.project.wizard.line", + inverse_name="wizard_id", + string="Lines", + ) + + def split_project(self): + self.ensure_one() + if not self.line_ids: + raise UserError(_("Please add a new project name")) + ResProject = self.env["res.project"] + # Archive parent project record + self.parent_project_id.action_archive() + # Create new project record + vals = [line._prepare_project_val() for line in self.line_ids] + new_projects = ResProject.with_context( + split_project=True, # for do something before create project + parent_project_id=self.parent_project_id.id, + ).create(vals) + return { + "name": _("Project"), + "type": "ir.actions.act_window", + "res_model": "res.project", + "view_mode": "list,form", + "context": self.env.context, + "domain": [("id", "in", new_projects.ids)], + } + + +class SplitProjectWizardLine(models.TransientModel): + _name = "split.project.wizard.line" + _description = "Split Project Wizard Line" + + wizard_id = fields.Many2one(comodel_name="split.project.wizard") + project_name = fields.Char() + + def _prepare_project_val(self): + self.ensure_one() + wizard = self.wizard_id + return { + "name": self.project_name, + "parent_project_id": wizard.parent_project_id.id, + "parent_project_name": wizard.parent_project_name, + "date_from": wizard.date_from, + "date_to": wizard.date_to, + "project_manager_id": wizard.project_manager_id.id, + "department_id": wizard.department_id.id, + "company_id": self.env.company.id, + "member_ids": [(6, 0, wizard.member_ids.ids)], + } diff --git a/res_project/wizard/split_project_wizard_view.xml b/res_project/wizard/split_project_wizard_view.xml new file mode 100644 index 00000000..013f7af8 --- /dev/null +++ b/res_project/wizard/split_project_wizard_view.xml @@ -0,0 +1,41 @@ + + + split.project.wizard.form + split.project.wizard + +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+
+