diff --git a/bemade_fsm/__init__.py b/bemade_fsm/__init__.py index 9b42961..a27d6a2 100644 --- a/bemade_fsm/__init__.py +++ b/bemade_fsm/__init__.py @@ -1,2 +1,3 @@ from . import models +from . import reports from . import wizard diff --git a/bemade_fsm/__manifest__.py b/bemade_fsm/__manifest__.py index 92e49ff..4edc69b 100644 --- a/bemade_fsm/__manifest__.py +++ b/bemade_fsm/__manifest__.py @@ -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', diff --git a/bemade_fsm/models/fsm_visit.py b/bemade_fsm/models/fsm_visit.py index c610fc1..7274988 100644 --- a/bemade_fsm/models/fsm_visit.py +++ b/bemade_fsm/models/fsm_visit.py @@ -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 diff --git a/bemade_fsm/models/sale_order_line.py b/bemade_fsm/models/sale_order_line.py index 6db9964..838f723 100644 --- a/bemade_fsm/models/sale_order_line.py +++ b/bemade_fsm/models/sale_order_line.py @@ -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 diff --git a/bemade_fsm/models/task.py b/bemade_fsm/models/task.py index 0967fcb..e144222 100644 --- a/bemade_fsm/models/task.py +++ b/bemade_fsm/models/task.py @@ -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): diff --git a/bemade_fsm/reports/__init__.py b/bemade_fsm/reports/__init__.py new file mode 100644 index 0000000..bb06496 --- /dev/null +++ b/bemade_fsm/reports/__init__.py @@ -0,0 +1 @@ +from . import worksheet_custom_reports diff --git a/bemade_fsm/reports/worksheet_custom_report_templates.xml b/bemade_fsm/reports/worksheet_custom_report_templates.xml index 3defd15..dabf58f 100644 --- a/bemade_fsm/reports/worksheet_custom_report_templates.xml +++ b/bemade_fsm/reports/worksheet_custom_report_templates.xml @@ -444,7 +444,7 @@