Fixes to bemade_fsm to get all tests passing.

This commit is contained in:
Marc Durepos 2024-04-01 14:10:56 -04:00
parent dfd99bdd2c
commit 4faeb0523f
16 changed files with 105 additions and 167 deletions

View file

@ -1,2 +1,3 @@
from . import models
from . import reports
from . import wizard

View file

@ -28,14 +28,10 @@
'website': 'http://www.bemade.org',
'license': 'OPL-1',
'depends': [
'project_enterprise',
'project_forecast',
'stock',
'sale',
'sale_project',
'sale_stock',
'sale_planning',
'worksheet',
'sale_project',
'account',
'project_enterprise',
'industry_fsm_stock',
'industry_fsm_report',
'industry_fsm_sale_report',

View file

@ -43,6 +43,10 @@ class FSMVisit(models.Model):
inverse_name="visit_id"
)
visit_no = fields.Integer(
compute="_compute_visit_no",
)
@api.depends('task_ids')
def _compute_task_id(self):
for rec in self:
@ -68,3 +72,19 @@ class FSMVisit(models.Model):
'name': vals_list[i].get('label', False),
})
return recs
@api.depends(
'so_section_id',
'sale_order_id',
'sale_order_id.visit_ids',
'sale_order_id.visit_ids.so_section_id',
'sale_order_id.visit_ids.so_section_id.sequence'
)
def _compute_visit_no(self):
for rec in self:
ordered_visit_lines = self.sale_order_id.visit_ids.so_section_id.sorted("sequence")
# Just a straight O(n) search here since n will always be relatively small
for index, line in enumerate(ordered_visit_lines):
if line == rec.so_section_id:
rec.visit_no = index + 1
return

View file

@ -161,11 +161,14 @@ class SaleOrderLine(models.Model):
# 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, index + 1)
line.visit_id.task_id = line._generate_task_for_visit_line(
project_id, index + 1,
sum(task_ids.mapped("allocated_hours"))
)
task_ids.write({'parent_id': line.visit_id.task_id.id})
(self.mapped('task_id') | self.visit_ids.task_id).filtered("is_fsm").synchronize_name_fsm()
def _generate_task_for_visit_line(self, project, visit_no: int):
def _generate_task_for_visit_line(self, project, visit_no: int, allocated_hours: int):
self.ensure_one()
allocated_hours = sum(self.get_section_line_ids().task_id.mapped('allocated_hours'))
@ -176,8 +179,8 @@ class SaleOrderLine(models.Model):
'sale_order_id': self.order_id.id,
'partner_id': self.order_id.partner_shipping_id.id,
'visit_id': self.visit_id.id,
'date_deadline': self.visit_id.approx_date,
'allocated_hours': allocated_hours,
'date_deadline': self.visit_id.approx_date,
'user_ids': False, # Force to empty or it uses the current user
})
return task

View file

