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