Compare commits
31 commits
18.0
...
bemade_fsm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08114ad4f7 | ||
|
|
e5394babc3 | ||
|
|
c5fb6b608e | ||
|
|
0e0be67d62 | ||
|
|
a6f192d81e | ||
|
|
aa9b8bf21c | ||
|
|
9248af2aaa | ||
|
|
ca54e5739c | ||
|
|
b211679a09 | ||
|
|
3054f0d3d9 | ||
|
|
4e74d7a557 | ||
|
|
fd314d2429 | ||
|
|
6bd52ae118 | ||
|
|
59fc723026 | ||
|
|
bcdf2bf860 | ||
|
|
5bb1023847 | ||
|
|
8233f678cf | ||
|
|
933092771e | ||
|
|
238cde41e6 | ||
|
|
7ea3458ba4 | ||
|
|
48abe06a51 | ||
|
|
4010fd57a2 | ||
|
|
9b9e3a51db | ||
|
|
146426e10b | ||
|
|
8e9bc22b8e | ||
|
|
22939a9fb5 | ||
|
|
90153da600 | ||
|
|
fc74046d32 | ||
|
|
39467f96e2 | ||
|
|
73f53dead9 | ||
|
|
c51d2f313c |
37 changed files with 1486 additions and 451 deletions
|
|
@ -20,37 +20,41 @@
|
|||
########################################################################################
|
||||
{
|
||||
'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',
|
||||
'author': 'Bemade Inc.',
|
||||
'website': 'http://www.bemade.org',
|
||||
'license': 'OPL-1',
|
||||
'depends': ['project',
|
||||
'depends': ['project_enterprise',
|
||||
'project_forecast',
|
||||
'stock',
|
||||
'sale',
|
||||
'sale_project',
|
||||
'sale_stock',
|
||||
'industry_fsm',
|
||||
'industry_fsm_sale',
|
||||
'sale_planning',
|
||||
'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,
|
||||
|
|
|
|||
48
bemade_fsm/data/fsm_data.xml
Normal file
48
bemade_fsm/data/fsm_data.xml
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="planning_project_stage_waiting_parts" model="project.task.type">
|
||||
<field name="sequence">2</field>
|
||||
<field name="name">Waiting on Parts</field>
|
||||
<field name="legend_blocked">Blocked</field>
|
||||
<field name="fold" eval="False"/>
|
||||
<field name="is_closed" eval="False"/>
|
||||
<field name="project_ids" eval="[(4,ref('industry_fsm.fsm_project'))]"/>
|
||||
</record>
|
||||
<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"/>
|
||||
<field name="project_ids" eval="[(4,ref('industry_fsm.fsm_project'))]"/>
|
||||
</record>
|
||||
<record id="planning_project_stage_exception" model="project.task.type">
|
||||
<field name="sequence">19</field>
|
||||
<field name="name">Exception</field>
|
||||
<field name="legend_blocked">Blocked</field>
|
||||
<field name="fold" eval="False"/>
|
||||
<field name="is_closed" eval="False"/>
|
||||
<field name="project_ids" eval="[(4,ref('industry_fsm.fsm_project'))]"/>
|
||||
</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,8 @@ 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",)
|
||||
|
||||
description = fields.Text(string="Description",
|
||||
tracking=True)
|
||||
|
|
@ -53,24 +48,17 @@ 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):
|
||||
for rec in self:
|
||||
rec.partner_id = rec.partner_location_id and rec.partner_location_id.root_ancestor
|
||||
|
||||
@api.model
|
||||
def _search_partner(self, operator, value):
|
||||
return [('partner_location_id.root_ancestor', operator, value)]
|
||||
|
||||
@api.depends('pid_tag', 'name')
|
||||
def _compute_complete_name(self):
|
||||
for rec in self:
|
||||
rec.complete_name = "[%s] %s" % (rec.pid_tag or ' ', rec.name)
|
||||
|
||||
@api.model
|
||||
def name_search(self, name='', args=None, operator='ilike', limit=100):
|
||||
args = args or []
|
||||
|
|
@ -96,3 +84,8 @@ class Equipment(models.Model):
|
|||
'res_model': 'bemade_fsm.equipment',
|
||||
'type': 'ir.actions.act_window',
|
||||
}
|
||||
|
||||
@api.depends('pid_tag', 'name')
|
||||
def _compute_complete_name(self):
|
||||
for rec in self:
|
||||
rec.complete_name = "[%s] %s" % (rec.pid_tag or ' ', rec.name)
|
||||
|
|
|
|||
58
bemade_fsm/models/fsm_visit.py
Normal file
58
bemade_fsm/models/fsm_visit.py
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
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",
|
||||
ondelete="cascade")
|
||||
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")
|
||||
summarized_equipment_ids = fields.Many2many(comodel_name="bemade_fsm.equipment",
|
||||
string="Equipment to Service",
|
||||
compute="_compute_summarized_equipment_ids")
|
||||
task_id = fields.Many2one(comodel_name="project.task",
|
||||
compute="_compute_task_id",
|
||||
string="Service Visit",)
|
||||
task_ids = fields.One2many(comodel_name="project.task",
|
||||
inverse_name="visit_id")
|
||||
|
||||
@api.depends('task_ids')
|
||||
def _compute_task_id(self):
|
||||
for rec in self:
|
||||
rec.task_id = rec.task_ids and rec.task_ids[0]
|
||||
|
||||
@api.depends('so_section_id', 'sale_order_id.summary_equipment_ids')
|
||||
def _compute_summarized_equipment_ids(self):
|
||||
for rec in self:
|
||||
lines = rec.so_section_id.get_section_lines()
|
||||
equipment_ids = []
|
||||
for line in lines:
|
||||
for equipment in line.equipment_ids:
|
||||
equipment_ids.append(equipment)
|
||||
rec.summarized_equipment_ids = equipment_ids
|
||||
|
||||
@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
|
||||
|
|
@ -5,3 +5,6 @@ class ProductTemplate(models.Model):
|
|||
_inherit = 'product.template'
|
||||
|
||||
task_template_id = fields.Many2one("project.task.template", string="Task Template", ondelete='restrict')
|
||||
is_field_service = fields.Boolean("Plan as field service",
|
||||
help="Products planned as field service will have travel time "
|
||||
"considered in planning.")
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
from odoo import api, fields, models, Command
|
||||
|
||||
|
||||
class Partner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
|
|
@ -7,17 +8,16 @@ class Partner(models.Model):
|
|||
string='Equipment Count')
|
||||
|
||||
owned_equipment_ids = fields.One2many(comodel_name="bemade_fsm.equipment",
|
||||
inverse_name="partner_id",
|
||||
compute="_compute_owned_equipment_ids",
|
||||
string="Owned Equipments")
|
||||
|
||||
|
||||
equipment_ids = fields.One2many(comodel_name='bemade_fsm.equipment',
|
||||
inverse_name='partner_location_id',
|
||||
string='Site Equipment')
|
||||
|
||||
is_site_contact = fields.Boolean(string='Is a site contact',
|
||||
compute="_compute_is_site_contact",
|
||||
search="_search_is_site_contact",)
|
||||
search="_search_is_site_contact", )
|
||||
|
||||
site_ids = fields.Many2many(string='Work Sites',
|
||||
comodel_name='res.partner',
|
||||
|
|
@ -42,6 +42,13 @@ class Partner(models.Model):
|
|||
domain=[('is_company', '=', False)],
|
||||
tracking=True)
|
||||
|
||||
@api.depends('equipment_ids', 'child_ids.company_type', 'child_ids.equipment_ids')
|
||||
def _compute_owned_equipment_ids(self):
|
||||
for rec in self:
|
||||
ids = rec.equipment_ids | rec.child_ids.filtered(
|
||||
lambda l: l.company_type == 'company').mapped('equipment_ids')
|
||||
rec.owned_equipment_ids = ids or False
|
||||
|
||||
@api.depends('site_ids')
|
||||
def _compute_is_site_contact(self):
|
||||
for rec in self:
|
||||
|
|
@ -50,7 +57,8 @@ class Partner(models.Model):
|
|||
@api.depends('equipment_ids')
|
||||
def _compute_equipment_count(self):
|
||||
for rec in self:
|
||||
all_equipment_ids = self.env['bemade_fsm.equipment'].search([('partner_id', '=', rec.id)])
|
||||
all_equipment_ids = self.env['bemade_fsm.equipment'].search(
|
||||
[('partner_location_id', '=', rec.id)])
|
||||
rec.equipment_count = len(all_equipment_ids)
|
||||
|
||||
@api.model
|
||||
|
|
|
|||
|
|
@ -1,12 +1,22 @@
|
|||
from odoo import fields, models, api, _, Command
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = 'sale.order'
|
||||
|
||||
equipment_id = fields.Many2one(comodel_name="bemade_fsm.equipment",
|
||||
string="Equipment to Service",
|
||||
tracking=True)
|
||||
valid_equipment_ids = fields.One2many(comodel_name="bemade_fsm.equipment",
|
||||
related="partner_id.owned_equipment_ids")
|
||||
default_equipment_ids = fields.Many2many(comodel_name="bemade_fsm.equipment",
|
||||
string="Default Equipment to Service",
|
||||
help="The default equipment to service for new sale order lines.",
|
||||
compute="_compute_default_equipment",
|
||||
inverse="_inverse_default_equipment",
|
||||
store=True, )
|
||||
|
||||
summary_equipment_ids = fields.Many2many(comodel_name="bemade_fsm.equipment",
|
||||
string="Equipment Being Serviced",
|
||||
compute="_compute_summary_equipment_ids")
|
||||
|
||||
site_contacts = fields.Many2many(comodel_name='res.partner',
|
||||
relation="sale_order_site_contacts_rel",
|
||||
|
|
@ -21,8 +31,22 @@ 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.depends('order_line.equipment_ids')
|
||||
def _compute_summary_equipment_ids(self):
|
||||
for rec in self:
|
||||
rec.summary_equipment_ids = rec.order_line.mapped('equipment_ids')
|
||||
|
||||
@api.onchange('partner_shipping_id')
|
||||
def _onchange_partner_shipping_id(self):
|
||||
super()._onchange_partner_shipping_id()
|
||||
self._compute_default_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,18 +55,65 @@ class SaleOrder(models.Model):
|
|||
def _inverse_default_contacts(self):
|
||||
pass
|
||||
|
||||
@api.depends('partner_id')
|
||||
def _compute_equipment(self):
|
||||
@api.depends('partner_id', 'partner_shipping_id', 'partner_shipping_id.equipment_ids',
|
||||
'partner_id.owned_equipment_ids')
|
||||
def _compute_default_equipment(self):
|
||||
for rec in self:
|
||||
rec.equipment_ids = self.partner_shipping_id.equipment_ids if len(
|
||||
self.partner_shipping_id.equipment_ids) <= 1 else False
|
||||
if rec.partner_shipping_id.equipment_ids:
|
||||
ids = rec.partner_shipping_id.equipment_ids
|
||||
else:
|
||||
ids = rec.partner_id.owned_equipment_ids
|
||||
rec.default_equipment_ids = ids if len(ids) < 4 else False
|
||||
|
||||
def _inverse_equipment(self):
|
||||
def _inverse_default_equipment(self):
|
||||
pass
|
||||
|
||||
|
||||
class SaleOrderLine(models.Model):
|
||||
_inherit = 'sale.order.line'
|
||||
valid_equipment_ids = fields.One2many(comodel_name="bemade_fsm.equipment",
|
||||
related="order_id.partner_id.owned_equipment_ids")
|
||||
visit_ids = fields.One2many(comodel_name="bemade_fsm.visit",
|
||||
inverse_name="so_section_id",)
|
||||
visit_id = fields.Many2one(comodel_name="bemade_fsm.visit",
|
||||
compute="_compute_visit_id",
|
||||
string = "Visit",
|
||||
ondelete='cascade',
|
||||
store=True)
|
||||
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.")
|
||||
equipment_ids = fields.Many2many(string="Equipment to Service",
|
||||
comodel_name="bemade_fsm.equipment",
|
||||
relation="bemade_fsm_equipment_sale_order_line_rel",
|
||||
column1="sale_order_line_id",
|
||||
column2="equipment_id")
|
||||
is_field_service = fields.Boolean(string="Is Field Service",
|
||||
compute="_compute_is_field_service",
|
||||
store=True)
|
||||
|
||||
@api.depends('visit_ids')
|
||||
def _compute_visit_id(self):
|
||||
for rec in self:
|
||||
rec.visit_id = rec.visit_ids and rec.visit_ids[0]
|
||||
|
||||
@api.depends('product_id')
|
||||
def _compute_is_field_service(self):
|
||||
for rec in self:
|
||||
rec.is_field_service = rec.product_id.is_field_service
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals):
|
||||
recs = super().create(vals)
|
||||
for rec in recs:
|
||||
if rec.order_id.default_equipment_ids and not rec.equipment_ids:
|
||||
rec.equipment_ids = rec.order_id.default_equipment_ids
|
||||
return recs
|
||||
|
||||
def _timesheet_create_task(self, project):
|
||||
""" Generate task for the given so line, and link it.
|
||||
|
|
@ -69,6 +140,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 +154,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'] = template.equipment_ids.ids
|
||||
return vals
|
||||
|
||||
tmpl = self.product_id.task_template_id
|
||||
|
|
@ -98,5 +175,97 @@ 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.equipment_ids:
|
||||
task.equipment_ids = self.equipment_ids.ids
|
||||
task.name = _generate_task_name(tmpl)
|
||||
return task
|
||||
|
||||
def _timesheet_service_generation(self):
|
||||
super()._timesheet_service_generation()
|
||||
visit_lines = self.filtered(lambda l: l.visit_id)
|
||||
for line in visit_lines:
|
||||
task_ids = line.get_section_lines().mapped('task_id')
|
||||
if not task_ids:
|
||||
continue
|
||||
if len(set([task.project_id for task in task_ids])) > 1:
|
||||
# Can't group up the tasks if they're part of different projects
|
||||
return
|
||||
project_id = task_ids[0].project_id
|
||||
line.visit_id.task_id = line._generate_task_for_visit_line(project_id)
|
||||
task_ids.write({'parent_id': line.visit_id.task_id.id})
|
||||
|
||||
def _generate_task_for_visit_line(self, project):
|
||||
self.ensure_one()
|
||||
task = self.env['project.task'].create({
|
||||
'name': self.order_id.name + ": " + self.name,
|
||||
'description': f"Parent task for {self.order_id.name}, visit {self.name}",
|
||||
'project_id': project.id,
|
||||
'equipment_ids': self.get_section_lines().mapped('equipment_ids').ids,
|
||||
'sale_order_id': self.order_id.id,
|
||||
'partner_id': self.order_id.partner_shipping_id.id,
|
||||
'visit_id': self.visit_id.id,
|
||||
})
|
||||
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):
|
||||
for rec in self:
|
||||
rec.is_fully_delivered = rec._iterate_items_compute_bool(lambda l: l.qty_to_deliver == 0)
|
||||
|
||||
@api.depends('is_fully_delivered')
|
||||
def _compute_is_fully_invoiced(self):
|
||||
for rec in self:
|
||||
if not rec.is_fully_delivered:
|
||||
rec.is_fully_delivered_and_invoiced = False
|
||||
return
|
||||
rec.is_fully_delivered_and_invoiced = rec._iterate_items_compute_bool(lambda l: l.qty_to_invoice == 0)
|
||||
|
||||
def get_section_lines(self):
|
||||
""" Returns a RecordSet containing the sale order lines that fall under this section. """
|
||||
self.ensure_one()
|
||||
assert self.display_type == 'line_section', 'Method called incorrectly on non-section order line.'
|
||||
found = False
|
||||
lines = []
|
||||
for line in self.order_id.order_line:
|
||||
if line == self:
|
||||
found = True
|
||||
continue
|
||||
if not found:
|
||||
continue
|
||||
if line.display_type == 'line_section': # Stop when we hit the next section
|
||||
break
|
||||
else:
|
||||
lines.append(line)
|
||||
return self.env['sale.order.line'].union(*lines)
|
||||
|
||||
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
|
||||
|
||||
@api.depends('product_uom', 'product_uom_qty', 'product_id.planning_enabled', 'state',
|
||||
'product_id.task_template_id')
|
||||
def _compute_planning_hours_to_plan(self):
|
||||
# Override the method from sale_planning to use time estimates from the task template if appropriate
|
||||
super()._compute_planning_hours_to_plan()
|
||||
templated_lines = self.filtered(
|
||||
lambda l: l.product_id.task_template_id and l.product_id.task_template_id.planned_hours)
|
||||
for line in templated_lines:
|
||||
line.planning_hours_to_plan = line.product_id.task_template_id.planned_hours
|
||||
if line.product_uom_category_id == self.env.ref('uom.product_uom_unit').category_id:
|
||||
line.planning_hours_to_plan *= line.product_uom_qty
|
||||
|
|
|
|||
|
|
@ -1,11 +1,18 @@
|
|||
from odoo import fields, models, api, Command, _
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.exceptions import ValidationError, UserError
|
||||
from odoo.osv import expression
|
||||
from collections import defaultdict, namedtuple
|
||||
|
||||
|
||||
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,7 +30,72 @@ class Task(models.Model):
|
|||
inverse="_inverse_contacts",
|
||||
store=True)
|
||||
|
||||
@api.depends('sale_line_id.order_id.site_contacts', 'sale_line_id.order_id.work_order_contacts')
|
||||
planned_date_begin = fields.Datetime("Start Date", tracking=True,
|
||||
task_dependency_tracking=True,
|
||||
compute="_compute_planned_dates",
|
||||
inverse="_inverse_planned_dates",
|
||||
store=True)
|
||||
planned_date_end = fields.Datetime("End Date", tracking=True,
|
||||
task_dependency_tracking=True,
|
||||
compute="_compute_planned_dates",
|
||||
inverse="_inverse_planned_dates",
|
||||
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",
|
||||
store=True)
|
||||
|
||||
visit_id = fields.Many2one(comodel_name='bemade_fsm.visit')
|
||||
|
||||
# user_id = fields.Many2one('res.users', compute='_compute_user_id')
|
||||
#
|
||||
# @api.depends('user_ids')
|
||||
# def _compute_user_id(self):
|
||||
# for rec in self:
|
||||
# rec.user_id = rec.user_ids and rec.user_ids[0] or 0
|
||||
|
||||
def _get_related_planning_slots(self):
|
||||
domain = expression.AND([
|
||||
self._get_domain_compute_forecast_hours(),
|
||||
[('task_id', 'in', self.ids + self._get_all_subtasks().ids)]
|
||||
])
|
||||
return self.env['planning.slot'].search(domain)
|
||||
|
||||
@api.depends('forecast_hours')
|
||||
def _compute_planned_dates(self):
|
||||
forecast_data = self._get_related_planning_slots()
|
||||
mapped_data = {}
|
||||
TimeSpan = namedtuple('timespan', ['start', 'end'])
|
||||
for d in forecast_data:
|
||||
if d not in mapped_data:
|
||||
mapped_data.update({d: TimeSpan(d.start_datetime, d.end_datetime)})
|
||||
continue
|
||||
if mapped_data[d].start > d.start_datetime:
|
||||
mapped_data[d].start = d.start_datetime
|
||||
if mapped_data[d].end < d.end_datetime:
|
||||
mapped_data[d].end = d.end_datetime
|
||||
for rec in self:
|
||||
if rec not in mapped_data:
|
||||
if not rec.planned_date_end:
|
||||
rec.planned_date_end = False
|
||||
if not rec.planned_date_begin:
|
||||
rec.planned_date_begin = False
|
||||
continue
|
||||
rec.planned_date_begin, rec.planned_date_end = mapped_data[rec]
|
||||
|
||||
def _inverse_planned_dates(self):
|
||||
""" Modifying the planned dates for tasks with existing planning records
|
||||
(planning.slot) has no defined safe behaviour, so we block it."""
|
||||
if self._get_related_planning_slots():
|
||||
raise UserError(_("Modifying the planned start or end time on a task is not "
|
||||
"permitted when that task already has planning records "
|
||||
"associated to it. Please modify or delete the planning "
|
||||
"records instead."))
|
||||
|
||||
@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."""
|
||||
|
|
@ -45,3 +117,12 @@ 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.visit_id', 'project_id.is_fsm', 'project_id.allow_billable')
|
||||
def _compute_allow_billable(self):
|
||||
for rec in self:
|
||||
# If an FSM task has a parent that is linked to an SO line, then the parent is the billable one
|
||||
if rec.parent_id and not rec.parent_id.visit_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)]})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_bemade_fsm_task_template,bemade_fsm_task_template,model_project_task_template,project.group_project_manager,1,1,1,1
|
||||
access_bemade_fsm_task_template,bemade_fsm_task_template,model_project_task_template,project.group_project_user,1,1,1,1
|
||||
access_bemade_fsm_task_template_manager,bemade_fsm_task_template,model_project_task_template,project.group_project_manager,1,1,1,1
|
||||
access_bemade_fsm_task_template_user,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 () {
|
||||
},
|
||||
|
|
@ -100,13 +98,7 @@ tour.register('equipment_sale_order_tour', {
|
|||
content: 'Navigate to the Field Service tab.',
|
||||
trigger: 'a.nav-link[role="tab"]:contains(Field Service)'
|
||||
}, {
|
||||
content: 'Click Edit',
|
||||
trigger: 'button.o_form_button_edit',
|
||||
}, {
|
||||
content: 'Click the equipment dropdown.',
|
||||
trigger: '.o_field_widget[name="equipment_id"] input',
|
||||
}, {
|
||||
content: 'Check that the equipment is in the dropdown.',
|
||||
content: 'Check that the equipment is listed in the tab.',
|
||||
trigger: `li.ui-menu-item > a:contains(${TEST_EQPT1})`,
|
||||
}
|
||||
]);
|
||||
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,168 @@ 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, shipping_location=None):
|
||||
partner = partner or cls._generate_partner()
|
||||
vals = {'partner_id': partner.id,
|
||||
'client_order_ref': client_order_ref}
|
||||
if equipment:
|
||||
vals.update({'default_equipment_ids': [Command.set([equipment.id])]})
|
||||
if shipping_location:
|
||||
vals.update({'partner_shipping_id': shipping_location.id})
|
||||
return cls.env['sale.order'].create(vals)
|
||||
|
||||
@classmethod
|
||||
def _generate_sale_order_line(cls, sale_order, product=None, qty=1.0, uom=None, price=100.0, tax_id=False):
|
||||
if not product:
|
||||
product = cls._generate_product()
|
||||
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, customer=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 [])],
|
||||
'customer': customer and customer.id or False,
|
||||
})
|
||||
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)
|
||||
|
|
|
|||
96
bemade_fsm/tests/test_fsm_visit.py
Normal file
96
bemade_fsm/tests/test_fsm_visit.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
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._generate_sale_order_line(sale_order=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._generate_sale_order_line(so)
|
||||
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._generate_sale_order_line(so)
|
||||
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 test_visit_groups_section_tasks_when_confirmed(self):
|
||||
partner = self._generate_partner()
|
||||
so = self._generate_sale_order()
|
||||
visit = self._generate_visit(sale_order=so)
|
||||
sol1 = self._generate_sale_order_line(sale_order=so)
|
||||
sol2 = self._generate_sale_order_line(sale_order=so)
|
||||
visit.so_section_id.sequence = 1
|
||||
sol1.sequence = 2
|
||||
sol2.sequence = 3
|
||||
|
||||
so.action_confirm()
|
||||
|
||||
visit_task = visit.task_id
|
||||
self.assertTrue(visit_task)
|
||||
visit_subtasks = visit_task.child_ids
|
||||
self.assertTrue(visit_subtasks and sol1.task_id in visit_subtasks and sol2.task_id in visit_subtasks)
|
||||
|
||||
def test_adding_visit_creates_one_sale_order_line(self):
|
||||
partner = self._generate_partner()
|
||||
so = self._generate_sale_order()
|
||||
self._generate_sale_order_line(sale_order=so)
|
||||
self._generate_sale_order_line(sale_order=so)
|
||||
|
||||
self._generate_visit(sale_order=so)
|
||||
|
||||
self.assertEqual(len(so.order_line), 3)
|
||||
|
||||
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,
|
||||
}])
|
||||
|
|
@ -1,97 +1,254 @@
|
|||
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)
|
||||
|
||||
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
|
||||
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_single_equipment(self):
|
||||
""" 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_order_confirmation_multiple_equipment(self):
|
||||
""" All equipment items should flow from the sale order line to the final task """
|
||||
partner = self._generate_partner()
|
||||
for i in range(5):
|
||||
self._generate_equipment(partner=partner)
|
||||
sale_order = self._generate_sale_order(partner=partner) # No default equipment since more than 3 on partner
|
||||
sol1, sol2, sol3 = [self._generate_sale_order_line(sale_order=sale_order) for i in range(3)]
|
||||
sol1.equipment_ids = [Command.set([partner.equipment_ids[i].id for i in range(2)])]
|
||||
sol3.equipment_ids = [Command.set([partner.equipment_ids[i].id for i in range(2, 5)])]
|
||||
|
||||
sale_order.action_confirm()
|
||||
|
||||
self.assertEqual(sol1.equipment_ids, sol1.task_id.equipment_ids)
|
||||
self.assertEqual(sol2.equipment_ids, sol2.task_id.equipment_ids)
|
||||
self.assertEqual(sol3.equipment_ids, sol3.task_id.equipment_ids)
|
||||
|
||||
|
||||
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_sale_order_line_gets_default_equipment(self):
|
||||
""" Sale order lines created on an SO with default equipment set should inherit that default equipment. """
|
||||
partner = self._generate_partner()
|
||||
self._generate_equipment(partner=partner)
|
||||
sale_order = self._generate_sale_order(partner=partner)
|
||||
|
||||
sol = self._generate_sale_order_line(sale_order=sale_order)
|
||||
|
||||
self.assertEqual(sol.equipment_ids, partner.equipment_ids)
|
||||
|
||||
def test_sale_order_gets_correct_default_equipment_from_partner(self):
|
||||
""" Should pick up equipment from the partner."""
|
||||
partner = self._generate_partner()
|
||||
self._generate_equipment(partner=partner)
|
||||
|
||||
sale_order = self._generate_sale_order(partner=partner)
|
||||
|
||||
self.assertEqual(sale_order.default_equipment_ids, partner.owned_equipment_ids)
|
||||
|
||||
def test_sale_order_default_equipment_maximum_number(self):
|
||||
parent = self._generate_partner()
|
||||
child = self._generate_partner(parent=parent)
|
||||
for i in range(3):
|
||||
self._generate_equipment(child)
|
||||
|
||||
sale_order = self._generate_sale_order(partner=parent)
|
||||
|
||||
self.assertEqual(sale_order.default_equipment_ids, parent.owned_equipment_ids)
|
||||
|
||||
def test_sale_order_no_default_equipment_with_more_than_three_owned_on_partner(self):
|
||||
parent = self._generate_partner()
|
||||
child = self._generate_partner(parent=parent)
|
||||
for i in range(4):
|
||||
self._generate_equipment(child)
|
||||
|
||||
sale_order = self._generate_sale_order(partner=parent)
|
||||
|
||||
self.assertEqual(sale_order.default_equipment_ids, parent.owned_equipment_ids)
|
||||
|
||||
def test_sale_order_resets_default_equipment_on_partner_change(self):
|
||||
partner_1 = self._generate_partner()
|
||||
partner_2 = self._generate_partner()
|
||||
self._generate_equipment(partner=partner_1)
|
||||
sale_order = self._generate_sale_order(partner_1)
|
||||
form = Form(sale_order)
|
||||
|
||||
form.partner_id = partner_2
|
||||
form.save()
|
||||
|
||||
self.assertFalse(sale_order.default_equipment_ids)
|
||||
|
||||
def test_sale_order_prioritize_shipping_location_equipments(self):
|
||||
parent = self._generate_partner()
|
||||
child = self._generate_partner(parent=parent, location_type='delivery')
|
||||
self._generate_equipment(partner=parent)
|
||||
self._generate_equipment(partner=child)
|
||||
|
||||
sale_order = self._generate_sale_order(partner=parent, shipping_location=child)
|
||||
|
||||
self.assertEqual(sale_order.default_equipment_ids, child.equipment_ids)
|
||||
|
||||
def test_default_equipment_transfers_to_sale_order_line(self):
|
||||
partner = self._generate_partner()
|
||||
for i in range(3):
|
||||
self._generate_equipment(partner=partner)
|
||||
sale_order = self._generate_sale_order(partner=partner)
|
||||
|
||||
for i in range(3):
|
||||
self._generate_sale_order_line(sale_order=sale_order)
|
||||
|
||||
for line in sale_order.order_line:
|
||||
self.assertEqual(line.equipment_ids, partner.equipment_ids)
|
||||
|
||||
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,109 +1,77 @@
|
|||
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 odoo.tools import mute_logger
|
||||
from psycopg2.errors import ForeignKeyViolation
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class TestTaskTemplateCommon(FSMManagerUserTransactionCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.PLANNED_HOURS = 6
|
||||
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.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,
|
||||
})
|
||||
|
||||
# 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])]})
|
||||
|
||||
# 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,
|
||||
})
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestTaskTemplate(TestTaskTemplateCommon):
|
||||
class TestTaskTemplate(BemadeFSMBaseTest):
|
||||
|
||||
def test_delete_task_template(self):
|
||||
"""User should never be able to delete a task template used on a product"""
|
||||
task_template = self._generate_task_template(names=['Template 1'])
|
||||
product = self._generate_product(name="Test Product 1", task_template=task_template)
|
||||
with self.assertRaises(ForeignKeyViolation):
|
||||
self.task1.unlink()
|
||||
with mute_logger('odoo.sql_db'):
|
||||
task_template.unlink()
|
||||
|
||||
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()
|
||||
parent_task = self._generate_task_template(structure=[2, 1],
|
||||
names=['Parent Template', 'Child Template',
|
||||
'Grandchild Template'])
|
||||
grandchild_task = parent_task.subtasks[0].subtasks[0]
|
||||
|
||||
parent_task.subtasks[0].unlink()
|
||||
|
||||
# Reading deleted child's name field should be impossible
|
||||
with self.assertRaises(MissingError):
|
||||
test = self.grandchild_task.name
|
||||
test = 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)
|
||||
task = self._generate_task_template(customer=partner1, equipment=equipment1)
|
||||
form = Form(task)
|
||||
|
||||
# Switching the partner should trigger on_change that makes sure equipments are linked to the new partner
|
||||
form.customer = partner2
|
||||
form.save()
|
||||
|
||||
self.assertFalse(equipment1 in task.equipment_ids)
|
||||
|
||||
def test_hours_estimate_used_for_planning(self):
|
||||
partner = self._generate_partner()
|
||||
so = self._generate_sale_order(partner=partner)
|
||||
task_template = self._generate_task_template(planned_hours=8)
|
||||
product = self._generate_product(uom=self.env.ref('uom.product_uom_unit'), task_template=task_template)
|
||||
|
||||
sol = self._generate_sale_order_line(sale_order=so, product=product)
|
||||
|
||||
self.assertEqual(sol.planning_hours_to_plan, 8)
|
||||
|
||||
def test_hours_estimate_multiplied_for_multiple_units_sold(self):
|
||||
partner = self._generate_partner()
|
||||
so = self._generate_sale_order(partner=partner)
|
||||
task_template = self._generate_task_template(planned_hours=8)
|
||||
product = self._generate_product(uom=self.env.ref('uom.product_uom_unit'), task_template=task_template)
|
||||
|
||||
sol = self._generate_sale_order_line(sale_order=so, product=product, qty=3.0)
|
||||
|
||||
self.assertEqual(sol.planning_hours_to_plan, 24)
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestTaskTemplateTour(HttpCase, TestTaskTemplateCommon):
|
||||
|
||||
@tagged('-at_install', 'post_install', 'slow')
|
||||
class TestTaskTemplateTour(HttpCase, BemadeFSMBaseTest):
|
||||
|
||||
def test_task_template_tour(self):
|
||||
user = self._generate_project_manager_user('Mister PM', 'misterpm')
|
||||
task_template = self._generate_task_template(names=['Template 1'])
|
||||
self._generate_product(name="Test Product 1", task_template=task_template)
|
||||
self.start_tour('/web', 'task_template_tour',
|
||||
login='misterpm', )
|
||||
login=user.login, )
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@
|
|||
<field name="location_notes"/>
|
||||
</group>
|
||||
<group name="right">
|
||||
<field name="partner_id"/>
|
||||
<field name="partner_location_id"
|
||||
groups="sale.group_delivery_invoice_address"
|
||||
context="{'default_type': 'delivery', 'show_address': 1}"
|
||||
|
|
@ -23,19 +22,6 @@
|
|||
<field name="tag_ids" widget="many2many_tags" options="{'no_open': False}"/>
|
||||
</group>
|
||||
</group>
|
||||
<!-- <notebook>-->
|
||||
<!-- <page string="Interventions">-->
|
||||
<!-- <field name="intervention_ids" mode="tree">-->
|
||||
<!-- <tree string="Interventions" create="0" edit="0">-->
|
||||
<!-- <field name="name"/>-->
|
||||
<!-- <field name="description"/>-->
|
||||
<!-- <field name="date_planned"/>-->
|
||||
<!-- <field name="state"/>-->
|
||||
<!-- </tree>-->
|
||||
<!-- </field>-->
|
||||
<!-- </page>-->
|
||||
<!-- </notebook>-->
|
||||
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids" widget="mail_followers"/>
|
||||
|
|
@ -50,36 +36,16 @@
|
|||
<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>
|
||||
|
||||
<!-- BV: AN OTHER SEARCH VIEW TO BRING BACK
|
||||
<record id="equipment_view_search" model="ir.ui.view">
|
||||
<field name="name">bemade_fsm.equipment.search</field>
|
||||
<field name="model">bemade_fsm.equipment</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Equipments">
|
||||
<field name="name"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="pid_tag"/>
|
||||
<field name="partner_location_id"/>
|
||||
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Partner" context="{'group_by':'partner_id'}"/>
|
||||
<filter string="Location" context="{'group_by':'partner_location_id'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
-->
|
||||
|
||||
<record model="ir.actions.act_window" id="action_window_equipment">
|
||||
<field name="name">Equipment</field>
|
||||
<field name="res_model">bemade_fsm.equipment</field>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@
|
|||
<field name="task_template_id"
|
||||
attrs="{'invisible': [('service_tracking', 'not in', ('task_global_project', 'task_in_project'))]}"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='planning_enabled']" position="after">
|
||||
<field name="is_field_service"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<record id="product_search_form_view_inherit_bemade_fsm" model="ir.ui.view">
|
||||
|
|
|
|||
|
|
@ -8,17 +8,45 @@
|
|||
<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">
|
||||
<field name="equipment_id"
|
||||
domain="[('partner_location_id', '=', partner_shipping_id)]"
|
||||
context="{'default_partner_location_id': partner_shipping_id,}"/>
|
||||
<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="valid_equipment_ids" invisible="1"/>
|
||||
<field name="summary_equipment_ids"
|
||||
context="{'default_partner_location_id': partner_shipping_id,}"
|
||||
widget="many2many_tags"/>
|
||||
<field name="site_contacts"
|
||||
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"/>
|
||||
<field name="work_order_contacts"
|
||||
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"/>
|
||||
|
||||
<field name="default_equipment_ids"
|
||||
context="{'default_partner_location_id': partner_shipping_id,}"
|
||||
widget="many2many_tags"
|
||||
domain="[('id', 'in', valid_equipment_ids)]"/>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
<xpath expr="//tree//field[@name='name']" position="after">
|
||||
<field name="valid_equipment_ids" invisible="1"/>
|
||||
<field name="equipment_ids"
|
||||
widget="many2many_tags"
|
||||
domain="[('id', 'in', valid_equipment_ids)]"/>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -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,16 @@
|
|||
</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-->
|
||||
|
|
@ -29,48 +41,79 @@
|
|||
('parent_id', '=', False)]
|
||||
</field>
|
||||
</record>
|
||||
<record id="industry_fsm.project_task_action_fsm_map" model="ir.actions.act_window">
|
||||
<record id="industry_fsm.project_task_action_fsm_map"
|
||||
model="ir.actions.act_window">
|
||||
<field name="domain">[('is_fsm', '=', True),
|
||||
('display_project_id', '!=', False),
|
||||
('parent_id', '=', False)]
|
||||
</field>
|
||||
</record>
|
||||
<record id="industry_fsm.project_task_action_to_schedule_fsm" model="ir.actions.act_window">
|
||||
<record id="industry_fsm.project_task_action_to_schedule_fsm"
|
||||
model="ir.actions.act_window">
|
||||
<field name="domain">[('is_fsm', '=', True),
|
||||
('display_project_id', '!=', False),
|
||||
('parent_id', '=', False)]
|
||||
</field>
|
||||
</record>
|
||||
<record id="industry_fsm.project_task_action_all_fsm" model="ir.actions.act_window">
|
||||
<record id="industry_fsm.project_task_action_all_fsm"
|
||||
model="ir.actions.act_window">
|
||||
<field name="domain">[('is_fsm', '=', True),
|
||||
('display_project_id', '!=', False),
|
||||
('parent_id', '=', False)]
|
||||
</field>
|
||||
</record>
|
||||
<record id="industry_fsm_sale.project_task_action_to_invoice_fsm" model="ir.actions.act_window">
|
||||
<record id="industry_fsm_sale.project_task_action_to_invoice_fsm"
|
||||
model="ir.actions.act_window">
|
||||
<field name="domain">[('is_fsm', '=', True),
|
||||
('display_project_id', '!=', False),
|
||||
('parent_id', '=', False)]
|
||||
</field>
|
||||
</record>
|
||||
<!-- Add parent_id = false to domain for planning actions as well -->
|
||||
<record id="industry_fsm.project_task_action_fsm_planning_groupby_user" model="ir.actions.act_window">
|
||||
<record id="industry_fsm.project_task_action_fsm_planning_groupby_user"
|
||||
model="ir.actions.act_window">
|
||||
<field name="domain">[('is_fsm', '=', True),
|
||||
('display_project_id', '!=', False),
|
||||
('parent_id', '=', False)]
|
||||
</field>
|
||||
</record>
|
||||
<record id="industry_fsm.project_task_action_fsm_planning_groupby_project" model="ir.actions.act_window">
|
||||
<record id="industry_fsm.project_task_action_fsm_planning_groupby_project"
|
||||
model="ir.actions.act_window">
|
||||
<field name="domain">[('is_fsm', '=', True),
|
||||
('display_project_id', '!=', False),
|
||||
('parent_id', '=', False)]
|
||||
</field>
|
||||
</record>
|
||||
<record id="industry_fsm_report.project_task_action_fsm_planning_groupby_worksheet" model="ir.actions.act_window">
|
||||
<record id="industry_fsm_report.project_task_action_fsm_planning_groupby_worksheet"
|
||||
model="ir.actions.act_window">
|
||||
<field name="domain">[('is_fsm', '=', True),
|
||||
('display_project_id', '!=', False),
|
||||
('parent_id', '=', False)]
|
||||
</field>
|
||||
</record>
|
||||
<!-- <record id="project_task_view_calendar_fsm" model="ir.ui.view">-->
|
||||
<!-- <field name="name">bemade_fsm.project_task_view_calendar_fsm</field>-->
|
||||
<!-- <field name="inherit_id" ref="industry_fsm.project_task_view_calendar_fsm"/>-->
|
||||
<!-- <field name="model">project.task</field>-->
|
||||
<!-- <field name="arch" type="xml">-->
|
||||
<!-- <field name="user_ids" position="attributes">-->
|
||||
<!-- <attribute name="filters">1</attribute>-->
|
||||
<!-- <attribute name="color">user_ids</attribute>-->
|
||||
<!-- </field>-->
|
||||
<!-- </field>-->
|
||||
<!-- </record>-->
|
||||
<record id="project_task_view_calendar_fsm_no_worksheet" model="ir.ui.view">
|
||||
<field name="name">bemade_fsm.project_task_view_calendar_no_worksheet</field>
|
||||
<field name="inherit_id"
|
||||
ref="industry_fsm_report.project_task_view_calendar_fsm_worksheet"/>
|
||||
<field name="model">project.task</field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='worksheet_template_id']"
|
||||
position="replace"></xpath>
|
||||
<xpath expr="//calendar" position="attributes">
|
||||
<attribute name="color">user_ids</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
1
bemade_geo_routing/__init__.py
Normal file
1
bemade_geo_routing/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import models
|
||||
35
bemade_geo_routing/__manifest__.py
Normal file
35
bemade_geo_routing/__manifest__.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
#
|
||||
# Bemade Inc.
|
||||
#
|
||||
# Copyright (C) July 2023 Bemade Inc. (<https://www.bemade.org>).
|
||||
# Author: Marc Durepos (Contact : marc@bemade.org)
|
||||
#
|
||||
# This program is under the terms of the Odoo Proprietary License v1.0 (OPL-1)
|
||||
# It is forbidden to publish, distribute, sublicense, or sell copies of the Software
|
||||
# or modified copies of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
# DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
{
|
||||
'name': 'Bemade Geo Distance',
|
||||
'version': '15.0.1.0.0',
|
||||
'summary': 'Utility module for calculating the distance driving between addresses.',
|
||||
'description': """Module for calculating the driving distance between addresses using the Google Route API.
|
||||
**IMPORTANT NOTE:** You must have an active Google Route API key configured in the Geo Localization
|
||||
settings (under General Settings).""",
|
||||
'category': 'Generic Modules/Others',
|
||||
'author': 'Bemade Inc.',
|
||||
'website': 'https://www.bemade.org',
|
||||
'license': 'OPL-1',
|
||||
'depends': ['base_geolocalize'],
|
||||
'data': [],
|
||||
'demo': [],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
}
|
||||
1
bemade_geo_routing/models/__init__.py
Normal file
1
bemade_geo_routing/models/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import bemade_geo_router
|
||||
95
bemade_geo_routing/models/bemade_geo_router.py
Normal file
95
bemade_geo_routing/models/bemade_geo_router.py
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
from odoo import models, fields, tools, _, api
|
||||
from odoo.exceptions import UserError
|
||||
import requests
|
||||
import logging
|
||||
import datetime
|
||||
from typing import Literal
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GeoRouter(models.AbstractModel):
|
||||
""" Abstract class used to call Google Route API and convert responses
|
||||
into useful distance and route data.
|
||||
"""
|
||||
_name = 'base.geo_router'
|
||||
_description = "Geographic Router"
|
||||
|
||||
units = Literal['metric', 'imperial']
|
||||
|
||||
@api.model
|
||||
def _get_api_key(self):
|
||||
apikey = self.env['ir.config_parameter'].sudo().get_param('base_geolocalize.google_map_api_key')
|
||||
if not apikey:
|
||||
raise UserError(_(
|
||||
"API key for GeoCoding (Places) required.\n"
|
||||
"Visit https://developers.google.com/maps/documentation/geocoding/get-api-key for more information."
|
||||
))
|
||||
return apikey
|
||||
|
||||
@api.model
|
||||
def get_driving_distance_time(self, origin, destination, departure_time: datetime.datetime = None,
|
||||
arrival_time: datetime.datetime = None, units: units = 'metric',
|
||||
avoid_tolls=False, avoid_highways=False, avoid_ferries=False) -> (float, float):
|
||||
""" Calculates the route between two addresses and returns the distance it either in Kilometers or Miles and
|
||||
the time in minutes.
|
||||
|
||||
:param origin: The origin address (res.partner) from which to drive (origin).
|
||||
:param destination: The destination address (res.partner).
|
||||
:param departure_time: The departure time to use when planning the route (traffic aware).
|
||||
Cannot be set simultaneously with arrival_time.
|
||||
:param arrival_time: The arrival time to use when planning the route (traffic aware).
|
||||
Cannot be set simultaneously with departure_time.
|
||||
:param units: Type of units to return: 'imperial' for Miles, 'metric' for Kilometers.
|
||||
:param avoid_tolls: Whether Google should calculate a route avoiding tolls.
|
||||
:param avoid_highways: Whether Google should calculate a route avoiding highways.
|
||||
:param avoid_ferries: Whether Google should calculate a route avoiding ferries.
|
||||
:returns: tuple(distance: float, time: float)
|
||||
"""
|
||||
if departure_time and arrival_time:
|
||||
raise ValueError("Route cannot be calculated with both an arrival time and departure time.")
|
||||
apikey = self._get_api_key()
|
||||
origin_addr = self.env['base.geocoder'].geo_query_address(street=origin.street, zip=origin.zip,
|
||||
city=origin.city, state=origin.state_id.name,
|
||||
country=origin.country_id.name)
|
||||
destination_addr = self.env['base.geocoder'].geo_query_address(street=destination.street, zip=destination.zip,
|
||||
city=destination.city,
|
||||
state=destination.state_id.name,
|
||||
country=destination.country_id.name)
|
||||
params = {
|
||||
'origin': {
|
||||
'address': origin_addr,
|
||||
},
|
||||
'destination': {
|
||||
'address': destination_addr,
|
||||
},
|
||||
'travelMode': 'DRIVE',
|
||||
'computeAlternativeRoutes': False,
|
||||
'routeModifiers': {
|
||||
'avoidTolls': avoid_tolls,
|
||||
'avoidHighways': avoid_highways,
|
||||
'avoidFerries': avoid_ferries,
|
||||
},
|
||||
'languageCode': 'en-US',
|
||||
'units': units,
|
||||
}
|
||||
if departure_time or arrival_time:
|
||||
params.update({
|
||||
'routingPreference': 'TRAFFIC_AWARE',
|
||||
'departureTime' if departure_time else 'arrivalTime': departure_time or arrival_time
|
||||
})
|
||||
|
||||
url = "https://routes.googleapis.com/directions/v2:computeRoutes"
|
||||
distance_field = 'distanceMeters' if units == 'metric' else 'distanceMiles'
|
||||
result = requests.post(url,
|
||||
json=params,
|
||||
headers={'X-Goog-Api-Key': apikey,
|
||||
'Content-Type': 'application/json',
|
||||
'X-Goog-FieldMask': f"routes.duration,routes.{distance_field}"}).json()
|
||||
if 'routes' not in result:
|
||||
_logger.error(f"Failed to retrieve route between {origin_addr} and {destination_addr}. "
|
||||
f"Google response: {result}.")
|
||||
route = result['routes'][0]
|
||||
distance = (route[distance_field] / 1000) if units == 'metric' else route[distance_field]
|
||||
time = int(route['duration'].strip('s')) / 60
|
||||
return distance, time
|
||||
1
bemade_geo_routing/tests/__init__.py
Normal file
1
bemade_geo_routing/tests/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import test_geo_router
|
||||
53
bemade_geo_routing/tests/test_geo_router.py
Normal file
53
bemade_geo_routing/tests/test_geo_router.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
from odoo.tests import TransactionCase, tagged
|
||||
import datetime
|
||||
import pytz
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestGeoRouter(TransactionCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.apikey = cls.env['base.geo_router']._get_api_key()
|
||||
|
||||
def test_distance_time_basic(self):
|
||||
bemade, montreal = self._generate_partners()
|
||||
|
||||
distance, time = self.env['base.geo_router'].get_driving_distance_time(bemade, montreal)
|
||||
|
||||
self.assertTrue(20 < distance < 40)
|
||||
self.assertTrue(12 < time < 60)
|
||||
|
||||
def test_time_traffic(self):
|
||||
bemade, montreal = self._generate_partners()
|
||||
est = pytz.timezone('US/Eastern')
|
||||
today_naive = datetime.date.today()
|
||||
today = datetime.datetime(today_naive.year, today_naive.month, today_naive.day, 8, 30, tzinfo=est)
|
||||
arrival_time_local = today + datetime.timedelta(days=(2 - today.weekday()) % 7)
|
||||
arrival_time_utc = arrival_time_local.astimezone(pytz.utc)
|
||||
arrival_time = arrival_time_utc.strftime('%Y-%m-%dT%H:%M:%S.%fZ')
|
||||
print(arrival_time)
|
||||
|
||||
distance, time = self.env['base.geo_router'].get_driving_distance_time(bemade, montreal,
|
||||
arrival_time=arrival_time)
|
||||
|
||||
self.assertTrue(25 < time < 90)
|
||||
|
||||
@classmethod
|
||||
def _generate_partners(self):
|
||||
bemade = self.env['res.partner'].create({
|
||||
'name': 'Bemade Inc.',
|
||||
'street': '255 North Montcalm Blvd.',
|
||||
'city': 'Candiac',
|
||||
'zip': 'J5R 3L6',
|
||||
'state_id': self.env.ref('base.state_ca_qc').id,
|
||||
'country_id': self.env.ref('base.ca').id,
|
||||
})
|
||||
montreal = self.env['res.partner'].create({
|
||||
'name': 'City of Montreal',
|
||||
'street': '220 East René-Lévesque Blvd.',
|
||||
'city': 'Montreal',
|
||||
'zip': 'H2X 3A7',
|
||||
'state_id': self.env.ref('base.state_ca_qc').id,
|
||||
'country_id': self.env.ref('base.ca').id,
|
||||
})
|
||||
return bemade, montreal
|
||||
1
bemade_planning_travel/__init__.py
Normal file
1
bemade_planning_travel/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import models
|
||||
40
bemade_planning_travel/__manifest__.py
Normal file
40
bemade_planning_travel/__manifest__.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
#
|
||||
# Bemade Inc.
|
||||
#
|
||||
# Copyright (C) July 2023 Bemade Inc. (<https://www.bemade.org>).
|
||||
# Author: Marc Durepos (Contact : marc@bemade.org)
|
||||
#
|
||||
# This program is under the terms of the Odoo Proprietary License v1.0 (OPL-1)
|
||||
# It is forbidden to publish, distribute, sublicense, or sell copies of the Software
|
||||
# or modified copies of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
# DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
{
|
||||
'name': 'Planning Travel Time',
|
||||
'version': '15.0.1.0.0',
|
||||
'summary': "Plan travel time for services planned via Odoo's Planning module.",
|
||||
'description': """
|
||||
Adds the ability to add travel planning records to existing planning records
|
||||
linked to sale order lines. Travel planning records book time in the scheduled
|
||||
resource's calendar. Travel distance is based on the resource's work address, if
|
||||
available. Otherwise, travel is assumed to be between the client location and
|
||||
the company's address.""",
|
||||
'category': 'Services/Field Service',
|
||||
'author': 'Bemade Inc.',
|
||||
'website': 'https://www.bemade.org',
|
||||
'license': 'OPL-1',
|
||||
'depends': ['sale_planning',
|
||||
'industry_fsm',
|
||||
'bemade_geo_routing'],
|
||||
'data': [''],
|
||||
'demo': [''],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
}
|
||||
1
bemade_planning_travel/models/__init__.py
Normal file
1
bemade_planning_travel/models/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import planning_slot
|
||||
42
bemade_planning_travel/models/planning_slot.py
Normal file
42
bemade_planning_travel/models/planning_slot.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class PlanningSlot(models.Model):
|
||||
_inherit = "planning.slot"
|
||||
|
||||
inbound_travel_slot = fields.Many2one(comodel_name="planning.slot",
|
||||
string="Inbound Travel Plan", )
|
||||
outbound_travel_slot = fields.Many2one(comodel_name="planning.slot",
|
||||
string="Outbound Travel Plan")
|
||||
|
||||
# Fields for travel slots
|
||||
travel_origin_slot = fields.One2many(comodel_name="planning.slot",
|
||||
inverse_name="outbound_travel_slot")
|
||||
travel_destination_slot = fields.One2many(comodel_name="planning.slot",
|
||||
inverse_name="inbound_travel_slot")
|
||||
is_travel_slot = fields.Boolean()
|
||||
|
||||
def action_plan_travel(self):
|
||||
# Get previous and next planning records for the same day, same resource
|
||||
for rec in self:
|
||||
prev_slot = rec._get_previous_same_day_same_resource_slot()
|
||||
next_slot = rec._get_next_same_day_same_resource_slot()
|
||||
|
||||
|
||||
def _get_previous_same_day_same_resource_slot(self):
|
||||
start = self.start_datetime.replace(hour=0, minute=0, second=0,
|
||||
microsecond=0)
|
||||
domain = [('is_travel_slot', '=', False),
|
||||
('start_datetime', '>=', start),
|
||||
('end_datetime', '<=', self.start_datetime),
|
||||
('resource_id', '=', self.resource_id.id)]
|
||||
return self.env['planning.slot'].search(domain)
|
||||
|
||||
def _get_next_same_day_same_resource_slot(self):
|
||||
end = self.end_datetime.replace(hour=23, minute=59, second=59,
|
||||
microsecond=999999)
|
||||
domain = [('is_travel_slot', '=', False),
|
||||
('start_datetime', '>=', self.end_datetime),
|
||||
('end_datetime', '<=', end),
|
||||
('resource_id', '=', self.resource_id.id)]
|
||||
return self.env['planning.slot'].search(domain)
|
||||
Loading…
Reference in a new issue