bemade_fsm: Initial commit as submodule.

This commit is contained in:
Marc Durepos 2023-06-09 10:24:06 -04:00
commit b2a42e4ce8
19 changed files with 391 additions and 0 deletions

1
__init__.py Normal file
View file

@ -0,0 +1 @@
from . import models

22
__manifest__.py Normal file
View file

@ -0,0 +1,22 @@
{
'name': 'Durpro Field Service Management',
'version': '15.0.1.0.0',
'summary': 'Adds functionality necessary for managing field service operations at Durpro.',
'description': 'Adds functionality necessary for managing field service operations at Durpro.',
'category': 'Services/Field Service',
'author': 'Bemade Inc.',
'website': 'http://www.bemade.org',
'license': 'LGPL-3',
'depends': ['project', 'stock', 'sale', 'sale_project', 'sale_stock'],
'data': ['views/task_template_views.xml',
'security/ir.model.access.csv',
'views/product_views.xml',
],
'assets': {
'web.assets_tests': [
'durpro_fsm/static/tests/tours/task_template_tour.js',
],
},
'installable': True,
'auto_install': False
}

Binary file not shown.

3
models/__init__.py Normal file
View file

@ -0,0 +1,3 @@
from . import task_template
from . import product_template
from . import sale_order

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,7 @@
from odoo import fields, models, api
class ProductTemplate(models.Model):
_inherit = 'product.template'
task_template_id = fields.Many2one("project.task.template", string="Task Template", ondelete='restrict')

61
models/sale_order.py Normal file
View file

@ -0,0 +1,61 @@
from odoo import fields, models, api, _, Command
class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'
def _timesheet_create_task(self, project):
""" Generate task for the given so line, and link it.
:param project: record of project.project in which the task should be created
:return task: record of the created task
Override to add the logic needed to implement task templates."""
def _create_task_from_template(project, template, parent):
""" Recursively generates the task and any subtasks from a project.task.template. Subtasks are left without
a specific project_id so that only the top-level tasks shows up on the project task list/kanban views.
:param project: project.project record to set on the task's project_id field.
Pass the project.project model to leave task project_id blank. DO NOT pass False or None as this will
cause an error in _timesheet_create_task_prepare_values(project).
:param template: project.task.template to use to create the task.
:param parent: project.task to set as the parent to this task.
"""
values = _timesheet_create_task_prepare_values_from_template(project, template, parent)
task = self.env['project.task'].sudo().create(values)
subtasks = []
for subtask in tmpl.subtasks:
# Recurse, but pass the Project model so that no project_id gets set on the subtasks
subtask = _create_task_from_template(self.env['project.project'], tmpl, task)
subtasks.append(subtask)
task.write({'child_ids': [Command.set([t.id for t in subtasks])]})
return task
def _timesheet_create_task_prepare_values_from_template(project, template, parent):
""" Copies the values from a project.task.template over to the set of values used to create a project.task.
:param project: project.project record to set on the task's project_id field.
Pass the project.project model or an empty recordset to leave task project_id blank.
DO NOT pass False or None as this will cause an error in _timesheet_create_task_prepare_values(project).
:param template: project.task.template to use to create the task.
:param parent: project.task to set as the parent to this task.
"""
vals = self._timesheet_create_task_prepare_values(project)
vals.update({'name': f"{vals['name']} ({template.name})"})
vals['description'] = template.description or vals['description']
vals['parent_id'] = parent and parent.id
vals['user_ids'] = template.assignees.ids
vals['tag_ids'] = template.tags.ids
return vals
tmpl = self.product_id.task_template_id
if not tmpl:
task = super()._timesheet_create_task(project)
else:
task = _create_task_from_template(project, tmpl, None)
self.write({'task_id': task.id})
# post message on task
task_msg = _(
"This task has been created from: <a href=# data-oe-model=sale.order data-oe-id=%d>%s</a> (%s)") % (
self.order_id.id, self.order_id.name, self.product_id.name)
task.message_post(body=task_msg)
return task

31
models/task_template.py Normal file
View file

@ -0,0 +1,31 @@
from odoo import models, fields, api, _
class TaskTemplate(models.Model):
_name = 'project.task.template'
@api.model
def _current_company(self):
return self.env.company
name = fields.Char(string="Task Title", required=True)
description = fields.Html(string="Description")
assignees = fields.Many2many("res.users", string="Default Assignees", help="Employees assigned to tasks created from this template.")
customer = fields.Many2one("res.partner", string="Default Customer", help="Default customer for tasks created from this template.")
project = fields.Many2one("project.project", string="Default Project", help="Default project for tasks created from this template.")
tags = fields.Many2many("project.tags", string="Default Tags", help="Default tags for tasks created from this template.")
parent = fields.Many2one("project.task.template", string="Parent Task Template")
subtasks = fields.One2many("project.task.template", inverse_name="parent", string="Subtask Templates")
sequence = fields.Integer(string="Sequence")
company_id = fields.Many2one("res.company", string="Company", index=1, default=_current_company)
def action_open_task(self):
return {
'view_mode': 'form',
'res_model': 'project.task.template',
'res_id': self.id,
'type': 'ir.actions.act_window',
'context': self._context
}

