bemade_fsm: Due date and initially planned hours on SO confirmation.
Fixes Dur-Pro/bemade-addons#86 and Dur-Pro/bemade-addons87
This commit is contained in:
parent
e604757617
commit
e1bf6e18b6
4 changed files with 107 additions and 30 deletions
|
|
@ -39,7 +39,7 @@ class FSMVisit(models.Model):
|
||||||
@api.depends('so_section_id', 'sale_order_id.summary_equipment_ids')
|
@api.depends('so_section_id', 'sale_order_id.summary_equipment_ids')
|
||||||
def _compute_summarized_equipment_ids(self):
|
def _compute_summarized_equipment_ids(self):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
lines = rec.so_section_id.get_section_lines()
|
lines = rec.so_section_id.get_section_line_ids()
|
||||||
equipment_ids = []
|
equipment_ids = []
|
||||||
for line in lines:
|
for line in lines:
|
||||||
for equipment in line.equipment_ids:
|
for equipment in line.equipment_ids:
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from odoo import fields, models, api, _, Command
|
from odoo import fields, models, api, _, Command
|
||||||
from odoo.exceptions import ValidationError
|
from odoo.exceptions import ValidationError
|
||||||
|
from odoo.tools import float_round
|
||||||
|
|
||||||
|
|
||||||
class SaleOrder(models.Model):
|
class SaleOrder(models.Model):
|
||||||
|
|
@ -55,7 +56,8 @@ class SaleOrder(models.Model):
|
||||||
def _inverse_default_contacts(self):
|
def _inverse_default_contacts(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@api.depends('partner_id', 'partner_shipping_id', 'partner_shipping_id.equipment_ids',
|
@api.depends('partner_id', 'partner_shipping_id',
|
||||||
|
'partner_shipping_id.equipment_ids',
|
||||||
'partner_id.owned_equipment_ids')
|
'partner_id.owned_equipment_ids')
|
||||||
def _compute_default_equipment(self):
|
def _compute_default_equipment(self):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
|
|
@ -74,10 +76,10 @@ class SaleOrderLine(models.Model):
|
||||||
valid_equipment_ids = fields.One2many(comodel_name="bemade_fsm.equipment",
|
valid_equipment_ids = fields.One2many(comodel_name="bemade_fsm.equipment",
|
||||||
related="order_id.valid_equipment_ids")
|
related="order_id.valid_equipment_ids")
|
||||||
visit_ids = fields.One2many(comodel_name="bemade_fsm.visit",
|
visit_ids = fields.One2many(comodel_name="bemade_fsm.visit",
|
||||||
inverse_name="so_section_id",)
|
inverse_name="so_section_id", )
|
||||||
visit_id = fields.Many2one(comodel_name="bemade_fsm.visit",
|
visit_id = fields.Many2one(comodel_name="bemade_fsm.visit",
|
||||||
compute="_compute_visit_id",
|
compute="_compute_visit_id",
|
||||||
string = "Visit",
|
string="Visit",
|
||||||
ondelete='cascade',
|
ondelete='cascade',
|
||||||
store=True)
|
store=True)
|
||||||
is_fully_delivered = fields.Boolean(string="Fully Delivered",
|
is_fully_delivered = fields.Boolean(string="Fully Delivered",
|
||||||
|
|
@ -97,6 +99,9 @@ class SaleOrderLine(models.Model):
|
||||||
compute="_compute_is_field_service",
|
compute="_compute_is_field_service",
|
||||||
store=True)
|
store=True)
|
||||||
|
|
||||||
|
task_duration = fields.Float(string="Estimated Duration",
|
||||||
|
compute="_compute_task_duration", )
|
||||||
|
|
||||||
@api.depends('visit_ids')
|
@api.depends('visit_ids')
|
||||||
def _compute_visit_id(self):
|
def _compute_visit_id(self):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
|
|
@ -129,7 +134,9 @@ class SaleOrderLine(models.Model):
|
||||||
:param template: project.task.template to use to create the task.
|
:param template: project.task.template to use to create the task.
|
||||||
:param parent: project.task to set as the parent to this task.
|
:param parent: project.task to set as the parent to this task.
|
||||||
"""
|
"""
|
||||||
values = _timesheet_create_task_prepare_values_from_template(project, template, parent)
|
values = _timesheet_create_task_prepare_values_from_template(project,
|
||||||
|
template,
|
||||||
|
parent)
|
||||||
task = self.env['project.task'].sudo().create(values)
|
task = self.env['project.task'].sudo().create(values)
|
||||||
subtasks = []
|
subtasks = []
|
||||||
for t in template.subtasks:
|
for t in template.subtasks:
|
||||||
|
|
@ -144,7 +151,8 @@ class SaleOrderLine(models.Model):
|
||||||
template_name = template and template.name
|
template_name = template and template.name
|
||||||
return f"{self.order_id.name}: {self.order_id.partner_shipping_id.name} - {self.name} ({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):
|
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.
|
""" Copies the values from a project.task.template over to the set of values used to create a project.task.
|
||||||
|
|
||||||
:param project: project.project record to set on the task's project_id field.
|
:param project: project.project record to set on the task's project_id field.
|
||||||
|
|
@ -177,6 +185,7 @@ class SaleOrderLine(models.Model):
|
||||||
task.message_post(body=task_msg)
|
task.message_post(body=task_msg)
|
||||||
if not task.equipment_ids and self.equipment_ids:
|
if not task.equipment_ids and self.equipment_ids:
|
||||||
task.equipment_ids = self.equipment_ids.ids
|
task.equipment_ids = self.equipment_ids.ids
|
||||||
|
task.planned_hours = self.task_duration
|
||||||
task.name = _generate_task_name(tmpl)
|
task.name = _generate_task_name(tmpl)
|
||||||
return task
|
return task
|
||||||
|
|
||||||
|
|
@ -184,7 +193,7 @@ class SaleOrderLine(models.Model):
|
||||||
super()._timesheet_service_generation()
|
super()._timesheet_service_generation()
|
||||||
visit_lines = self.filtered(lambda l: l.visit_id)
|
visit_lines = self.filtered(lambda l: l.visit_id)
|
||||||
for line in visit_lines:
|
for line in visit_lines:
|
||||||
task_ids = line.get_section_lines().mapped('task_id')
|
task_ids = line.get_section_line_ids().mapped('task_id')
|
||||||
if not task_ids:
|
if not task_ids:
|
||||||
continue
|
continue
|
||||||
if len(set([task.project_id for task in task_ids])) > 1:
|
if len(set([task.project_id for task in task_ids])) > 1:
|
||||||
|
|
@ -196,22 +205,27 @@ class SaleOrderLine(models.Model):
|
||||||
|
|
||||||
def _generate_task_for_visit_line(self, project):
|
def _generate_task_for_visit_line(self, project):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
|
|
||||||
task = self.env['project.task'].create({
|
task = self.env['project.task'].create({
|
||||||
'name': self.order_id.name + ": " + self.name,
|
'name': self.order_id.name + ": " + self.name,
|
||||||
'description': f"Parent task for {self.order_id.name}, visit {self.name}",
|
'description': f"Parent task for {self.order_id.name}, visit {self.name}",
|
||||||
'project_id': project.id,
|
'project_id': project.id,
|
||||||
'equipment_ids': self.get_section_lines().mapped('equipment_ids').ids,
|
'equipment_ids': self.get_section_line_ids().mapped('equipment_ids').ids,
|
||||||
'sale_order_id': self.order_id.id,
|
'sale_order_id': self.order_id.id,
|
||||||
'partner_id': self.order_id.partner_shipping_id.id,
|
'partner_id': self.order_id.partner_shipping_id.id,
|
||||||
'visit_id': self.visit_id.id,
|
'visit_id': self.visit_id.id,
|
||||||
|
'date_deadline': self.visit_id.approx_date,
|
||||||
|
'planned_hours': self.task_duration,
|
||||||
})
|
})
|
||||||
return task
|
return task
|
||||||
|
|
||||||
@api.depends('order_id.order_line', 'display_type', 'qty_to_deliver', 'order_id.order_line.qty_to_deliver',
|
@api.depends('order_id.order_line', 'display_type', 'qty_to_deliver',
|
||||||
|
'order_id.order_line.qty_to_deliver',
|
||||||
'order_id.order_line.display_type')
|
'order_id.order_line.display_type')
|
||||||
def _compute_is_fully_delivered(self):
|
def _compute_is_fully_delivered(self):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
rec.is_fully_delivered = rec._iterate_items_compute_bool(lambda l: l.qty_to_deliver == 0)
|
rec.is_fully_delivered = rec._iterate_items_compute_bool(
|
||||||
|
lambda l: l.qty_to_deliver == 0)
|
||||||
|
|
||||||
@api.depends('is_fully_delivered')
|
@api.depends('is_fully_delivered')
|
||||||
def _compute_is_fully_invoiced(self):
|
def _compute_is_fully_invoiced(self):
|
||||||
|
|
@ -219,15 +233,15 @@ class SaleOrderLine(models.Model):
|
||||||
if not rec.is_fully_delivered:
|
if not rec.is_fully_delivered:
|
||||||
rec.is_fully_delivered_and_invoiced = False
|
rec.is_fully_delivered_and_invoiced = False
|
||||||
return
|
return
|
||||||
rec.is_fully_delivered_and_invoiced = rec._iterate_items_compute_bool(lambda l: l.qty_to_invoice == 0)
|
rec.is_fully_delivered_and_invoiced = rec._iterate_items_compute_bool(
|
||||||
|
lambda l: l.qty_to_invoice == 0)
|
||||||
|
|
||||||
def get_section_lines(self):
|
def get_section_line_ids(self):
|
||||||
""" Returns a RecordSet containing the sale order lines that fall under this section. """
|
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
assert self.display_type == 'line_section', 'Method called incorrectly on non-section order line.'
|
assert self.display_type == 'line_section', "Cannot get section lines for a non-section."
|
||||||
found = False
|
found = False
|
||||||
lines = []
|
lines = []
|
||||||
for line in self.order_id.order_line:
|
for line in self.order_id.order_line.sorted(lambda l: l.sequence):
|
||||||
if line == self:
|
if line == self:
|
||||||
found = True
|
found = True
|
||||||
continue
|
continue
|
||||||
|
|
@ -258,14 +272,42 @@ class SaleOrderLine(models.Model):
|
||||||
return val
|
return val
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@api.depends('product_uom', 'product_uom_qty', 'product_id.planning_enabled', 'state',
|
@api.depends('task_duration', 'product_id', 'product_id.planning_enabled', 'state')
|
||||||
'product_id.task_template_id')
|
|
||||||
def _compute_planning_hours_to_plan(self):
|
def _compute_planning_hours_to_plan(self):
|
||||||
# Override the method from sale_planning to use time estimates from the task template if appropriate
|
# Override the method from sale_planning to use time estimates from the task
|
||||||
super()._compute_planning_hours_to_plan()
|
# template if appropriate. Also compute the value for visit lines.
|
||||||
templated_lines = self.filtered(
|
planning_lines = self.filtered_domain([
|
||||||
lambda l: l.product_id.task_template_id and l.product_id.task_template_id.planned_hours)
|
('product_id.planning_enabled', '=', True),
|
||||||
|
('state', 'not in', ['draft', 'sent'])
|
||||||
|
])
|
||||||
|
for line in planning_lines:
|
||||||
|
line.planning_hours_to_plan = line.task_duration
|
||||||
|
for line in self - planning_lines:
|
||||||
|
line.planning_hours_to_plan = 0.0
|
||||||
|
|
||||||
|
@api.depends('product_id', 'visit_id')
|
||||||
|
def _compute_task_duration(self):
|
||||||
|
uom_hour = self.env.ref('uom.product_uom_hour')
|
||||||
|
uom_unit = self.env.ref('uom.product_uom_unit')
|
||||||
|
templated_lines = self.filtered(lambda l: l.product_id.task_template_id
|
||||||
|
and l.product_id.task_template_id.
|
||||||
|
planned_hours)
|
||||||
|
visit_lines = self.filtered(lambda l: l.visit_id)
|
||||||
|
regular_lines = self - templated_lines - visit_lines
|
||||||
|
for line in regular_lines:
|
||||||
|
if line.product_uom == uom_hour or line.product_uom == uom_unit:
|
||||||
|
line.task_duration = line.product_uom_qty
|
||||||
|
else:
|
||||||
|
line.task_duration = float_round(
|
||||||
|
line.product_uom._compute_quantity(line.product_uom_qty, uom_hour,
|
||||||
|
raise_if_failure=False),
|
||||||
|
precision_digits=2)
|
||||||
for line in templated_lines:
|
for line in templated_lines:
|
||||||
line.planning_hours_to_plan = line.product_id.task_template_id.planned_hours
|
line.task_duration = line.product_id.task_template_id.planned_hours
|
||||||
if line.product_uom_category_id == self.env.ref('uom.product_uom_unit').category_id:
|
if line.product_uom_category_id == self.env.ref(
|
||||||
line.planning_hours_to_plan *= line.product_uom_qty
|
'uom.product_uom_unit').category_id:
|
||||||
|
line.task_duration *= line.product_uom_qty
|
||||||
|
visit_lines = self.filtered(lambda l: l.visit_id)
|
||||||
|
for line in visit_lines:
|
||||||
|
line.task_duration = sum(line.get_section_line_ids().
|
||||||
|
mapped('task_duration'))
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from odoo.tests import TransactionCase, tagged, Form
|
from odoo.tests import TransactionCase, tagged, Form
|
||||||
from .test_bemade_fsm_common import BemadeFSMBaseTest
|
from .test_bemade_fsm_common import BemadeFSMBaseTest
|
||||||
|
from datetime import date, timedelta
|
||||||
|
|
||||||
|
|
||||||
@tagged('-at_install', 'post_install')
|
@tagged('-at_install', 'post_install')
|
||||||
|
|
@ -56,7 +57,6 @@ class FSMVisitTest(BemadeFSMBaseTest):
|
||||||
self.assertTrue(visit.is_invoiced)
|
self.assertTrue(visit.is_invoiced)
|
||||||
|
|
||||||
def test_visit_groups_section_tasks_when_confirmed(self):
|
def test_visit_groups_section_tasks_when_confirmed(self):
|
||||||
partner = self._generate_partner()
|
|
||||||
so = self._generate_sale_order()
|
so = self._generate_sale_order()
|
||||||
visit = self._generate_visit(sale_order=so)
|
visit = self._generate_visit(sale_order=so)
|
||||||
sol1 = self._generate_sale_order_line(sale_order=so)
|
sol1 = self._generate_sale_order_line(sale_order=so)
|
||||||
|
|
@ -70,7 +70,40 @@ class FSMVisitTest(BemadeFSMBaseTest):
|
||||||
visit_task = visit.task_id
|
visit_task = visit.task_id
|
||||||
self.assertTrue(visit_task)
|
self.assertTrue(visit_task)
|
||||||
visit_subtasks = visit_task.child_ids
|
visit_subtasks = visit_task.child_ids
|
||||||
self.assertTrue(visit_subtasks and sol1.task_id in visit_subtasks and sol2.task_id in visit_subtasks)
|
self.assertTrue(
|
||||||
|
visit_subtasks and sol1.task_id in visit_subtasks and sol2.task_id in visit_subtasks)
|
||||||
|
|
||||||
|
def test_visit_task_gets_correct_due_date_on_confirmation(self):
|
||||||
|
partner = self._generate_partner()
|
||||||
|
so = self._generate_sale_order(partner=partner)
|
||||||
|
visit = self._generate_visit(sale_order=so)
|
||||||
|
sol1 = self._generate_sale_order_line(sale_order=so, qty=4.0)
|
||||||
|
sol2 = self._generate_sale_order_line(sale_order=so, qty=4.0)
|
||||||
|
visit.approx_date = date.today() + timedelta(days=7)
|
||||||
|
visit.so_section_id.sequence = 1
|
||||||
|
sol1.sequence = 2
|
||||||
|
sol2.sequence = 3
|
||||||
|
|
||||||
|
so.action_confirm()
|
||||||
|
|
||||||
|
visit_task = visit.task_id
|
||||||
|
self.assertEqual(visit_task.date_deadline, visit.approx_date)
|
||||||
|
|
||||||
|
def test_visit_task_gets_correct_duration_on_confirmation(self):
|
||||||
|
partner = self._generate_partner()
|
||||||
|
so = self._generate_sale_order(partner=partner)
|
||||||
|
visit = self._generate_visit(sale_order=so)
|
||||||
|
sol1 = self._generate_sale_order_line(sale_order=so, qty=4.0)
|
||||||
|
sol2 = self._generate_sale_order_line(sale_order=so, qty=4.0)
|
||||||
|
visit.approx_date = date.today() + timedelta(days=7)
|
||||||
|
visit.so_section_id.sequence = 1
|
||||||
|
sol1.sequence = 2
|
||||||
|
sol2.sequence = 3
|
||||||
|
|
||||||
|
so.action_confirm()
|
||||||
|
|
||||||
|
visit_task = visit.task_id
|
||||||
|
self.assertEqual(visit_task.planned_hours, 8.0)
|
||||||
|
|
||||||
def test_adding_visit_creates_one_sale_order_line(self):
|
def test_adding_visit_creates_one_sale_order_line(self):
|
||||||
partner = self._generate_partner()
|
partner = self._generate_partner()
|
||||||
|
|
@ -83,7 +116,8 @@ class FSMVisitTest(BemadeFSMBaseTest):
|
||||||
self.assertEqual(len(so.order_line), 3)
|
self.assertEqual(len(so.order_line), 3)
|
||||||
|
|
||||||
def _invoice_sale_order(self, so):
|
def _invoice_sale_order(self, so):
|
||||||
wiz = self.env['sale.advance.payment.inv'].with_context({'active_ids': [so.id]}).create({})
|
wiz = self.env['sale.advance.payment.inv'].with_context(
|
||||||
|
{'active_ids': [so.id]}).create({})
|
||||||
wiz.create_invoices()
|
wiz.create_invoices()
|
||||||
inv = so.invoice_ids[-1]
|
inv = so.invoice_ids[-1]
|
||||||
inv.action_post()
|
inv.action_post()
|
||||||
|
|
|
||||||
|
|
@ -52,17 +52,18 @@ class TestTaskTemplate(BemadeFSMBaseTest):
|
||||||
|
|
||||||
sol = self._generate_sale_order_line(sale_order=so, product=product)
|
sol = self._generate_sale_order_line(sale_order=so, product=product)
|
||||||
|
|
||||||
self.assertEqual(sol.planning_hours_to_plan, 8)
|
self.assertEqual(sol.task_duration, 8)
|
||||||
|
|
||||||
def test_hours_estimate_multiplied_for_multiple_units_sold(self):
|
def test_hours_estimate_multiplied_for_multiple_units_sold(self):
|
||||||
partner = self._generate_partner()
|
partner = self._generate_partner()
|
||||||
so = self._generate_sale_order(partner=partner)
|
so = self._generate_sale_order(partner=partner)
|
||||||
task_template = self._generate_task_template(planned_hours=8)
|
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)
|
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)
|
sol = self._generate_sale_order_line(sale_order=so, product=product, qty=3.0)
|
||||||
|
|
||||||
self.assertEqual(sol.planning_hours_to_plan, 24)
|
self.assertEqual(sol.task_duration, 24)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue