bemade_fsm: Changes to site and work order contact behaviour

1. Site contacts and work order contacts now get set on create and write
   of all FSM project tasks. If the task has no parent_id, it will try
   to set them from the sale order if it has one. Otherwise, they are
   simply inherited from the parent.
2. Changing the site or work order contact on a task no longer feeds
   back to the sale order.
3. Changing the site or work order contacts on a parent task propagates
   the change to its subtasks.
This commit is contained in:
Marc Durepos 2024-05-02 09:56:52 -04:00
parent ba40259525
commit dc04ab952b
6 changed files with 102 additions and 76 deletions

View file

@ -20,7 +20,7 @@
########################################################################################
{
'name': 'Improved Field Service Management',
'version': '17.0.0.1.1',
'version': '17.0.0.2.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',

View file

@ -48,14 +48,14 @@ class Equipment(models.Model):
args = args or []
if name:
equipments = self.search([
'|', '|', '|',
'|', '|',
('pid_tag', operator, name),
('name', operator, name),
('partner_location_id.name', operator, name)],
limit=limit)
else:
equipments = self.search(args, limit=limit)
return equipments.name_get()
return [(equipment.id, equipment.display_name) for equipment in equipments]
def action_view_equipment(self):
return {

View file

@ -1,7 +1,4 @@
from odoo import fields, models, api, Command, _
from odoo.exceptions import ValidationError, UserError
from odoo.osv import expression
from collections import defaultdict, namedtuple
from odoo.addons.project.models.project_task import CLOSED_STATES
import re
@ -23,9 +20,6 @@ class Task(models.Model):
relation="task_work_order_contact_rel",
column1="task_id",
column2="res_partner_id",
compute="_compute_contacts",
inverse="_inverse_contacts",
store=True
)
site_contacts = fields.Many2many(
@ -33,9 +27,6 @@ class Task(models.Model):
relation="task_site_contact_rel",
column1="task_id",
column2="res_partner_id",
compute="_compute_contacts",
inverse="_inverse_contacts",
store=True
)
# Override related field to make it return false if this is an FSM subtask
@ -77,6 +68,12 @@ class Task(models.Model):
def create(self, vals):
res = super().create(vals)
for rec in res:
if rec.parent_id and rec.is_fsm:
rec.partner_id = rec.parent_id.partner_id
if not rec.work_order_contacts and rec.parent_id:
rec.work_order_contacts = rec.parent_id.work_order_contacts
if not rec.site_contacts and rec.parent_id:
rec.site_contacts = rec.parent_id.site_contacts
if rec.sale_order_id:
seq = 1
prev_seqs = self.sale_order_id.tasks_ids and \
@ -87,6 +84,11 @@ class Task(models.Model):
seq += max(map(lambda n: int(n.group(1)) if n else 0, matches))
rec.work_order_number = rec.sale_order_id.name.replace('SO', 'SVR', 1) \
+ f"-{seq}"
# If the task is linked to a sales order and has no parent, it should inherit SO work order contacts
if not rec.parent_id and not rec.work_order_contacts and rec.sale_order_id.work_order_contacts:
rec.work_order_contacts = rec.sale_order_id.work_order_contacts
if not rec.parent_id and not rec.site_contacts and rec.sale_order_id.site_contacts:
rec.site_contacts = rec.sale_order_id.site_contacts
return res
def write(self, vals):
@ -101,6 +103,10 @@ class Task(models.Model):
# Here we use child_ids instead of _get_all_subtasks() so as to allow for setting propagate_assignment
# to false on a child task.
to_propagate.child_ids.write({'user_ids': vals['user_ids']})
if 'site_contacts' in vals and self.child_ids:
self._get_all_subtasks().write({'site_contacts': [Command.set(self.site_contacts.ids)]})
if 'work_order_contacts' in vals and self.child_ids:
self._get_all_subtasks().write({'work_order_contacts': [Command.set(self.work_order_contacts.ids)]})
return res
@api.depends('sale_order_id')
@ -123,30 +129,6 @@ class Task(models.Model):
for project in self.project_id
}
@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
is related to one, and from the task's customer if there is no sale order related to the task."""
for rec in self:
site_contacts = self.sale_line_id and self.sale_line_id.order_id.site_contacts or \
self.partner_id.site_contacts
work_order_contacts = self.sale_line_id and self.sale_line_id.order_id.work_order_contacts or \
self.partner_id.work_order_contacts
rec.write({
'site_contacts': [Command.set(site_contacts.ids)],
'work_order_contacts': [Command.set(work_order_contacts.ids)]
})
def _inverse_contacts(self):
""" If the task is linked to a sales order, the sales order should have its contacts updated to match."""
for rec in self:
if rec.sale_line_id:
rec.sale_line_id.order_id.write({
'work_order_contacts': [Command.set(rec.work_order_contacts.ids)],
'site_contacts': [Command.set(rec.site_contacts.ids)],
})
@api.depends('parent_id.visit_id', 'project_id.is_fsm', 'project_id.allow_billable')
def _compute_allow_billable(self):
for rec in self:

View file

@ -211,33 +211,6 @@ class TestSalesOrder(BemadeFSMBaseTest):
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)
def test_tasks_created_at_order_confirmation_have_no_assignees(self):
so, visit, sol1, sol2 = self._generate_so_with_one_visit_two_lines()
user = self._generate_project_user(name="User", login='login')

View file

@ -1,5 +1,5 @@
from .test_bemade_fsm_common import BemadeFSMBaseTest
from odoo.tests.common import tagged
from odoo.tests.common import tagged, Form
from odoo import Command
@ -10,18 +10,21 @@ class TaskTest(BemadeFSMBaseTest):
def setUpClass(cls):
# Chose to set up all tests the same way since this code was becoming very redundant
super().setUpClass()
cls.so = cls._generate_sale_order()
cls.template = cls._generate_task_template(names=['Parent', 'Child', 'Grandchild'], structure=[2, 1])
cls.product = cls._generate_product(task_template=cls.template)
cls.sol = cls._generate_sale_order_line(sale_order=cls.so, product=cls.product)
cls.user = cls._generate_project_manager_user('Bob', 'Bob')
cls.so.action_confirm()
cls.task = cls.sol.task_id
def _generate_so_with_multilevel_task_template(self):
so = self._generate_sale_order()
template = self._generate_task_template(names=['Parent', 'Child', 'Grandchild'], structure=[2, 1])
product = self._generate_product(task_template=template)
sol = self._generate_sale_order_line(sale_order=so, product=product)
return so, sol
def test_reassigning_assignment_propagating_task_changes_subtasks(self):
task = self.task
task.propagate_assignment = True
so, sol = self._generate_so_with_multilevel_task_template()
so.action_confirm()
task = sol.task_id
task.propagate_assignment = True
task.write({
'user_ids': [Command.set([self.user.id])],
'propagate_assignment': True,
@ -30,7 +33,10 @@ class TaskTest(BemadeFSMBaseTest):
self.assertTrue(all([t.user_ids == self.user for t in task | task._get_all_subtasks()]))
def test_reassigning_task_doesnt_propagate_by_default(self):
task = self.task
so, sol = self._generate_so_with_multilevel_task_template()
so.action_confirm()
task = sol.task_id
task.write({
'user_ids': [Command.set([self.user.id])],
})
@ -38,7 +44,9 @@ class TaskTest(BemadeFSMBaseTest):
self.assertFalse(any([t.user_ids for t in task.child_ids.child_ids]))
def test_unset_propagate_assignment_unsets_for_all_children(self):
task = self.task
so, sol = self._generate_so_with_multilevel_task_template()
so.action_confirm()
task = sol.task_id
# First, set propagation and assign
task.propagate_assignment = True
task.write({
@ -53,3 +61,66 @@ class TaskTest(BemadeFSMBaseTest):
})
self.assertTrue(all([not t.user_ids for t in task | task.child_ids]))
self.assertTrue(all([t.user_ids == self.user for t in task.child_ids.child_ids]))
def test_task_gets_work_order_contacts_from_sale_order(self):
so, sol = self._generate_so_with_multilevel_task_template()
work_order_contacts = self._generate_partner(parent=so.partner_id) | self._generate_partner(
parent=so.partner_id)
so.write({'work_order_contacts': [(6, 0, work_order_contacts.ids)]})
so.action_confirm()
task = sol.task_id
self.assertEqual(task.work_order_contacts, so.work_order_contacts)
# Just a safeguard to make sure we set it properly on the SO
self.assertEqual(len(task.work_order_contacts), 2)
# Make sure all subtasks got the same
for subtask in task._get_all_subtasks():
self.assertEqual(subtask.work_order_contacts, so.work_order_contacts)
def test_task_gets_site_contacts_from_sale_order(self):
so, sol = self._generate_so_with_multilevel_task_template()
site_contacts = self._generate_partner(parent=so.partner_id) | self._generate_partner(parent=so.partner_id)
so.write({'site_contacts': [(6, 0, site_contacts.ids)]})
so.action_confirm()
task = sol.task_id
self.assertEqual(task.site_contacts, so.site_contacts)
# Just a safeguard to make sure we set it properly on the SO
self.assertEqual(len(task.site_contacts), 2)
# Make sure all subtasks got the same
for subtask in task._get_all_subtasks():
self.assertEqual(subtask.site_contacts, so.site_contacts)
def test_task_gets_work_order_contacts_from_parent(self):
so, sol = self._generate_so_with_multilevel_task_template()
work_order_contacts = self._generate_partner(parent=so.partner_id) | self._generate_partner(parent=so.partner_id)
so.write({'work_order_contacts': [(6, 0, work_order_contacts.ids)]})
so.action_confirm()
task = sol.task_id
task.write({'work_order_contacts': [Command.link(self._generate_partner(parent=so.partner_id).id)]})
for subtask in task._get_all_subtasks():
self.assertEqual(subtask.work_order_contacts, task.work_order_contacts)
with Form(task) as task_form:
with task_form.child_ids.new() as subtask:
subtask.name = "Subtask 1"
subtask = task.child_ids[-1]
self.assertEqual(subtask.work_order_contacts, task.work_order_contacts)
def test_task_gets_site_contacts_from_parent(self):
so, sol = self._generate_so_with_multilevel_task_template()
site_contacts = self._generate_partner(parent=so.partner_id) | self._generate_partner(parent=so.partner_id)
so.write({'site_contacts': [(6, 0, site_contacts.ids)]})
so.action_confirm()
task = sol.task_id
task.write({'site_contacts': [Command.link(self._generate_partner(parent=so.partner_id).id)]})
for subtask in task._get_all_subtasks():
self.assertEqual(subtask.site_contacts, task.site_contacts)
with Form(task) as task_form:
with task_form.child_ids.new() as subtask:
subtask.name = "Subtask 1"
subtask = task.child_ids[-1]
self.assertEqual(subtask.site_contacts, task.site_contacts)

View file

@ -4,8 +4,8 @@
<field name="name">Equipments</field>
<field name="res_model">bemade_fsm.equipment</field>
<field name="view_mode">tree,form</field>
<field name="context">{'default_partner_id': active_id}</field>
<field name="domain">[("partner_id", "=", active_id)]</field>
<field name="context">{'default_partner_location_id': active_id}</field>
<field name="domain">[("partner_location_id", "=", active_id)]</field>
</record>
<record id="partner_equipment_location_view_form" model="ir.ui.view">