Fixes to bemade_fsm to get all tests passing.
This commit is contained in:
parent
dfd99bdd2c
commit
4faeb0523f
16 changed files with 105 additions and 167 deletions
|
|
@ -1,2 +1,3 @@
|
|||
from . import models
|
||||
from . import reports
|
||||
from . import wizard
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
1
bemade_fsm/reports/__init__.py
Normal file
1
bemade_fsm/reports/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import worksheet_custom_reports
|
||||
|
|
@ -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="">
|
||||
|
|
|
|||
11
bemade_fsm/reports/worksheet_custom_reports.py
Normal file
11
bemade_fsm/reports/worksheet_custom_reports.py
Normal 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
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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]))
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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'])
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue