bemade_fsm: Initial commit as submodule.
This commit is contained in:
commit
b2a42e4ce8
19 changed files with 391 additions and 0 deletions
1
__init__.py
Normal file
1
__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
from . import models
|
||||||
22
__manifest__.py
Normal file
22
__manifest__.py
Normal 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
|
||||||
|
}
|
||||||
BIN
__pycache__/__init__.cpython-310.pyc
Normal file
BIN
__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
3
models/__init__.py
Normal file
3
models/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
from . import task_template
|
||||||
|
from . import product_template
|
||||||
|
from . import sale_order
|
||||||
BIN
models/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
models/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
models/__pycache__/product_template.cpython-310.pyc
Normal file
BIN
models/__pycache__/product_template.cpython-310.pyc
Normal file
Binary file not shown.
BIN
models/__pycache__/sale_order.cpython-310.pyc
Normal file
BIN
models/__pycache__/sale_order.cpython-310.pyc
Normal file
Binary file not shown.
BIN
models/__pycache__/task_template.cpython-310.pyc
Normal file
BIN
models/__pycache__/task_template.cpython-310.pyc
Normal file
Binary file not shown.
7
models/product_template.py
Normal file
7
models/product_template.py
Normal 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
61
models/sale_order.py
Normal 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
31
models/task_template.py
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
3
security/ir.model.access.csv
Normal file
3
security/ir.model.access.csv
Normal 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
|
||||||
|
47
static/tests/tours/task_template_tour.js
Normal file
47
static/tests/tours/task_template_tour.js
Normal 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
1
tests/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
from . import test_task_template
|
||||||
BIN
tests/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
tests/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
tests/__pycache__/test_task_template.cpython-310.pyc
Normal file
BIN
tests/__pycache__/test_task_template.cpython-310.pyc
Normal file
Binary file not shown.
97
tests/test_task_template.py
Normal file
97
tests/test_task_template.py
Normal 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
17
views/product_views.xml
Normal 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>
|
||||||
101
views/task_template_views.xml
Normal file
101
views/task_template_views.xml
Normal 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>
|
||||||
Loading…
Reference in a new issue