From 81624ea26ca68e09e3a1cff5492898a43c3f4dcb Mon Sep 17 00:00:00 2001 From: Marc Durepos Date: Wed, 29 Nov 2023 20:45:09 -0500 Subject: [PATCH] bemade_fsm: all tests passing for 16.0 - Got rid of planned_date_begin and planned_date_end fields. Commented out all related code for now. These were fields pending actual feature development anyway. - Added a product cateogory with income account and expense account to the _generate_product common test method. This fixes an error where Odoo complained about needing a due date for lines with account type = 'asset_receivable'. - Rewrote and refactored Task.action_fsm_validate to have it match with code from industry_fsm in 16.0. NOTE: This change depends on code pushed in: - sale_workflow [16.0 ba7402471] - odoo_addons [16.0 766cd78] - PurchasedAddons [16.0 e5e9b7b] --- bemade_fsm/models/task.py | 165 +++++++++++++-------- bemade_fsm/tests/test_bemade_fsm_common.py | 26 +++- bemade_fsm/tests/test_fsm_visit.py | 8 +- bemade_fsm/views/sale_order_views.xml | 6 +- 4 files changed, 133 insertions(+), 72 deletions(-) diff --git a/bemade_fsm/models/task.py b/bemade_fsm/models/task.py index fd4fab0..b9b5a92 100644 --- a/bemade_fsm/models/task.py +++ b/bemade_fsm/models/task.py @@ -37,23 +37,23 @@ 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 - ) + # 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( @@ -112,43 +112,40 @@ class Task(models.Model): for project in self.project_id } - 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) + # 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('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') @@ -183,17 +180,59 @@ class Task(models.Model): else: rec.allow_billable = rec.project_id.allow_billable - def action_fsm_validate(self): - visits = self.filtered(lambda t: t.visit_id) - non_visits = self - visits - super(Task, non_visits).action_fsm_validate() + def action_fsm_validate(self, stop_running_timers=False): + 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 - visits._stop_all_timers_and_create_timesheets() - closed_stage_by_project = visits._get_closed_stage_by_project() - super(Task, visits.child_ids).action_fsm_validate() - for visit in visits: - stage = closed_stage_by_project[visit.project_id] - visits.write({'stage_id': stage.id, 'fsm_done': True}) + 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) def synchronize_name_fsm(self): """ Applies naming to the entire task tree for tasks that are part of this diff --git a/bemade_fsm/tests/test_bemade_fsm_common.py b/bemade_fsm/tests/test_bemade_fsm_common.py index 613db7b..695c489 100644 --- a/bemade_fsm/tests/test_bemade_fsm_common.py +++ b/bemade_fsm/tests/test_bemade_fsm_common.py @@ -69,8 +69,11 @@ class BemadeFSMBaseTest(TransactionCase): @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} + vals = { + 'partner_id': partner.id, + 'client_order_ref': client_order_ref, + 'payment_term_id': cls.env.ref('account.account_payment_term_immediate').id, + } if equipment: vals.update({'default_equipment_ids': [Command.set([equipment.id])]}) if shipping_location: @@ -97,6 +100,22 @@ class BemadeFSMBaseTest(TransactionCase): 'partner_location_id': partner and partner.id or False, }) + @classmethod + def _generate_product_category(cls): + company_id = cls.env.company.id + return cls.env['product.category'].create({ + 'name': 'Test Category', + 'parent_id': cls.env.ref('product.product_category_all', False).id or False, + 'property_account_income_categ_id': cls.env['account.account'].search( + [('account_type', '=', 'income'), ('company_id', '=', company_id)], + limit=1 + ).id, + 'property_account_expense_categ_id': cls.env['account.account'].search( + [('account_type', '=', 'expense_direct_cost'), ('company_id', '=', company_id)], + limit=1 + ).id, + }) + @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): @@ -113,6 +132,7 @@ class BemadeFSMBaseTest(TransactionCase): 'service_policy': service_policy, 'uom_id': uom_id, 'uom_po_id': uom_id, + 'categ_id': cls._generate_product_category().id, }) @classmethod @@ -171,7 +191,7 @@ class BemadeFSMBaseTest(TransactionCase): def _invoice_sale_order(self, so): wiz = self.env['sale.advance.payment.inv'].with_context( - {'active_ids': [so.id]}).create({}) + {'active_ids': [so.id]}).create([{}]) wiz.create_invoices() inv = so.invoice_ids[-1] inv.action_post() diff --git a/bemade_fsm/tests/test_fsm_visit.py b/bemade_fsm/tests/test_fsm_visit.py index a6d3721..96fc03d 100644 --- a/bemade_fsm/tests/test_fsm_visit.py +++ b/bemade_fsm/tests/test_fsm_visit.py @@ -40,7 +40,7 @@ class FSMVisitTest(BemadeFSMBaseTest): so.action_confirm() task = so.order_line.filtered(lambda l: l.task_id).task_id - task.action_fsm_validate() + task.action_fsm_validate(stop_running_timers=True) self.assertTrue(visit.is_completed) @@ -50,7 +50,7 @@ class FSMVisitTest(BemadeFSMBaseTest): 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() + task.action_fsm_validate(stop_running_timers=True) self._invoice_sale_order(so) @@ -106,7 +106,7 @@ class FSMVisitTest(BemadeFSMBaseTest): so.action_confirm() parent, child1, child2 = visit.task_id, sol1.task_id, sol2.task_id - parent.action_fsm_validate() + parent.action_fsm_validate(stop_running_timers=True) self.assertTrue(parent.is_closed) self.assertTrue(child1.is_closed) @@ -119,6 +119,6 @@ class FSMVisitTest(BemadeFSMBaseTest): so, visit, sol1, sol2 = self._generate_so_with_one_visit_two_lines() so.action_confirm() - visit.task_id.action_fsm_validate() + visit.task_id.action_fsm_validate(stop_running_timers=True) self.assertEqual(len(so.order_line), 3) diff --git a/bemade_fsm/views/sale_order_views.xml b/bemade_fsm/views/sale_order_views.xml index 6baed0c..369b30b 100644 --- a/bemade_fsm/views/sale_order_views.xml +++ b/bemade_fsm/views/sale_order_views.xml @@ -16,7 +16,8 @@ + widget="many2many_tags" + groups="account.group_delivery_invoice_address"/> + domain="[('id', 'in', valid_equipment_ids)]" + groups="account.group_delivery_invoice_address"/>