Compare commits
14 commits
18.0
...
bemade_fsm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48410382b2 | ||
|
|
a923b3c892 | ||
|
|
cd144953c5 | ||
|
|
7a573d58ed | ||
|
|
804ce5b599 | ||
|
|
cf3fccfa32 | ||
|
|
e1a923466c | ||
|
|
b0c87b81d1 | ||
|
|
b90b98e2b2 | ||
|
|
fb83435c89 | ||
|
|
8bca6a6150 | ||
|
|
0eada63857 | ||
|
|
403d6c0ee7 | ||
|
|
a216cc95d6 |
24 changed files with 789 additions and 350 deletions
|
|
@ -20,7 +20,7 @@
|
|||
########################################################################################
|
||||
{
|
||||
'name': 'Improved Field Service Management',
|
||||
'version': '15.0.0.2.0',
|
||||
'version': '15.0.0.3.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',
|
||||
|
|
@ -32,25 +32,27 @@
|
|||
'sale',
|
||||
'sale_project',
|
||||
'sale_stock',
|
||||
'industry_fsm',
|
||||
'industry_fsm_sale',
|
||||
'industry_fsm_stock',
|
||||
'industry_fsm_report',
|
||||
'bemade_partner_root_ancestor',
|
||||
'mail',
|
||||
],
|
||||
'data': ['views/task_template_views.xml',
|
||||
'views/equipment.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'views/product_views.xml',
|
||||
'views/res_partner.xml',
|
||||
'views/menus.xml',
|
||||
'views/task_views.xml',
|
||||
'views/sale_order_views.xml',
|
||||
],
|
||||
'data': [
|
||||
'data/fsm_data.xml',
|
||||
'views/task_template_views.xml',
|
||||
'views/equipment.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'views/product_views.xml',
|
||||
'views/res_partner.xml',
|
||||
'views/menus.xml',
|
||||
'views/task_views.xml',
|
||||
'views/sale_order_views.xml',
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_tests': [
|
||||
'bemade_fsm/static/tests/tours/task_template_tour.js',
|
||||
'bemade_fsm/static/tests/tours/equipment_tour.js',
|
||||
'bemade_fsm/static/tests/tours/sale_order_tour.js',
|
||||
],
|
||||
},
|
||||
'installable': True,
|
||||
|
|
|
|||
31
bemade_fsm/data/fsm_data.xml
Normal file
31
bemade_fsm/data/fsm_data.xml
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="planning_project_stage_work_completed" model="project.task.type">
|
||||
<field name="sequence">15</field>
|
||||
<field name="name">Work Executed</field>
|
||||
<field name="legend_blocked">Blocked</field>
|
||||
<field name="fold" eval="False"/>
|
||||
<field name="is_closed" eval="False"/>
|
||||
</record>
|
||||
<!-- Since the Field Service project has no_update="1" we use a workaround here -->
|
||||
<function model="ir.model.data" name="write">
|
||||
<function name="search" model="ir.model.data">
|
||||
<value eval="[('name', '=', 'fsm_project'), ('module', '=', 'industry_fsm'), ('model', '=', 'project.project')]"/>
|
||||
</function>
|
||||
<value eval="{'noupdate': False}"/>
|
||||
</function>
|
||||
<record id="industry_fsm.fsm_project" model="project.project">
|
||||
<field name="type_ids"
|
||||
eval="[(4, ref('industry_fsm.planning_project_stage_0')), (4, ref('industry_fsm.planning_project_stage_1')), (4, ref('industry_fsm.planning_project_stage_2')), (4, ref('planning_project_stage_work_completed')), (4, ref('industry_fsm.planning_project_stage_3')), (4, ref('industry_fsm.planning_project_stage_4'))]"/>
|
||||
<field name="allow_subtasks"
|
||||
eval="True"/>
|
||||
</record>
|
||||
<function model="ir.model.data" name="write">
|
||||
<function name="search" model="ir.model.data">
|
||||
<value eval="[('name', '=', 'fsm_project'), ('module', '=', 'industry_fsm'), ('model', '=', 'project.project')]"/>
|
||||
</function>
|
||||
<value eval="{'noupdate': True}"/>
|
||||
</function>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -4,3 +4,4 @@ from . import sale_order
|
|||
from . import equipment
|
||||
from . import task
|
||||
from . import res_partner
|
||||
from . import fsm_visit
|
||||
|
|
|
|||
|
|
@ -35,13 +35,12 @@ class Equipment(models.Model):
|
|||
|
||||
tag_ids = fields.Many2many('bemade_fsm.equipment.tag',
|
||||
string='Application',
|
||||
tracking=True,
|
||||
help="Classify and analyze your equipment categories like: Boiler, Laboratory, "
|
||||
"Waste water, Pure water")
|
||||
partner_id = fields.Many2one('res.partner',
|
||||
string="Owner",
|
||||
compute="_compute_partner",
|
||||
search="_search_partner",)
|
||||
search="_search_partner", )
|
||||
|
||||
description = fields.Text(string="Description",
|
||||
tracking=True)
|
||||
|
|
@ -53,9 +52,11 @@ class Equipment(models.Model):
|
|||
|
||||
location_notes = fields.Text(string="Physical Location Notes",
|
||||
tracking=True)
|
||||
task_ids = fields.One2many(comodel_name='project.task',
|
||||
inverse_name='equipment_id',
|
||||
string='Interventions')
|
||||
task_ids = fields.Many2many(comodel_name='project.task',
|
||||
relation="bemade_fsm_task_equipment_rel",
|
||||
column1="equipment_id",
|
||||
column2="task_id",
|
||||
string='Interventions')
|
||||
|
||||
@api.depends('partner_location_id')
|
||||
def _compute_partner(self):
|
||||
|
|
|
|||
37
bemade_fsm/models/fsm_visit.py
Normal file
37
bemade_fsm/models/fsm_visit.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
from odoo import models, fields, api, _
|
||||
|
||||
|
||||
class FSMVisit(models.Model):
|
||||
_name = "bemade_fsm.visit"
|
||||
_description = 'Represents a single visit by assigned service personnel.'
|
||||
|
||||
label = fields.Text(string="Label",
|
||||
required=True,
|
||||
related='so_section_id.name',
|
||||
readonly=False)
|
||||
approx_date = fields.Date(string='Approximate Date')
|
||||
so_section_id = fields.Many2one(comodel_name="sale.order.line",
|
||||
string="Sale Order Section",
|
||||
help="The section on the sale order that represents the labour and parts for "
|
||||
"this visit")
|
||||
sale_order_id = fields.Many2one(comodel_name="sale.order",
|
||||
string="Sales Order",
|
||||
required=True)
|
||||
is_completed = fields.Boolean(string="Completed",
|
||||
related="so_section_id.is_fully_delivered")
|
||||
is_invoiced = fields.Boolean(string="Invoiced",
|
||||
related="so_section_id.is_fully_delivered_and_invoiced")
|
||||
|
||||
def _compute_is_invoiced(self):
|
||||
self.is_invoiced = False
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
recs = super().create(vals_list)
|
||||
for i, rec in enumerate(recs):
|
||||
rec.so_section_id = rec.env['sale.order.line'].create({
|
||||
'order_id': rec.sale_order_id.id,
|
||||
'display_type': 'line_section',
|
||||
'name': vals_list[i]['label'],
|
||||
})
|
||||
return recs
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
from odoo import fields, models, api, _, Command
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
|
|
@ -6,7 +7,10 @@ class SaleOrder(models.Model):
|
|||
|
||||
equipment_id = fields.Many2one(comodel_name="bemade_fsm.equipment",
|
||||
string="Equipment to Service",
|
||||
tracking=True)
|
||||
tracking=True,
|
||||
compute="_compute_equipment",
|
||||
inverse="_inverse_equipment",
|
||||
store=True)
|
||||
|
||||
site_contacts = fields.Many2many(comodel_name='res.partner',
|
||||
relation="sale_order_site_contacts_rel",
|
||||
|
|
@ -21,8 +25,17 @@ class SaleOrder(models.Model):
|
|||
inverse='_inverse_default_contacts',
|
||||
string='Work Order Recipients',
|
||||
store=True)
|
||||
visit_ids = fields.One2many(comodel_name='bemade_fsm.visit',
|
||||
inverse_name="sale_order_id",
|
||||
readonly=False)
|
||||
|
||||
@api.depends('partner_id')
|
||||
@api.onchange('partner_shipping_id')
|
||||
def _onchange_partner_shipping_id(self):
|
||||
super()._onchange_partner_shipping_id()
|
||||
self._compute_equipment()
|
||||
self._compute_default_contacts()
|
||||
|
||||
@api.depends('partner_shipping_id')
|
||||
def _compute_default_contacts(self):
|
||||
for rec in self:
|
||||
rec.site_contacts = rec.partner_shipping_id.site_contacts
|
||||
|
|
@ -31,10 +44,10 @@ class SaleOrder(models.Model):
|
|||
def _inverse_default_contacts(self):
|
||||
pass
|
||||
|
||||
@api.depends('partner_id')
|
||||
@api.depends('partner_shipping_id')
|
||||
def _compute_equipment(self):
|
||||
for rec in self:
|
||||
rec.equipment_ids = self.partner_shipping_id.equipment_ids if len(
|
||||
rec.equipment_id = self.partner_shipping_id.equipment_ids if len(
|
||||
self.partner_shipping_id.equipment_ids) <= 1 else False
|
||||
|
||||
def _inverse_equipment(self):
|
||||
|
|
@ -44,6 +57,18 @@ class SaleOrder(models.Model):
|
|||
class SaleOrderLine(models.Model):
|
||||
_inherit = 'sale.order.line'
|
||||
|
||||
visit_id = fields.One2many(comodel_name="bemade_fsm.visit",
|
||||
inverse_name="so_section_id",
|
||||
string="Visit")
|
||||
is_fully_delivered = fields.Boolean(string="Fully Delivered",
|
||||
compute="_compute_is_fully_delivered",
|
||||
help="Indicates whether a line or all the lines in a section have been"
|
||||
"entirely delivered.")
|
||||
is_fully_delivered_and_invoiced = fields.Boolean(string="Fully Invoiced",
|
||||
compute="_compute_is_fully_invoiced",
|
||||
help="Indicates whether a line or all the lines in a section have been"
|
||||
"entirely delivered and invoiced.")
|
||||
|
||||
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
|
||||
|
|
@ -69,6 +94,10 @@ class SaleOrderLine(models.Model):
|
|||
task.child_ids.write({'sale_order_id': None, 'sale_line_id': None, })
|
||||
return task
|
||||
|
||||
def _generate_task_name(template=None):
|
||||
template_name = template and template.name
|
||||
return f"{self.order_id.name}: {self.order_id.partner_shipping_id.name} - {self.name} ({template_name})"
|
||||
|
||||
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.
|
||||
|
||||
|
|
@ -79,12 +108,14 @@ class SaleOrderLine(models.Model):
|
|||
:param parent: project.task to set as the parent to this task.
|
||||
"""
|
||||
vals = self._timesheet_create_task_prepare_values(project)
|
||||
vals['name'] = f"{vals['name']} ({template.name})" if not parent else template.name
|
||||
vals['name'] = _generate_task_name(template) if not parent else 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
|
||||
vals['planned_hours'] = template.planned_hours
|
||||
if template.equipment_ids:
|
||||
vals['equipment_ids'] = [Command.set(template.equipment_ids.ids)]
|
||||
return vals
|
||||
|
||||
tmpl = self.product_id.task_template_id
|
||||
|
|
@ -98,5 +129,38 @@ class SaleOrderLine(models.Model):
|
|||
"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)
|
||||
task.equipment_id = self.order_id.equipment_id
|
||||
if not task.equipment_ids and self.order_id.equipment_id:
|
||||
task.write({'equipment_ids': [Command.set([self.order_id.equipment_id.id])]})
|
||||
task.name = _generate_task_name(tmpl)
|
||||
return task
|
||||
|
||||
@api.depends('order_id.order_line', 'display_type', 'qty_to_deliver', 'order_id.order_line.qty_to_deliver',
|
||||
'order_id.order_line.display_type')
|
||||
def _compute_is_fully_delivered(self):
|
||||
self.is_fully_delivered = self._iterate_items_compute_bool(lambda l: l.qty_to_deliver == 0)
|
||||
|
||||
@api.depends('is_fully_delivered')
|
||||
def _compute_is_fully_invoiced(self):
|
||||
if not self.is_fully_delivered:
|
||||
self.is_fully_delivered_and_invoiced = False
|
||||
return
|
||||
self.is_fully_delivered_and_invoiced = self._iterate_items_compute_bool(lambda l: l.qty_to_invoice == 0)
|
||||
|
||||
def _iterate_items_compute_bool(self, single_line_func):
|
||||
if not self.display_type:
|
||||
return single_line_func(self)
|
||||
elif self.display_type == 'line_note':
|
||||
return True
|
||||
else:
|
||||
for line in self.order_id.order_line:
|
||||
found = False
|
||||
if line == self:
|
||||
found = True
|
||||
if not found:
|
||||
continue
|
||||
if found and line.display_type == 'line_section':
|
||||
return True
|
||||
val = single_line_func(self)
|
||||
if not val:
|
||||
return val
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -5,7 +5,12 @@ from odoo.exceptions import ValidationError
|
|||
class Task(models.Model):
|
||||
_inherit = "project.task"
|
||||
|
||||
equipment_id = fields.Many2one("bemade_fsm.equipment", string="Equipment to Service", tracking=True)
|
||||
equipment_ids = fields.Many2many(comodel_name="bemade_fsm.equipment",
|
||||
relation="bemade_fsm_task_equipment_rel",
|
||||
column1="task_id",
|
||||
column2="equipment_id",
|
||||
string="Equipment to Service",
|
||||
tracking=True,)
|
||||
|
||||
work_order_contacts = fields.Many2many(comodel_name="res.partner",
|
||||
relation="task_work_order_contact_rel",
|
||||
|
|
@ -23,6 +28,10 @@ class Task(models.Model):
|
|||
inverse="_inverse_contacts",
|
||||
store=True)
|
||||
|
||||
# Override related field to make it return false if this is an FSM subtask
|
||||
allow_billable = fields.Boolean(string="Can be billed",
|
||||
related=False,
|
||||
compute="_compute_allow_billable",)
|
||||
@api.depends('sale_line_id.order_id.site_contacts', 'sale_line_id.order_id.work_order_contacts')
|
||||
def _compute_contacts(self):
|
||||
""" The work order contacts and site contacts for a given task are taken from the sale order if the task
|
||||
|
|
@ -45,3 +54,11 @@ class Task(models.Model):
|
|||
'work_order_contacts': [Command.set(rec.work_order_contacts.ids)],
|
||||
'site_contacts': [Command.set(rec.site_contacts.ids)],
|
||||
})
|
||||
|
||||
@api.depends('parent_id', 'project_id')
|
||||
def _compute_allow_billable(self):
|
||||
for rec in self:
|
||||
if rec.parent_id and rec.project_id and rec.project_id.is_fsm:
|
||||
rec.allow_billable = False
|
||||
else:
|
||||
rec.allow_billable = rec.project_id.allow_billable
|
||||
|
|
|
|||
|
|
@ -1,24 +1,34 @@
|
|||
from odoo import models, fields, api, _
|
||||
from odoo import models, fields, api, _, Command
|
||||
|
||||
|
||||
class TaskTemplate(models.Model):
|
||||
_name = 'project.task.template'
|
||||
_description = "Template for new project tasks"
|
||||
|
||||
@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.")
|
||||
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", ondelete='cascade')
|
||||
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)
|
||||
planned_hours = fields.Float("Initially Planned Hours")
|
||||
equipment_ids = fields.Many2many(comodel_name="bemade_fsm.equipment",
|
||||
relation="bemade_fsm_task_template_equipment_rel",
|
||||
column1="task_template_id",
|
||||
column2="equipment_id",
|
||||
string="Equipment to Service",)
|
||||
|
||||
def action_open_task(self):
|
||||
return {
|
||||
|
|
@ -29,4 +39,8 @@ class TaskTemplate(models.Model):
|
|||
'context': self._context
|
||||
}
|
||||
|
||||
|
||||
@api.onchange('customer')
|
||||
def _onchange_customer(self):
|
||||
for rec in self:
|
||||
new_equipment_ids = [eq.id for eq in rec.equipment_ids if eq.partner_location_id == rec.customer]
|
||||
rec.write({'equipment_ids': [Command.set(new_equipment_ids)]})
|
||||
|
|
|
|||
|
|
@ -3,4 +3,5 @@ access_bemade_fsm_task_template,bemade_fsm_task_template,model_project_task_temp
|
|||
access_bemade_fsm_task_template,bemade_fsm_task_template,model_project_task_template,project.group_project_user,1,1,1,1
|
||||
access_bemade_fsm_equipment,bemade_fsm_equipment,model_bemade_fsm_equipment,base.group_user,1,1,1,1
|
||||
access_bemade_fsm_equipment_tag,bemade_fsm_equipment_tag,model_bemade_fsm_equipment_tag,base.group_user,1,1,1,1
|
||||
access_bemade_fsm_equipment_type,access_bemade_fsm_equipment_type,model_bemade_fsm_equipment_type,base.group_user,1,0,0,0
|
||||
access_bemade_fsm_equipment_type,access_bemade_fsm_equipment_type,model_bemade_fsm_equipment_type,base.group_user,1,0,0,0
|
||||
access_bemade_fsm_visit,access_bemade_fsm_visit,model_bemade_fsm_visit,base.group_user,1,1,1,1
|
||||
|
|
|
|||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import tour from 'web_tour.tour';
|
||||
|
||||
const TEST_COMPANY = "Test Partner Company";
|
||||
const TEST_COMPANY = "Test Partner";
|
||||
const TEST_EQPT1 = "Test Equipment 1";
|
||||
const TEST_EQPT2 = "Test Equipment 2";
|
||||
tour.register('equipment_base_tour', {
|
||||
|
|
@ -21,7 +21,6 @@ tour.register('equipment_base_tour', {
|
|||
}, {
|
||||
content: 'Click the create button',
|
||||
trigger: '.o_list_button_add',
|
||||
extra_trigger: 'li.breadcrumb-item.active:has(span:contains(Equipment))',
|
||||
}, {
|
||||
content: 'Add a tag',
|
||||
trigger: 'input[name="pid_tag"]',
|
||||
|
|
@ -33,13 +32,13 @@ tour.register('equipment_base_tour', {
|
|||
}, {
|
||||
content: 'Set the partner',
|
||||
trigger: 'div[name="partner_location_id"] div div input',
|
||||
run: 'text Test Partner Company',
|
||||
run: `text ${TEST_COMPANY}`,
|
||||
}, {
|
||||
content: 'Click the partner in the dropdown',
|
||||
trigger: `li a.dropdown-item:contains(${TEST_COMPANY})`,
|
||||
}, {
|
||||
content: 'Save equipment',
|
||||
trigger: 'button.o_form_button_save',
|
||||
trigger: 'button.o_list_button_save',
|
||||
}, {
|
||||
/* Navigate to the client and make sure that there are two equipments saved (one from the Python test case) */
|
||||
content: 'Navigate to the Clients submenu',
|
||||
|
|
@ -65,7 +64,6 @@ tour.register('equipment_base_tour', {
|
|||
extra_trigger: `h1 span.o_field_partner_autocomplete[name="name"]:contains(${TEST_COMPANY})`,
|
||||
}, {
|
||||
content: 'Make sure we have a first test equipment',
|
||||
/*trigger: `div[name="equipment_ids"]:has(td:contains(${TEST_EQPT1}))`,*/
|
||||
trigger: `td:contains(${TEST_EQPT1})`,
|
||||
run: function () {
|
||||
},
|
||||
|
|
|
|||
40
bemade_fsm/static/tests/tours/sale_order_tour.js
Normal file
40
bemade_fsm/static/tests/tours/sale_order_tour.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import tour from 'web_tour.tour';
|
||||
|
||||
const SO_NAME = "TEST ORDER 2"
|
||||
const PRODUCT_NAME = "Test Product 3"
|
||||
tour.register('sale_order_tour', {
|
||||
test: true,
|
||||
url: '/web',
|
||||
},
|
||||
[tour.stepUtils.showAppsMenuItem(), {
|
||||
content: 'Navigate to the Service menu',
|
||||
trigger: '.o_app[data-menu-xmlid="sale.sale_menu_root"]',
|
||||
}, {
|
||||
content: 'Search for the sales order',
|
||||
trigger: 'input.o_searchview_input',
|
||||
run: `text ${SO_NAME}`,
|
||||
}, {
|
||||
content: 'Validate Search',
|
||||
trigger: '.o_menu_item.o_selection_focus',
|
||||
run: 'click',
|
||||
}, {
|
||||
content: 'Open the test order',
|
||||
trigger: `.o_data_cell[name="client_order_ref"]:contains(${SO_NAME})`,
|
||||
}, {
|
||||
content: 'Click the view tasks button',
|
||||
trigger: 'button[name="action_view_task"]',
|
||||
}, /*{
|
||||
content: 'Click the first task',
|
||||
trigger: `div.o_kanban_record:has(span:contains(${PRODUCT_NAME}))`,
|
||||
},*/ {
|
||||
content: 'Click on the ready to invoice button',
|
||||
trigger: 'button[name="action_fsm_validate"]',
|
||||
extra_trigger: `li.breadcrumb-item.active:has(span:contains(${PRODUCT_NAME}))`
|
||||
}, {
|
||||
content: 'View the SO',
|
||||
trigger: 'button[name="action_view_so"]',
|
||||
// extra_trigger: 'button[title="Current state"]:contains(Done)',
|
||||
}
|
||||
]);
|
||||
|
|
@ -38,7 +38,7 @@ tour.register('task_equipment_tour', {
|
|||
trigger: `li a.dropdown-item:contains(${TEST_COMPANY})`,
|
||||
}, {
|
||||
content: 'Save equipment',
|
||||
trigger: 'button.o_form_button_save',
|
||||
trigger: 'button.o_list_button_save',
|
||||
}, {
|
||||
/* Navigate to the client and make sure that there are two equipments saved (one from the Python test case) */
|
||||
content: 'Navigate to the Clients submenu',
|
||||
|
|
|
|||
|
|
@ -3,4 +3,4 @@ from . import test_task_template
|
|||
from . import test_sale_order
|
||||
from . import test_equipment
|
||||
from . import test_fsm_contact_setting
|
||||
from . import test_so_task_contacts
|
||||
from . import test_fsm_visit
|
||||
|
|
|
|||
|
|
@ -3,37 +3,163 @@ from odoo import Command
|
|||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class FSMManagerUserTransactionCase(TransactionCase):
|
||||
class BemadeFSMBaseTest(TransactionCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
def _generate_project_manager_user(cls, name, login):
|
||||
group_ids = cls.__get_user_groups()
|
||||
user_group_project_manager = cls.env.ref('project.group_project_manager')
|
||||
user_group_fsm_manager = cls.env.ref('industry_fsm.group_fsm_manager')
|
||||
group_ids.append(user_group_fsm_manager.id)
|
||||
group_ids.append(user_group_project_manager.id)
|
||||
|
||||
return cls.__generate_user(name, login, group_ids)
|
||||
|
||||
@classmethod
|
||||
def _generate_project_user(cls, name, login):
|
||||
group_ids = cls.__get_user_groups()
|
||||
return cls.__generate_user(name, login, group_ids)
|
||||
|
||||
@classmethod
|
||||
def __generate_user(cls, name, login, group_ids):
|
||||
return cls.env['res.users'].with_context({'no_reset_password': True}).create({
|
||||
'name': name,
|
||||
'login': login,
|
||||
'password': login,
|
||||
'email': f"{login}@test.co",
|
||||
'groups_id': [Command.set(group_ids)]
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def __get_user_groups(cls):
|
||||
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_fsm_user = cls.env.ref('industry_fsm.group_fsm_user')
|
||||
user_group_fsm_manager = cls.env.ref('industry_fsm.group_fsm_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_group_sales_manager = cls.env.ref('sales_team.group_sale_manager')
|
||||
user_product_customer = cls.env.ref('customer_product_code.group_product_customer_code_user')
|
||||
|
||||
group_ids = [user_group_employee.id,
|
||||
user_group_project_user.id,
|
||||
user_group_project_manager.id,
|
||||
user_group_fsm_user.id,
|
||||
user_group_fsm_manager.id,
|
||||
user_group_sales_user.id,
|
||||
user_group_sales_manager.id, ]
|
||||
user_group_sales_manager.id,
|
||||
user_group_sales_user.id, ]
|
||||
if user_product_customer:
|
||||
group_ids.append(user_product_customer.id)
|
||||
return group_ids
|
||||
|
||||
# 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': [Command.set(group_ids)],
|
||||
@classmethod
|
||||
def _generate_partner(cls, name: str = 'Test Company', company_type: str = 'company', parent=None,
|
||||
location_type='other'):
|
||||
""" Generates a partner with basic address filled in.
|
||||
|
||||
:param name: The partner's name.
|
||||
:param company_type: The type of partner, either 'company' or 'person' are accepted."""
|
||||
return cls.env['res.partner'].create({
|
||||
'name': name,
|
||||
'company_type': company_type,
|
||||
'street': '123 Street St.',
|
||||
'city': 'Montreal',
|
||||
'state_id': cls.env.ref('base.state_ca_qc').id,
|
||||
'country_id': cls.env.ref('base.ca').id,
|
||||
'parent_id': parent and parent.id or False,
|
||||
'type': location_type,
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def _generate_sale_order(cls, partner=None, client_order_ref='Test Order', equipment=None):
|
||||
partner = partner or cls._generate_partner()
|
||||
return cls.env['sale.order'].create({
|
||||
'partner_id': partner.id,
|
||||
'client_order_ref': client_order_ref,
|
||||
'equipment_id': equipment and equipment.id or False,
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def _generate_sale_order_line(cls, sale_order, product, qty=1.0, uom=None, price=100.0, tax_id=False):
|
||||
return cls.env['sale.order.line'].create({
|
||||
'order_id': sale_order.id,
|
||||
'product_id': product.id,
|
||||
'product_uom_qty': qty,
|
||||
'product_uom': uom and uom.id or cls.env.ref("uom.product_uom_hour").id,
|
||||
'price_unit': price,
|
||||
'tax_id': tax_id,
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def _generate_equipment(cls, name='test equipment', partner=None):
|
||||
return cls.env['bemade_fsm.equipment'].create({
|
||||
'name': name,
|
||||
'partner_location_id': partner and partner.id or False,
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def _generate_product(cls, name='Test Product', product_type='service', service_tracking='task_global_project',
|
||||
project=None, task_template=None, service_policy='delivered_manual', uom=None):
|
||||
if 'project' in service_tracking and not project:
|
||||
project = cls.env.ref("industry_fsm.fsm_project")
|
||||
uom_id = uom and uom.id or cls.env.ref("uom.product_uom_hour").id or False
|
||||
return cls.env['product.product'].create({
|
||||
'name': name,
|
||||
'type': product_type,
|
||||
'service_tracking': service_tracking,
|
||||
'project_id': service_tracking in ('task_global_project', 'project_only') and project.id or False,
|
||||
'project_template_id': service_tracking == 'task_in_project' and project.id or False,
|
||||
'task_template_id': task_template and task_template.id or False,
|
||||
'service_policy': service_policy,
|
||||
'uom_id': uom_id,
|
||||
'uom_po_id': uom_id,
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def _generate_fsm_project(cls, name='Test Project'):
|
||||
return cls.env['project.project'].create({
|
||||
'name': name,
|
||||
'allow_material': True,
|
||||
'allow_timesheets': True,
|
||||
'allow_subtasks': True,
|
||||
'allow_quotations': True,
|
||||
'allow_worksheets': True,
|
||||
'is_fsm': True,
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def _generate_task_template(cls, parent=None, structure=None, names=None, planned_hours=1,
|
||||
equipment=None):
|
||||
""" Generates a task template with the specified structure and naming.
|
||||
|
||||
:param parent: The parent task template for the top-level task template being generated
|
||||
:param structure: A list of integers describing the number of tasks for each level of descendants. An empty
|
||||
list represents only one top-level task template. If no structure is given, an empty list
|
||||
will be used in its place.
|
||||
:param names: The name prefixes to be given to the task templates at each level. Each prefix will be followed
|
||||
by a sequential integer for its level. Child 1, Child 2, Grandchild 1, etc. If no names argument
|
||||
is passed, a default ['Task Template'] argument will be used.
|
||||
:param planned_hours: The number of planned hours for the top-level task template being generated.
|
||||
:param equipment: The equipment to add as linked equipment to the task template."""
|
||||
if not names:
|
||||
names = ['Task Template']
|
||||
if not structure:
|
||||
structure = []
|
||||
if len(structure) != len(names) - 1:
|
||||
raise ValueError("The length of the structure argument must contain exactly one element less than the "
|
||||
"names argument.")
|
||||
name = names.pop(0)
|
||||
template = cls.env['project.task.template'].create({
|
||||
'name': name,
|
||||
'parent': parent and parent.id or False,
|
||||
'planned_hours': planned_hours,
|
||||
'equipment_ids': [Command.set(equipment and [equipment.id] or [])],
|
||||
})
|
||||
parent = template
|
||||
while structure:
|
||||
subtasks = []
|
||||
for i in range(0, structure[0]):
|
||||
subtasks.append(cls.env['project.task.template'].create({
|
||||
'parent': parent.id,
|
||||
'name': names[0] + f" {i}",
|
||||
}))
|
||||
structure.pop(0)
|
||||
names.pop(0)
|
||||
parent = subtasks[0]
|
||||
return template
|
||||
|
|
|
|||
|
|
@ -1,53 +1,36 @@
|
|||
from odoo.tests.common import HttpCase, tagged
|
||||
from .test_bemade_fsm_common import FSMManagerUserTransactionCase
|
||||
from .test_bemade_fsm_common import BemadeFSMBaseTest
|
||||
from odoo import Command
|
||||
from odoo.exceptions import MissingError
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class TestEquipmentCommon(FSMManagerUserTransactionCase):
|
||||
class TestEquipment(BemadeFSMBaseTest):
|
||||
def test_crud(self):
|
||||
partner_company = self._generate_partner()
|
||||
partner_contact = self._generate_partner('Site Contact', 'person', partner_company)
|
||||
equipment = self._generate_equipment('Test Equipment 1', partner_company)
|
||||
|
||||
# Just make sure the basic ORM stuff is OK
|
||||
self.assertTrue(equipment in partner_company.equipment_ids)
|
||||
self.assertTrue(len(partner_company.equipment_ids) == 1)
|
||||
|
||||
# Delete should cascade
|
||||
partner_company.write({'equipment_ids': [Command.set([])]})
|
||||
with self.assertRaises(MissingError):
|
||||
equipment.name
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install', 'slow')
|
||||
class TestEquipmentTours(HttpCase, BemadeFSMBaseTest):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
# Set up the test partner
|
||||
cls.partner_company = cls.env['res.partner'].create({
|
||||
'name': 'Test Partner Company',
|
||||
'company_type': 'company',
|
||||
'street': '123 Street St.',
|
||||
'city': 'Montreal',
|
||||
'state_id': cls.env['res.country.state'].search([('name', 'ilike', 'Quebec%')]).id,
|
||||
'country_id': cls.env['res.country'].search([('name', '=', 'Canada')]).id
|
||||
})
|
||||
|
||||
cls.partner_contact = cls.env['res.partner'].create({
|
||||
'name': 'Site Contact',
|
||||
'company_type': 'person',
|
||||
'parent_id': cls.partner_company.id,
|
||||
})
|
||||
|
||||
cls.equipment = cls.env['bemade_fsm.equipment'].create({
|
||||
'name': 'Test Equipment 1',
|
||||
'partner_location_id': cls.partner_company.id,
|
||||
})
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestEquipmentBase(TestEquipmentCommon):
|
||||
|
||||
def test_crd(self):
|
||||
# Just make sure the basic ORM stuff is OK
|
||||
self.assertTrue(self.equipment in self.partner_company.equipment_ids)
|
||||
self.assertTrue(len(self.partner_company.equipment_ids) == 1)
|
||||
self.partner_company.write({'equipment_ids': [Command.set([])]})
|
||||
# Delete should cascade
|
||||
with self.assertRaises(MissingError):
|
||||
self.equipment.name
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestEquipmentTours(HttpCase, TestEquipmentCommon):
|
||||
partner = cls._generate_partner("Test Partner")
|
||||
cls._generate_partner('Site Contact', 'person', parent=partner)
|
||||
cls._generate_equipment(name='Test Equipment 1', partner=partner)
|
||||
cls.user = cls._generate_project_manager_user("Mister PM", 'misterpm')
|
||||
|
||||
def test_equipment_base_tour(self):
|
||||
self.start_tour('/web', 'equipment_base_tour',
|
||||
|
|
|
|||
|
|
@ -1,56 +1,89 @@
|
|||
from odoo.tests import TransactionCase, HttpCase, tagged
|
||||
from odoo.tests import TransactionCase, HttpCase, tagged, Form
|
||||
from odoo import Command
|
||||
from .test_bemade_fsm_common import FSMManagerUserTransactionCase
|
||||
from .test_bemade_fsm_common import BemadeFSMBaseTest
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class SaleOrderFSMContactsCase(FSMManagerUserTransactionCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
Partner = cls.env['res.partner']
|
||||
cls.parent_co = Partner.create({
|
||||
'name': 'Parent Co',
|
||||
'company_type': 'company',
|
||||
})
|
||||
cls.contact_1 = Partner.create({
|
||||
'name': 'Contact 1',
|
||||
'company_type': 'person',
|
||||
'parent_id': cls.parent_co.id,
|
||||
})
|
||||
cls.contact_2 = Partner.create({
|
||||
'name': 'Contact 2',
|
||||
'company_type': 'person',
|
||||
'parent_id': cls.parent_co.id,
|
||||
})
|
||||
class SaleOrderFSMContactsCase(BemadeFSMBaseTest):
|
||||
|
||||
def test_site_contacts(self):
|
||||
parent_co = self._generate_partner('Parent Co')
|
||||
contact_1 = self._generate_partner('Contact 1', 'person', parent_co)
|
||||
contact_2 = self._generate_partner('Contact 2', 'person', parent_co)
|
||||
|
||||
# Make sure the SO pulls the defaults from the partner correctly
|
||||
self.parent_co.write({'site_contacts': [Command.set([self.contact_1.id, self.contact_2.id])]})
|
||||
so = self.env['sale.order'].create({
|
||||
'partner_id': self.parent_co.id,
|
||||
})
|
||||
self.assertTrue(so.site_contacts == self.parent_co.site_contacts)
|
||||
parent_co.write({'site_contacts': [Command.set([contact_1.id, contact_2.id])]})
|
||||
so = self._generate_sale_order(parent_co)
|
||||
self.assertTrue(so.site_contacts == parent_co.site_contacts)
|
||||
|
||||
# Make sure updating the site contacts on the SO doesn't feed back to the partner
|
||||
so.write({'site_contacts': [Command.set([self.contact_1.id])]})
|
||||
self.assertTrue(self.contact_1 in so.site_contacts)
|
||||
self.assertTrue(self.contact_2 not in so.site_contacts)
|
||||
so.write({'site_contacts': [Command.set([contact_1.id])]})
|
||||
self.assertTrue(contact_1 in so.site_contacts)
|
||||
self.assertTrue(contact_2 not in so.site_contacts)
|
||||
self.assertTrue(so.site_contacts != so.partner_id.site_contacts and len(so.partner_id.site_contacts) == 2)
|
||||
|
||||
def test_default_workorder_contacts(self):
|
||||
parent_co = self._generate_partner('Parent Co')
|
||||
contact_1 = self._generate_partner('Contact 1', 'person', parent_co)
|
||||
contact_2 = self._generate_partner('Contact 2', 'person', parent_co)
|
||||
|
||||
# Make sure the SO pulls the defaults from the partner correctly
|
||||
self.parent_co.write({'work_order_contacts': [Command.set([self.contact_1.id, self.contact_2.id])]})
|
||||
so = self.env['sale.order'].create({
|
||||
'partner_id': self.parent_co.id,
|
||||
})
|
||||
self.assertTrue(self.contact_1 in self.parent_co.work_order_contacts)
|
||||
self.assertTrue(self.contact_2 in self.parent_co.work_order_contacts)
|
||||
self.assertTrue(self.contact_1 in so.work_order_contacts)
|
||||
self.assertTrue(self.contact_2 in so.work_order_contacts)
|
||||
parent_co.write({'work_order_contacts': [Command.set([contact_1.id, contact_2.id])]})
|
||||
so = self._generate_sale_order(parent_co)
|
||||
self.assertTrue(contact_1 in parent_co.work_order_contacts)
|
||||
self.assertTrue(contact_2 in parent_co.work_order_contacts)
|
||||
self.assertTrue(contact_1 in so.work_order_contacts)
|
||||
self.assertTrue(contact_2 in so.work_order_contacts)
|
||||
|
||||
# Make sure setting the work order contacts on the SO doesn't feed back to the partner
|
||||
so.write({'work_order_contacts': [Command.set([self.contact_1.id])]})
|
||||
self.assertTrue(self.contact_1 in so.work_order_contacts)
|
||||
self.assertTrue(self.contact_2 not in so.work_order_contacts)
|
||||
so.write({'work_order_contacts': [Command.set([contact_1.id])]})
|
||||
self.assertTrue(contact_1 in so.work_order_contacts)
|
||||
self.assertTrue(contact_2 not in so.work_order_contacts)
|
||||
self.assertTrue(
|
||||
so.work_order_contacts != so.partner_id.work_order_contacts and len(so.partner_id.work_order_contacts) == 2)
|
||||
|
||||
def test_multilayer_site_contacts(self):
|
||||
parent_co = self._generate_partner('Parent Co')
|
||||
shipping_location = self._generate_partner('Shipping Location', 'company', parent_co, 'delivery')
|
||||
wo_contact_1 = self._generate_partner('WO Contact 1', 'person', shipping_location)
|
||||
wo_contact_2 = self._generate_partner('WO Contact 2', 'person', shipping_location)
|
||||
site_contact_1 = self._generate_partner('Site Contact 1', 'person', shipping_location)
|
||||
site_contact_2 = self._generate_partner('Site Contact 2', 'person', shipping_location)
|
||||
shipping_location.write({
|
||||
'work_order_contacts': [Command.set([wo_contact_1.id, wo_contact_2.id])],
|
||||
'site_contacts': [Command.set([site_contact_1.id, site_contact_2.id])]
|
||||
})
|
||||
|
||||
so = self._generate_sale_order(parent_co)
|
||||
so.write({'partner_shipping_id': shipping_location.id})
|
||||
|
||||
self.assertEqual(so.site_contacts, shipping_location.site_contacts)
|
||||
self.assertEqual(so.work_order_contacts, shipping_location.work_order_contacts)
|
||||
|
||||
def test_onchange_shipping_address(self):
|
||||
parent_co = self._generate_partner('Parent Co')
|
||||
shipping_location = self._generate_partner('Shipping Location', 'company', parent_co, 'delivery')
|
||||
wo_contact_1 = self._generate_partner('WO Contact 1', 'person', shipping_location)
|
||||
wo_contact_2 = self._generate_partner('WO Contact 2', 'person', shipping_location)
|
||||
site_contact_1 = self._generate_partner('Site Contact 1', 'person', shipping_location)
|
||||
site_contact_2 = self._generate_partner('Site Contact 2', 'person', shipping_location)
|
||||
shipping_location.write({
|
||||
'work_order_contacts': [Command.set([wo_contact_1.id, wo_contact_2.id])],
|
||||
'site_contacts': [Command.set([site_contact_1.id, site_contact_2.id])]
|
||||
})
|
||||
|
||||
so = self._generate_sale_order(parent_co)
|
||||
|
||||
# Set back to a location without site or work order contacts
|
||||
form = Form(so)
|
||||
form.partner_shipping_id = parent_co
|
||||
form.save()
|
||||
# Make sure the contacts were reset on the SO
|
||||
self.assertFalse(so.work_order_contacts)
|
||||
self.assertFalse(so.site_contacts)
|
||||
|
||||
# Now set back to the location with the FSM contacts and make sure they get set on the SO
|
||||
form.partner_shipping_id = shipping_location
|
||||
form.save()
|
||||
self.assertEqual(so.work_order_contacts, shipping_location.work_order_contacts)
|
||||
self.assertEqual(so.site_contacts, shipping_location.site_contacts)
|
||||
|
|
|
|||
79
bemade_fsm/tests/test_fsm_visit.py
Normal file
79
bemade_fsm/tests/test_fsm_visit.py
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
from odoo.tests import TransactionCase, tagged, Form
|
||||
from .test_bemade_fsm_common import BemadeFSMBaseTest
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class FSMVisitTest(BemadeFSMBaseTest):
|
||||
|
||||
def test_create_visit_sets_name_on_section(self):
|
||||
so = self._generate_sale_order()
|
||||
self._add_service_so_line(so)
|
||||
|
||||
visit = self._generate_visit(so)
|
||||
|
||||
self.assertTrue(visit.so_section_id)
|
||||
self.assertEqual(visit.so_section_id.name, visit.label)
|
||||
|
||||
def test_change_visit_section_name(self):
|
||||
so = self._generate_sale_order()
|
||||
visit = self._generate_visit(so, label="First Label")
|
||||
line = visit.so_section_id
|
||||
|
||||
line.name = "Second Label"
|
||||
|
||||
self.assertEqual(visit.label, "Second Label")
|
||||
|
||||
def test_change_visit_label_changes_section_name(self):
|
||||
so = self._generate_sale_order()
|
||||
visit = self._generate_visit(so, label="First Label")
|
||||
line = visit.so_section_id
|
||||
|
||||
visit.label = "Second Label"
|
||||
|
||||
self.assertEqual(line.name, "Second Label")
|
||||
|
||||
def test_visit_completes_when_task_completes(self):
|
||||
so = self._generate_sale_order()
|
||||
visit = self._generate_visit(so)
|
||||
self._add_service_so_line(so, task=True)
|
||||
so.action_confirm()
|
||||
task = so.order_line.filtered(lambda l: l.task_id).task_id
|
||||
|
||||
task.action_fsm_validate()
|
||||
|
||||
self.assertTrue(visit.is_completed)
|
||||
|
||||
def test_visit_shows_invoiced_when_invoiced(self):
|
||||
so = self._generate_sale_order()
|
||||
visit = self._generate_visit(so)
|
||||
self._add_service_so_line(so, task=True)
|
||||
so.action_confirm()
|
||||
task = so.order_line.filtered(lambda l: l.task_id).task_id
|
||||
task.action_fsm_validate()
|
||||
|
||||
self._invoice_sale_order(so)
|
||||
|
||||
self.assertTrue(visit.is_invoiced)
|
||||
|
||||
def _invoice_sale_order(self, so):
|
||||
wiz = self.env['sale.advance.payment.inv'].with_context({'active_ids': [so.id]}).create({})
|
||||
wiz.create_invoices()
|
||||
inv = so.invoice_ids[-1]
|
||||
inv.action_post()
|
||||
return inv
|
||||
|
||||
def _generate_visit(self, sale_order, label="Test Label"):
|
||||
return self.env['bemade_fsm.visit'].create([{
|
||||
'sale_order_id': sale_order.id,
|
||||
'label': label,
|
||||
}])
|
||||
|
||||
def _add_service_so_line(self, sale_order, task: bool = False):
|
||||
""" Generates a sales order line for a service product.
|
||||
|
||||
:param sale_order: The sales order to which the new line is to be added
|
||||
:param task: If true, the created line will be for a product with service_tracking=task_global_project
|
||||
"""
|
||||
service_tracking = 'task_global_project' if task else 'no'
|
||||
product = self._generate_product(service_tracking=service_tracking)
|
||||
return self._generate_sale_order_line(sale_order, product)
|
||||
|
|
@ -1,97 +1,164 @@
|
|||
from .test_task_template import TestTaskTemplateCommon
|
||||
from .test_equipment import TestEquipmentCommon
|
||||
from odoo.tests.common import tagged
|
||||
from .test_task_template import BemadeFSMBaseTest
|
||||
from odoo.tests.common import tagged, HttpCase, Form
|
||||
from odoo import Command
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class TestSalesOrder(TestTaskTemplateCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.partner = cls.env['res.partner'].create({
|
||||
'name': 'Test Partner',
|
||||
})
|
||||
cls.sale_order1 = 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_order1.id,
|
||||
'tax_id': False,
|
||||
})
|
||||
cls.sol_serv_order_task_in_project = cls.env['sale.order.line'].create({
|
||||
'name': cls.product_task_in_project.name,
|
||||
'product_id': cls.product_task_in_project.id,
|
||||
'product_uom_qty': 1,
|
||||
'product_uom': cls.product_task_in_project.uom_id.id,
|
||||
'price_unit': 150.0,
|
||||
'order_id': cls.sale_order1.id,
|
||||
'tax_id': False,
|
||||
})
|
||||
cls.sale_order2 = cls.env['sale.order'].create({
|
||||
'partner_id': cls.partner.id,
|
||||
'client_order_ref': 'TEST ORDER',
|
||||
})
|
||||
cls.sol_tree_order = cls.env['sale.order.line'].create({
|
||||
'name': cls.product_task_tree_global_project.name,
|
||||
'product_id': cls.product_task_tree_global_project.id,
|
||||
'product_uom_qty': 1,
|
||||
'product_uom': cls.product_task_tree_global_project.uom_id.id,
|
||||
'price_unit': 120.0,
|
||||
'order_id': cls.sale_order2.id,
|
||||
'tax_id': False,
|
||||
})
|
||||
cls.sol_serv_order_task_in_project = cls.env['sale.order.line'].create({
|
||||
'name': cls.product_task_tree_in_project.name,
|
||||
'product_id': cls.product_task_tree_in_project.id,
|
||||
'product_uom_qty': 1,
|
||||
'product_uom': cls.product_task_tree_in_project.uom_id.id,
|
||||
'price_unit': 150.0,
|
||||
'order_id': cls.sale_order2.id,
|
||||
'tax_id': False,
|
||||
})
|
||||
|
||||
class TestSalesOrder(BemadeFSMBaseTest):
|
||||
@tagged('-at_install', 'post_install')
|
||||
def test_order_confirmation_simple_template(self):
|
||||
""" Confirming the order should create a task in the global project. """
|
||||
so = self.sale_order1
|
||||
""" Confirming the order should create a task in the global project based on the task template. """
|
||||
partner = self._generate_partner()
|
||||
so = self._generate_sale_order(partner=partner)
|
||||
task_template = self._generate_task_template(planned_hours=8)
|
||||
product = self._generate_product(task_template=task_template)
|
||||
sol = self._generate_sale_order_line(so, product=product)
|
||||
|
||||
so.action_confirm()
|
||||
sol1 = so.order_line[0]
|
||||
sol2 = so.order_line[1]
|
||||
task1 = sol1.task_id
|
||||
task2 = sol2.task_id
|
||||
self.assertTrue(task1)
|
||||
self.assertTrue(task2)
|
||||
self.assertTrue(self.task1.name in task1.name)
|
||||
self.assertTrue(self.task1.name in task2.name)
|
||||
self.assertTrue(self.task1.planned_hours == task1.planned_hours)
|
||||
|
||||
task = sol.task_id
|
||||
self.assertTrue(task)
|
||||
self.assertTrue(task_template.name in task.name)
|
||||
self.assertTrue(task_template.planned_hours == task.planned_hours)
|
||||
|
||||
def test_order_confirmation_tree_template(self):
|
||||
def assert_structure(sol):
|
||||
self.assertTrue(sol.task_id.child_ids and len(sol.task_id.child_ids) == 2)
|
||||
self.assertTrue(self.parent_task.name in sol.task_id.name)
|
||||
self.assertTrue(self.child_task_1.name in sol.task_id.child_ids[0].name)
|
||||
self.assertTrue(self.child_task_2.name in sol.task_id.child_ids[1].name)
|
||||
self.assertTrue(sol.task_id.child_ids[1].child_ids and len(sol.task_id.child_ids.child_ids) == 1)
|
||||
self.assertTrue(self.grandchild_task.name in sol.task_id.child_ids.child_ids[0].name)
|
||||
partner = self._generate_partner()
|
||||
so = self._generate_sale_order(partner=partner)
|
||||
parent_task = self._generate_task_template(structure=[2, 1],
|
||||
names=['Parent Template', 'Child Template',
|
||||
'Grandchild Template'])
|
||||
child_task_1 = parent_task.subtasks[0]
|
||||
child_task_2 = parent_task.subtasks[1]
|
||||
grandchild_task = parent_task.subtasks[0].subtasks[0]
|
||||
product = self._generate_product(task_template=parent_task)
|
||||
sol = self._generate_sale_order_line(so, product=product)
|
||||
|
||||
so = self.sale_order2
|
||||
so.action_confirm()
|
||||
sol1 = so.order_line[0]
|
||||
sol2 = so.order_line[1]
|
||||
assert_structure(sol1)
|
||||
assert_structure(sol2)
|
||||
|
||||
self.assertTrue(sol.task_id.child_ids and len(sol.task_id.child_ids) == 2)
|
||||
self.assertTrue(parent_task.name in sol.task_id.name)
|
||||
self.assertTrue(child_task_1.name in sol.task_id.child_ids[0].name)
|
||||
self.assertTrue(child_task_2.name in sol.task_id.child_ids[1].name)
|
||||
self.assertTrue(sol.task_id.child_ids[0].child_ids and len(sol.task_id.child_ids[0].child_ids) == 1)
|
||||
self.assertTrue(grandchild_task.name in sol.task_id.child_ids.child_ids[0].name)
|
||||
|
||||
def test_order_confirmation_equipment(self):
|
||||
so = self.sale_order1
|
||||
equipment = self.env['bemade_fsm.equipment'].create(
|
||||
{'name': 'test equipment', 'partner_location_id': so.partner_shipping_id.id})
|
||||
so.equipment_id = equipment.id
|
||||
""" The equipment selected on the SO should transfer to the task."""
|
||||
partner = self._generate_partner()
|
||||
equipment = self._generate_equipment(partner=partner)
|
||||
so = self._generate_sale_order(partner=partner, equipment=equipment)
|
||||
task_template = self._generate_task_template(planned_hours=8)
|
||||
product1 = self._generate_product(task_template=task_template)
|
||||
product2 = self._generate_product()
|
||||
sol1 = self._generate_sale_order_line(so, product=product1)
|
||||
sol2 = self._generate_sale_order_line(so, product=product2)
|
||||
|
||||
so.action_confirm()
|
||||
task = so.order_line[0].task_id
|
||||
self.assertTrue(task.equipment_id == equipment)
|
||||
|
||||
task1 = sol1.task_id
|
||||
task2 = sol2.task_id
|
||||
self.assertEqual(task1.equipment_ids[0], equipment)
|
||||
self.assertEqual(task2.equipment_ids[0], equipment)
|
||||
|
||||
def test_task_template_with_equipment_flow(self):
|
||||
""" The equipment selected on a task template should flow down to the task created on SO confirmation."""
|
||||
partner = self._generate_partner()
|
||||
equipment = self._generate_equipment(partner=partner)
|
||||
so = self._generate_sale_order(partner=partner)
|
||||
task_template = self._generate_task_template(equipment=equipment)
|
||||
product = self._generate_product(task_template=task_template)
|
||||
sol = self._generate_sale_order_line(so, product=product)
|
||||
|
||||
so.action_confirm()
|
||||
|
||||
self.assertEqual(sol.task_id.equipment_ids[0], equipment)
|
||||
|
||||
def test_task_mark_done(self):
|
||||
""" Marking the task linked to an SO line should mark the line delivered. Marking sub-tasks done should not."""
|
||||
partner = self._generate_partner()
|
||||
so = self._generate_sale_order(partner=partner)
|
||||
task_template = self._generate_task_template(structure=[2], names=["Parent Task", "Subtask"])
|
||||
product = self._generate_product(task_template=task_template)
|
||||
sol = self._generate_sale_order_line(so, product=product)
|
||||
so.action_confirm()
|
||||
parent_task = sol.task_id
|
||||
subtasks = parent_task._get_all_subtasks()
|
||||
|
||||
# Marking the subtasks done should not increment delivered quantity
|
||||
subtasks.action_fsm_validate()
|
||||
self.assertEqual(sol.qty_delivered, 0)
|
||||
|
||||
# Marking the top-level tasks done should set the delivered quantity to some non-zero value based on the UOM
|
||||
parent_task.action_fsm_validate()
|
||||
self.assertTrue(sol.qty_delivered != 0)
|
||||
|
||||
def test_task_contacts_through_sale_order(self):
|
||||
""" Make sure the site contacts and work order contacts transfer correctly from the SO to the task."""
|
||||
|
||||
partner = self._generate_partner()
|
||||
contact1 = self._generate_partner('Site contact', 'person', partner)
|
||||
contact2 = self._generate_partner('Work order contact', 'person', partner)
|
||||
partner.write({
|
||||
'site_contacts': [Command.set([contact1.id])],
|
||||
'work_order_contacts': [Command.set([contact2.id])],
|
||||
})
|
||||
so = self._generate_sale_order(partner)
|
||||
product = self._generate_product()
|
||||
sol = self._generate_sale_order_line(sale_order=so, product=product)
|
||||
|
||||
so.action_confirm()
|
||||
|
||||
self.assertEqual(so.work_order_contacts, partner.work_order_contacts)
|
||||
self.assertEqual(so.site_contacts, partner.site_contacts)
|
||||
self.assertEqual(sol.task_id.work_order_contacts, partner.work_order_contacts)
|
||||
self.assertEqual(sol.task_id.site_contacts, partner.site_contacts)
|
||||
|
||||
def test_changing_task_contacts_mirrors_with_sale_order(self):
|
||||
partner = self._generate_partner()
|
||||
contact = self._generate_partner("Contact", "person", partner)
|
||||
so = self._generate_sale_order(partner)
|
||||
product = self._generate_product()
|
||||
sol = self._generate_sale_order_line(so, product)
|
||||
so.action_confirm()
|
||||
task = sol.task_id
|
||||
task_form = Form(task)
|
||||
|
||||
# Now change the site/work order contact on the task and make sure it feeds back to the sales order
|
||||
task_form.site_contacts.add(contact)
|
||||
task_form.work_order_contacts.add(contact)
|
||||
task_form.save()
|
||||
|
||||
self.assertEqual(task.site_contacts, so.site_contacts)
|
||||
self.assertEqual(task.work_order_contacts, so.work_order_contacts)
|
||||
|
||||
# Test changing it on the SO feeds back to the task as well
|
||||
f = Form(so)
|
||||
f.work_order_contacts.remove(contact.id)
|
||||
f.site_contacts.remove(contact.id)
|
||||
f.save()
|
||||
|
||||
self.assertEqual(task.site_contacts, so.site_contacts)
|
||||
self.assertEqual(task.work_order_contacts, so.work_order_contacts)
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install", "slow")
|
||||
class TestSaleOrderTour(HttpCase, TestSalesOrder):
|
||||
def test_sale_order_tour_no_invoice_button_for_non_manager(self):
|
||||
so = self._generate_sale_order(client_order_ref='TEST ORDER 2')
|
||||
self._generate_sale_order_line(so, self._generate_product(name='Test Product 3'))
|
||||
self._generate_project_user('Mr. User', 'mruser')
|
||||
so.action_confirm()
|
||||
# Make sure a non-manager cannot mark a task as ready to invoice
|
||||
with self.assertRaises(AssertionError) as e:
|
||||
self.start_tour('/web', 'sale_order_tour',
|
||||
login='mruser', )
|
||||
self.assertTrue("Click on the ready to invoice button" in str(e.exception))
|
||||
|
||||
def test_task_mark_to_invoice(self):
|
||||
# Make sure that when a manager clicks the ready to invoice button, the qty delivered is updated on the SO
|
||||
so = self._generate_sale_order(client_order_ref='TEST ORDER 2')
|
||||
sol = self._generate_sale_order_line(so, self._generate_product(name='Test Product 3'))
|
||||
so.action_confirm()
|
||||
user = self._generate_project_manager_user('Mr. User', 'mruser')
|
||||
self.start_tour('/web', 'sale_order_tour', login=user.login)
|
||||
self.assertTrue(sol.qty_delivered != 0)
|
||||
|
|
|
|||
|
|
@ -1,58 +0,0 @@
|
|||
from .test_sale_order import TestSalesOrder
|
||||
from odoo import Command
|
||||
from odoo.tests.common import Form
|
||||
|
||||
|
||||
class TestSaleOrderTaskContacts(TestSalesOrder):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
Partner = cls.env['res.partner']
|
||||
cls.partner2 = Partner.create({
|
||||
'name': 'New Partner',
|
||||
'company_type': 'company',
|
||||
})
|
||||
cls.contact = Partner.create({
|
||||
'name': 'Contact',
|
||||
'company_type': 'person',
|
||||
'parent_id': cls.partner2.id,
|
||||
})
|
||||
cls.contact2 = cls.env['res.partner'].create({
|
||||
'name': 'Contact 2',
|
||||
'company_type': 'person',
|
||||
'parent_id': cls.partner2.id,
|
||||
})
|
||||
|
||||
def _test_task_contacts_from_so(self, field):
|
||||
""" Shorthand function for testing both work_order_contacts and site_contacts fields on SOs and tasks."""
|
||||
def ga(obj):
|
||||
return getattr(obj, field)
|
||||
# Add the default site/work order contact to the partner, create an SO with the partner
|
||||
self.partner2.write({field: [Command.set([self.contact.id])]})
|
||||
self.sale_order1.write(
|
||||
{'partner_id': self.partner2.id, field: [Command.set(ga(self.partner2).ids)]})
|
||||
so = self.sale_order1
|
||||
self.assertTrue(ga(so))
|
||||
# Confirm the SO and check that the task got the default carried over
|
||||
so.action_confirm()
|
||||
task = so.order_line[0].task_id
|
||||
self.assertTrue(ga(task))
|
||||
self.assertTrue(ga(task) == ga(so))
|
||||
# Now change the site/work order contact on the task and make sure it feeds back to the sales order
|
||||
f = Form(task)
|
||||
ga(f).add(self.contact2)
|
||||
f.save()
|
||||
self.assertTrue(ga(so) == ga(task))
|
||||
# Test changing it on the SO feeds back to the task as well
|
||||
f = Form(so)
|
||||
ga(f).remove(self.contact.id)
|
||||
f.save()
|
||||
self.assertTrue(ga(so) == ga(task))
|
||||
|
||||
def test_work_order_contacts_on_task(self):
|
||||
# Make sure work order contacts on the sales order transfer to the task on order confirmation
|
||||
self._test_task_contacts_from_so('work_order_contacts')
|
||||
|
||||
def test_site_contacts_on_task(self):
|
||||
# Make sure site contacts from the sales order transfer to the task on order confirmation
|
||||
self._test_task_contacts_from_so('site_contacts')
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
from .test_bemade_fsm_common import FSMManagerUserTransactionCase
|
||||
from odoo.tests.common import HttpCase, tagged
|
||||
from .test_bemade_fsm_common import BemadeFSMBaseTest
|
||||
from odoo.tests.common import HttpCase, tagged, Form
|
||||
from odoo.exceptions import MissingError
|
||||
from odoo import Command
|
||||
from psycopg2.errors import ForeignKeyViolation
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class TestTaskTemplateCommon(FSMManagerUserTransactionCase):
|
||||
class TestTaskTemplateCommon(BemadeFSMBaseTest):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
|
|
@ -18,70 +18,27 @@ class TestTaskTemplateCommon(FSMManagerUserTransactionCase):
|
|||
'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.project_template = cls.env['project.project'].create({
|
||||
'name': 'Test Project Template',
|
||||
})
|
||||
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,
|
||||
'project_template_id': cls.project_template.id,
|
||||
'uom_po_id': hours_uom.id,
|
||||
'uom_id': hours_uom.id,
|
||||
})
|
||||
cls.project = cls.env.ref('industry_fsm.fsm_project')
|
||||
|
||||
cls.product_task_global_project = cls._generate_product(name='Test Product 1', task_template=cls.task1)
|
||||
cls.project_template = cls._generate_fsm_project('Test Project Template')
|
||||
cls.product_task_in_project = cls._generate_product(name='Test Product 2', project=cls.project_template,
|
||||
service_tracking='task_in_project', task_template=cls.task1)
|
||||
|
||||
# Set up a task template tree with 2 children and 1 grandchild
|
||||
cls.parent_task = cls.env['project.task.template'].create({
|
||||
'name': 'Parent Template',
|
||||
'planned_hours': cls.PLANNED_HOURS,
|
||||
})
|
||||
cls.child_task_1 = cls.env['project.task.template'].create({
|
||||
'name': 'Child Template 1',
|
||||
'parent': cls.parent_task.id,
|
||||
})
|
||||
cls.child_task_2 = cls.env['project.task.template'].create({
|
||||
'name': 'Child Template 2',
|
||||
'parent': cls.parent_task.id,
|
||||
})
|
||||
cls.parent_task.write({'subtasks': [Command.set([cls.child_task_1.id, cls.child_task_2.id])]})
|
||||
cls.grandchild_task = cls.env['project.task.template'].create({
|
||||
'name': 'Grandchild Template',
|
||||
'parent': cls.child_task_2.id
|
||||
})
|
||||
cls.child_task_2.write({'subtasks': [Command.set([cls.grandchild_task.id])]})
|
||||
cls.parent_task = cls._generate_task_template(structure=[2, 1],
|
||||
names=['Parent Template', 'Child Template',
|
||||
'Grandchild Template'])
|
||||
cls.child_task_1 = cls.parent_task.subtasks[0]
|
||||
cls.child_task_2 = cls.parent_task.subtasks[1]
|
||||
cls.grandchild_task = cls.child_task_1.subtasks[0]
|
||||
|
||||
# Create products using the task tree we just created
|
||||
cls.product_task_tree_global_project = cls.env['product.product'].create({
|
||||
'name': 'Test Product 3',
|
||||
'type': 'service',
|
||||
'service_tracking': 'task_global_project',
|
||||
'project_id': cls.project.id,
|
||||
'task_template_id': cls.parent_task.id,
|
||||
'uom_id': hours_uom.id,
|
||||
'uom_po_id': hours_uom.id,
|
||||
})
|
||||
cls.product_task_tree_in_project = cls.env['product.product'].create({
|
||||
'name': 'Test Product 2',
|
||||
'type': 'service',
|
||||
'service_tracking': 'task_in_project',
|
||||
'task_template_id': cls.parent_task.id,
|
||||
'project_template_id': cls.project_template.id,
|
||||
'uom_po_id': hours_uom.id,
|
||||
'uom_id': hours_uom.id,
|
||||
})
|
||||
cls.product_task_tree_global_project = cls._generate_product(name='Test Product 3',
|
||||
task_template=cls.parent_task)
|
||||
cls.product_task_tree_in_project = cls._generate_product(name="Test Product 2", project=cls.project_template,
|
||||
service_tracking='task_in_project',
|
||||
task_template=cls.parent_task)
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
|
|
@ -95,14 +52,31 @@ class TestTaskTemplate(TestTaskTemplateCommon):
|
|||
def test_delete_subtask_template(self):
|
||||
""" Deletion of a child task should be OK even if the parent is on a product. Children of the deleted
|
||||
subtask should be deleted."""
|
||||
self.child_task_2.unlink()
|
||||
self.child_task_1.unlink()
|
||||
# Reading deleted child's name field should be impossible
|
||||
with self.assertRaises(MissingError):
|
||||
test = self.grandchild_task.name
|
||||
|
||||
def test_dissociating_customer_resets_equipment_appropriately(self):
|
||||
partner1 = self._generate_partner()
|
||||
partner2 = self._generate_partner()
|
||||
equipment1 = self._generate_equipment(partner=partner1)
|
||||
form = Form(self.task1)
|
||||
form.customer = partner1
|
||||
form.equipment_ids.add(equipment1)
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
# Switching the partner should trigger on_change that makes sure equipments are linked to the new partner
|
||||
form.customer = partner2
|
||||
|
||||
self.assertFalse(equipment1 in self.task1.equipment_ids)
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install', 'slow')
|
||||
class TestTaskTemplateTour(HttpCase, TestTaskTemplateCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls._generate_project_manager_user('Mister PM', 'misterpm')
|
||||
|
||||
def test_task_template_tour(self):
|
||||
self.start_tour('/web', 'task_template_tour',
|
||||
|
|
|
|||
|
|
@ -50,12 +50,12 @@
|
|||
<field name="name">bemade_fsm.equipment.tree</field>
|
||||
<field name="model">bemade_fsm.equipment</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Equipment">
|
||||
<tree string="Equipment" editable="bottom">
|
||||
<field name="pid_tag"/>
|
||||
<field name="name"/>
|
||||
<field name="description"/>
|
||||
<field name="tag_ids" widget="many2many_tags" options="{'no_open': False}"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="partner_location_id"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,11 @@
|
|||
<field name="arch" type="xml">
|
||||
<xpath expr="//page[@name='other_information']" position="before">
|
||||
<page name="field_service" string="Field Service">
|
||||
<group name="field_service_info">
|
||||
<group name="fsm_visits" string="Service Visits">
|
||||
<field name="visit_ids"
|
||||
context="{'tree_view_ref': 'bemade_fsm.bemade_fsm_visit_tree'}"/>
|
||||
</group>
|
||||
<group name="field_service_info" string="Contacts and Equipment">
|
||||
<field name="equipment_id"
|
||||
domain="[('partner_location_id', '=', partner_shipping_id)]"
|
||||
context="{'default_partner_location_id': partner_shipping_id,}"/>
|
||||
|
|
@ -21,5 +25,17 @@
|
|||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<record id="bemade_fsm_visit_tree" model="ir.ui.view">
|
||||
<field name="name">bemade_fsm.visit.tree</field>
|
||||
<field name="model">bemade_fsm.visit</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree editable="bottom">
|
||||
<field name="label"/>
|
||||
<field name="approx_date"/>
|
||||
<field name="is_completed" widget="boolean"/>
|
||||
<field name="is_invoiced" widget="boolean"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -23,6 +23,9 @@
|
|||
</group>
|
||||
<group>
|
||||
<field name="customer"/>
|
||||
<field name="equipment_ids"
|
||||
domain="[('partner_location_id', '=', customer)]"
|
||||
context="{'tree_view_ref': 'bemade_fsm.equipment_view_tree'}"/>
|
||||
<field name="tags" widget="many2many_tags"/>
|
||||
<field name="company_id" />
|
||||
</group>
|
||||
|
|
|
|||
|
|
@ -4,11 +4,13 @@
|
|||
<record id="bemade_fsm_project_task_form_inherit" model="ir.ui.view">
|
||||
<field name="name">bemade_fsm.project_task.form</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="inherit_id" ref="project.view_task_form2"/>
|
||||
<field name="inherit_id" ref="industry_fsm.view_task_form2_inherit"/>
|
||||
<field name="priority" eval="8"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='partner_id']" position="after">
|
||||
<field name="equipment_id" domain="[('partner_location_id', '=', partner_id)]"/>
|
||||
<field name="equipment_ids"
|
||||
domain="[('partner_location_id', '=', partner_id)]"
|
||||
context="{'tree_view_ref': 'bemade_fsm.equipment_view_tree'}"/>
|
||||
</xpath>
|
||||
<xpath expr="//page[@name='extra_info']" position="after">
|
||||
<page string="Field Service" name="field_service">
|
||||
|
|
@ -20,6 +22,14 @@
|
|||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
<button name="action_fsm_validate" class='btn-primary' position="attributes">
|
||||
<attribute name="string">Mark as Delivered</attribute>
|
||||
<attribute name="groups">industry_fsm.group_fsm_manager</attribute>
|
||||
</button>
|
||||
<button name="action_fsm_validate" class='btn-secondary' position="attributes">
|
||||
<attribute name="string">Mark as Delivered</attribute>
|
||||
<attribute name="groups">industry_fsm.group_fsm_manager</attribute>
|
||||
</button>
|
||||
</field>
|
||||
</record>
|
||||
<!-- Add parent_id = false to domain for My Tasks, All Tasks: To Schedule, All Tasks and To Invoice-->
|
||||
|
|
|
|||
Loading…
Reference in a new issue