@ -38,24 +38,6 @@ class Task(models.Model):
store=True
)
# planned_date_begin = fields.Datetime(
# string="Planned Start Date",
# tracking=True,
# task_dependency_tracking=True,
# compute="_compute_planned_dates",
# inverse="_inverse_planned_dates",
# store=True
# )
#
# planned_date_end = fields.Datetime(
# string="Planned 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",
@ -91,7 +73,6 @@ class Task(models.Model):
for rec in self:
rec.is_closed = rec.state in CLOSED_STATES
@api.model_create_multi
def create(self, vals):
res = super().create(vals)
@ -142,41 +123,6 @@ class Task(models.Model):
for project in self.project_id
}
# def _get_related_planning_slots(self):
# domain = [('task_id', 'in', self.ids + self._get_all_subtasks().ids), ('start_datetime', '!=', False)]
# return self.env['planning.slot'].search(domain)
# @api.depends('planned_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):
@ -210,59 +156,18 @@ class Task(models.Model):
else:
rec.allow_billable = rec.project_id.allow_billable
def _fsm_create_sale_order_line(self):
# Override to not generate new lines for tasks that are just linked to a visit item
self.ensure_one()
if self.visit_id:
return
else:
super()._fsm_create_sale_order_line()
def action_fsm_validate(self, stop_running_timers=False):
# Override to close out subtasks as well
all_tasks = self | self._get_all_subtasks()
non_visit_tasks = all_tasks.filtered(lambda task: not task.visit_id)
visit_tasks = all_tasks.filtered(lambda task: bool(task.visit_id))
result = visit_tasks._validate_task_without_creating_sale_order_line(stop_running_timers)
if isinstance(result, dict):
return result
result = super(Task, non_visit_tasks).action_fsm_validate(stop_running_timers)
if isinstance(result, dict):
return result
return result
def _validate_task_without_creating_sale_order_line(self, stop_running_timers=False):
# Reproduce the functionality of action_fsm_validate but don't apply logic from industry_fsm_sale
# so as to avoid creating order lines for visits
Timer = self.env['timer.timer']
tasks_running_timer_ids = Timer.search([('res_model', '=', 'project.task'), ('res_id', 'in', self.ids)])
timesheets = self.env['account.analytic.line'].sudo().search([('task_id', 'in', self.ids)])
timesheets_running_timer_ids = None
if timesheets:
timesheets_running_timer_ids = Timer.search([
('res_model', '=', 'account.analytic.line'),
('res_id', 'in', timesheets.ids)])
if tasks_running_timer_ids or timesheets_running_timer_ids:
if stop_running_timers:
self._stop_all_timers_and_create_timesheets(tasks_running_timer_ids, timesheets_running_timer_ids,
timesheets)
else:
wizard = self.env['project.task.stop.timers.wizard'].create({
'line_ids': [Command.create({'task_id': task.id}) for task in self],
})
return {
'name': _('Do you want to stop the running timers?'),
'type': 'ir.actions.act_window',
'view_mode': 'form',
'view_id': self.env.ref('industry_fsm.view_task_stop_timer_wizard_form').id,
'target': 'new',
'res_model': 'project.task.stop.timers.wizard',
'res_id': wizard.id,
}
closed_stage_by_project = {
project.id:
project.type_ids.filtered(lambda stage: stage.fold)[:1] or project.type_ids[-1:]
for project in self.project_id
}
for task in self:
# determine closed stage for task
closed_stage = closed_stage_by_project.get(self.project_id.id)
values = {'fsm_done': True}
if closed_stage:
values['stage_id'] = closed_stage.id
task.write(values)
return super(Task, all_tasks).action_fsm_validate(stop_running_timers)
def _get_full_hierarchy(self):
if self.child_ids:
@ -285,15 +190,24 @@ class Task(models.Model):
assert rec.is_fsm, "This method should only be called on FSM tasks."
template = rec.sale_line_id and rec.sale_line_id.product_id.task_template_id
name_parts = rec.sale_line_id and rec.sale_line_id.name.split('\n')
title = name_parts and name_parts[0] or rec.sale_line_id.product_id.name
if not rec.parent_id:
rec.name = f"{rec.sale_order_id.partner_shipping_id.name} - " \
f"{title}"
if template:
rec.name += f" ({template.name})"
if template:
title = template.name
elif rec.sale_line_id:
name_parts = rec.sale_line_id.name.split('\n')
title = name_parts and name_parts[0] or rec.sale_line_id.product_id.name
elif rec.visit_id:
title = rec.visit_id.label
else:
rec.name = template.name or title or rec.name
rec.name = rec.name
return
client_name = rec.sale_order_id.partner_shipping_id.name
if not rec.parent_id:
rec.name = f"{rec.work_order_number} - {client_name} - " f"{title}"
else:
rec.name = title
@property
def root_ancestor(self):

View file

@ -0,0 +1 @@
from . import worksheet_custom_reports

View file

@ -444,7 +444,7 @@
<div class="page">
<t t-call="bemade_fsm.workorder_page_info_block"/>
<t t-call="bemade_fsm.workorder_page_timesheet_entries"/>
<t t-if="doc.company_id.split_time_from_materials_on_service_work_orders">
<t t-if="split_time_materials">
<t t-call="bemade_fsm.workorder_page_materials_table"/>
</t>
<t t-else="">

View file

@ -0,0 +1,11 @@
from odoo import models
class TaskCustomReport(models.AbstractModel):
_inherit = "report.industry_fsm_report.worksheet_custom"
def _get_report_values(self, docids, data=None):
vals = super()._get_report_values(docids, data)
split_time_materials = self.env.company.split_time_from_materials_on_service_work_orders
vals.update({"split_time_materials": split_time_materials})
return vals

View file

@ -4,7 +4,6 @@ from odoo import Command
@tagged("-at_install", "post_install")
class BemadeFSMBaseTest(TransactionCase):
@classmethod
def _generate_project_manager_user(cls, name, login):
group_ids = cls.__get_user_groups()
@ -110,6 +109,7 @@ class BemadeFSMBaseTest(TransactionCase):
'name': name,
'type': product_type,
'service_tracking': service_tracking,
'service_type': 'timesheet',
'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,
@ -124,7 +124,6 @@ class BemadeFSMBaseTest(TransactionCase):
'name': name,
'allow_material': True,
'allow_timesheets': True,
'allow_subtasks': True,
'allow_quotations': True,
'allow_worksheets': True,
'is_fsm': True,

View file

@ -19,7 +19,3 @@ class TestEquipment(BemadeFSMBaseTest):
partner_company.write({'equipment_ids': [Command.set([])]})
with self.assertRaises(MissingError):
equipment.name
def test_compute_complete_name_when_name_blank(self):
equipment = self._generate_equipment(name=False)
complete_name = equipment.complete_name

View file