View file

@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_durpro_fsm_task_template,durpro_fsm_task_template,model_project_task_template,project.group_project_manager,1,1,1,1
access_durpro_fsm_task_template,durpro_fsm_task_template,model_project_task_template,project.group_project_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_durpro_fsm_task_template durpro_fsm_task_template model_project_task_template project.group_project_manager 1 1 1 1
3 access_durpro_fsm_task_template durpro_fsm_task_template model_project_task_template project.group_project_user 1 1 1 1

View file

@ -0,0 +1,47 @@
/** @odoo-module */
import tour from 'web_tour.tour';
tour.register('task_template_tour', {
test: true,
url: '/web',
}, /* Make sure the test task is visible via the Project > Task Templates path */
[tour.stepUtils.showAppsMenuItem(), {
trigger: '.o_app[data-menu-xmlid="project.menu_main_pm"]',
}, {
content: 'Open the task templates list view.',
trigger: '.o_nav_entry[data-menu-xmlid="durpro_fsm.task_template_menu"]',
}, {
content: 'Open the existing task template.',
trigger: '.o_data_cell:contains("Template 1"):parent()',
}, {
content: 'Confirm that the header contains the task title.',
trigger: 'span[name="name"]:contains("Template 1")',
}, /* Make sure we can see the task template from the product */
tour.stepUtils.toggleHomeMenu(),
{
content: 'Open the sales menu.',
trigger: '.o_app[data-menu-xmlid="sale.sale_menu_root"]',
}, {
content: 'Open the product dropdown menu.',
trigger: 'button[data-menu-xmlid="sale.product_menu_catalog"]',
}, {
content: 'Click the products menu item.',
trigger: 'a.dropdown-item[data-menu-xmlid="sale.menu_product_template_action"]',
}, {
content: 'Search for the test product.',
trigger: 'input.o_searchview_input',
extra_trigger: 'li.breadcrumb-item.active:has(span:contains(Products))',
run: 'text Test Product 1',
}, {
trigger: '.o_menu_item.o_selection_focus',
content: 'Validate search',
run: 'click',
}, {
content: 'Open the test product.',
trigger: 'div.o_kanban_record:has(span:contains(Test Product 1))',
}, {
content: 'Ensure the product_template_id field is displayed with the "Template 1" mention.',
trigger: 'a.o_quick_editable[name="task_template_id"]:first-child:contains("Template 1")',
},
])

1
tests/__init__.py Normal file
View file

@ -0,0 +1 @@
from . import test_task_template

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,97 @@
from odoo.tests.common import TransactionCase, HttpCase, tagged
from odoo.exceptions import UserError
class TestTaskTemplateCommon(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
user_group_employee = cls.env.ref('base.group_user')
user_group_project_user = cls.env.ref('project.group_project_user')
user_group_project_manager = cls.env.ref('project.group_project_manager')
user_group_sales_manager = cls.env.ref('sales_team.group_sale_manager')
user_group_sales_user = cls.env.ref('sales_team.group_sale_salesman')
user_product_customer = cls.env.ref('customer_product_code.group_product_customer_code_user')
group_ids = [user_group_employee, user_group_project_user, user_group_project_manager, user_group_sales_user,
user_group_sales_manager]
if user_product_customer:
group_ids.append(user_product_customer)
# Test user with project access rights for the various tests
Users = cls.env['res.users'].with_context({'no_reset_password': True})
cls.user = Users.create({
'name': 'Project Manager',
'login': 'misterpm',
'password': 'misterpm',
'email': 'mrpm@testco.com',
'signature': 'Mr. PM',
'groups_id': [(6, 0, [user_group_employee.id, user_group_project_user.id, user_group_project_manager.id,
user_group_sales_user.id])]
})
hours_uom = cls.env['uom.uom'].search([('name', '=', 'Hour')]) or False
# Test product to use with the various tests
cls.task1 = cls.env['project.task.template'].create({
'name': 'Template 1',
})
cls.project = cls.env['project.project'].create({
'name': 'Test Project',
})
cls.product_task_global_project = cls.env['product.product'].create({
'name': 'Test Product 1',
'type': 'service',
'service_tracking': 'task_global_project',
'project_id': cls.project.id,
'task_template_id': cls.task1.id,
'uom_id': hours_uom.id,
'uom_po_id': hours_uom.id,
})
cls.product_task_in_project = cls.env['product.product'].create({
'name': 'Test Product 2',
'type': 'service',
'service_tracking': 'task_in_project',
'task_template_id': cls.task1.id,
'uom_po_id': hours_uom.id,
'uom_id': hours_uom.id,
})
cls.partner = cls.env['res.partner'].create({'name': 'Test Partner'})
cls.sale_order = cls.env['sale.order'].create({
'partner_id': cls.partner.id,
'client_order_ref': 'TEST ORDER',
})
cls.sol_serv_order = cls.env['sale.order.line'].create({
'name': cls.product_task_global_project.name,
'product_id': cls.product_task_global_project.id,
'product_uom_qty': 1,
'product_uom': cls.product_task_global_project.uom_id.id,
'price_unit': 120.0,
'order_id': cls.sale_order.id,
'tax_id': False,
})
@tagged('-at_install', 'post_install')
class TestTaskTemplate(TestTaskTemplateCommon):
def test_delete_task_template(self):
"""User should never be able to delete a task template used on a product"""
with self.assertRaises(UserError):
self.task1.unlink()
def test_order_confirmation_single_task(self):
""" Confirming the order should create a task in the global project. """
self.sale_order.action_confirm()
so = self.sale_order
sol = self.sale_order.order_line[0]
task = sol.task_id
self.assertTrue(task)
@tagged('-at_install', 'post_install')
class TestTaskTemplateTour(HttpCase, TestTaskTemplateCommon):
def test_task_template_tour(self):
self.start_tour('/web', 'task_template_tour',
login='misterpm', )

17
views/product_views.xml Normal file
View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="product_template_form_inherit" model="ir.ui.view">
<field name="name">durpro_fsm.product.template.form</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="sale_project.product_template_form_view_invoice_policy_inherit_sale_project"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='project_id']" position="after">
<field name="task_template_id"
attrs="{'invisible': [('service_tracking', 'not in', ('task_global_project', 'task_in_project'))]}"/>
</xpath>
</field>
</record>
</data>
</odoo>

View file

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="task_template_form_view" model="ir.ui.view">
<field name="name">durpro_fsm.task_template.form</field>
<field name="model">project.task.template</field>
<field name="arch" type="xml">
<form string="Task Template">
<sheet>
<div class="oe_title">
<label for="name"/>
<h1>
<field name="name" placeholder="Title"/>
</h1>
</div>
<group>
<group>
<field name="project"/>
<field name="assignees" widget="many2many_avatar_user"/>
<field name="parent"/>
</group>
<group>
<field name="customer"/>
<field name="tags" widget="many2many_tags"/>
<field name="company_id" />
</group>
</group>
<notebook>
<page name="description_page" string="Description">
<field name="description" type="html" options="{'collaborative': true}"/>
</page>
<page name="subtasks_page" string="Subtasks">
<field name="subtasks">
<tree editable="bottom">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="customer"/>
<field name="assignees" widget="many2many_avatar_user"/>
<button name="action_open_task" type="object" title="View Task"
string="View Task" class="btn btn-link pull-right"/>
</tree>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="task_template_tree_view" model="ir.ui.view">
<field name="name">project.task_template.tree</field>
<field name="model">project.task.template</field>
<field name="arch" type="xml">
<tree string="Task Template">
<field name="name"/>
<field name="assignees" widget="many2many_avatar_user"/>
<field name="project"/>
<field name="parent"/>
</tree>
</field>
</record>
<record id="task_template_search_view" model="ir.ui.view">
<field name="name">project.task_template.search</field>
<field name="model">project.task.template</field>
<field name="arch" type="xml">
<search string="Task Template">
<field name="name"/>
<field name="project"/>
<field name="assignees"/>
<field name="parent"/>
<field name="subtasks"/>
<group expand="1" string="Group By">
<filter string="Project" name="groupby_project" domain="[]"
context="{'group_by':'project'}"/>
<filter string="Parent Task" name="groupby_parent" domain="[]"
context="{'group_by':'parent'}"/>
<filter string="Customer" name="groupby_customer" domain="[]"
context="{'group_by':'customer'}"/>
</group>
</search>
</field>
</record>
<record id="task_template_act_window" model="ir.actions.act_window">
<field name="name">Task Template</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">project.task.template</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
There are no task templates, click above to create one.
</p>
</field>
</record>
<menuitem name="Task Templates" id="task_template_menu" parent="project.menu_main_pm"
action="task_template_act_window" groups="project.group_project_manager,project.group_project_user"/>
</data>
</odoo>