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]
This commit is contained in:
Marc Durepos 2023-11-29 20:45:09 -05:00
parent 23d7d34e71
commit 81624ea26c
4 changed files with 133 additions and 72 deletions

View file

@ -37,23 +37,23 @@ class Task(models.Model):
store=True store=True
) )
planned_date_begin = fields.Datetime( # planned_date_begin = fields.Datetime(
string="Planned Start Date", # string="Planned Start Date",
tracking=True, # tracking=True,
task_dependency_tracking=True, # task_dependency_tracking=True,
compute="_compute_planned_dates", # compute="_compute_planned_dates",
inverse="_inverse_planned_dates", # inverse="_inverse_planned_dates",
store=True # store=True
) # )
#
planned_date_end = fields.Datetime( # planned_date_end = fields.Datetime(
string="Planned End Date", # string="Planned End Date",
tracking=True, # tracking=True,
task_dependency_tracking=True, # task_dependency_tracking=True,
compute="_compute_planned_dates", # compute="_compute_planned_dates",
inverse="_inverse_planned_dates", # inverse="_inverse_planned_dates",
store=True # store=True
) # )
# Override related field to make it return false if this is an FSM subtask # Override related field to make it return false if this is an FSM subtask
allow_billable = fields.Boolean( allow_billable = fields.Boolean(
@ -112,43 +112,40 @@ class Task(models.Model):
for project in self.project_id for project in self.project_id
} }
def _get_related_planning_slots(self): # def _get_related_planning_slots(self):
domain = expression.AND([ # domain = [('task_id', 'in', self.ids + self._get_all_subtasks().ids), ('start_datetime', '!=', False)]
self._get_domain_compute_forecast_hours(), # return self.env['planning.slot'].search(domain)
[('task_id', 'in', self.ids + self._get_all_subtasks().ids)]
])
return self.env['planning.slot'].search(domain)
@api.depends('planned_hours') # @api.depends('planned_hours')
def _compute_planned_dates(self): # def _compute_planned_dates(self):
forecast_data = self._get_related_planning_slots() # forecast_data = self._get_related_planning_slots()
mapped_data = {} # mapped_data = {}
TimeSpan = namedtuple('timespan', ['start', 'end']) # TimeSpan = namedtuple('timespan', ['start', 'end'])
for d in forecast_data: # for d in forecast_data:
if d not in mapped_data: # if d not in mapped_data:
mapped_data.update({d: TimeSpan(d.start_datetime, d.end_datetime)}) # mapped_data.update({d: TimeSpan(d.start_datetime, d.end_datetime)})
continue # continue
if mapped_data[d].start > d.start_datetime: # if mapped_data[d].start > d.start_datetime:
mapped_data[d].start = d.start_datetime # mapped_data[d].start = d.start_datetime
if mapped_data[d].end < d.end_datetime: # if mapped_data[d].end < d.end_datetime:
mapped_data[d].end = d.end_datetime # mapped_data[d].end = d.end_datetime
for rec in self: # for rec in self:
if rec not in mapped_data: # if rec not in mapped_data:
if not rec.planned_date_end: # if not rec.planned_date_end:
rec.planned_date_end = False # rec.planned_date_end = False
if not rec.planned_date_begin: # if not rec.planned_date_begin:
rec.planned_date_begin = False # rec.planned_date_begin = False
continue # continue
rec.planned_date_begin, rec.planned_date_end = mapped_data[rec] # rec.planned_date_begin, rec.planned_date_end = mapped_data[rec]
#
def _inverse_planned_dates(self): # def _inverse_planned_dates(self):
""" Modifying the planned dates for tasks with existing planning records # """ Modifying the planned dates for tasks with existing planning records
(planning.slot) has no defined safe behaviour, so we block it.""" # (planning.slot) has no defined safe behaviour, so we block it."""
if self._get_related_planning_slots(): # if self._get_related_planning_slots():
raise UserError(_("Modifying the planned start or end time on a task is not " # raise UserError(_("Modifying the planned start or end time on a task is not "
"permitted when that task already has planning records " # "permitted when that task already has planning records "
"associated to it. Please modify or delete the planning " # "associated to it. Please modify or delete the planning "
"records instead.")) # "records instead."))
@api.depends('sale_line_id.order_id.site_contacts', @api.depends('sale_line_id.order_id.site_contacts',
'sale_line_id.order_id.work_order_contacts') 'sale_line_id.order_id.work_order_contacts')
@ -183,17 +180,59 @@ class Task(models.Model):
else: else:
rec.allow_billable = rec.project_id.allow_billable rec.allow_billable = rec.project_id.allow_billable
def action_fsm_validate(self): def action_fsm_validate(self, stop_running_timers=False):
visits = self.filtered(lambda t: t.visit_id) all_tasks = self | self._get_all_subtasks()
non_visits = self - visits non_visit_tasks = all_tasks.filtered(lambda task: not task.visit_id)
super(Task, non_visits).action_fsm_validate() 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() def _validate_task_without_creating_sale_order_line(self, stop_running_timers=False):
closed_stage_by_project = visits._get_closed_stage_by_project() # Reproduce the functionality of action_fsm_validate but don't apply logic from industry_fsm_sale
super(Task, visits.child_ids).action_fsm_validate() # so as to avoid creating order lines for visits
for visit in visits: Timer = self.env['timer.timer']
stage = closed_stage_by_project[visit.project_id] tasks_running_timer_ids = Timer.search([('res_model', '=', 'project.task'), ('res_id', 'in', self.ids)])
visits.write({'stage_id': stage.id, 'fsm_done': True}) 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): def synchronize_name_fsm(self):
""" Applies naming to the entire task tree for tasks that are part of this """ Applies naming to the entire task tree for tasks that are part of this

View file

@ -69,8 +69,11 @@ class BemadeFSMBaseTest(TransactionCase):
@classmethod @classmethod
def _generate_sale_order(cls, partner=None, client_order_ref='Test Order', equipment=None, shipping_location=None): def _generate_sale_order(cls, partner=None, client_order_ref='Test Order', equipment=None, shipping_location=None):
partner = partner or cls._generate_partner() partner = partner or cls._generate_partner()
vals = {'partner_id': partner.id, vals = {
'client_order_ref': client_order_ref} 'partner_id': partner.id,
'client_order_ref': client_order_ref,
'payment_term_id': cls.env.ref('account.account_payment_term_immediate').id,
}
if equipment: if equipment:
vals.update({'default_equipment_ids': [Command.set([equipment.id])]}) vals.update({'default_equipment_ids': [Command.set([equipment.id])]})
if shipping_location: if shipping_location:
@ -97,6 +100,22 @@ class BemadeFSMBaseTest(TransactionCase):
'partner_location_id': partner and partner.id or False, '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 @classmethod
def _generate_product(cls, name='Test Product', product_type='service', service_tracking='task_global_project', 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): project=None, task_template=None, service_policy='delivered_manual', uom=None):
@ -113,6 +132,7 @@ class BemadeFSMBaseTest(TransactionCase):
'service_policy': service_policy, 'service_policy': service_policy,
'uom_id': uom_id, 'uom_id': uom_id,
'uom_po_id': uom_id, 'uom_po_id': uom_id,
'categ_id': cls._generate_product_category().id,
}) })
@classmethod @classmethod
@ -171,7 +191,7 @@ class BemadeFSMBaseTest(TransactionCase):
def _invoice_sale_order(self, so): def _invoice_sale_order(self, so):
wiz = self.env['sale.advance.payment.inv'].with_context( wiz = self.env['sale.advance.payment.inv'].with_context(
{'active_ids': [so.id]}).create({}) {'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()

View file

@ -40,7 +40,7 @@ class FSMVisitTest(BemadeFSMBaseTest):
so.action_confirm() so.action_confirm()
task = so.order_line.filtered(lambda l: l.task_id).task_id 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) self.assertTrue(visit.is_completed)
@ -50,7 +50,7 @@ class FSMVisitTest(BemadeFSMBaseTest):
self._generate_sale_order_line(so) self._generate_sale_order_line(so)
so.action_confirm() so.action_confirm()
task = so.order_line.filtered(lambda l: l.task_id).task_id 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) self._invoice_sale_order(so)
@ -106,7 +106,7 @@ class FSMVisitTest(BemadeFSMBaseTest):
so.action_confirm() so.action_confirm()
parent, child1, child2 = visit.task_id, sol1.task_id, sol2.task_id 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(parent.is_closed)
self.assertTrue(child1.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, visit, sol1, sol2 = self._generate_so_with_one_visit_two_lines()
so.action_confirm() 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) self.assertEqual(len(so.order_line), 3)

View file

@ -16,7 +16,8 @@
<field name="valid_equipment_ids" invisible="1"/> <field name="valid_equipment_ids" invisible="1"/>
<field name="summary_equipment_ids" <field name="summary_equipment_ids"
context="{'default_partner_location_id': partner_shipping_id,}" context="{'default_partner_location_id': partner_shipping_id,}"
widget="many2many_tags"/> widget="many2many_tags"
groups="account.group_delivery_invoice_address"/>
<field name="site_contacts" <field name="site_contacts"
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"/> context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"/>
<field name="work_order_contacts" <field name="work_order_contacts"
@ -25,7 +26,8 @@
<field name="default_equipment_ids" <field name="default_equipment_ids"
context="{'default_partner_location_id': partner_shipping_id,}" context="{'default_partner_location_id': partner_shipping_id,}"
widget="many2many_tags" widget="many2many_tags"
domain="[('id', 'in', valid_equipment_ids)]"/> domain="[('id', 'in', valid_equipment_ids)]"
groups="account.group_delivery_invoice_address"/>
</group> </group>
</page> </page>
</xpath> </xpath>