@ -61,6 +61,7 @@ class SaleOrderFSMContactsCase(BemadeFSMBaseTest):
self.assertEqual(so.work_order_contacts, shipping_location.work_order_contacts)
def test_onchange_shipping_address(self):
self.env.user.groups_id += self.env.ref('account.group_delivery_invoice_address')
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)

View file

@ -89,6 +89,8 @@ class FSMVisitTest(BemadeFSMBaseTest):
so.action_confirm()
visit_task = visit.task_id
self.assertEqual(sol1.task_id.allocated_hours, 4.0)
self.assertEqual(sol2.task_id.allocated_hours, 4.0)
self.assertEqual(visit_task.allocated_hours, 8.0)
def test_adding_visit_creates_one_sale_order_line(self):
@ -127,9 +129,9 @@ class FSMVisitTest(BemadeFSMBaseTest):
def test_confirming_so_names_visit_properly(self):
""" Visits should be named <SO NUMBER> - Visit <visit #> - <visit label>"""
so, visit, sol1, sol2 = self._generate_so_with_one_visit_two_lines()
so.name = "SO12345"
so.action_confirm()
task = visit.task_id
supposed_name = f"{so.name} - Visit 1 - {visit.label}"
supposed_name = f"SVR12345-1 - Test Company - Test Label"
self.assertEqual(task.name, supposed_name)

View file

@ -33,7 +33,6 @@ class TaskTest(BemadeFSMBaseTest):
task = self.task
task.write({
'user_ids': [Command.set([self.user.id])],
'propagate_assignment': True,
})
self.assertFalse(any([t.user_ids for t in task.child_ids.child_ids]))

View file

@ -4,9 +4,11 @@ from odoo.tests import Form
class TestTaskReport(BemadeFSMBaseTest):
def test_split_time_materials_setting(self):
settings = Form(self.env['res.config.settings'])
settings.module_bemade_fsm_separate_time_on_work_orders = True
settings.save()
with Form(self.env['res.config.settings']) as settings:
settings.module_bemade_fsm_separate_time_on_work_orders = True
with Form(self.env['res.config.settings']) as new_settings:
self.assertTrue(settings.module_bemade_fsm_separate_time_on_work_orders)
so = self._generate_sale_order()
service_product = self._generate_product()
@ -15,10 +17,11 @@ class TestTaskReport(BemadeFSMBaseTest):
product_type='product',
service_tracking='no',
)
visit = self._generate_visit(sale_order=so)
sol = self._generate_sale_order_line(sale_order=so, product=service_product)
sol2 = self._generate_sale_order_line(sale_order=so, product=material_product)
so.action_confirm()
task = sol.task_id
task = visit.task_id
html_content = self.env['ir.actions.report']._render(
'industry_fsm_report.worksheet_custom',

View file

@ -44,27 +44,6 @@ class TestTaskTemplate(BemadeFSMBaseTest):
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.task_duration, 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.task_duration, 24)
def test_child_task_names_are_short_version(self):
so, visit, sol1, sol2 = self._generate_so_with_one_visit_two_lines()
template = self._generate_task_template(names=['Task'])

View file

@ -4,20 +4,33 @@ from odoo import models, fields, api
class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"
module_bemade_fsm_separate_time_on_work_orders = fields.Boolean("Separate Time from Materials on Work Order")
module_bemade_fsm_create_default_fsm_visit = fields.Boolean("Create Default Visit for FSM Sales Orders")
def set_values(self):
super().set_values()
self.env.company.split_time_from_materials_on_service_work_orders = \
self.module_bemade_fsm_separate_time_on_work_orders
self.env.company.create_default_fsm_visit = self.module_bemade_fsm_create_default_fsm_visit
module_bemade_fsm_separate_time_on_work_orders = fields.Boolean(
"Separate Time from Materials on Work Order",
config_parameter="bemade_fsm_separate_time_on_work_orders",
related="company_id.split_time_from_materials_on_service_work_orders",
company_dependent=True,
readonly=False,
)
module_bemade_fsm_create_default_fsm_visit = fields.Boolean(
"Create Default Visit for FSM Sales Orders",
config_parameter="bemade_fsm_create_default_fsm_visit",
related="company_id.create_default_fsm_visit",
company_dependent=True,
readonly=False,
)
@api.model
def get_values(self):
res = super().get_values()
res.update({
'module_bemade_fsm_separate_time_on_work_orders':
self.env.company.split_time_from_materials_on_service_work_orders,
'module_bemade_fsm_create_default_fsm_visit': self.env.company.create_default_fsm_visit
'module_bemade_fsm_create_default_fsm_visit': self.env.company.create_default_fsm_visit,
})
return res
def set_values(self):
super().set_values()
self.env.company.split_time_from_materials_on_service_work_orders = \
self.module_bemade_fsm_separate_time_on_work_orders
self.env.company.create_default_fsm_visit = self. module_bemade_fsm_create_default_fsm_visit