g This is a combination of 2 commits.
[MIG] bemade_fsm to 18.0 Aside from fixing standard 17.0..18.0 stuff and fixing XML ID references: 1. **Remove `_get_closed_stage_by_project()`**: This method is obsolete - it was copied from base FSM but no longer exists in 18.0. Base system now uses `is_closed` field. 2. **Implement State Propagation**: Enhance the `write()` method to propagate done/cancelled states to child tasks using `is_closed` field logic. 3. **Test Task Creation**: Thoroughly test the custom task creation logic against base 18.0 All 56 tests are passing.
This commit is contained in:
parent
c7d0a1be2b
commit
5793351bbe
44 changed files with 5102 additions and 0 deletions
3
bemade_fsm/__init__.py
Normal file
3
bemade_fsm/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from . import models
|
||||
from . import reports
|
||||
from . import wizard
|
||||
70
bemade_fsm/__manifest__.py
Normal file
70
bemade_fsm/__manifest__.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
######################################################################################
|
||||
#
|
||||
# Bemade Inc.
|
||||
#
|
||||
# Copyright (C) 2023-June Bemade Inc. (<https://www.bemade.org>).
|
||||
# Author: Marc Durepos (Contact : mdurepos@durpro.com)
|
||||
#
|
||||
# This program is under the terms of the Odoo Proprietary License v1.0 (OPL-1)
|
||||
# It is forbidden to publish, distribute, sublicense, or sell copies of the Software
|
||||
# or modified copies of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
# DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
########################################################################################
|
||||
{
|
||||
"name": "Improved Field Service Management",
|
||||
"version": "18.0.0.4.3",
|
||||
"summary": (
|
||||
"Adds functionality necessary for managing field service operations at Durpro."
|
||||
),
|
||||
"category": "Services/Field Service",
|
||||
"author": "Bemade Inc.",
|
||||
"website": "http://www.bemade.org",
|
||||
"license": "LGPL-3",
|
||||
"depends": [
|
||||
"sale_stock",
|
||||
"sale_project",
|
||||
"account",
|
||||
"project_enterprise",
|
||||
"industry_fsm_stock",
|
||||
"industry_fsm_report",
|
||||
"industry_fsm_sale_report",
|
||||
"fsm_equipment",
|
||||
"bemade_partner_root_ancestor",
|
||||
"mail",
|
||||
],
|
||||
"data": [
|
||||
"data/fsm_data.xml",
|
||||
"views/task_template_views.xml",
|
||||
"security/ir.model.access.csv",
|
||||
"views/product_views.xml",
|
||||
"views/res_partner.xml",
|
||||
"views/menus.xml",
|
||||
"views/task_views.xml",
|
||||
"views/sale_order_views.xml",
|
||||
"reports/worksheet_custom_report_templates.xml",
|
||||
"reports/worksheet_custom_reports.xml",
|
||||
"wizard/new_task_from_template.xml",
|
||||
"wizard/res_config_settings.xml",
|
||||
],
|
||||
"assets": {
|
||||
"web.report_assets_common": ["bemade_fsm/static/src/scss/bemade_fsm.scss"],
|
||||
"web.assets_backend": [
|
||||
# BV: need to readd these files
|
||||
# 'bemade_fsm/static/src/js/kanban_view.js',
|
||||
# 'bemade_fsm/static/src/js/list_view.js',
|
||||
],
|
||||
"web.assets_qweb": [
|
||||
"bemade_fsm/static/src/xml/project_view_buttons.xml",
|
||||
],
|
||||
},
|
||||
"installable": True,
|
||||
"auto_install": False,
|
||||
}
|
||||
21
bemade_fsm/data/fsm_data.xml
Normal file
21
bemade_fsm/data/fsm_data.xml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="planning_project_stage_waiting_parts" model="project.task.type">
|
||||
<field name="sequence">2</field>
|
||||
<field name="name">Waiting on Parts</field>
|
||||
<field name="fold" eval="False" />
|
||||
<field name="project_ids" eval="[(4,ref('industry_fsm.fsm_project'))]" />
|
||||
</record>
|
||||
<record id="planning_project_stage_work_completed" model="project.task.type">
|
||||
<field name="sequence">15</field>
|
||||
<field name="name">Work Executed</field>
|
||||
<field name="fold" eval="False" />
|
||||
<field name="project_ids" eval="[(4,ref('industry_fsm.fsm_project'))]" />
|
||||
</record>
|
||||
<record id="planning_project_stage_exception" model="project.task.type">
|
||||
<field name="sequence">19</field>
|
||||
<field name="name">Exception</field>
|
||||
<field name="fold" eval="False" />
|
||||
<field name="project_ids" eval="[(4,ref('industry_fsm.fsm_project'))]" />
|
||||
</record>
|
||||
</odoo>
|
||||
1098
bemade_fsm/i18n/fr.po
Normal file
1098
bemade_fsm/i18n/fr.po
Normal file
File diff suppressed because it is too large
Load diff
9
bemade_fsm/models/__init__.py
Normal file
9
bemade_fsm/models/__init__.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
from . import task_template
|
||||
from . import product_template
|
||||
from . import sale_order_line
|
||||
from . import sale_order
|
||||
from . import task
|
||||
from . import res_partner
|
||||
from . import fsm_visit
|
||||
from . import res_company
|
||||
from . import project
|
||||
104
bemade_fsm/models/fsm_visit.py
Normal file
104
bemade_fsm/models/fsm_visit.py
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class FSMVisit(models.Model):
|
||||
_name = "bemade_fsm.visit"
|
||||
_description = "Represents a single visit by assigned service personnel."
|
||||
|
||||
label = fields.Text(
|
||||
string="Label",
|
||||
required=True,
|
||||
related="so_section_id.name",
|
||||
readonly=False,
|
||||
copy=True,
|
||||
)
|
||||
|
||||
approx_date = fields.Date(string="Approximate Date", copy=False)
|
||||
|
||||
so_section_id = fields.Many2one(
|
||||
comodel_name="sale.order.line",
|
||||
string="Sale Order Section",
|
||||
help=(
|
||||
"The section on the sale order that represents the labour and parts for"
|
||||
" this visit"
|
||||
),
|
||||
ondelete="cascade",
|
||||
)
|
||||
|
||||
sale_order_id = fields.Many2one(
|
||||
comodel_name="sale.order",
|
||||
string="Sales Order",
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
is_completed = fields.Boolean(
|
||||
string="Completed", related="so_section_id.is_fully_delivered"
|
||||
)
|
||||
|
||||
is_invoiced = fields.Boolean(
|
||||
string="Invoiced", related="so_section_id.is_fully_delivered_and_invoiced"
|
||||
)
|
||||
|
||||
summarized_equipment_ids = fields.Many2many(
|
||||
comodel_name="fsm.equipment",
|
||||
string="Equipment to Service",
|
||||
compute="_compute_summarized_equipment_ids",
|
||||
)
|
||||
|
||||
task_id = fields.Many2one(
|
||||
comodel_name="project.task",
|
||||
compute="_compute_task_id",
|
||||
string="Service Visit",
|
||||
)
|
||||
|
||||
task_ids = fields.One2many(comodel_name="project.task", 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:
|
||||
rec.task_id = rec.task_ids and rec.task_ids[0]
|
||||
|
||||
@api.depends("so_section_id", "sale_order_id.summary_equipment_ids")
|
||||
def _compute_summarized_equipment_ids(self):
|
||||
for rec in self:
|
||||
lines = rec.so_section_id.get_section_line_ids()
|
||||
equipment_ids = []
|
||||
for line in lines:
|
||||
for equipment in line.equipment_ids:
|
||||
equipment_ids.append(equipment)
|
||||
rec.summarized_equipment_ids = equipment_ids
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
recs = super().create(vals_list)
|
||||
for i, rec in enumerate(recs.filtered(lambda visit: not visit.so_section_id)):
|
||||
rec.so_section_id = rec.env["sale.order.line"].create(
|
||||
{
|
||||
"order_id": rec.sale_order_id.id,
|
||||
"display_type": "line_section",
|
||||
"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
|
||||
19
bemade_fsm/models/product_template.py
Normal file
19
bemade_fsm/models/product_template.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
from odoo import fields, models
|
||||
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
_inherit = "product.template"
|
||||
|
||||
task_template_id = fields.Many2one(
|
||||
comodel_name="project.task.template",
|
||||
string="Task Template",
|
||||
ondelete="restrict",
|
||||
)
|
||||
|
||||
is_field_service = fields.Boolean(
|
||||
string="Plan as field service",
|
||||
help=(
|
||||
"Products planned as field service will have travel time considered in"
|
||||
" planning."
|
||||
),
|
||||
)
|
||||
13
bemade_fsm/models/project.py
Normal file
13
bemade_fsm/models/project.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
from odoo import models
|
||||
|
||||
|
||||
class Project(models.Model):
|
||||
_inherit = "project.project"
|
||||
|
||||
def _fetch_sale_order_item_ids(
|
||||
self, domain_per_model=None, limit=None, offset=None
|
||||
):
|
||||
# Override to flush the ORM cache to the database prior to running the query
|
||||
# Temporary fix until Odoo fixes this method (PR #160067 submitted for this)
|
||||
self.env.flush_all()
|
||||
return super()._fetch_sale_order_item_ids(domain_per_model, limit, offset)
|
||||
8
bemade_fsm/models/res_company.py
Normal file
8
bemade_fsm/models/res_company.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
from odoo import models, fields
|
||||
|
||||
|
||||
class Company(models.Model):
|
||||
_inherit = "res.company"
|
||||
|
||||
split_time_from_materials_on_service_work_orders = fields.Boolean(default=False)
|
||||
create_default_fsm_visit = fields.Boolean(default=False)
|
||||
61
bemade_fsm/models/res_partner.py
Normal file
61
bemade_fsm/models/res_partner.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class Partner(models.Model):
|
||||
_inherit = "res.partner"
|
||||
|
||||
is_site_contact = fields.Boolean(
|
||||
string="Is a site contact",
|
||||
compute="_compute_is_site_contact",
|
||||
search="_search_is_site_contact",
|
||||
)
|
||||
|
||||
is_service_site = fields.Boolean(
|
||||
help="A partner is a service site if they have one or more equipments, "
|
||||
"site contacts or work order contacts."
|
||||
)
|
||||
|
||||
site_ids = fields.Many2many(
|
||||
string="Work Sites",
|
||||
comodel_name="res.partner",
|
||||
relation="res_partner_site_contact_rel",
|
||||
column1="site_contact_id",
|
||||
column2="site_id",
|
||||
tracking=True,
|
||||
domain=[("is_service_site", "=", True)],
|
||||
)
|
||||
|
||||
site_contacts = fields.Many2many(
|
||||
comodel_name="res.partner",
|
||||
relation="res_partner_site_contact_rel",
|
||||
column1="site_id",
|
||||
column2="site_contact_id",
|
||||
domain=[("is_service_site", "=", False)],
|
||||
tracking=True,
|
||||
)
|
||||
|
||||
work_order_contacts = fields.Many2many(
|
||||
string="Work Order Recipients",
|
||||
comodel_name="res.partner",
|
||||
relation="res_partner_work_order_contacts_rel",
|
||||
column1="res_partner_id",
|
||||
column2="work_order_contact_id",
|
||||
domain=[("is_service_site", "=", False)],
|
||||
tracking=True,
|
||||
)
|
||||
|
||||
@api.depends("site_ids")
|
||||
def _compute_is_site_contact(self):
|
||||
for rec in self:
|
||||
rec.is_site_contact = rec.site_ids
|
||||
|
||||
@api.model
|
||||
def _search_is_site_contact(self, operator, value):
|
||||
return [("site_ids", "!=", False)]
|
||||
|
||||
@api.depends("equipment_ids", "site_contacts", "work_order_contacts")
|
||||
def _compute_is_service_site(self):
|
||||
for rec in self:
|
||||
rec.is_service_site = (
|
||||
rec.equipment_ids or rec.site_contacts or rec.work_order_contacts
|
||||
)
|
||||
163
bemade_fsm/models/sale_order.py
Normal file
163
bemade_fsm/models/sale_order.py
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
from odoo import fields, models, api, _, Command
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = "sale.order"
|
||||
|
||||
valid_equipment_ids = fields.One2many(
|
||||
comodel_name="fsm.equipment",
|
||||
related="partner_id.commercial_partner_id.owned_equipment_ids",
|
||||
)
|
||||
|
||||
default_equipment_ids = fields.Many2many(
|
||||
comodel_name="fsm.equipment",
|
||||
string="Default Equipment to Service",
|
||||
help="The default equipment to service for new sale order lines.",
|
||||
compute="_compute_default_equipment",
|
||||
inverse="_inverse_default_equipment",
|
||||
store=True,
|
||||
)
|
||||
|
||||
summary_equipment_ids = fields.Many2many(
|
||||
comodel_name="fsm.equipment",
|
||||
string="Equipment Being Serviced",
|
||||
compute="_compute_summary_equipment_ids",
|
||||
)
|
||||
|
||||
site_contacts = fields.Many2many(
|
||||
comodel_name="res.partner",
|
||||
relation="sale_order_site_contacts_rel",
|
||||
compute="_compute_default_contacts",
|
||||
inverse="_inverse_default_contacts",
|
||||
store=True,
|
||||
)
|
||||
|
||||
work_order_contacts = fields.Many2many(
|
||||
comodel_name="res.partner",
|
||||
relation="sale_order_work_order_contacts_rel",
|
||||
compute="_compute_default_contacts",
|
||||
inverse="_inverse_default_contacts",
|
||||
string="Work Order Recipients",
|
||||
store=True,
|
||||
)
|
||||
|
||||
visit_ids = fields.One2many(
|
||||
comodel_name="bemade_fsm.visit", inverse_name="sale_order_id", readonly=False
|
||||
)
|
||||
|
||||
is_fsm = fields.Boolean(
|
||||
compute="_compute_is_fsm",
|
||||
string="Is FSM",
|
||||
store=True,
|
||||
)
|
||||
|
||||
def get_relevant_order_lines(self, task_id):
|
||||
self.ensure_one()
|
||||
linked_lines = self.order_line.filtered(
|
||||
lambda line: line.task_id == task_id
|
||||
or line == task_id.visit_id.so_section_id
|
||||
)
|
||||
visit_lines = linked_lines.filtered(lambda line: line.visit_id)
|
||||
for line in visit_lines:
|
||||
linked_lines |= line.get_section_line_ids()
|
||||
return linked_lines
|
||||
|
||||
@api.depends("order_line.equipment_ids")
|
||||
def _compute_summary_equipment_ids(self):
|
||||
for rec in self:
|
||||
rec.summary_equipment_ids = rec.order_line.mapped("equipment_ids")
|
||||
|
||||
@api.onchange("partner_shipping_id")
|
||||
def _onchange_partner_shipping_id(self):
|
||||
res = super()._onchange_partner_shipping_id()
|
||||
self._compute_default_equipment()
|
||||
self._compute_default_contacts()
|
||||
return res
|
||||
|
||||
@api.depends("partner_shipping_id")
|
||||
def _compute_default_contacts(self):
|
||||
for rec in self:
|
||||
rec.site_contacts = rec.partner_shipping_id.site_contacts
|
||||
rec.work_order_contacts = rec.partner_shipping_id.work_order_contacts
|
||||
|
||||
def _inverse_default_contacts(self):
|
||||
pass
|
||||
|
||||
@api.depends(
|
||||
"partner_id",
|
||||
"partner_shipping_id",
|
||||
"partner_shipping_id.equipment_ids",
|
||||
"partner_id.owned_equipment_ids",
|
||||
)
|
||||
def _compute_default_equipment(self):
|
||||
for rec in self:
|
||||
if rec.partner_shipping_id.equipment_ids:
|
||||
ids = rec.partner_shipping_id.equipment_ids
|
||||
else:
|
||||
ids = rec.partner_id.owned_equipment_ids
|
||||
rec.default_equipment_ids = ids if len(ids) < 4 else False
|
||||
|
||||
def _inverse_default_equipment(self):
|
||||
pass
|
||||
|
||||
def copy(self, default=None):
|
||||
rec = super().copy(default)
|
||||
rec.visit_ids = [Command.set(rec.order_line.visit_ids.ids)]
|
||||
return rec
|
||||
|
||||
def _create_default_visit(self):
|
||||
"""Called when an order is confirmed with lines that will create an FSM task,
|
||||
in order to make sure there is a visit line grouping all the service being done.
|
||||
"""
|
||||
self.ensure_one()
|
||||
visit = self.env["bemade_fsm.visit"].create(
|
||||
{
|
||||
"label": _("Service Visit"),
|
||||
"sale_order_id": self.id,
|
||||
}
|
||||
)
|
||||
# Make sure it goes to the top of the list
|
||||
visit.so_section_id.sequence = 0
|
||||
|
||||
def _create_or_organize_visits_if_needed(self):
|
||||
"""Adds a visit line to the top of the order if there are not already visit
|
||||
lines for an order with lines that will create an FSM task."""
|
||||
for order in self.filtered("company_id.create_default_fsm_visit"):
|
||||
if not order.visit_ids and order.is_fsm:
|
||||
order._create_default_visit()
|
||||
if order.is_fsm:
|
||||
# Make sure that all the lines producing FSM tasks are under a visit
|
||||
visit_line_ids = (
|
||||
order.mapped("visit_ids")
|
||||
.mapped("so_section_id")
|
||||
.mapped("section_line_ids")
|
||||
)
|
||||
if any(
|
||||
[
|
||||
True
|
||||
for line in order.order_line.filtered(
|
||||
lambda line: not line.display_type
|
||||
)
|
||||
if line not in visit_line_ids
|
||||
]
|
||||
):
|
||||
# If not, promote the first visit to the top of the order items list
|
||||
for line in order.order_line:
|
||||
line.sequence += 1
|
||||
order.mapped("visit_ids").mapped("so_section_id")[0].sequence = 0
|
||||
|
||||
@api.depends("order_line.is_fsm")
|
||||
def _compute_is_fsm(self):
|
||||
for rec in self:
|
||||
rec.is_fsm = any([line.is_fsm for line in rec.order_line])
|
||||
|
||||
def action_confirm(self):
|
||||
self._create_or_organize_visits_if_needed()
|
||||
return super().action_confirm()
|
||||
|
||||
def write(self, vals):
|
||||
res = super().write(vals)
|
||||
if "partner_shipping_id" in vals:
|
||||
for rec in self:
|
||||
rec.tasks_ids.write({"partner_id": rec.partner_shipping_id.id})
|
||||
return res
|
||||
304
bemade_fsm/models/sale_order_line.py
Normal file
304
bemade_fsm/models/sale_order_line.py
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
from odoo import fields, models, api, _, Command
|
||||
|
||||
|
||||
class SaleOrderLine(models.Model):
|
||||
_inherit = "sale.order.line"
|
||||
valid_equipment_ids = fields.One2many(
|
||||
comodel_name="fsm.equipment", related="order_id.valid_equipment_ids"
|
||||
)
|
||||
|
||||
visit_ids = fields.One2many(
|
||||
comodel_name="bemade_fsm.visit",
|
||||
inverse_name="so_section_id",
|
||||
string="Visits",
|
||||
copy=True,
|
||||
)
|
||||
|
||||
visit_id = fields.Many2one(
|
||||
comodel_name="bemade_fsm.visit",
|
||||
compute="_compute_visit_id",
|
||||
string="Visit",
|
||||
ondelete="cascade",
|
||||
store=True,
|
||||
)
|
||||
|
||||
is_fully_delivered = fields.Boolean(
|
||||
string="Fully Delivered",
|
||||
compute="_compute_is_fully_delivered",
|
||||
help=(
|
||||
"Indicates whether a line or all the lines in a section have been entirely"
|
||||
" delivered."
|
||||
),
|
||||
)
|
||||
|
||||
is_fully_delivered_and_invoiced = fields.Boolean(
|
||||
string="Fully Invoiced",
|
||||
compute="_compute_is_fully_invoiced",
|
||||
help=(
|
||||
"Indicates whether a line or all the lines in a section have been entirely"
|
||||
" delivered and invoiced."
|
||||
),
|
||||
)
|
||||
|
||||
equipment_ids = fields.Many2many(
|
||||
string="Equipment to Service",
|
||||
comodel_name="fsm.equipment",
|
||||
relation="bemade_fsm_equipment_sale_order_line_rel",
|
||||
column1="sale_order_line_id",
|
||||
column2="equipment_id",
|
||||
)
|
||||
|
||||
is_field_service = fields.Boolean(compute="_compute_is_field_service", store=True)
|
||||
|
||||
is_fsm = fields.Boolean(
|
||||
string="Is FSM",
|
||||
compute="_compute_is_fsm",
|
||||
store=True,
|
||||
)
|
||||
|
||||
section_line_ids = fields.One2many(
|
||||
comodel_name="sale.order.line",
|
||||
compute="_compute_section_line_ids",
|
||||
)
|
||||
|
||||
@api.depends("visit_ids")
|
||||
def _compute_visit_id(self):
|
||||
for rec in self:
|
||||
rec.visit_id = rec.visit_ids and rec.visit_ids[0]
|
||||
|
||||
@api.depends("product_id")
|
||||
def _compute_is_field_service(self):
|
||||
for rec in self:
|
||||
rec.is_field_service = rec.product_id.is_field_service
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
recs = super().create(vals_list)
|
||||
for rec in recs:
|
||||
if rec.order_id.default_equipment_ids and not rec.equipment_ids:
|
||||
rec.equipment_ids = rec.order_id.default_equipment_ids
|
||||
return recs
|
||||
|
||||
def copy_data(self, default=None):
|
||||
if default is None:
|
||||
default = {}
|
||||
if "visit_ids" not in default:
|
||||
default["visit_ids"] = [
|
||||
(0, 0, visit.copy_data()[0]) for visit in self.visit_ids
|
||||
]
|
||||
return super().copy_data(default)
|
||||
|
||||
def _timesheet_create_task(self, project):
|
||||
"""Generate task for the given so line, and link it.
|
||||
:param project: record of project.project in which the task should be created
|
||||
:return task: record of the created task
|
||||
|
||||
Override to add the logic needed to implement task templates and equipment linkages.
|
||||
"""
|
||||
|
||||
def _create_task_from_template(project, template, parent):
|
||||
"""Recursively generates the task and any subtasks from a project.task.template.
|
||||
|
||||
:param project: project.project record to set on the task's project_id field.
|
||||
:param template: project.task.template to use to create the task.
|
||||
:param parent: project.task to set as the parent to this task.
|
||||
"""
|
||||
values = _timesheet_create_task_prepare_values_from_template(
|
||||
project, template, parent
|
||||
)
|
||||
task = self.env["project.task"].sudo().create(values)
|
||||
subtasks = []
|
||||
for t in template.subtasks:
|
||||
subtask = _create_task_from_template(project, t, task)
|
||||
subtasks.append(subtask)
|
||||
|
||||
# We don't want to see the sub-tasks on the SO
|
||||
if task.child_ids:
|
||||
task.child_ids.write(
|
||||
{
|
||||
"sale_order_id": None,
|
||||
"sale_line_id": None,
|
||||
}
|
||||
)
|
||||
|
||||
return task
|
||||
|
||||
def _timesheet_create_task_prepare_values_from_template(
|
||||
project, template, parent
|
||||
):
|
||||
"""Copies the values from a project.task.template over to the set of values used to create a project.task.
|
||||
|
||||
:param project: project.project record to set on the task's project_id field.
|
||||
Pass the project.project model or an empty recordset to leave task project_id blank.
|
||||
DO NOT pass False or None as this will cause an error in _timesheet_create_task_prepare_values(project).
|
||||
:param template: project.task.template to use to create the task.
|
||||
:param parent: project.task to set as the parent to this task.
|
||||
"""
|
||||
vals = self._timesheet_create_task_prepare_values(project)
|
||||
vals["name"] = template.name
|
||||
vals["description"] = (
|
||||
template.description or "" if parent else vals["description"]
|
||||
)
|
||||
vals["parent_id"] = parent and parent.id
|
||||
vals["user_ids"] = template.assignees.ids
|
||||
vals["tag_ids"] = template.tags.ids
|
||||
vals["allocated_hours"] = template.planned_hours
|
||||
vals["sequence"] = template.sequence
|
||||
# Use shipping address for FSM tasks for consistency
|
||||
if project and project.is_fsm:
|
||||
vals["partner_id"] = self.order_id.partner_shipping_id.id
|
||||
else:
|
||||
vals["partner_id"] = self.order_id.partner_id.id
|
||||
if template.equipment_ids:
|
||||
vals["equipment_ids"] = template.equipment_ids.ids
|
||||
return vals
|
||||
|
||||
tmpl = self.product_id.task_template_id
|
||||
if not tmpl:
|
||||
task = super()._timesheet_create_task(project)
|
||||
# For FSM tasks without a template, update partner_id to use shipping address
|
||||
if project.is_fsm and task:
|
||||
task.partner_id = self.order_id.partner_shipping_id.id
|
||||
else:
|
||||
task = _create_task_from_template(project, tmpl, None)
|
||||
self.write({"task_id": task.id})
|
||||
# post message on task
|
||||
task_msg = _(
|
||||
"This task has been created from: <a href=# data-oe-model=sale.order"
|
||||
" data-oe-id=%(so_id)d>%(so_name)s</a> (%(product_name)s)"
|
||||
) % {
|
||||
"so_id": self.order_id.id,
|
||||
"so_name": self.order_id.name,
|
||||
"product_name": self.product_id.name,
|
||||
}
|
||||
task.message_post(body=task_msg)
|
||||
|
||||
if not task.equipment_ids and self.equipment_ids:
|
||||
task.equipment_ids = self.equipment_ids.ids
|
||||
return task
|
||||
|
||||
def _timesheet_service_generation(self):
|
||||
super()._timesheet_service_generation()
|
||||
visit_lines = self.filtered(lambda line: line.visit_id)
|
||||
for index, line in enumerate(visit_lines):
|
||||
task_ids = line.get_section_line_ids().mapped("task_id")
|
||||
if not task_ids:
|
||||
continue
|
||||
if len(set([task.project_id for task in task_ids])) > 1:
|
||||
# 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, 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, allocated_hours: int
|
||||
):
|
||||
self.ensure_one()
|
||||
|
||||
allocated_hours = sum(
|
||||
self.get_section_line_ids().task_id.mapped("allocated_hours")
|
||||
)
|
||||
task = self.env["project.task"].create(
|
||||
{
|
||||
"name": (
|
||||
f"{self.order_id.name} - "
|
||||
+ _("Visit")
|
||||
+ f" {visit_no} - {self.name}"
|
||||
),
|
||||
"project_id": project.id,
|
||||
"equipment_ids": (
|
||||
self.get_section_line_ids().mapped("equipment_ids").ids
|
||||
),
|
||||
"sale_order_id": self.order_id.id,
|
||||
"partner_id": self.order_id.partner_shipping_id.id,
|
||||
"visit_id": self.visit_id.id,
|
||||
"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
|
||||
|
||||
@api.depends(
|
||||
"order_id.order_line",
|
||||
"display_type",
|
||||
"qty_to_deliver",
|
||||
"order_id.order_line.qty_to_deliver",
|
||||
"order_id.order_line.display_type",
|
||||
)
|
||||
def _compute_is_fully_delivered(self):
|
||||
for rec in self:
|
||||
rec.is_fully_delivered = rec._iterate_items_compute_bool(
|
||||
lambda line: line.qty_to_deliver == 0
|
||||
)
|
||||
|
||||
@api.depends("is_fully_delivered")
|
||||
def _compute_is_fully_invoiced(self):
|
||||
for rec in self:
|
||||
if not rec.is_fully_delivered:
|
||||
rec.is_fully_delivered_and_invoiced = False
|
||||
return
|
||||
rec.is_fully_delivered_and_invoiced = rec._iterate_items_compute_bool(
|
||||
lambda line: line.qty_to_invoice == 0
|
||||
)
|
||||
|
||||
def get_section_line_ids(self):
|
||||
"""Return a recordset of sale.order.line records that are in this sale order section."""
|
||||
self.ensure_one()
|
||||
assert (
|
||||
self.display_type == "line_section"
|
||||
), "Cannot get section lines for a non-section."
|
||||
found = False
|
||||
lines = []
|
||||
for line in self.order_id.order_line.sorted(lambda line: line.sequence):
|
||||
if line == self:
|
||||
found = True
|
||||
continue
|
||||
if not found:
|
||||
continue
|
||||
if line.display_type == "line_section": # Stop when we hit the next section
|
||||
break
|
||||
else:
|
||||
lines.append(line)
|
||||
return self.env["sale.order.line"].union(*lines)
|
||||
|
||||
@api.depends("display_type", "order_id.order_line")
|
||||
def _compute_section_line_ids(self):
|
||||
for rec in self:
|
||||
if rec.display_type == "line_section":
|
||||
rec.section_line_ids = [Command.set(rec.get_section_line_ids().ids)]
|
||||
else:
|
||||
rec.section_line_ids = False
|
||||
|
||||
def _iterate_items_compute_bool(self, single_line_func):
|
||||
if not self.display_type:
|
||||
return single_line_func(self)
|
||||
elif self.display_type == "line_note":
|
||||
return True
|
||||
else:
|
||||
for line in self.order_id.order_line:
|
||||
found = False
|
||||
if line == self:
|
||||
found = True
|
||||
if not found:
|
||||
continue
|
||||
if found and line.display_type == "line_section":
|
||||
return True
|
||||
val = single_line_func(self)
|
||||
if not val:
|
||||
return val
|
||||
return True
|
||||
|
||||
@api.depends("product_id.type", "product_id.service_tracking")
|
||||
def _compute_is_fsm(self):
|
||||
for rec in self:
|
||||
rec.is_fsm = (
|
||||
rec.product_id.type == "service"
|
||||
and rec.product_id.service_tracking == "task_global_project"
|
||||
)
|
||||
257
bemade_fsm/models/task.py
Normal file
257
bemade_fsm/models/task.py
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
from odoo import fields, models, api, Command
|
||||
from odoo.addons.project.models.project_task import CLOSED_STATES
|
||||
import re
|
||||
from typing import cast, List
|
||||
|
||||
|
||||
class Task(models.Model):
|
||||
_inherit = "project.task"
|
||||
|
||||
work_order_contacts = fields.Many2many(
|
||||
comodel_name="res.partner",
|
||||
relation="task_work_order_contact_rel",
|
||||
column1="task_id",
|
||||
column2="res_partner_id",
|
||||
)
|
||||
|
||||
site_contacts = fields.Many2many(
|
||||
comodel_name="res.partner",
|
||||
relation="task_site_contact_rel",
|
||||
column1="task_id",
|
||||
column2="res_partner_id",
|
||||
)
|
||||
|
||||
# Override related field to make it return false if this is an FSM subtask
|
||||
allow_billable = fields.Boolean(
|
||||
string="Can be billed",
|
||||
related=False,
|
||||
compute="_compute_allow_billable",
|
||||
store=True,
|
||||
)
|
||||
|
||||
visit_id = fields.Many2one(
|
||||
comodel_name="bemade_fsm.visit",
|
||||
string="Visit",
|
||||
)
|
||||
|
||||
relevant_order_lines = fields.Many2many(
|
||||
comodel_name="sale.order.line",
|
||||
store=False,
|
||||
compute="_compute_relevant_order_lines",
|
||||
)
|
||||
|
||||
work_order_number = fields.Char(readonly=True)
|
||||
|
||||
propagate_assignment = fields.Boolean(
|
||||
help="Propagate assignment of this task to all subtasks.",
|
||||
default=False,
|
||||
)
|
||||
|
||||
is_closed = fields.Boolean(
|
||||
compute="_compute_is_closed",
|
||||
)
|
||||
|
||||
root_ancestor = fields.Many2one(
|
||||
comodel_name="project.task",
|
||||
compute="_compute_root_ancestor",
|
||||
recursive=True,
|
||||
)
|
||||
|
||||
def _compute_is_closed(self):
|
||||
for rec in self:
|
||||
rec.is_closed = rec.state in CLOSED_STATES
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
res = super().create(vals_list)
|
||||
for rec in res:
|
||||
if rec.parent_id and rec.is_fsm:
|
||||
# Always ensure FSM subtasks have a partner_id set from their parent
|
||||
rec.partner_id = rec.parent_id.partner_id
|
||||
if not rec.work_order_contacts and rec.parent_id:
|
||||
rec.work_order_contacts = rec.parent_id.work_order_contacts
|
||||
if not rec.site_contacts and rec.parent_id:
|
||||
rec.site_contacts = rec.parent_id.site_contacts
|
||||
if rec.sale_order_id:
|
||||
seq = 1
|
||||
prev_seqs = (
|
||||
self.sale_order_id.tasks_ids
|
||||
and self.sale_order_id.tasks_ids.mapped("work_order_number")
|
||||
)
|
||||
if prev_seqs:
|
||||
pattern = re.compile(r"(\d+)$")
|
||||
matches = map(
|
||||
lambda n: pattern.search(n), cast(List[str], prev_seqs)
|
||||
)
|
||||
seq += max(map(lambda n: int(n.group(1)) if n else 0, matches))
|
||||
rec.work_order_number = (
|
||||
rec.sale_order_id.name.replace("SO", "SVR", 1) + f"-{seq}"
|
||||
)
|
||||
# If the task is linked to a sales order and has no parent, it should inherit SO work order contacts
|
||||
if (
|
||||
not rec.parent_id
|
||||
and not rec.work_order_contacts
|
||||
and rec.sale_order_id.work_order_contacts
|
||||
):
|
||||
rec.work_order_contacts = rec.sale_order_id.work_order_contacts
|
||||
if (
|
||||
not rec.parent_id
|
||||
and not rec.site_contacts
|
||||
and rec.sale_order_id.site_contacts
|
||||
):
|
||||
rec.site_contacts = rec.sale_order_id.site_contacts
|
||||
return res
|
||||
|
||||
def write(self, vals):
|
||||
res = super().write(vals)
|
||||
if not self: # End recursion on empty RecordSet
|
||||
return res
|
||||
if "propagate_assignment" in vals:
|
||||
# When a user sets propagate assignment, it should propagate that setting all the way down the chain
|
||||
self.child_ids.write({"propagate_assignment": vals["propagate_assignment"]})
|
||||
if "user_ids" in vals:
|
||||
to_propagate = self.filtered(lambda task: task.propagate_assignment)
|
||||
# Here we use child_ids instead of _get_all_subtasks() so as to allow for setting propagate_assignment
|
||||
# to false on a child task.
|
||||
to_propagate.child_ids.write({"user_ids": vals["user_ids"]})
|
||||
for rec in self:
|
||||
if rec.child_ids:
|
||||
child_vals = {}
|
||||
if "site_contacts" in vals:
|
||||
child_vals.update(
|
||||
site_contacts=[Command.set(rec.site_contacts.ids)]
|
||||
)
|
||||
if "work_order_contacts" in vals:
|
||||
child_vals.update(
|
||||
work_order_contacts=[Command.set(rec.work_order_contacts.ids)]
|
||||
)
|
||||
if "partner_id" in vals:
|
||||
child_vals.update(partner_id=vals["partner_id"])
|
||||
if "state" in vals and rec.state in CLOSED_STATES:
|
||||
# Propagate task completion or cancelling to subtasks
|
||||
child_vals.update(state=rec.state)
|
||||
if child_vals:
|
||||
rec.child_ids.write(child_vals)
|
||||
return res
|
||||
|
||||
@api.depends("sale_order_id")
|
||||
def _compute_relevant_order_lines(self):
|
||||
for rec in self:
|
||||
rec.relevant_order_lines = (
|
||||
rec.sale_order_id
|
||||
and rec.sale_order_id.get_relevant_order_lines(rec)
|
||||
or False
|
||||
)
|
||||
|
||||
@api.depends("parent_id.visit_id", "project_id.is_fsm", "project_id.allow_billable")
|
||||
def _compute_allow_billable(self):
|
||||
for rec in self:
|
||||
# If an FSM task has a parent that is linked to an SO line, then the parent is the billable one
|
||||
if (
|
||||
rec.parent_id
|
||||
and not rec.parent_id.visit_id
|
||||
and rec.project_id
|
||||
and rec.project_id.is_fsm
|
||||
):
|
||||
rec.allow_billable = False
|
||||
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()
|
||||
return super(Task, all_tasks).action_fsm_validate(stop_running_timers)
|
||||
|
||||
def _get_full_hierarchy(self):
|
||||
if self.child_ids:
|
||||
return self | self.child_ids._get_full_hierarchy()
|
||||
return self
|
||||
|
||||
def synchronize_name_fsm(self):
|
||||
"""Applies naming to the entire task tree for tasks that are part of this
|
||||
recordset. Root tasks are named:
|
||||
|
||||
Partner Shipping Name - Sale Line Name (Template Name)
|
||||
|
||||
Child tasks with sale_line_id are named by their template if set, sale line name
|
||||
if not.
|
||||
|
||||
Child tasks not linked to sale lines are left with their original names."""
|
||||
|
||||
all_tasks = self | self._get_all_subtasks()
|
||||
for rec in all_tasks:
|
||||
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
|
||||
|
||||
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 = 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} - {title}"
|
||||
else:
|
||||
rec.name = title
|
||||
|
||||
@api.depends("parent_id", "parent_id.root_ancestor")
|
||||
def _compute_root_ancestor(self):
|
||||
for rec in self:
|
||||
rec.root_ancestor = rec.parent_id and rec.parent_id.root_ancestor or self
|
||||
|
||||
@api.depends(
|
||||
"partner_id",
|
||||
"sale_line_id.order_partner_id",
|
||||
"parent_id.sale_line_id",
|
||||
"project_id.sale_line_id",
|
||||
"milestone_id.sale_line_id",
|
||||
"allow_billable",
|
||||
)
|
||||
def _compute_sale_line(self):
|
||||
"""Override to prevent subtasks from inheriting parent's sale_line_id.
|
||||
|
||||
In the base implementation, if a task and its parent share the same commercial partner,
|
||||
the task will inherit the parent's sale_line_id. This causes issues with our FSM tasks
|
||||
where we explicitly want subtasks to NOT have a sale_line_id set.
|
||||
"""
|
||||
|
||||
# Only run on root tasks
|
||||
subtasks = self.filtered("parent_id")
|
||||
(subtasks - subtasks.filtered("sale_line_id")).sale_line_id = False
|
||||
super(Task, self - subtasks)._compute_sale_line()
|
||||
|
||||
@api.depends("parent_id.partner_id", "project_id")
|
||||
def _compute_partner_id(self):
|
||||
"""Override to prevent clearing partner_id for FSM tasks.
|
||||
|
||||
In the base implementation, if a task has a partner_id but no project_id or parent_id,
|
||||
the partner_id is cleared. This causes issues with our FSM tasks where we want to
|
||||
preserve the partner_id even if project_id or parent_id is temporarily not set.
|
||||
"""
|
||||
# Only run the standard logic on non-FSM tasks
|
||||
non_fsm_tasks = self.filtered(lambda t: not t.is_fsm)
|
||||
super(Task, non_fsm_tasks)._compute_partner_id()
|
||||
|
||||
# For FSM tasks, only set partner_id if it's not already set
|
||||
fsm_tasks = self - non_fsm_tasks
|
||||
for task in fsm_tasks:
|
||||
if not task.partner_id:
|
||||
task.partner_id = self._get_default_partner_id(
|
||||
task.project_id, task.parent_id
|
||||
)
|
||||
122
bemade_fsm/models/task_template.py
Normal file
122
bemade_fsm/models/task_template.py
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
from odoo import models, fields, api, Command
|
||||
|
||||
|
||||
class TaskTemplate(models.Model):
|
||||
_name = "project.task.template"
|
||||
_description = "Template for new project tasks"
|
||||
|
||||
@api.model
|
||||
def _current_company(self):
|
||||
return self.env.company
|
||||
|
||||
name = fields.Char(string="Task Title", required=True)
|
||||
|
||||
description = fields.Html()
|
||||
|
||||
assignees = fields.Many2many(
|
||||
comodel_name="res.users",
|
||||
string="Default Assignees",
|
||||
help="Employees assigned to tasks created from this template.",
|
||||
)
|
||||
|
||||
customer = fields.Many2one(
|
||||
comodel_name="res.partner",
|
||||
string="Default Customer",
|
||||
help="Default customer for tasks created from this template.",
|
||||
default=lambda self: self.parent.customer if self.parent else False,
|
||||
)
|
||||
|
||||
project = fields.Many2one(
|
||||
comodel_name="project.project",
|
||||
string="Default Project",
|
||||
help="Default project for tasks created from this template.",
|
||||
)
|
||||
|
||||
tags = fields.Many2many(
|
||||
comodel_name="project.tags",
|
||||
string="Default Tags",
|
||||
help="Default tags for tasks created from this template.",
|
||||
)
|
||||
|
||||
parent = fields.Many2one(
|
||||
comodel_name="project.task.template",
|
||||
string="Parent Task Template",
|
||||
ondelete="cascade",
|
||||
)
|
||||
|
||||
subtasks = fields.One2many(
|
||||
comodel_name="project.task.template",
|
||||
inverse_name="parent",
|
||||
string="Subtask Templates",
|
||||
)
|
||||
sequence = fields.Integer()
|
||||
|
||||
company_id = fields.Many2one(
|
||||
comodel_name="res.company", string="Company", index=1, default=_current_company
|
||||
)
|
||||
|
||||
planned_hours = fields.Float("Initially Planned Hours")
|
||||
|
||||
equipment_ids = fields.Many2many(
|
||||
comodel_name="fsm.equipment",
|
||||
relation="bemade_fsm_task_template_equipment_rel",
|
||||
column1="task_template_id",
|
||||
column2="equipment_id",
|
||||
string="Equipment to Service",
|
||||
)
|
||||
|
||||
def action_open_task(self):
|
||||
return {
|
||||
"view_mode": "form",
|
||||
"res_model": "project.task.template",
|
||||
"res_id": self.id,
|
||||
"type": "ir.actions.act_window",
|
||||
"context": self._context,
|
||||
}
|
||||
|
||||
@api.onchange("customer")
|
||||
def _onchange_customer(self):
|
||||
for rec in self:
|
||||
new_equipment_ids = [
|
||||
eq.id for eq in rec.equipment_ids if eq.partner_id == rec.customer
|
||||
]
|
||||
rec.write({"equipment_ids": [Command.set(new_equipment_ids)]})
|
||||
|
||||
def _prepare_new_task_values_from_self(self, project, name=False, parent_id=False):
|
||||
partner_id = self.customer or self.parent.customer or False
|
||||
if not partner_id and parent_id:
|
||||
parent = self.env["project.task"].browse(parent_id)
|
||||
partner_id = parent.partner_id or parent.sale_order_id.partner_shipping_id
|
||||
vals = {
|
||||
"project_id": project.id,
|
||||
"name": name or self.name,
|
||||
"description": self.description,
|
||||
"parent_id": parent_id,
|
||||
"user_ids": self.assignees.ids,
|
||||
"tag_ids": self.tags.ids,
|
||||
"allocated_hours": self.planned_hours,
|
||||
"sequence": self.sequence,
|
||||
"equipment_ids": (
|
||||
[Command.set(self.equipment_ids.ids)] if self.equipment_ids else False
|
||||
),
|
||||
"partner_id": partner_id and partner_id.id,
|
||||
"company_id": self.company_id.id,
|
||||
}
|
||||
return vals
|
||||
|
||||
def create_task_from_self(self, project, name=False, parent_id=False):
|
||||
"""Create a project.task from this template and return it.
|
||||
Can be called on a RecordSet of multiple templates.
|
||||
|
||||
:param project: project.project record the task should be added to
|
||||
:param name: name for the new task (defaults to template name)
|
||||
:param parent_id: parent task for the new task (none by default)
|
||||
:return: project.task record created from this template
|
||||
"""
|
||||
tasks = self.env["project.task"]
|
||||
for rec in self:
|
||||
vals = rec._prepare_new_task_values_from_self(project, name, parent_id)
|
||||
task = rec.env["project.task"].create(vals)
|
||||
rec.subtasks.create_task_from_self(project, name=False, parent_id=task.id)
|
||||
tasks |= task
|
||||
return tasks
|
||||
1
bemade_fsm/reports/__init__.py
Normal file
1
bemade_fsm/reports/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import worksheet_custom_reports
|
||||
581
bemade_fsm/reports/worksheet_custom_report_templates.xml
Normal file
581
bemade_fsm/reports/worksheet_custom_report_templates.xml
Normal file
|
|
@ -0,0 +1,581 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<template id="workorder_page_materials_table">
|
||||
<t
|
||||
t-set="order_lines"
|
||||
t-value="doc.relevant_order_lines.filtered(lambda l:
|
||||
l.product_id.type != 'service' and not l.is_downpayment and not l.visit_id)"
|
||||
/>
|
||||
<t t-if="order_lines">
|
||||
<t t-set="visit_lines" t-value="order_lines.mapped('visit_id')" />
|
||||
<t
|
||||
t-set="section_lines"
|
||||
t-value="order_lines.filtered(lambda l: l.display_type == 'line_section')"
|
||||
/>
|
||||
</t>
|
||||
<div
|
||||
t-if="order_lines.filtered(lambda l: not l.display_type)"
|
||||
style="page-break-inside: avoid;"
|
||||
>
|
||||
<h2 t-if="order_lines">Materials</h2>
|
||||
<div t-if="order_lines" class="table-responsive-sm">
|
||||
<table class="table table-sm o_main_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left">Description</th>
|
||||
<th class="text-right">Ordered</th>
|
||||
<th class="text-right">Delivered</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="sale_tbody">
|
||||
<t t-foreach="order_lines" t-as="line">
|
||||
<t t-set="is_task" t-value="line == doc.sale_line_id" />
|
||||
<tr
|
||||
t-att-class="'bg-200 font-weight-bold o_line_section' if line.display_type == 'line_section' else 'font-italic o_line_note' if line.display_type == 'line_note' else ''"
|
||||
>
|
||||
<t
|
||||
t-if="not line.display_type and not line.is_downpayment"
|
||||
>
|
||||
<td><span t-field="line.name" /></td>
|
||||
<td class="text-right">
|
||||
<span t-field="line.product_uom_qty" />
|
||||
<span
|
||||
t-field="line.product_uom"
|
||||
groups="uom.group_uom"
|
||||
/>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span t-field="line.qty_delivered" />
|
||||
<span
|
||||
t-field="line.product_uom"
|
||||
groups="uom.group_uom"
|
||||
/>
|
||||
</td>
|
||||
</t>
|
||||
<t t-if="line.display_type == 'line_section'">
|
||||
<td colspan="99">
|
||||
<span t-field="line.name" />
|
||||
</td>
|
||||
</t>
|
||||
<t t-if="line.display_type == 'line_note'">
|
||||
<td colspan="99">
|
||||
<span t-field="line.name" />
|
||||
</td>
|
||||
</t>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template id="workorder_page_sale_order_table_with_pricing_and_labour">
|
||||
<t t-set="order_lines" t-value="doc.relevant_order_lines" />
|
||||
<t t-if="order_lines">
|
||||
<t t-set="visit_lines" t-value="order_lines.mapped('visit_id')" />
|
||||
<t
|
||||
t-if="visit_lines"
|
||||
t-set="root_tasks"
|
||||
t-value="visit_lines.mapped('task_id')"
|
||||
/>
|
||||
<t
|
||||
t-else=""
|
||||
t-set="root_tasks"
|
||||
t-value="order_lines.mapped('task_id').filtered(lambda l: not l.parent_id)"
|
||||
/>
|
||||
<t t-set="final_subtotal" t-value="0" />
|
||||
<t t-set="final_tax" t-value="0" />
|
||||
<t t-set="final_total" t-value="0" />
|
||||
<t t-set="is_any_total_discount_line" t-value="False" />
|
||||
<t
|
||||
t-set="section_lines"
|
||||
t-value="order_lines.filtered(lambda l: l.display_type == 'line_section')"
|
||||
/>
|
||||
<t t-foreach="order_lines" t-as="line">
|
||||
<t
|
||||
t-set="final_subtotal"
|
||||
t-value="final_subtotal + line.delivered_price_subtotal"
|
||||
/>
|
||||
<t
|
||||
t-set="final_total"
|
||||
t-value="final_total + line.delivered_price_total"
|
||||
/>
|
||||
<t t-set="final_tax" t-value="final_tax + line.delivered_price_tax" />
|
||||
<t
|
||||
t-set="is_any_total_discount_line"
|
||||
t-value="is_any_total_discount_line or (line.discount and line.price_unit != 0 and line.delivered_price_total == 0)"
|
||||
/>
|
||||
</t>
|
||||
<t
|
||||
t-set="display_discount"
|
||||
t-value="any(line.discount for line in order_lines)"
|
||||
/>
|
||||
</t>
|
||||
<t t-else="" t-set="root_tasks" t-value="doc.root_ancestor" />
|
||||
<div t-if="order_lines.filtered(lambda l: not l.display_type)">
|
||||
<h2 t-if="order_lines">Time & Material</h2>
|
||||
<div t-if="order_lines" class="table-responsive-sm">
|
||||
<table class="table table-sm o_main_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left">Description</th>
|
||||
<th class="text-right">Ordered</th>
|
||||
<th class="text-right">Delivered</th>
|
||||
<th class="text-right">Unit Price</th>
|
||||
<th
|
||||
t-if="display_discount"
|
||||
class="text-right"
|
||||
groups="sale.group_discount_per_so_line"
|
||||
>
|
||||
<span>Disc.%</span>
|
||||
</th>
|
||||
<th class="text-right">
|
||||
<span>
|
||||
Amount</span>
|
||||
<span>
|
||||
Total Price</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="sale_tbody">
|
||||
<t t-set="current_subtotal" t-value="0" />
|
||||
<t t-foreach="order_lines" t-as="line">
|
||||
<t t-set="is_task" t-value="line == doc.sale_line_id" />
|
||||
<t
|
||||
t-set="is_total_discount"
|
||||
t-value="line.discount and line.price_unit != 0 and line.delivered_price_total == 0"
|
||||
/>
|
||||
<t
|
||||
t-set="current_subtotal"
|
||||
t-value="current_subtotal + line.delivered_price_subtotal"
|
||||
/>
|
||||
<t
|
||||
t-set="current_total"
|
||||
t-value="current_subtotal + line.delivered_price_total"
|
||||
/>
|
||||
<tr
|
||||
t-att-class="'bg-200 font-weight-bold o_line_section' if line.display_type == 'line_section' else 'font-italic o_line_note' if line.display_type == 'line_note' else ''"
|
||||
>
|
||||
<t
|
||||
t-if="not line.display_type and not line.is_downpayment"
|
||||
>
|
||||
<td><span t-field="line.name" /></td>
|
||||
<td class="text-right">
|
||||
<span t-field="line.product_uom_qty" />
|
||||
<span
|
||||
t-field="line.product_uom"
|
||||
groups="uom.group_uom"
|
||||
/>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span t-field="line.qty_delivered" />
|
||||
<span
|
||||
t-field="line.product_uom"
|
||||
groups="uom.group_uom"
|
||||
/>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span t-field="line.price_unit" />
|
||||
</td>
|
||||
<td
|
||||
t-if="display_discount"
|
||||
class="text-right"
|
||||
groups="sale.group_discount_per_so_line"
|
||||
>
|
||||
<span t-field="line.discount" />
|
||||
</td>
|
||||
<td class="text-right o_price_total">
|
||||
<span t-field="line.delivered_price_subtotal" />
|
||||
</td>
|
||||
</t>
|
||||
<t t-if="line.display_type == 'line_section'">
|
||||
<td colspan="99">
|
||||
<span t-field="line.name" />
|
||||
</td>
|
||||
<t t-set="current_section" t-value="line" />
|
||||
<t t-set="current_subtotal" t-value="0" />
|
||||
</t>
|
||||
<t t-if="line.display_type == 'line_note'">
|
||||
<td colspan="99">
|
||||
<span t-field="line.name" />
|
||||
</td>
|
||||
</t>
|
||||
</tr>
|
||||
<t
|
||||
t-if="current_section and len(section_lines) > 1 and (line_last or order_lines[line_index+1].display_type == 'line_section')"
|
||||
>
|
||||
<tr class="is-subtotal text-right">
|
||||
<td colspan="99">
|
||||
<strong class="mr16">Section
|
||||
Subtotal</strong>
|
||||
<span
|
||||
t-esc="current_subtotal"
|
||||
t-options='{"widget": "monetary", "display_currency": doc.sale_order_id.pricelist_id.currency_id}'
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="container_subtotal" name="so_total_summary">
|
||||
<div id="total" class="row justify-content-end" name="total">
|
||||
<div
|
||||
t-attf-class="#{'col-auto' if report_type != 'html' else 'col-sm-2'}"
|
||||
>
|
||||
<table class="table table-sm">
|
||||
<tr
|
||||
t-if="final_tax"
|
||||
class="border-black o_subtotal"
|
||||
style=""
|
||||
>
|
||||
<td><strong>Untaxed amount</strong></td>
|
||||
<td class="text-right">
|
||||
<span
|
||||
t-esc="final_subtotal"
|
||||
t-options='{"widget": "monetary", "display_currency": doc.sale_order_id.pricelist_id.currency_id}'
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr t-if="final_tax" class="border-black o_subtotal">
|
||||
<td><strong>Taxes</strong></td>
|
||||
<td class="text-right">
|
||||
<span
|
||||
t-esc="final_tax"
|
||||
t-options='{"widget": "monetary", "display_currency": doc.sale_order_id.pricelist_id.currency_id}'
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr t-if="final_total" class="border-black o_total">
|
||||
<td><strong>Total</strong></td>
|
||||
<td class="text-right">
|
||||
<span
|
||||
t-esc="final_total"
|
||||
t-options='{"widget": "monetary", "display_currency": doc.sale_order_id.pricelist_id.currency_id}'
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template id="subtask_list">
|
||||
<ul t-if="task.child_ids" class="ml-1 pl-1" style="list-style-type: none;">
|
||||
<t t-foreach="task.child_ids" t-as="subtask">
|
||||
<li class="ml-1">
|
||||
<span t-out="subtask.name" />
|
||||
<t t-call="bemade_fsm.subtask_list">
|
||||
<t t-set="task" t-value="subtask" />
|
||||
</t>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</template>
|
||||
<template id="workorder_page_tasks_table">
|
||||
<t t-set="interventions" t-value="doc.child_ids" />
|
||||
<t t-foreach="interventions" t-as="intervention">
|
||||
<div style="page-break-inside: avoid;">
|
||||
<h2 t-out="str(intervention_index + 1) + '. ' + intervention.name" />
|
||||
<h3 t-if="intervention.equipment_ids">Equipment:
|
||||
<t t-foreach="intervention.equipment_ids" t-as="equipment_id">
|
||||
<span
|
||||
t-out="equipment_id.display_name + (', ' if not equipment_id_last else '')"
|
||||
/>
|
||||
</t>
|
||||
</h3>
|
||||
<div>
|
||||
<span t-esc="intervention.description" />
|
||||
</div>
|
||||
<t t-set="tasks" t-value="intervention.child_ids" />
|
||||
<div t-if="tasks" class="table-responsive-sm">
|
||||
<table class="table table-sm o_main_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="7%">Status</th>
|
||||
<th width="39%">Task</th>
|
||||
<th width="54%">Comments</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="tasks" t-as="task">
|
||||
<tr class="p-1 m-1">
|
||||
<td t-out="task.stage_id.name" />
|
||||
<td>
|
||||
<span t-out="task.name" />
|
||||
</td>
|
||||
<td t-out="task.description or ''" />
|
||||
</tr>
|
||||
<tr t-if="task.child_ids" class="mt-0 pt-0">
|
||||
<td />
|
||||
<td>
|
||||
<t t-call="bemade_fsm.subtask_list" />
|
||||
</td>
|
||||
<td />
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</template>
|
||||
<template id="workorder_page_info_block">
|
||||
<h1>
|
||||
<span name="work_order_number" t-out="doc.name" />
|
||||
</h1>
|
||||
<h4 t-if="doc.sale_order_id and doc.sale_order_id.client_order_ref">
|
||||
Purchase Order: <span
|
||||
name="po_number"
|
||||
t-out="doc.sale_order_id.client_order_ref"
|
||||
/>
|
||||
</h4>
|
||||
<hr />
|
||||
<div class="row" name="address_and_time">
|
||||
<div
|
||||
t-attf-class="{{('col-6' if report_type == 'pdf' else 'col-md-6 col-12') + ' mb-3'}}"
|
||||
>
|
||||
<t t-if="doc.partner_id">
|
||||
<div><h6>Customer: </h6></div>
|
||||
<div
|
||||
t-esc="doc.partner_id"
|
||||
t-options='{
|
||||
"widget": "contact",
|
||||
"fields": ["name", "address",],
|
||||
"lang": "fr_FR"
|
||||
}'
|
||||
/>
|
||||
</t>
|
||||
</div>
|
||||
<div
|
||||
t-attf-class="{{('col-6' if report_type == 'pdf' else 'col-md-6 col-12') + ' mb-3'}}"
|
||||
t-if="doc.planned_date_begin or doc.date_deadline"
|
||||
>
|
||||
<div t-if="doc.planned_date_begin"><h6>Planned start: </h6></div>
|
||||
<div class="mb-3">
|
||||
<div t-esc="context_timestamp(doc.planned_date_begin).strftime('%Y-%m-%d %H:%M')" />
|
||||
</div>
|
||||
<div t-if="doc.date_deadline"><h6>Planned end: </h6></div>
|
||||
<div class="mb-3">
|
||||
<div t-out="context_timestamp(doc.date_deadline).strftime('%Y-%m-%d %H:%M')" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" name="site_and_work_order_contacts">
|
||||
<div
|
||||
t-attf-class="{{'col-6' if report_type == 'pdf' else 'col-md-6 col-12'}}"
|
||||
>
|
||||
<div t-if="doc.site_contacts"><h6>Site Contacts: </h6></div>
|
||||
<t t-foreach="doc.site_contacts" t-as="contact">
|
||||
<div class="mb-3">
|
||||
<div
|
||||
t-esc="contact"
|
||||
t-options='{
|
||||
"widget": "contact",
|
||||
"fields": ["name", "phone", "email"]
|
||||
}'
|
||||
/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<div
|
||||
t-attf-class="{{'col-6' if report_type == 'pdf' else 'col-md-6 col-12'}}"
|
||||
>
|
||||
<div t-if="doc.work_order_contacts"><h6>Work Order
|
||||
Contacts: </h6></div>
|
||||
<t t-foreach="doc.work_order_contacts" t-as="contact">
|
||||
<div class="mb-3">
|
||||
<div
|
||||
t-esc="contact"
|
||||
t-options='{
|
||||
"widget": "contact",
|
||||
"fields": ["name", "phone", "email"]
|
||||
}'
|
||||
/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div
|
||||
t-if="not is_html_empty(doc.description)"
|
||||
class="row"
|
||||
name="visit_description"
|
||||
>
|
||||
<div class="col-12">
|
||||
<span t-out="doc.description" />
|
||||
</div>
|
||||
</div>
|
||||
<hr t-if="not is_html_empty(doc.description)" />
|
||||
</template>
|
||||
<template id="workorder_equipment_summary">
|
||||
<div
|
||||
t-if="doc.equipment_ids and len(doc.child_ids) > 1"
|
||||
style="page-break-inside: avoid;"
|
||||
>
|
||||
<h3>Equipment Serviced</h3>
|
||||
<div class="table-responsive-sm">
|
||||
<table class="table table-sm o_main_table overflow-hidden">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tag</th>
|
||||
<th>Name</th>
|
||||
<th>Application</th>
|
||||
<th>Location</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="doc.equipment_ids" t-as="equipment">
|
||||
<tr>
|
||||
<td><span t-esc="equipment.code" /></td>
|
||||
<td><span t-esc="equipment.name" /></td>
|
||||
<td>
|
||||
<t t-foreach="equipment.tag_ids" t-as="application">
|
||||
<span
|
||||
t-esc="application.name + (', ' if not application_last else '')"
|
||||
/>
|
||||
</t>
|
||||
</td>
|
||||
<td
|
||||
t-esc="equipment.partner_id"
|
||||
t-options='{
|
||||
"widget": "contact",
|
||||
"fields": ["name", "address"]}'
|
||||
/>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template id="workorder_page_timesheet_entries">
|
||||
<t t-set="timesheets" t-value="doc.timesheet_ids" />
|
||||
<div t-if="timesheets" style="page-break-inside: avoid;">
|
||||
<h1>Time</h1>
|
||||
<div class="table-responsive-sm">
|
||||
<table class="table table-sm o_main_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="14%">Date</th>
|
||||
<th width="30%">Technician</th>
|
||||
<th width="49%">Work Completed</th>
|
||||
<th width="7%" class="text-right">Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="timesheets" t-as="timesheet" class="p-1 m-1">
|
||||
<td>
|
||||
<span t-out="timesheet.date.strftime('%Y-%m-%d')" />
|
||||
</td>
|
||||
<td>
|
||||
<span t-field="timesheet.employee_id.name" />
|
||||
</td>
|
||||
<td>
|
||||
<span t-field="timesheet.name" />
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
t-esc="timesheet.unit_amount"
|
||||
t-options="{
|
||||
'widget': 'duration',
|
||||
'digital': True,
|
||||
'unit': 'hour',
|
||||
'round': 'minute'
|
||||
}"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template id="workorder_page_signature_block">
|
||||
<div t-if="doc.worksheet_signature">
|
||||
<div
|
||||
t-if="report_type == html"
|
||||
class="ribbon"
|
||||
style="
|
||||
position: absolute;
|
||||
right: 0px; top: 0px;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
width: 75px; height: 75px;
|
||||
text-align: right;"
|
||||
>
|
||||
<span
|
||||
style="
|
||||
font-size: 10px;
|
||||
color: #fff;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
font-weight: bold; line-height: 20px;
|
||||
transform: rotate(45deg);
|
||||
width: 100px; height: auto; display: block;
|
||||
background: green;
|
||||
position: absolute;
|
||||
top: 19px; right: -21px; left: auto;
|
||||
padding: 0;"
|
||||
>
|
||||
Signed
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
t-attf-class="#{' col-12 col-lg-3' if report_type != 'html' else '
|
||||
col-sm-7 col-md-4'} ml-auto text-right"
|
||||
style="page-break-inside: avoid"
|
||||
>
|
||||
<h5>Signature</h5>
|
||||
<img
|
||||
t-att-src="image_data_uri(doc.worksheet_signature)"
|
||||
style="max-height: 6rem; max-width: 100%; color:black;"
|
||||
/><br />
|
||||
<span t-field="doc.worksheet_signed_by" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template id="work_order_page">
|
||||
<div class="page">
|
||||
<t t-call="bemade_fsm.workorder_page_info_block" />
|
||||
<t t-call="bemade_fsm.workorder_page_timesheet_entries" />
|
||||
<t t-if="split_time_materials">
|
||||
<t t-call="bemade_fsm.workorder_page_materials_table" />
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t
|
||||
t-call="bemade_fsm.workorder_page_sale_order_table_with_pricing_and_labour"
|
||||
/>
|
||||
</t>
|
||||
<t t-call="bemade_fsm.workorder_equipment_summary" />
|
||||
<t t-call="bemade_fsm.workorder_page_signature_block" />
|
||||
<t t-call="bemade_fsm.workorder_page_tasks_table" />
|
||||
</div>
|
||||
</template>
|
||||
<template id="work_order">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="doc">
|
||||
<t t-set="doc" t-value="doc.root_ancestor.with_context(tz=doc.partner_id.tz)" t-if="doc.parent_id"/>
|
||||
<t t-set="lang" t-value="(doc.work_order_contacts and doc.work_order_contacts[0].lang) or (doc.partner_id and doc.partner_id.lang) or (doc.user_id and doc.user_id.lang) or user.lang"/>
|
||||
<t t-call="web.external_layout">
|
||||
<t t-call="bemade_fsm.work_order_page" t-lang="lang"/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
<template
|
||||
id="worksheet_custom"
|
||||
inherit_id="industry_fsm.worksheet_custom"
|
||||
priority="100"
|
||||
>
|
||||
<xpath expr="//t[@t-call='web.external_layout']" position="replace">
|
||||
<t t-set="lang" t-value="(doc.work_order_contacts and doc.work_order_contacts[0].lang) or (doc.partner_id and doc.partner_id.lang) or (doc.user_id and doc.user_id.lang) or user.lang"/>
|
||||
<t t-call="web.external_layout" t-lang="lang">
|
||||
<t t-call="bemade_fsm.work_order_page" t-lang="lang"/>
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
13
bemade_fsm/reports/worksheet_custom_reports.py
Normal file
13
bemade_fsm/reports/worksheet_custom_reports.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
from odoo import models
|
||||
|
||||
|
||||
class TaskCustomReport(models.AbstractModel):
|
||||
_inherit = "report.industry_fsm.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
|
||||
11
bemade_fsm/reports/worksheet_custom_reports.xml
Normal file
11
bemade_fsm/reports/worksheet_custom_reports.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="industry_fsm.task_custom_report" model="ir.actions.report">
|
||||
<field name="print_report_name">'%s %s' % (
|
||||
object.planned_date_begin.strftime(
|
||||
'%Y-%m-%d') if object.planned_date_begin else time.strftime('%Y-%m-%d'),
|
||||
object.name
|
||||
)
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
5
bemade_fsm/security/ir.model.access.csv
Normal file
5
bemade_fsm/security/ir.model.access.csv
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_bemade_fsm_task_template_manager,bemade_fsm_task_template,model_project_task_template,project.group_project_manager,1,1,1,1
|
||||
access_bemade_fsm_task_template_user,bemade_fsm_task_template,model_project_task_template,project.group_project_user,1,1,1,1
|
||||
access_bemade_fsm_visit,access_bemade_fsm_visit,model_bemade_fsm_visit,base.group_user,1,1,1,1
|
||||
access_bemade_fsm_task_wizard,access_bemade_fsm_task_wizard,model_project_task_from_template_wizard,project.group_project_user,1,1,1,1
|
||||
|
37
bemade_fsm/static/src/js/kanban_view.js
Normal file
37
bemade_fsm/static/src/js/kanban_view.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import KanbanController from 'web.KanbanController';
|
||||
import KanbanView from 'web.KanbanView';
|
||||
const viewRegistry = require('web.view_registry')
|
||||
|
||||
const ProjectKanbanController = KanbanController.extend({
|
||||
buttons_template: 'project.KanbanView.buttons',
|
||||
custom_events: _.extend({}, KanbanController.prototype.custom_events, {
|
||||
create_from_template: '_onCreateFromTemplate',
|
||||
}),
|
||||
_onCreateFromTemplate(ev) {
|
||||
ev.stopPropagation();
|
||||
const record = this.model.get(this.handle, {raw: true});
|
||||
let context = record.context;
|
||||
context.active_id = record.project_id;
|
||||
this.do_action({
|
||||
type: 'ir.actions.act_window',
|
||||
res_model: 'project.task.from.template.wizard',
|
||||
views: [[false, 'form']],
|
||||
target: 'new',
|
||||
context: {...record.context, res_model: 'project.task'},
|
||||
});
|
||||
},
|
||||
renderButtons ($node) {
|
||||
this._super.apply(this, arguments);
|
||||
this.$buttons.on('click', 'button.o-kanban-button-new-from-template', this._onCreateFromTemplate.bind(this))
|
||||
},
|
||||
})
|
||||
|
||||
const ProjectKanbanView = KanbanView.extend({
|
||||
config: _.extend({}, KanbanView.prototype.config, {
|
||||
Controller: ProjectKanbanController,
|
||||
})
|
||||
});
|
||||
|
||||
viewRegistry.add('project_kanban', ProjectKanbanView)
|
||||
36
bemade_fsm/static/src/js/list_view.js
Normal file
36
bemade_fsm/static/src/js/list_view.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import ListController from 'web.ListController';
|
||||
import ListView from 'web.ListView';
|
||||
const viewRegistry = require('web.view_registry')
|
||||
|
||||
const ProjectListController = ListController.extend({
|
||||
buttons_template: 'project.ListView.buttons',
|
||||
custom_events: _.extend({}, ListController.prototype.custom_events, {
|
||||
create_from_template: '_onCreateFromTemplate',
|
||||
}),
|
||||
_onCreateFromTemplate(ev) {
|
||||
ev.stopPropagation();
|
||||
const record = this.model.get(this.handle, {raw: true});
|
||||
this.do_action({
|
||||
type: 'ir.actions.act_window',
|
||||
res_model: 'project.task.from.template.wizard',
|
||||
views: [[false, 'form']],
|
||||
target: 'new',
|
||||
context: {...record.context, res_model: 'project.task'},
|
||||
});
|
||||
},
|
||||
renderButtons ($node) {
|
||||
self = this;
|
||||
this._super.apply(this, arguments);
|
||||
this.$buttons.on('click', 'button.o_list_button_add_from_template', this._onCreateFromTemplate.bind(this))
|
||||
},
|
||||
})
|
||||
|
||||
const ProjectListView = ListView.extend({
|
||||
config: _.extend({}, ListView.prototype.config, {
|
||||
Controller: ProjectListController,
|
||||
})
|
||||
});
|
||||
|
||||
viewRegistry.add('project_list', ProjectListView)
|
||||
14
bemade_fsm/static/src/scss/bemade_fsm.scss
Normal file
14
bemade_fsm/static/src/scss/bemade_fsm.scss
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
/* This prevents repeating the header, which overlaps with the body due to a bug in
|
||||
wkhtmltopdf rendering.
|
||||
*/
|
||||
thead {
|
||||
display: table-row-group;
|
||||
}
|
||||
|
||||
tfoot {
|
||||
display: table-row-group;
|
||||
}
|
||||
|
||||
tr {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
21
bemade_fsm/static/src/xml/project_view_buttons.xml
Normal file
21
bemade_fsm/static/src/xml/project_view_buttons.xml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<!-- Add a button to create a task from a template to task (project) list and kanban views -->
|
||||
<t t-name='project.KanbanView.buttons' t-inherit="web.KanbanView.Buttons" t-inherit-mode="primary">
|
||||
<xpath expr="//button[contains(@t-attf-class, 'o-kanban-button-new')]" position="after">
|
||||
<button title="Create Task from Template" t-if="!noCreate" type="button"
|
||||
t-attf-class="btn {{ btnClass }} o-kanban-button-new-from-template">
|
||||
<t t-esc="create_text && (create_text + ' from Template') || _t('Create from Template')"/>
|
||||
</button>
|
||||
</xpath>
|
||||
</t>
|
||||
<t t-name='project.ListView.buttons' t-inherit="web.ListView.Buttons" t-inherit-mode="primary">
|
||||
<xpath expr="//button[hasclass('o_list_button_add')]" position="after">
|
||||
<!-- Create is enabled in the parent template at this point; check is done prior -->
|
||||
<button type="button" class="btn ml-1 btn-primary o_list_button_add_from_template"
|
||||
title="Create Task from Template">
|
||||
Create from Template
|
||||
</button>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
9
bemade_fsm/tests/__init__.py
Normal file
9
bemade_fsm/tests/__init__.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
from . import test_bemade_fsm_common
|
||||
from . import test_task_template
|
||||
from . import test_sale_order
|
||||
from . import test_fsm_contact_setting
|
||||
from . import test_fsm_visit
|
||||
from . import test_task
|
||||
from . import test_task_report
|
||||
from . import test_settings
|
||||
from . import test_equipment
|
||||
292
bemade_fsm/tests/test_bemade_fsm_common.py
Normal file
292
bemade_fsm/tests/test_bemade_fsm_common.py
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
from odoo.tests.common import TransactionCase, tagged
|
||||
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()
|
||||
user_group_project_manager = cls.env.ref("project.group_project_manager")
|
||||
user_group_fsm_manager = cls.env.ref("industry_fsm.group_fsm_manager")
|
||||
group_ids.append(user_group_fsm_manager.id)
|
||||
group_ids.append(user_group_project_manager.id)
|
||||
|
||||
return cls.__generate_user(name, login, group_ids)
|
||||
|
||||
@classmethod
|
||||
def _generate_project_user(cls, name, login):
|
||||
group_ids = cls.__get_user_groups()
|
||||
return cls.__generate_user(name, login, group_ids)
|
||||
|
||||
@classmethod
|
||||
def __generate_user(cls, name, login, group_ids):
|
||||
return (
|
||||
cls.env["res.users"]
|
||||
.with_context(no_reset_password=True)
|
||||
.create(
|
||||
{
|
||||
"name": name,
|
||||
"login": login,
|
||||
"password": login,
|
||||
"email": f"{login}@test.co",
|
||||
"groups_id": [Command.set(group_ids)],
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def __get_user_groups(cls):
|
||||
user_group_employee = cls.env.ref("base.group_user")
|
||||
user_group_project_user = cls.env.ref("project.group_project_user")
|
||||
user_group_fsm_user = cls.env.ref("industry_fsm.group_fsm_user")
|
||||
user_group_sales_user = cls.env.ref("sales_team.group_sale_salesman")
|
||||
user_group_sales_manager = cls.env.ref("sales_team.group_sale_manager")
|
||||
user_group_delivery_address = cls.env.ref(
|
||||
"account.group_delivery_invoice_address"
|
||||
)
|
||||
user_product_customer = cls.env.ref(
|
||||
"customer_product_code.group_product_customer_code_user", # pyright: ignore[reportGeneralTypeIssues]
|
||||
raise_if_not_found=False,
|
||||
)
|
||||
|
||||
group_ids = [
|
||||
user_group_employee.id,
|
||||
user_group_project_user.id,
|
||||
user_group_fsm_user.id,
|
||||
user_group_sales_manager.id,
|
||||
user_group_sales_user.id,
|
||||
user_group_delivery_address.id,
|
||||
]
|
||||
if user_product_customer:
|
||||
group_ids.append(user_product_customer.id)
|
||||
return group_ids
|
||||
|
||||
@classmethod
|
||||
def _generate_partner(
|
||||
cls,
|
||||
name: str = "Test Company",
|
||||
company_type: str = "company",
|
||||
parent=None,
|
||||
location_type="other",
|
||||
):
|
||||
"""Generates a partner with basic address filled in.
|
||||
|
||||
:param name: The partner's name.
|
||||
:param company_type: The type of partner, either 'company' or 'person' are
|
||||
accepted.
|
||||
"""
|
||||
return cls.env["res.partner"].create(
|
||||
{
|
||||
"name": name,
|
||||
"company_type": company_type,
|
||||
"street": "123 Street St.",
|
||||
"city": "Montreal",
|
||||
"state_id": cls.env.ref("base.state_ca_qc").id,
|
||||
"country_id": cls.env.ref("base.ca").id,
|
||||
"parent_id": parent and parent.id or False,
|
||||
"type": location_type,
|
||||
}
|
||||
)
|
||||
|
||||
@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}
|
||||
if equipment:
|
||||
vals.update({"default_equipment_ids": [Command.set([equipment.id])]})
|
||||
if shipping_location:
|
||||
vals.update({"partner_shipping_id": shipping_location.id})
|
||||
return cls.env["sale.order"].create(vals)
|
||||
|
||||
@classmethod
|
||||
def _generate_sale_order_line(
|
||||
cls, sale_order, product=None, qty=1.0, uom=None, price=100.0, tax_id=False
|
||||
):
|
||||
if not product:
|
||||
product = cls._generate_product()
|
||||
return cls.env["sale.order.line"].create(
|
||||
{
|
||||
"order_id": sale_order.id,
|
||||
"product_id": product.id,
|
||||
"product_uom_qty": qty,
|
||||
"product_uom": uom and uom.id or cls.env.ref("uom.product_uom_hour").id,
|
||||
"price_unit": price,
|
||||
"tax_id": tax_id,
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _generate_equipment(cls, name="test equipment", partner=None):
|
||||
return cls.env["fsm.equipment"].create(
|
||||
{
|
||||
"name": name,
|
||||
"partner_id": partner and partner.id or False,
|
||||
}
|
||||
)
|
||||
|
||||
@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,
|
||||
):
|
||||
if "project" in service_tracking and not project:
|
||||
project = cls.env.ref("industry_fsm.fsm_project")
|
||||
uom_id = uom and uom.id or cls.env.ref("uom.product_uom_hour").id or False
|
||||
return cls.env["product.product"].create(
|
||||
{
|
||||
"name": name,
|
||||
"type": product_type,
|
||||
"service_tracking": service_tracking,
|
||||
"service_type": "timesheet",
|
||||
"project_id": (
|
||||
service_tracking in ("task_global_project", "project_only")
|
||||
and project
|
||||
and project.id
|
||||
or False
|
||||
),
|
||||
"project_template_id": (
|
||||
service_tracking == "task_in_project"
|
||||
and project
|
||||
and project.id
|
||||
or False
|
||||
),
|
||||
"task_template_id": task_template and task_template.id or False,
|
||||
"service_policy": service_policy,
|
||||
"uom_id": uom_id,
|
||||
"uom_po_id": uom_id,
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _generate_fsm_project(cls, name="Test Project"):
|
||||
return cls.env["project.project"].create(
|
||||
{
|
||||
"name": name,
|
||||
"allow_material": True,
|
||||
"allow_timesheets": True,
|
||||
"allow_quotations": True,
|
||||
"allow_worksheets": True,
|
||||
"is_fsm": True,
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _generate_task_template(
|
||||
cls,
|
||||
parent=None,
|
||||
structure=None,
|
||||
names=None,
|
||||
planned_hours=1,
|
||||
equipment=None,
|
||||
customer=None,
|
||||
):
|
||||
"""Generates a task template with the specified structure and naming.
|
||||
|
||||
:param parent: The parent task template for the top-level task template being
|
||||
generated
|
||||
:param structure: A list of integers describing the number of tasks for each
|
||||
level of descendants. An empty list represents only one
|
||||
top-level task template. If no structure is given, an empty
|
||||
list will be used in its place.
|
||||
:param names: The name prefixes to be given to the task templates at each level.
|
||||
Each prefix will be followed by a sequential integer for its
|
||||
level. Child 1, Child 2, Grandchild 1, etc. If no names argument
|
||||
is passed, a default ['Task Template'] argument will be used.
|
||||
:param planned_hours: The number of planned hours for the top-level task
|
||||
template being generated.
|
||||
:param equipment: The equipment to add as linked equipment to the task template.
|
||||
"""
|
||||
if not names:
|
||||
names = ["Task Template"]
|
||||
if not structure:
|
||||
structure = []
|
||||
if len(structure) != len(names) - 1:
|
||||
raise ValueError(
|
||||
"The length of the structure argument must contain exactly one element"
|
||||
" less than the names argument."
|
||||
)
|
||||
name = names.pop(0)
|
||||
template = cls.env["project.task.template"].create(
|
||||
{
|
||||
"name": name,
|
||||
"parent": parent and parent.id or False,
|
||||
"planned_hours": planned_hours,
|
||||
"equipment_ids": [Command.set(equipment and [equipment.id] or [])],
|
||||
"customer": customer and customer.id or False,
|
||||
}
|
||||
)
|
||||
parent = template
|
||||
while structure:
|
||||
subtasks = []
|
||||
for i in range(0, structure[0]):
|
||||
subtasks.append(
|
||||
cls.env["project.task.template"].create(
|
||||
{
|
||||
"parent": parent.id,
|
||||
"name": names[0] + f" {i}",
|
||||
}
|
||||
)
|
||||
)
|
||||
structure.pop(0)
|
||||
names.pop(0)
|
||||
parent = subtasks[0]
|
||||
return template
|
||||
|
||||
def _invoice_sale_order(self, so):
|
||||
wiz = (
|
||||
self.env["sale.advance.payment.inv"]
|
||||
.with_context(active_ids=[so.id])
|
||||
.create({})
|
||||
)
|
||||
wiz.create_invoices()
|
||||
inv = so.invoice_ids[-1]
|
||||
inv.action_post()
|
||||
return inv
|
||||
|
||||
def _generate_visit(self, sale_order, label="Test Label"):
|
||||
return self.env["bemade_fsm.visit"].create(
|
||||
[
|
||||
{
|
||||
"sale_order_id": sale_order.id,
|
||||
"label": label,
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
def _generate_so_with_one_visit_two_lines(self):
|
||||
so = self._generate_sale_order()
|
||||
visit = self._generate_visit(sale_order=so)
|
||||
sol1 = self._generate_sale_order_line(sale_order=so)
|
||||
sol2 = self._generate_sale_order_line(sale_order=so)
|
||||
visit.so_section_id.sequence = 1
|
||||
sol1.sequence = 2
|
||||
sol2.sequence = 3
|
||||
return so, visit, sol1, sol2
|
||||
|
||||
def _generate_so_with_one_visit_two_lines_and_descendants(self):
|
||||
so = self._generate_sale_order()
|
||||
visit = self._generate_visit(sale_order=so)
|
||||
task_template = self._generate_task_template(
|
||||
structure=[2, 2, 2],
|
||||
names=["Parent", "Child", "Grandchild", "Great-grandchild"],
|
||||
)
|
||||
product = self._generate_product(task_template=task_template)
|
||||
sol1 = self._generate_sale_order_line(sale_order=so, product=product)
|
||||
sol2 = self._generate_sale_order_line(sale_order=so, product=product)
|
||||
visit.so_section_id.sequence = 1
|
||||
sol1.sequence = 2
|
||||
sol2.sequence = 3
|
||||
return so, visit, sol1, sol2
|
||||
32
bemade_fsm/tests/test_equipment.py
Normal file
32
bemade_fsm/tests/test_equipment.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
from odoo.addons.bemade_fsm.tests.test_bemade_fsm_common import BemadeFSMBaseTest
|
||||
from odoo.tests import tagged, Form
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class TestEquipment(BemadeFSMBaseTest):
|
||||
def test_equipment_search_domain_on_sale_order(self):
|
||||
"""Equipment from other clients was showing up in sale order line
|
||||
equipment choices. Make sure this doesn't happen."""
|
||||
partner = self._generate_partner()
|
||||
partner_2 = self._generate_partner()
|
||||
equipment_1 = self._generate_equipment(partner=partner)
|
||||
equipment_2 = self._generate_equipment(partner=partner_2)
|
||||
sale_order = self._generate_sale_order(partner=partner)
|
||||
product = self._generate_product()
|
||||
self.assertEqual(sale_order.valid_equipment_ids, equipment_1)
|
||||
|
||||
name_search_results = self.env["fsm.equipment"].name_search(
|
||||
args=[
|
||||
"&",
|
||||
["id", "in", sale_order.valid_equipment_ids.ids],
|
||||
"!",
|
||||
["id", "in", []],
|
||||
],
|
||||
limit=8,
|
||||
name="test",
|
||||
operator="ilike",
|
||||
)
|
||||
self.assertNotIn(
|
||||
(equipment_2.id, equipment_2.display_name), name_search_results
|
||||
)
|
||||
self.assertIn((equipment_1.id, equipment_1.display_name), name_search_results)
|
||||
129
bemade_fsm/tests/test_fsm_contact_setting.py
Normal file
129
bemade_fsm/tests/test_fsm_contact_setting.py
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
from odoo.tests import tagged, Form
|
||||
from odoo import Command
|
||||
from .test_bemade_fsm_common import BemadeFSMBaseTest
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class SaleOrderFSMContactsCase(BemadeFSMBaseTest):
|
||||
def test_site_contacts(self):
|
||||
parent_co = self._generate_partner("Parent Co")
|
||||
contact_1 = self._generate_partner("Contact 1", "person", parent_co)
|
||||
contact_2 = self._generate_partner("Contact 2", "person", parent_co)
|
||||
|
||||
# Make sure the SO pulls the defaults from the partner correctly
|
||||
parent_co.write({"site_contacts": [Command.set([contact_1.id, contact_2.id])]})
|
||||
so = self._generate_sale_order(parent_co)
|
||||
self.assertTrue(so.site_contacts == parent_co.site_contacts)
|
||||
|
||||
# Make sure updating the site contacts on the SO doesn't feed back to the
|
||||
# partner
|
||||
so.write({"site_contacts": [Command.set([contact_1.id])]})
|
||||
self.assertTrue(contact_1 in so.site_contacts)
|
||||
self.assertTrue(contact_2 not in so.site_contacts)
|
||||
self.assertTrue(
|
||||
so.site_contacts != so.partner_id.site_contacts
|
||||
and len(so.partner_id.site_contacts) == 2
|
||||
)
|
||||
|
||||
def test_default_workorder_contacts(self):
|
||||
parent_co = self._generate_partner("Parent Co")
|
||||
contact_1 = self._generate_partner("Contact 1", "person", parent_co)
|
||||
contact_2 = self._generate_partner("Contact 2", "person", parent_co)
|
||||
|
||||
# Make sure the SO pulls the defaults from the partner correctly
|
||||
parent_co.write(
|
||||
{"work_order_contacts": [Command.set([contact_1.id, contact_2.id])]}
|
||||
)
|
||||
so = self._generate_sale_order(parent_co)
|
||||
self.assertTrue(contact_1 in parent_co.work_order_contacts)
|
||||
self.assertTrue(contact_2 in parent_co.work_order_contacts)
|
||||
self.assertTrue(contact_1 in so.work_order_contacts)
|
||||
self.assertTrue(contact_2 in so.work_order_contacts)
|
||||
|
||||
# Make sure setting the work order contacts on the SO doesn't feed back to the
|
||||
# partner
|
||||
so.write({"work_order_contacts": [Command.set([contact_1.id])]})
|
||||
self.assertTrue(contact_1 in so.work_order_contacts)
|
||||
self.assertTrue(contact_2 not in so.work_order_contacts)
|
||||
self.assertTrue(
|
||||
so.work_order_contacts != so.partner_id.work_order_contacts
|
||||
and len(so.partner_id.work_order_contacts) == 2
|
||||
)
|
||||
|
||||
def test_multilayer_site_contacts(self):
|
||||
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
|
||||
)
|
||||
wo_contact_2 = self._generate_partner(
|
||||
"WO Contact 2", "person", shipping_location
|
||||
)
|
||||
site_contact_1 = self._generate_partner(
|
||||
"Site Contact 1", "person", shipping_location
|
||||
)
|
||||
site_contact_2 = self._generate_partner(
|
||||
"Site Contact 2", "person", shipping_location
|
||||
)
|
||||
shipping_location.write(
|
||||
{
|
||||
"work_order_contacts": [
|
||||
Command.set([wo_contact_1.id, wo_contact_2.id])
|
||||
],
|
||||
"site_contacts": [Command.set([site_contact_1.id, site_contact_2.id])],
|
||||
}
|
||||
)
|
||||
|
||||
so = self._generate_sale_order(parent_co)
|
||||
so.write({"partner_shipping_id": shipping_location.id})
|
||||
|
||||
self.assertEqual(so.site_contacts, shipping_location.site_contacts)
|
||||
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
|
||||
)
|
||||
wo_contact_2 = self._generate_partner(
|
||||
"WO Contact 2", "person", shipping_location
|
||||
)
|
||||
site_contact_1 = self._generate_partner(
|
||||
"Site Contact 1", "person", shipping_location
|
||||
)
|
||||
site_contact_2 = self._generate_partner(
|
||||
"Site Contact 2", "person", shipping_location
|
||||
)
|
||||
shipping_location.write(
|
||||
{
|
||||
"work_order_contacts": [
|
||||
Command.set([wo_contact_1.id, wo_contact_2.id])
|
||||
],
|
||||
"site_contacts": [Command.set([site_contact_1.id, site_contact_2.id])],
|
||||
}
|
||||
)
|
||||
|
||||
so = self._generate_sale_order(parent_co)
|
||||
|
||||
# Set back to a location without site or work order contacts
|
||||
form = Form(so)
|
||||
form.partner_shipping_id = parent_co
|
||||
form.save()
|
||||
# Make sure the contacts were reset on the SO
|
||||
self.assertFalse(so.work_order_contacts)
|
||||
self.assertFalse(so.site_contacts)
|
||||
|
||||
# Now set back to the location with the FSM contacts and make sure they get set
|
||||
# on the SO
|
||||
form.partner_shipping_id = shipping_location
|
||||
form.save()
|
||||
self.assertEqual(so.work_order_contacts, shipping_location.work_order_contacts)
|
||||
self.assertEqual(so.site_contacts, shipping_location.site_contacts)
|
||||
196
bemade_fsm/tests/test_fsm_visit.py
Normal file
196
bemade_fsm/tests/test_fsm_visit.py
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
from odoo.tests import tagged
|
||||
from .test_bemade_fsm_common import BemadeFSMBaseTest
|
||||
from datetime import date, timedelta
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class FSMVisitTest(BemadeFSMBaseTest):
|
||||
def test_create_visit_sets_name_on_section(self):
|
||||
so = self._generate_sale_order()
|
||||
self._generate_sale_order_line(sale_order=so)
|
||||
|
||||
visit = self._generate_visit(so)
|
||||
|
||||
self.assertTrue(visit.so_section_id)
|
||||
self.assertEqual(visit.so_section_id.name, visit.label)
|
||||
|
||||
def test_change_visit_section_name(self):
|
||||
so = self._generate_sale_order()
|
||||
visit = self._generate_visit(so, label="First Label")
|
||||
line = visit.so_section_id
|
||||
|
||||
line.name = "Second Label"
|
||||
|
||||
self.assertEqual(visit.label, "Second Label")
|
||||
|
||||
def test_change_visit_label_changes_section_name(self):
|
||||
so = self._generate_sale_order()
|
||||
visit = self._generate_visit(so, label="First Label")
|
||||
line = visit.so_section_id
|
||||
|
||||
visit.label = "Second Label"
|
||||
|
||||
self.assertEqual(line.name, "Second Label")
|
||||
|
||||
def test_visit_completes_when_task_completes(self):
|
||||
so = self._generate_sale_order()
|
||||
visit = self._generate_visit(so)
|
||||
self._generate_sale_order_line(so)
|
||||
so.action_confirm()
|
||||
task = so.order_line.filtered(lambda line: line.task_id).task_id
|
||||
|
||||
task.action_fsm_validate()
|
||||
|
||||
self.assertTrue(visit.is_completed)
|
||||
|
||||
def test_visit_shows_invoiced_when_invoiced(self):
|
||||
so = self._generate_sale_order()
|
||||
visit = self._generate_visit(so)
|
||||
self._generate_sale_order_line(so)
|
||||
so.action_confirm()
|
||||
task = so.order_line.filtered(lambda line: line.task_id).task_id
|
||||
task.action_fsm_validate()
|
||||
|
||||
self._invoice_sale_order(so)
|
||||
|
||||
self.assertTrue(visit.is_invoiced)
|
||||
|
||||
def test_visit_groups_section_tasks_when_confirmed(self):
|
||||
so, visit, sol1, sol2 = self._generate_so_with_one_visit_two_lines()
|
||||
|
||||
so.action_confirm()
|
||||
|
||||
visit_task = visit.task_id
|
||||
self.assertTrue(visit_task)
|
||||
visit_subtasks = visit_task.child_ids
|
||||
self.assertTrue(
|
||||
visit_subtasks
|
||||
and sol1.task_id in visit_subtasks
|
||||
and sol2.task_id in visit_subtasks
|
||||
)
|
||||
|
||||
def test_visit_task_gets_correct_due_date_on_confirmation(self):
|
||||
so, visit, sol1, sol2 = self._generate_so_with_one_visit_two_lines()
|
||||
|
||||
so.action_confirm()
|
||||
|
||||
visit_task = visit.task_id
|
||||
self.assertEqual(visit_task.date_deadline, visit.approx_date)
|
||||
|
||||
def test_visit_task_gets_correct_duration_on_confirmation(self):
|
||||
partner = self._generate_partner()
|
||||
so = self._generate_sale_order(partner=partner)
|
||||
visit = self._generate_visit(sale_order=so)
|
||||
sol1 = self._generate_sale_order_line(sale_order=so, qty=4.0)
|
||||
sol2 = self._generate_sale_order_line(sale_order=so, qty=4.0)
|
||||
visit.approx_date = date.today() + timedelta(days=7)
|
||||
visit.so_section_id.sequence = 1
|
||||
sol1.sequence = 2
|
||||
sol2.sequence = 3
|
||||
|
||||
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):
|
||||
self._generate_partner()
|
||||
so = self._generate_sale_order()
|
||||
self._generate_sale_order_line(sale_order=so)
|
||||
self._generate_sale_order_line(sale_order=so)
|
||||
|
||||
self._generate_visit(sale_order=so)
|
||||
|
||||
self.assertEqual(len(so.order_line), 3)
|
||||
|
||||
def test_marking_visit_task_done_completes_descendants(self):
|
||||
(
|
||||
so,
|
||||
visit,
|
||||
sol1,
|
||||
sol2,
|
||||
) = self._generate_so_with_one_visit_two_lines_and_descendants()
|
||||
so.action_confirm()
|
||||
parent = visit.task_id
|
||||
|
||||
parent.action_fsm_validate()
|
||||
|
||||
self._assert_is_done(parent)
|
||||
|
||||
def _assert_is_done(self, task):
|
||||
"""Recursively assert all tasks in a hierarchy are complete"""
|
||||
self.assertTrue(task.is_closed)
|
||||
for child in task.child_ids:
|
||||
self._assert_is_done(child)
|
||||
|
||||
def test_marking_visit_task_done_does_not_create_sale_order_line(self):
|
||||
so, visit, sol1, sol2 = self._generate_so_with_one_visit_two_lines()
|
||||
so.action_confirm()
|
||||
|
||||
visit.task_id.action_fsm_validate()
|
||||
|
||||
self.assertEqual(len(so.order_line), 3)
|
||||
|
||||
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 = "SVR12345-1 - Test Company - Test Label"
|
||||
self.assertEqual(task.name, supposed_name)
|
||||
|
||||
def test_subtasks_inherit_partner_from_parent_task(self):
|
||||
"""Test that subtasks of tasks created from FSM sales orders have their partner_id set correctly."""
|
||||
# Create a sale order with a shipping address different from the billing address
|
||||
partner = self._generate_partner(name="Customer")
|
||||
shipping_partner = self.env['res.partner'].create({
|
||||
'name': 'Shipping Address',
|
||||
'parent_id': partner.id,
|
||||
'type': 'delivery',
|
||||
})
|
||||
|
||||
# Create a sale order with the customer and shipping address
|
||||
so = self._generate_sale_order(partner=partner)
|
||||
so.partner_shipping_id = shipping_partner
|
||||
|
||||
# Create a visit
|
||||
visit = self._generate_visit(sale_order=so)
|
||||
|
||||
# Create a product with a task template that has subtasks
|
||||
parent_template = self._generate_task_template(
|
||||
structure=[1],
|
||||
names=["Parent Task", "Child Task"],
|
||||
)
|
||||
product = self._generate_product(task_template=parent_template)
|
||||
|
||||
# Add the product to the sale order
|
||||
sol = self._generate_sale_order_line(sale_order=so, product=product)
|
||||
|
||||
# Set the sequence to ensure proper ordering
|
||||
visit.so_section_id.sequence = 1
|
||||
sol.sequence = 2
|
||||
|
||||
# Confirm the sale order to create tasks
|
||||
so.action_confirm()
|
||||
|
||||
# Get the created tasks
|
||||
parent_task = sol.task_id
|
||||
self.assertTrue(parent_task, "Parent task should be created")
|
||||
|
||||
# Check that the parent task has a partner_id set
|
||||
self.assertTrue(parent_task.partner_id, "Parent task should have a partner set")
|
||||
|
||||
# Check that the subtask exists
|
||||
self.assertTrue(parent_task.child_ids, "Parent task should have subtasks")
|
||||
child_task = parent_task.child_ids[0]
|
||||
|
||||
# The key test: Check that the subtask has a partner_id set
|
||||
self.assertTrue(child_task.partner_id, "Subtask should have a partner_id set")
|
||||
|
||||
# Check that the subtask has the same partner as the parent task
|
||||
self.assertEqual(child_task.partner_id, parent_task.partner_id,
|
||||
"Subtask should have the same partner as its parent task")
|
||||
474
bemade_fsm/tests/test_sale_order.py
Normal file
474
bemade_fsm/tests/test_sale_order.py
Normal file
|
|
@ -0,0 +1,474 @@
|
|||
from .test_task_template import BemadeFSMBaseTest
|
||||
from odoo.tests.common import tagged, Form
|
||||
from odoo import Command
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class TestSalesOrder(BemadeFSMBaseTest):
|
||||
@tagged("-at_install", "post_install")
|
||||
def test_order_confirmation_simple_template(self):
|
||||
"""Confirming the order should create a task in the global project based on the
|
||||
task template."""
|
||||
partner = self._generate_partner()
|
||||
so = self._generate_sale_order(partner=partner)
|
||||
task_template = self._generate_task_template(planned_hours=8)
|
||||
product = self._generate_product(task_template=task_template)
|
||||
sol = self._generate_sale_order_line(so, product=product)
|
||||
|
||||
so.action_confirm()
|
||||
|
||||
task = sol.task_id
|
||||
self.assertTrue(task)
|
||||
self.assertTrue(task_template.name in task.name)
|
||||
self.assertTrue(task_template.planned_hours == task.allocated_hours)
|
||||
|
||||
def test_task_template_tree_order_confirmation(self):
|
||||
partner = self._generate_partner()
|
||||
so = self._generate_sale_order(partner=partner)
|
||||
parent_template = self._generate_task_template(
|
||||
structure=[2, 1],
|
||||
names=["Parent Template", "Child Template", "Grandchild Template"],
|
||||
)
|
||||
child_template_1 = parent_template.subtasks[0]
|
||||
child_template_2 = parent_template.subtasks[1]
|
||||
grandchild_template = parent_template.subtasks[0].subtasks[0]
|
||||
product = self._generate_product(task_template=parent_template)
|
||||
sol = self._generate_sale_order_line(so, product=product)
|
||||
|
||||
so.action_confirm()
|
||||
|
||||
parent = sol.task_id
|
||||
c1, c2 = parent.child_ids
|
||||
gc = c1.child_ids[:1]
|
||||
self.assertTrue(parent.child_ids and len(parent.child_ids) == 2)
|
||||
self.assertTrue(parent_template.name in parent.name)
|
||||
self.assertEqual(child_template_1.name, c1.name)
|
||||
self.assertEqual(child_template_2.name, c2.name)
|
||||
self.assertTrue(c1.child_ids and len(c1.child_ids) == 1)
|
||||
self.assertEqual(grandchild_template.name, gc.name)
|
||||
|
||||
def test_order_confirmation_single_equipment(self):
|
||||
"""The equipment selected on the SO should transfer to the task."""
|
||||
partner = self._generate_partner()
|
||||
equipment = self._generate_equipment(partner=partner)
|
||||
so = self._generate_sale_order(partner=partner, equipment=equipment)
|
||||
task_template = self._generate_task_template(planned_hours=8)
|
||||
product1 = self._generate_product(task_template=task_template)
|
||||
product2 = self._generate_product()
|
||||
sol1 = self._generate_sale_order_line(so, product=product1)
|
||||
sol2 = self._generate_sale_order_line(so, product=product2)
|
||||
|
||||
so.action_confirm()
|
||||
|
||||
task1 = sol1.task_id
|
||||
task2 = sol2.task_id
|
||||
self.assertEqual(task1.equipment_ids[0], equipment)
|
||||
self.assertEqual(task2.equipment_ids[0], equipment)
|
||||
|
||||
def test_order_confirmation_multiple_equipment(self):
|
||||
"""All equipment items should flow from the sale order line to the final task"""
|
||||
partner = self._generate_partner()
|
||||
for i in range(5):
|
||||
self._generate_equipment(partner=partner)
|
||||
sale_order = self._generate_sale_order(
|
||||
partner=partner
|
||||
) # No default equipment since more than 3 on partner
|
||||
sol1, sol2, sol3 = [
|
||||
self._generate_sale_order_line(sale_order=sale_order) for _ in range(3)
|
||||
]
|
||||
sol1.equipment_ids = [
|
||||
Command.set([partner.equipment_ids[i].id for i in range(2)])
|
||||
]
|
||||
sol3.equipment_ids = [
|
||||
Command.set([partner.equipment_ids[i].id for i in range(2, 5)])
|
||||
]
|
||||
|
||||
sale_order.action_confirm()
|
||||
|
||||
self.assertEqual(sol1.equipment_ids, sol1.task_id.equipment_ids)
|
||||
self.assertEqual(sol2.equipment_ids, sol2.task_id.equipment_ids)
|
||||
self.assertEqual(sol3.equipment_ids, sol3.task_id.equipment_ids)
|
||||
|
||||
def test_task_template_with_equipment_flow(self):
|
||||
"""The equipment selected on a task template should flow down to the task
|
||||
created on SO confirmation."""
|
||||
partner = self._generate_partner()
|
||||
equipment = self._generate_equipment(partner=partner)
|
||||
so = self._generate_sale_order(partner=partner)
|
||||
task_template = self._generate_task_template(equipment=equipment)
|
||||
product = self._generate_product(task_template=task_template)
|
||||
sol = self._generate_sale_order_line(so, product=product)
|
||||
|
||||
so.action_confirm()
|
||||
|
||||
self.assertEqual(sol.task_id.equipment_ids[0], equipment)
|
||||
|
||||
def test_sale_order_line_gets_default_equipment(self):
|
||||
"""Sale order lines created on a SO with default equipment set should inherit
|
||||
that default equipment."""
|
||||
partner = self._generate_partner()
|
||||
self._generate_equipment(partner=partner)
|
||||
sale_order = self._generate_sale_order(partner=partner)
|
||||
|
||||
sol = self._generate_sale_order_line(sale_order=sale_order)
|
||||
|
||||
self.assertEqual(sol.equipment_ids, partner.equipment_ids)
|
||||
|
||||
def test_sale_order_gets_correct_default_equipment_from_partner(self):
|
||||
"""Should pick up equipment from the partner."""
|
||||
partner = self._generate_partner()
|
||||
self._generate_equipment(partner=partner)
|
||||
|
||||
sale_order = self._generate_sale_order(partner=partner)
|
||||
|
||||
self.assertEqual(sale_order.default_equipment_ids, partner.owned_equipment_ids)
|
||||
|
||||
def test_sale_order_default_equipment_maximum_number(self):
|
||||
parent = self._generate_partner()
|
||||
child = self._generate_partner(parent=parent)
|
||||
for i in range(3):
|
||||
self._generate_equipment(partner=child)
|
||||
|
||||
sale_order = self._generate_sale_order(partner=parent)
|
||||
|
||||
self.assertEqual(sale_order.default_equipment_ids, parent.owned_equipment_ids)
|
||||
|
||||
def test_sale_order_no_default_equipment_with_more_than_three_owned_on_partner(
|
||||
self,
|
||||
):
|
||||
parent = self._generate_partner()
|
||||
child = self._generate_partner(parent=parent)
|
||||
for i in range(4):
|
||||
self._generate_equipment(partner=child)
|
||||
|
||||
sale_order = self._generate_sale_order(partner=parent)
|
||||
|
||||
self.assertEqual(sale_order.default_equipment_ids, self.env["fsm.equipment"])
|
||||
|
||||
def test_sale_order_resets_default_equipment_on_partner_change(self):
|
||||
partner_1 = self._generate_partner()
|
||||
partner_2 = self._generate_partner()
|
||||
self._generate_equipment(partner=partner_1)
|
||||
sale_order = self._generate_sale_order(partner_1)
|
||||
form = Form(sale_order)
|
||||
|
||||
form.partner_id = partner_2
|
||||
form.save()
|
||||
|
||||
self.assertFalse(sale_order.default_equipment_ids)
|
||||
|
||||
def test_sale_order_prioritize_shipping_location_equipments(self):
|
||||
parent = self._generate_partner()
|
||||
child = self._generate_partner(parent=parent, location_type="delivery")
|
||||
self._generate_equipment(partner=parent)
|
||||
self._generate_equipment(partner=child)
|
||||
|
||||
sale_order = self._generate_sale_order(partner=parent, shipping_location=child)
|
||||
|
||||
self.assertEqual(sale_order.default_equipment_ids, child.equipment_ids)
|
||||
|
||||
def test_default_equipment_transfers_to_sale_order_line(self):
|
||||
partner = self._generate_partner()
|
||||
for i in range(3):
|
||||
self._generate_equipment(partner=partner)
|
||||
sale_order = self._generate_sale_order(partner=partner)
|
||||
|
||||
for i in range(3):
|
||||
self._generate_sale_order_line(sale_order=sale_order)
|
||||
|
||||
for line in sale_order.order_line:
|
||||
self.assertEqual(line.equipment_ids, partner.equipment_ids)
|
||||
|
||||
def test_task_mark_done(self):
|
||||
"""Marking the task linked to a SO line should mark the line delivered.
|
||||
Marking sub-tasks done should not."""
|
||||
partner = self._generate_partner()
|
||||
so = self._generate_sale_order(partner=partner)
|
||||
task_template = self._generate_task_template(
|
||||
structure=[2], names=["Parent Task", "Subtask"]
|
||||
)
|
||||
product = self._generate_product(task_template=task_template)
|
||||
sol = self._generate_sale_order_line(so, product=product)
|
||||
so.action_confirm()
|
||||
parent_task = sol.task_id
|
||||
subtasks = parent_task._get_all_subtasks()
|
||||
|
||||
# Marking the subtasks done should not increment delivered quantity
|
||||
subtasks.action_fsm_validate(True)
|
||||
self.assertEqual(sol.qty_delivered, 0)
|
||||
|
||||
# Marking the top-level tasks done should set the delivered quantity to some
|
||||
# non-zero value based on the UOM
|
||||
parent_task.action_fsm_validate(True)
|
||||
self.assertTrue(sol.qty_delivered != 0)
|
||||
|
||||
def test_task_contacts_through_sale_order(self):
|
||||
"""Make sure the site contacts and work order contacts transfer correctly
|
||||
from the SO to the task."""
|
||||
|
||||
partner = self._generate_partner()
|
||||
contact1 = self._generate_partner("Site contact", "person", partner)
|
||||
contact2 = self._generate_partner("Work order contact", "person", partner)
|
||||
partner.write(
|
||||
{
|
||||
"site_contacts": [Command.set([contact1.id])],
|
||||
"work_order_contacts": [Command.set([contact2.id])],
|
||||
}
|
||||
)
|
||||
so = self._generate_sale_order(partner)
|
||||
product = self._generate_product()
|
||||
sol = self._generate_sale_order_line(sale_order=so, product=product)
|
||||
|
||||
so.action_confirm()
|
||||
|
||||
self.assertEqual(so.work_order_contacts, partner.work_order_contacts)
|
||||
self.assertEqual(so.site_contacts, partner.site_contacts)
|
||||
self.assertEqual(sol.task_id.work_order_contacts, partner.work_order_contacts)
|
||||
self.assertEqual(sol.task_id.site_contacts, partner.site_contacts)
|
||||
|
||||
def test_tasks_created_at_order_confirmation_have_no_assignees(self):
|
||||
so, visit, sol1, sol2 = self._generate_so_with_one_visit_two_lines()
|
||||
user = self._generate_project_user(name="User", login="login")
|
||||
|
||||
# We test as a specific user since testing as root may not produce the error
|
||||
so.with_user(user).action_confirm()
|
||||
|
||||
visit_task = visit.task_id
|
||||
subtask1 = visit_task.child_ids[0]
|
||||
subtask2 = visit_task.child_ids[1]
|
||||
self.assertFalse(visit_task.user_ids)
|
||||
self.assertFalse(subtask1.user_ids)
|
||||
self.assertFalse(subtask2.user_ids)
|
||||
|
||||
def test_long_line_name_overflows_to_task_description(self):
|
||||
so = self._generate_sale_order()
|
||||
product = self._generate_product()
|
||||
product.description_sale = (
|
||||
"This is a long product description.\n"
|
||||
"It even spans multiple lines.\n"
|
||||
"One could find this annoying in a task name."
|
||||
)
|
||||
|
||||
sol = self._generate_sale_order_line(sale_order=so, product=product)
|
||||
|
||||
so.action_confirm()
|
||||
task = sol.task_id
|
||||
|
||||
self.assertFalse("This is a long product description." in task.name)
|
||||
self.assertFalse("It even spans multiple lines." in task.name)
|
||||
self.assertFalse("One could find this annoying in a task name." in task.name)
|
||||
self.assertTrue("It even spans multiple lines." in task.description)
|
||||
self.assertTrue(
|
||||
"One could find this annoying in a task name." in task.description
|
||||
)
|
||||
|
||||
def test_subtask_templates_no_description_if_blank_on_template(self):
|
||||
so = self._generate_sale_order()
|
||||
template = self._generate_task_template(
|
||||
structure=[5], names=["Parent", "Child"]
|
||||
)
|
||||
template.description = ""
|
||||
template.subtasks[0].description = "Some fixed description"
|
||||
for t in template.subtasks[1:]:
|
||||
t.description = ""
|
||||
product = self._generate_product(task_template=template)
|
||||
sol = self._generate_sale_order_line(sale_order=so, product=product)
|
||||
|
||||
so.action_confirm()
|
||||
|
||||
task = sol.task_id
|
||||
self.assertEqual(
|
||||
task.child_ids[0].description, template.subtasks[0].description
|
||||
)
|
||||
for t in task.child_ids[1:]:
|
||||
self.assertFalse(t.description)
|
||||
|
||||
def test_duplicate_sale_order_duplicates_visits(self):
|
||||
"""Duplicated sales orders should have visits tied to their SO lines as in the
|
||||
original. The copied visits should not have approximate dates set, however."""
|
||||
so, visit, line1, line2 = self._generate_so_with_one_visit_two_lines()
|
||||
|
||||
so2 = so.copy()
|
||||
|
||||
self.assertTrue(so2.visit_ids)
|
||||
visit2 = so2.visit_ids[0]
|
||||
self.assertEqual(so2.order_line[0].visit_id, visit2)
|
||||
self.assertEqual(visit2.label, visit.label)
|
||||
self.assertFalse(visit2.approx_date)
|
||||
|
||||
def test_confirming_sale_order_creates_visit_if_none_created(self):
|
||||
so = self._generate_sale_order()
|
||||
so.company_id.create_default_fsm_visit = True
|
||||
product = self._generate_product()
|
||||
self._generate_sale_order_line(so, product)
|
||||
|
||||
so.action_confirm()
|
||||
|
||||
visit_line = so.order_line.sorted("sequence")[0]
|
||||
self.assertTrue(so.visit_ids)
|
||||
self.assertEqual(visit_line.visit_id, so.visit_ids)
|
||||
|
||||
def test_confirming_sale_order_with_visit_creates_no_new_lines(self):
|
||||
so = self._generate_sale_order()
|
||||
so.company_id.create_default_fsm_visit = True
|
||||
self._generate_product()
|
||||
self._generate_visit(so)
|
||||
|
||||
so.action_confirm()
|
||||
|
||||
self.assertEqual(len(so.visit_ids), 1)
|
||||
|
||||
def test_confirming_sale_order_creates_no_visit_if_setting_off(self):
|
||||
so = self._generate_sale_order()
|
||||
so.company_id.create_default_fsm_visit = False
|
||||
product = self._generate_product()
|
||||
self._generate_sale_order_line(so, product)
|
||||
|
||||
so.action_confirm()
|
||||
|
||||
self.assertFalse(so.visit_ids)
|
||||
|
||||
def test_changing_sale_order_customer_follows_to_tasks_and_subtasks(self):
|
||||
so = self._generate_sale_order()
|
||||
so = self._generate_sale_order()
|
||||
so.company_id.create_default_fsm_visit = False
|
||||
task_template = self._generate_task_template(
|
||||
parent=None,
|
||||
structure=[2, 2],
|
||||
names=["Parent", "Child", "Grandchild"],
|
||||
)
|
||||
product = self._generate_product(task_template=task_template)
|
||||
sol = self._generate_sale_order_line(so, product)
|
||||
|
||||
so.action_confirm()
|
||||
|
||||
parent_task = sol.task_id
|
||||
for task in parent_task._get_all_subtasks() | parent_task:
|
||||
self.assertEqual(so.partner_shipping_id, task.partner_id)
|
||||
|
||||
so.write(
|
||||
{
|
||||
"partner_shipping_id": self.env["res.partner"]
|
||||
.create(
|
||||
{
|
||||
"name": "New shipping address",
|
||||
"parent_id": so.partner_id.id,
|
||||
"type": "delivery",
|
||||
}
|
||||
)
|
||||
.id
|
||||
}
|
||||
)
|
||||
for task in parent_task._get_all_subtasks() | parent_task:
|
||||
self.assertEqual(
|
||||
so.partner_shipping_id,
|
||||
task.partner_id,
|
||||
f"{task.name} has a different partner than the SO",
|
||||
)
|
||||
|
||||
def test_task_hierarchy_maintained_after_cancel_reconfirm(self):
|
||||
"""Test that task hierarchy and project assignments are maintained when canceling
|
||||
and reconfirming a sale order with a templated FSM product."""
|
||||
self.env.user.groups_id |= self.env.ref(
|
||||
"account.group_delivery_invoice_address"
|
||||
)
|
||||
# Create a task template with subtasks
|
||||
parent_template = self._generate_task_template(
|
||||
structure=[2], # Two subtasks
|
||||
names=["Main Service", "Subtask"],
|
||||
planned_hours=8,
|
||||
)
|
||||
|
||||
# Create FSM product with template
|
||||
product = self._generate_product(task_template=parent_template)
|
||||
|
||||
# Create and confirm sale order
|
||||
partner = self._generate_partner()
|
||||
partner_2 = self._generate_partner(parent=partner, company_type="person")
|
||||
self.assertEqual(partner_2.commercial_partner_id, partner)
|
||||
so = self._generate_sale_order(partner=partner)
|
||||
sol = self._generate_sale_order_line(so, product=product)
|
||||
so.action_confirm()
|
||||
|
||||
# Get initial tasks and verify setup
|
||||
main_task = sol.task_id
|
||||
self.assertTrue(main_task, "Main task should be created")
|
||||
self.assertTrue(main_task.project_id, "Main task should have a project")
|
||||
|
||||
subtasks = main_task.child_ids
|
||||
self.assertEqual(len(subtasks), 2, "Should have created 2 subtasks")
|
||||
self.assertEqual(so.tasks_count, 1, "Should have only 1 task on confirmation")
|
||||
|
||||
# Verify initial task hierarchy
|
||||
initial_project = main_task.project_id
|
||||
for subtask in subtasks:
|
||||
self.assertEqual(
|
||||
subtask.project_id,
|
||||
initial_project,
|
||||
"Subtask should have same project as main task",
|
||||
)
|
||||
self.assertFalse(
|
||||
subtask.sale_order_id, "Subtask should not be linked to sale order"
|
||||
)
|
||||
self.assertFalse(
|
||||
subtask.sale_line_id, "Subtask should not be linked to sale order line"
|
||||
)
|
||||
|
||||
# Store initial names for comparison
|
||||
initial_subtask_names = subtasks.mapped("name")
|
||||
|
||||
original_task_names = (main_task | main_task._get_all_subtasks()).mapped("name")
|
||||
# Cancel and reconfirm the sale order
|
||||
so.with_context(disable_cancel_warning=True).action_cancel()
|
||||
|
||||
so.action_draft()
|
||||
so.write({"partner_shipping_id": partner_2.id})
|
||||
# Get new tasks
|
||||
new_main_task = sol.task_id
|
||||
self.assertEqual(
|
||||
new_main_task, main_task, "New main task should be same as old"
|
||||
)
|
||||
|
||||
new_subtasks = new_main_task.child_ids
|
||||
new_subtasks._compute_sale_line()
|
||||
self.assertEqual(
|
||||
len(new_subtasks), 2, "Should still have 2 subtasks after reconfirmation"
|
||||
)
|
||||
self.assertFalse(
|
||||
new_subtasks.sale_line_id,
|
||||
"Subtasks should not be linked to Sale Order Line",
|
||||
)
|
||||
new_task_names = (new_main_task | new_main_task._get_all_subtasks()).mapped(
|
||||
"name"
|
||||
)
|
||||
self.assertEqual(
|
||||
new_task_names, original_task_names, "New task names should be the same"
|
||||
)
|
||||
|
||||
# Verify task hierarchy is maintained
|
||||
self.assertEqual(
|
||||
new_main_task.project_id,
|
||||
initial_project,
|
||||
"New main task should have same project",
|
||||
)
|
||||
|
||||
for subtask in new_subtasks:
|
||||
self.assertEqual(
|
||||
subtask.project_id,
|
||||
initial_project,
|
||||
"New subtask should maintain same project as main task",
|
||||
)
|
||||
self.assertFalse(
|
||||
subtask.sale_order_id, "New subtask should not be linked to sale order"
|
||||
)
|
||||
self.assertFalse(
|
||||
subtask.sale_line_id,
|
||||
"New subtask should not be linked to sale order line",
|
||||
)
|
||||
|
||||
# Verify subtask names are maintained
|
||||
self.assertEqual(
|
||||
sorted(new_subtasks.mapped("name")),
|
||||
sorted(initial_subtask_names),
|
||||
"Subtask names should be maintained after reconfirmation",
|
||||
)
|
||||
48
bemade_fsm/tests/test_settings.py
Normal file
48
bemade_fsm/tests/test_settings.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
from odoo.tests import TransactionCase, Form, tagged
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class TestSettings(TransactionCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.test_partner_co = cls.env["res.partner"].create(
|
||||
{
|
||||
"name": "Test Co",
|
||||
}
|
||||
)
|
||||
cls.test_co = cls.env["res.company"].create(
|
||||
{
|
||||
"name": "Test Co",
|
||||
"country_id": cls.env.ref("base.ca").id,
|
||||
}
|
||||
)
|
||||
cls.env.user.company_id = cls.test_co
|
||||
|
||||
def test_enabling_separate_time_on_work_orders(self):
|
||||
wizard = self.env["res.config.settings"].create({})
|
||||
self.assertFalse(self.test_co.split_time_from_materials_on_service_work_orders)
|
||||
with Form(wizard) as form:
|
||||
form.separate_time_on_work_orders = True
|
||||
self.assertTrue(self.test_co.split_time_from_materials_on_service_work_orders)
|
||||
|
||||
def test_disabling_separate_time_on_work_orders(self):
|
||||
wizard = self.env["res.config.settings"].create({})
|
||||
self.test_co.split_time_from_materials_on_service_work_orders = True
|
||||
with Form(wizard) as form:
|
||||
form.separate_time_on_work_orders = False
|
||||
self.assertFalse(self.test_co.split_time_from_materials_on_service_work_orders)
|
||||
|
||||
def test_enabling_create_default_fsm_visit(self):
|
||||
wizard = self.env["res.config.settings"].create({})
|
||||
self.test_co.create_default_fsm_visit = False
|
||||
with Form(wizard) as form:
|
||||
form.create_default_fsm_visit = True
|
||||
self.assertTrue(self.test_co.create_default_fsm_visit)
|
||||
|
||||
def test_disabling_create_default_fsm_visit(self):
|
||||
wizard = self.env["res.config.settings"].create({})
|
||||
self.test_co.create_default_fsm_visit = True
|
||||
with Form(wizard) as form:
|
||||
form.create_default_fsm_visit = False
|
||||
self.assertFalse(self.test_co.create_default_fsm_visit)
|
||||
154
bemade_fsm/tests/test_task.py
Normal file
154
bemade_fsm/tests/test_task.py
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
from .test_bemade_fsm_common import BemadeFSMBaseTest
|
||||
from odoo.tests.common import tagged, Form
|
||||
from odoo import Command
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TaskTest(BemadeFSMBaseTest):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
# Chose to set up all tests the same way since this code was becoming very
|
||||
# redundant
|
||||
super().setUpClass()
|
||||
cls.user = cls._generate_project_manager_user("Bob", "Bob")
|
||||
|
||||
def _generate_so_with_multilevel_task_template(self):
|
||||
so = self._generate_sale_order()
|
||||
template = self._generate_task_template(
|
||||
names=["Parent", "Child", "Grandchild"], structure=[2, 1]
|
||||
)
|
||||
product = self._generate_product(task_template=template)
|
||||
sol = self._generate_sale_order_line(sale_order=so, product=product)
|
||||
return so, sol
|
||||
|
||||
def test_reassigning_assignment_propagating_task_changes_subtasks(self):
|
||||
so, sol = self._generate_so_with_multilevel_task_template()
|
||||
so.action_confirm()
|
||||
task = sol.task_id
|
||||
|
||||
task.propagate_assignment = True
|
||||
task.write(
|
||||
{
|
||||
"user_ids": [Command.set([self.user.id])],
|
||||
"propagate_assignment": True,
|
||||
}
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
all([t.user_ids == self.user for t in task | task._get_all_subtasks()])
|
||||
)
|
||||
|
||||
def test_reassigning_task_doesnt_propagate_by_default(self):
|
||||
so, sol = self._generate_so_with_multilevel_task_template()
|
||||
so.action_confirm()
|
||||
task = sol.task_id
|
||||
|
||||
task.write(
|
||||
{
|
||||
"user_ids": [Command.set([self.user.id])],
|
||||
}
|
||||
)
|
||||
|
||||
self.assertFalse(any([t.user_ids for t in task.child_ids.child_ids]))
|
||||
|
||||
def test_unset_propagate_assignment_unsets_for_all_children(self):
|
||||
so, sol = self._generate_so_with_multilevel_task_template()
|
||||
so.action_confirm()
|
||||
task = sol.task_id
|
||||
# First, set propagation and assign
|
||||
task.propagate_assignment = True
|
||||
task.write({"user_ids": [Command.set([self.user.id])]})
|
||||
# Then, unset propagation for the children and re-set assignment
|
||||
task.child_ids.write({"propagate_assignment": False})
|
||||
self.assertFalse(
|
||||
any([t.propagate_assignment for t in task._get_all_subtasks()])
|
||||
)
|
||||
# Then, test that assigning the parent only assigns its children, not its
|
||||
# grandchildren
|
||||
task.write({"user_ids": [Command.set([])]})
|
||||
self.assertTrue(all([not t.user_ids for t in task | task.child_ids]))
|
||||
self.assertTrue(
|
||||
all([t.user_ids == self.user for t in task.child_ids.child_ids])
|
||||
)
|
||||
|
||||
def test_task_gets_work_order_contacts_from_sale_order(self):
|
||||
so, sol = self._generate_so_with_multilevel_task_template()
|
||||
work_order_contacts = self._generate_partner(
|
||||
parent=so.partner_id
|
||||
) | self._generate_partner(parent=so.partner_id)
|
||||
so.write({"work_order_contacts": [(6, 0, work_order_contacts.ids)]})
|
||||
|
||||
so.action_confirm()
|
||||
task = sol.task_id
|
||||
|
||||
self.assertEqual(task.work_order_contacts, so.work_order_contacts)
|
||||
# Just a safeguard to make sure we set it properly on the SO
|
||||
self.assertEqual(len(task.work_order_contacts), 2)
|
||||
# Make sure all subtasks got the same
|
||||
for subtask in task._get_all_subtasks():
|
||||
self.assertEqual(subtask.work_order_contacts, so.work_order_contacts)
|
||||
|
||||
def test_task_gets_site_contacts_from_sale_order(self):
|
||||
so, sol = self._generate_so_with_multilevel_task_template()
|
||||
site_contacts = self._generate_partner(
|
||||
parent=so.partner_id
|
||||
) | self._generate_partner(parent=so.partner_id)
|
||||
so.write({"site_contacts": [(6, 0, site_contacts.ids)]})
|
||||
|
||||
so.action_confirm()
|
||||
task = sol.task_id
|
||||
|
||||
self.assertEqual(task.site_contacts, so.site_contacts)
|
||||
# Just a safeguard to make sure we set it properly on the SO
|
||||
self.assertEqual(len(task.site_contacts), 2)
|
||||
# Make sure all subtasks got the same
|
||||
for subtask in task._get_all_subtasks():
|
||||
self.assertEqual(subtask.site_contacts, so.site_contacts)
|
||||
|
||||
def test_task_gets_work_order_contacts_from_parent(self):
|
||||
so, sol = self._generate_so_with_multilevel_task_template()
|
||||
work_order_contacts = self._generate_partner(
|
||||
parent=so.partner_id
|
||||
) | self._generate_partner(parent=so.partner_id)
|
||||
so.write({"work_order_contacts": [(6, 0, work_order_contacts.ids)]})
|
||||
|
||||
so.action_confirm()
|
||||
task = sol.task_id
|
||||
task.write(
|
||||
{
|
||||
"work_order_contacts": [
|
||||
Command.link(self._generate_partner(parent=so.partner_id).id)
|
||||
]
|
||||
}
|
||||
)
|
||||
for subtask in task._get_all_subtasks():
|
||||
self.assertEqual(subtask.work_order_contacts, task.work_order_contacts)
|
||||
with Form(task) as task_form:
|
||||
with task_form.child_ids.new() as subtask:
|
||||
subtask.name = "Subtask 1"
|
||||
subtask = task.child_ids[-1]
|
||||
self.assertEqual(subtask.work_order_contacts, task.work_order_contacts)
|
||||
|
||||
def test_task_gets_site_contacts_from_parent(self):
|
||||
so, sol = self._generate_so_with_multilevel_task_template()
|
||||
site_contacts = self._generate_partner(
|
||||
parent=so.partner_id
|
||||
) | self._generate_partner(parent=so.partner_id)
|
||||
so.write({"site_contacts": [(6, 0, site_contacts.ids)]})
|
||||
|
||||
so.action_confirm()
|
||||
task = sol.task_id
|
||||
task.write(
|
||||
{
|
||||
"site_contacts": [
|
||||
Command.link(self._generate_partner(parent=so.partner_id).id)
|
||||
]
|
||||
}
|
||||
)
|
||||
for subtask in task._get_all_subtasks():
|
||||
self.assertEqual(subtask.site_contacts, task.site_contacts)
|
||||
with Form(task) as task_form:
|
||||
with task_form.child_ids.new() as subtask:
|
||||
subtask.name = "Subtask 1"
|
||||
subtask = task.child_ids[-1]
|
||||
self.assertEqual(subtask.site_contacts, task.site_contacts)
|
||||
62
bemade_fsm/tests/test_task_report.py
Normal file
62
bemade_fsm/tests/test_task_report.py
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
from .test_bemade_fsm_common import BemadeFSMBaseTest
|
||||
from odoo.tests import Form
|
||||
|
||||
|
||||
class TestTaskReport(BemadeFSMBaseTest):
|
||||
def test_split_time_materials_setting(self):
|
||||
with Form(self.env["res.config.settings"]) as settings:
|
||||
settings.separate_time_on_work_orders = True
|
||||
|
||||
with Form(self.env["res.config.settings"]):
|
||||
self.assertTrue(settings.separate_time_on_work_orders)
|
||||
|
||||
so = self._generate_sale_order()
|
||||
service_product = self._generate_product()
|
||||
material_product = self._generate_product(
|
||||
name="Material Product",
|
||||
product_type="consu",
|
||||
service_tracking="no",
|
||||
)
|
||||
visit = self._generate_visit(sale_order=so)
|
||||
self._generate_sale_order_line(sale_order=so, product=service_product)
|
||||
self._generate_sale_order_line(sale_order=so, product=material_product)
|
||||
so.action_confirm()
|
||||
task = visit.task_id
|
||||
|
||||
# Add timesheet entry to make the report renderable
|
||||
self.env["account.analytic.line"].create(
|
||||
{
|
||||
"name": "Test timesheet entry",
|
||||
"task_id": task.id,
|
||||
"project_id": task.project_id.id,
|
||||
"unit_amount": 2.0,
|
||||
"employee_id": self.env["hr.employee"]
|
||||
.create(
|
||||
{
|
||||
"name": "Test Employee",
|
||||
"user_id": self.env.user.id,
|
||||
}
|
||||
)
|
||||
.id,
|
||||
}
|
||||
)
|
||||
|
||||
html_content = (
|
||||
self.env["ir.actions.report"]
|
||||
._render(
|
||||
"industry_fsm.worksheet_custom", [task.id]
|
||||
)[ # pyright: ignore[reportOptionalSubscript]
|
||||
0
|
||||
]
|
||||
.decode("utf-8")
|
||||
.split("\n")
|
||||
)
|
||||
|
||||
strings_to_find = ["<h2>Materials</h2>", "<span>Material Product</span>"]
|
||||
|
||||
for line in strings_to_find:
|
||||
line_found = False
|
||||
for html_line in html_content:
|
||||
if line in html_line:
|
||||
line_found = True
|
||||
self.assertTrue(line_found, f"{line} should be in file.")
|
||||
80
bemade_fsm/tests/test_task_template.py
Normal file
80
bemade_fsm/tests/test_task_template.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
from .test_bemade_fsm_common import BemadeFSMBaseTest
|
||||
from odoo.tests.common import tagged, Form
|
||||
from odoo.exceptions import MissingError
|
||||
from odoo.tools import mute_logger
|
||||
import psycopg2
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class TestTaskTemplate(BemadeFSMBaseTest):
|
||||
def test_delete_task_template(self):
|
||||
"""User should never be able to delete a task template used on a product"""
|
||||
task_template = self._generate_task_template(names=["Template 1"])
|
||||
self._generate_product(name="Test Product 1", task_template=task_template)
|
||||
with self.assertRaises(psycopg2.errors.ForeignKeyViolation):
|
||||
with mute_logger("odoo.sql_db"):
|
||||
task_template.unlink()
|
||||
|
||||
def test_delete_subtask_template(self):
|
||||
"""Deletion of a child task should be OK even if the parent is on a product.
|
||||
Children of the deleted subtask should be deleted."""
|
||||
parent_task = self._generate_task_template(
|
||||
structure=[2, 1],
|
||||
names=["Parent Template", "Child Template", "Grandchild Template"],
|
||||
)
|
||||
grandchild_task = parent_task.subtasks[0].subtasks[0]
|
||||
|
||||
parent_task.subtasks[0].unlink()
|
||||
|
||||
# Reading deleted child's name field should be impossible
|
||||
with self.assertRaises(MissingError):
|
||||
_ = grandchild_task.name
|
||||
|
||||
def test_dissociating_customer_resets_equipment_appropriately(self):
|
||||
partner1 = self._generate_partner()
|
||||
partner2 = self._generate_partner()
|
||||
equipment1 = self._generate_equipment(partner=partner1)
|
||||
task = self._generate_task_template(customer=partner1, equipment=equipment1)
|
||||
form = Form(task)
|
||||
|
||||
# Switching the partner should trigger on_change that makes sure equipments are
|
||||
# linked to the new partner
|
||||
form.customer = partner2
|
||||
form.save()
|
||||
|
||||
self.assertFalse(equipment1 in task.equipment_ids)
|
||||
|
||||
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"])
|
||||
product = self._generate_product(task_template=template)
|
||||
sol1.name = "Short Name 1"
|
||||
sol2.name = "Short Name 2"
|
||||
sol3 = self._generate_sale_order_line(sale_order=so, product=product)
|
||||
|
||||
so.action_confirm()
|
||||
|
||||
self.assertEqual(sol1.task_id.name, "Short Name 1")
|
||||
self.assertEqual(sol2.task_id.name, "Short Name 2")
|
||||
self.assertEqual(sol3.task_id.name, "Task")
|
||||
|
||||
def test_task_creation_directly_from_template(self):
|
||||
project = self.env.ref("industry_fsm.fsm_project")
|
||||
template = self._generate_task_template(
|
||||
names=["Task", "Child", "Grandchild"], structure=[2, 1]
|
||||
)
|
||||
|
||||
task = template.create_task_from_self(project, "My new task")
|
||||
|
||||
self.assertEqual(len(task.child_ids), len(template.subtasks))
|
||||
self.assertEqual(
|
||||
len(task.child_ids[0].child_ids), len(template.subtasks[0].subtasks)
|
||||
)
|
||||
self.assertEqual(
|
||||
len(task.child_ids[1].child_ids), len(template.subtasks[1].subtasks)
|
||||
)
|
||||
self.assertEqual(task.name, "My new task")
|
||||
self.assertEqual(task.child_ids[0].name, template.subtasks[0].name)
|
||||
self.assertTrue(
|
||||
all([t.project_id == project for t in task | task._get_all_subtasks()])
|
||||
)
|
||||
17
bemade_fsm/views/menus.xml
Normal file
17
bemade_fsm/views/menus.xml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<menuitem
|
||||
id="project_task_template_menu"
|
||||
name="Task Templates"
|
||||
parent="project.menu_main_pm"
|
||||
action="task_template_act_window"
|
||||
groups="project.group_project_manager,project.group_project_user"
|
||||
/>
|
||||
<menuitem
|
||||
id="service_task_template_meny"
|
||||
name="Task Templates"
|
||||
action="task_template_act_window"
|
||||
parent="industry_fsm.fsm_menu_root"
|
||||
groups="project.group_project_manager,industry_fsm.group_fsm_manager"
|
||||
/>
|
||||
</odoo>
|
||||
32
bemade_fsm/views/product_views.xml
Normal file
32
bemade_fsm/views/product_views.xml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="product_template_form_inherit" model="ir.ui.view">
|
||||
<field name="name">bemade_fsm.product.template.form</field>
|
||||
<field name="model">product.template</field>
|
||||
<field
|
||||
name="inherit_id"
|
||||
ref="sale_project.product_template_form_view_invoice_policy_inherit_sale_project"
|
||||
/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='project_id']" position="after">
|
||||
<field
|
||||
name="task_template_id"
|
||||
invisible="service_tracking not in ('task_global_project', 'task_in_project')"
|
||||
domain="[('parent', '=', False)]"
|
||||
/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- BV: Did comment that one cause can not match inherit_id and don't understand it use -->
|
||||
<!-- <record id="product_search_form_view_inherit_bemade_fsm" model="ir.ui.view">-->
|
||||
<!-- <field name="name">bemade_fsm.product_search_form_view_inherit_bemade_fsm</field>-->
|
||||
<!-- <field name="model">product.product</field>-->
|
||||
<!-- <field name="inherit_id" ref="industry_fsm_sale.product_search_form_view_inherit_fsm_sale"/>-->
|
||||
<!-- <field name="arch" type="xml">-->
|
||||
<!-- <xpath expr="//searchpanel//field[@name='categ_id']" position="attributes">-->
|
||||
<!-- <attribute name="limit">0</attribute>-->
|
||||
<!-- </xpath>-->
|
||||
<!-- </field>-->
|
||||
<!-- </record>-->
|
||||
</odoo>
|
||||
52
bemade_fsm/views/res_partner.xml
Normal file
52
bemade_fsm/views/res_partner.xml
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<?xml version="1.0" ?>
|
||||
<odoo>
|
||||
<record id="partner_equipment_location_view_form" model="ir.ui.view">
|
||||
<field name="name">partner.equipment.location.view.form</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field
|
||||
name="inherit_id"
|
||||
ref="fsm_equipment.partner_equipment_location_view_form"
|
||||
/>
|
||||
<field name="arch" type="xml">
|
||||
<page name="equipment" position="attributes">
|
||||
<attribute name="invisible">site_ids</attribute>
|
||||
</page>
|
||||
<page name="equipment" position="after">
|
||||
<page
|
||||
name="Service Contacts"
|
||||
invisible="site_ids">
|
||||
<field
|
||||
name="site_contacts"
|
||||
context="{'list_view_ref': 'bemade_fsm.fsm_contacts_view_list'}"
|
||||
/>
|
||||
<field
|
||||
name="work_order_contacts"
|
||||
context="{'list_view_ref': 'bemade_fsm.fsm_contacts_view_list'}"
|
||||
/>
|
||||
</page>
|
||||
<field name="is_service_site" invisible="True"/>
|
||||
<page name="Service Sites" invisible="is_service_site">
|
||||
<group>
|
||||
<field name="site_ids">
|
||||
<list editable="bottom">
|
||||
<field name="name" widget="res_partner_many2one" />
|
||||
</list>
|
||||
</field>
|
||||
</group>
|
||||
</page>
|
||||
</page>
|
||||
</field>
|
||||
</record>
|
||||
<record id="fsm_contacts_view_list" model="ir.ui.view">
|
||||
<field name="name">bemade_fsm.contacts.list</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="arch" type="xml">
|
||||
<list editable="bottom">
|
||||
<field name="name" />
|
||||
<field name="email" widget="email" />
|
||||
<field name="phone" widget="phone" />
|
||||
<field name="mobile" widget="phone" />
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
64
bemade_fsm/views/sale_order_views.xml
Normal file
64
bemade_fsm/views/sale_order_views.xml
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="sale_order_form_inherit" model="ir.ui.view">
|
||||
<field name="name">bemade_fsm.sale_order.form</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="inherit_id" ref="sale.view_order_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//page[@name='other_information']" position="before">
|
||||
<page name="field_service" string="Field Service">
|
||||
<group name="fsm_visits" string="Service Visits">
|
||||
<field
|
||||
name="visit_ids"
|
||||
context="{'list_view_ref': 'bemade_fsm.bemade_fsm_visit_list'}"
|
||||
/>
|
||||
</group>
|
||||
<group name="field_service_info" string="Contacts and Equipment">
|
||||
<field name="valid_equipment_ids" invisible="1" />
|
||||
<field
|
||||
name="summary_equipment_ids"
|
||||
context="{'default_partner_id': partner_shipping_id,}"
|
||||
widget="many2many_tags"
|
||||
groups="account.group_delivery_invoice_address"
|
||||
/>
|
||||
<field
|
||||
name="site_contacts"
|
||||
context="{'list_view_ref': 'bemade_fsm.fsm_contacts_view_list'}"
|
||||
/>
|
||||
<field
|
||||
name="work_order_contacts"
|
||||
context="{'list_view_ref': 'bemade_fsm.fsm_contacts_view_list'}"
|
||||
/>
|
||||
|
||||
<field
|
||||
name="default_equipment_ids"
|
||||
context="{'default_partner_id': partner_shipping_id,}"
|
||||
widget="many2many_tags"
|
||||
groups="account.group_delivery_invoice_address"
|
||||
/>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
<xpath expr="//list//field[@name='name']" position="after">
|
||||
<field name="valid_equipment_ids" invisible="1" />
|
||||
<field
|
||||
name="equipment_ids"
|
||||
widget="many2many_tags"
|
||||
domain="[('id', 'in', valid_equipment_ids)]"
|
||||
/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<record id="bemade_fsm_visit_list" model="ir.ui.view">
|
||||
<field name="name">bemade_fsm.visit.list</field>
|
||||
<field name="model">bemade_fsm.visit</field>
|
||||
<field name="arch" type="xml">
|
||||
<list editable="bottom">
|
||||
<field name="label" />
|
||||
<field name="approx_date" />
|
||||
<field name="is_completed" widget="boolean" />
|
||||
<field name="is_invoiced" widget="boolean" />
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
127
bemade_fsm/views/task_template_views.xml
Normal file
127
bemade_fsm/views/task_template_views.xml
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="task_template_form_view" model="ir.ui.view">
|
||||
<field name="name">bemade_fsm.task_template.form</field>
|
||||
<field name="model">project.task.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Task Template">
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<label for="name" />
|
||||
<h1>
|
||||
<field name="name" placeholder="Title" />
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="project" />
|
||||
<field name="assignees" widget="many2many_avatar_user" />
|
||||
<field name="parent" />
|
||||
<field name="planned_hours" />
|
||||
</group>
|
||||
<group>
|
||||
<field name="customer" />
|
||||
<field
|
||||
name="equipment_ids"
|
||||
domain="[('partner_id', '=', customer)]"
|
||||
context="{'list_view_ref': 'fsm_equipment.equipment_view_list'}"
|
||||
/>
|
||||
<field name="tags" widget="many2many_tags" />
|
||||
<field name="company_id" />
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page name="description_page" string="Description">
|
||||
<field
|
||||
name="description"
|
||||
type="html"
|
||||
options="{'collaborative': true}"
|
||||
/>
|
||||
</page>
|
||||
<page name="subtasks_page" string="Subtasks">
|
||||
<field name="subtasks">
|
||||
<list editable="bottom">
|
||||
<field name="sequence" widget="handle" />
|
||||
<field name="name" />
|
||||
<field name="customer" />
|
||||
<field
|
||||
name="assignees"
|
||||
widget="many2many_avatar_user"
|
||||
/>
|
||||
<button
|
||||
name="action_open_task"
|
||||
type="object"
|
||||
title="View Task"
|
||||
string="View Task"
|
||||
class="btn btn-link pull-right"
|
||||
/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="task_template_list_view" model="ir.ui.view">
|
||||
<field name="name">project.task_template.list</field>
|
||||
<field name="model">project.task.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<list>
|
||||
<field name="name" />
|
||||
<field name="assignees" widget="many2many_avatar_user" />
|
||||
<field name="project" />
|
||||
<field name="parent" />
|
||||
<field name="planned_hours" />
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="task_template_search_view" model="ir.ui.view">
|
||||
<field name="name">project.task_template.search</field>
|
||||
<field name="model">project.task.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Task Template">
|
||||
<field name="name" />
|
||||
<field name="project" />
|
||||
<field name="assignees" />
|
||||
<field name="parent" />
|
||||
<field name="subtasks" />
|
||||
<field name="planned_hours" />
|
||||
<group expand="1" string="Group By">
|
||||
<filter
|
||||
string="Project"
|
||||
name="groupby_project"
|
||||
domain="[]"
|
||||
context="{'group_by':'project'}"
|
||||
/>
|
||||
<filter
|
||||
string="Parent Task"
|
||||
name="groupby_parent"
|
||||
domain="[]"
|
||||
context="{'group_by':'parent'}"
|
||||
/>
|
||||
<filter
|
||||
string="Customer"
|
||||
name="groupby_customer"
|
||||
domain="[]"
|
||||
context="{'group_by':'customer'}"
|
||||
/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="task_template_act_window" model="ir.actions.act_window">
|
||||
<field name="name">Task Template</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">project.task.template</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="oe_view_nocontent_create">
|
||||
There are no task templates, click above to create one.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
229
bemade_fsm/views/task_views.xml
Normal file
229
bemade_fsm/views/task_views.xml
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="bemade_fsm_project_task_form_inherit" model="ir.ui.view">
|
||||
<field name="name">bemade_fsm.project_task.form</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="inherit_id" ref="industry_fsm.view_task_form2_inherit" />
|
||||
<field name="priority" eval="8" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='name']/.." position="before">
|
||||
<h1 class="d-flex flex-row justify-content-between">
|
||||
<field
|
||||
name="work_order_number"
|
||||
invisible="work_order_number == False"
|
||||
/>
|
||||
</h1>
|
||||
</xpath>
|
||||
<xpath expr="//page[@name='extra_info']" position="after">
|
||||
<page string="Equipment and Contacts" name="equipment_contacts"
|
||||
translate="True">
|
||||
<group name="equipment_and_contacts">
|
||||
<field
|
||||
name="equipment_ids"
|
||||
domain="[('partner_id', '=', partner_id)]"
|
||||
context="{'list_view_ref': 'fsm_equipment.equipment_view_list'}"
|
||||
/>
|
||||
<field
|
||||
name="site_contacts"
|
||||
context="{'list_view_ref': 'bemade_fsm.fsm_contacts_view_list'}"
|
||||
/>
|
||||
<field
|
||||
name="work_order_contacts"
|
||||
context="{'list_view_ref': 'bemade_fsm.fsm_contacts_view_list'}"
|
||||
/>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
<button
|
||||
name="action_fsm_validate"
|
||||
class='btn-primary'
|
||||
position="attributes"
|
||||
>
|
||||
<attribute name="string">Mark as Delivered</attribute>
|
||||
<attribute name="groups">industry_fsm.group_fsm_manager</attribute>
|
||||
</button>
|
||||
<button
|
||||
name="action_fsm_validate"
|
||||
class='btn-secondary'
|
||||
position="attributes"
|
||||
>
|
||||
<attribute name="string">Mark as Delivered</attribute>
|
||||
<attribute name="groups">industry_fsm.group_fsm_manager</attribute>
|
||||
</button>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_task_form2_inherit" model="ir.ui.view">
|
||||
<field name="inherit_id" ref="project.view_task_form2" />
|
||||
<field name="model">project.task</field>
|
||||
<field name="name">bemade_fsm.project_task.form2</field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath
|
||||
expr="//field[@name='child_ids']/list//field[@name='name']"
|
||||
position="after"
|
||||
>
|
||||
<field name="description" string="Description/Comments" />
|
||||
</xpath>
|
||||
<field name="user_ids" position="after">
|
||||
<field name="propagate_assignment" />
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
<!-- Add parent_id = false to domain for My Tasks, All Tasks: To Schedule, All Tasks and To Invoice-->
|
||||
<record id="industry_fsm.project_task_action_fsm" model="ir.actions.act_window">
|
||||
<field name="context">{'search_default_is_parent_task': True}</field>
|
||||
</record>
|
||||
<record id="project_task_view_list_fsm_inherit" model="ir.ui.view">
|
||||
<field name="name">project.task.view.list.fsm.inherit</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="inherit_id" ref="industry_fsm.project_task_view_list_fsm" />
|
||||
<field name="arch" type="xml">
|
||||
<list position="attributes">
|
||||
<attribute name="js_class">project_list</attribute>
|
||||
</list>
|
||||
<field name="name" position="before">
|
||||
<field name="work_order_number" optional="show" />
|
||||
</field>
|
||||
<field name="company_id" position="attributes">
|
||||
<attribute name="optional">hide</attribute>
|
||||
</field>
|
||||
<field name="worksheet_template_id" position="attributes">
|
||||
<attribute name="optional">hide</attribute>
|
||||
</field>
|
||||
<field name="project_id" position="attributes">
|
||||
<attribute name="optional">hide</attribute>
|
||||
</field>
|
||||
<field name="progress" position="attributes">
|
||||
<attribute name="optional">hide</attribute>
|
||||
</field>
|
||||
<field name="activity_ids" position="attributes">
|
||||
<attribute name="optional">hide</attribute>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
<record id="industry_fsm.project_task_action_fsm_map" model="ir.actions.act_window">
|
||||
<field name="context">{'search_default_is_parent_task': True}</field>
|
||||
<field name="domain">[('is_fsm', '=', True),
|
||||
('project_id', '!=', False),
|
||||
('display_in_project', '=', True),
|
||||
('parent_id', '=', False)]
|
||||
</field>
|
||||
</record>
|
||||
<record
|
||||
id="industry_fsm.project_task_action_to_schedule_fsm"
|
||||
model="ir.actions.act_window"
|
||||
>
|
||||
<field name="context">{'search_default_is_parent_task': True}</field>
|
||||
<field name="domain">[('is_fsm', '=', True),
|
||||
('project_id', '!=', False),
|
||||
('display_in_project', '=', True),
|
||||
('parent_id', '=', False)]
|
||||
</field>
|
||||
</record>
|
||||
<record id="industry_fsm.project_task_action_all_fsm" model="ir.actions.act_window">
|
||||
<field name="context">{'search_default_is_parent_task': True}</field>
|
||||
<field name="domain">[('is_fsm', '=', True),
|
||||
('project_id', '!=', False),
|
||||
('display_in_project', '=', True),
|
||||
('parent_id', '=', False)]
|
||||
</field>
|
||||
</record>
|
||||
<record
|
||||
id="industry_fsm_sale.project_task_action_to_invoice_fsm"
|
||||
model="ir.actions.act_window"
|
||||
>
|
||||
<field name="context">{'search_default_is_parent_task': True}</field>
|
||||
<field name="domain">[('is_fsm', '=', True),
|
||||
('project_id', '!=', False),
|
||||
('display_in_project', '=', True),
|
||||
('parent_id', '=', False),
|
||||
('invoice_status', '=', 'to invoice')]
|
||||
</field>
|
||||
</record>
|
||||
<!-- Add parent_id = false to domain for planning actions as well -->
|
||||
<record
|
||||
id="industry_fsm.project_task_action_fsm_planning_groupby_user"
|
||||
model="ir.actions.act_window"
|
||||
>
|
||||
<field name="context">{'search_default_is_parent_task': True}</field>
|
||||
<field name="domain">[('is_fsm', '=', True),
|
||||
('project_id', '!=', False),
|
||||
('display_in_project', '=', True),
|
||||
('parent_id', '=', False)]
|
||||
</field>
|
||||
</record>
|
||||
<record
|
||||
id="industry_fsm.project_task_action_fsm_planning_groupby_project"
|
||||
model="ir.actions.act_window"
|
||||
>
|
||||
<field name="context">{'search_default_is_parent_task': True}</field>
|
||||
<field name="domain">[('is_fsm', '=', True),
|
||||
('project_id', '!=', False),
|
||||
('display_in_project', '=', True),
|
||||
('parent_id', '=', False)]
|
||||
</field>
|
||||
</record>
|
||||
<record
|
||||
id="industry_fsm_report.project_task_action_fsm_planning_groupby_worksheet"
|
||||
model="ir.actions.act_window"
|
||||
>
|
||||
<field name="context">{'search_default_is_parent_task': True}</field>
|
||||
<field name="domain">[('is_fsm', '=', True),
|
||||
('project_id', '!=', False),
|
||||
('display_in_project', '=', True),
|
||||
('parent_id', '=', False)]
|
||||
</field>
|
||||
</record>
|
||||
<!-- <record id="project_task_view_calendar_fsm" model="ir.ui.view">-->
|
||||
<!-- <field name="name">bemade_fsm.project_task_view_calendar_fsm</field>-->
|
||||
<!-- <field name="inherit_id" ref="industry_fsm.project_task_view_calendar_fsm"/>-->
|
||||
<!-- <field name="model">project.task</field>-->
|
||||
<!-- <field name="arch" type="xml">-->
|
||||
<!-- <field name="user_ids" position="attributes">-->
|
||||
<!-- <attribute name="filters">1</attribute>-->
|
||||
<!-- <attribute name="color">user_ids</attribute>-->
|
||||
<!-- </field>-->
|
||||
<!-- </field>-->
|
||||
<!-- </record>-->
|
||||
<record id="project_task_view_calendar_fsm_no_worksheet" model="ir.ui.view">
|
||||
<field name="name">bemade_fsm.project_task_view_calendar_no_worksheet</field>
|
||||
<field
|
||||
name="inherit_id"
|
||||
ref="industry_fsm_report.project_task_view_calendar_fsm_worksheet"
|
||||
/>
|
||||
<field name="model">project.task</field>
|
||||
<field name="priority" eval="100" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='worksheet_template_id']" position="replace" />
|
||||
<xpath expr="//calendar" position="attributes">
|
||||
<attribute name="color">user_ids</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<record id="project_task_view_search_fsm_inherit" model="ir.ui.view">
|
||||
<field name="name">project.task.view.search.fsm.inherit</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="inherit_id" ref="industry_fsm.project_task_view_search_fsm" />
|
||||
<field name="arch" type="xml">
|
||||
<filter name="schedule" position="attributes">
|
||||
<attribute name="domain">
|
||||
[
|
||||
'&',
|
||||
('fsm_done', '=', False),
|
||||
'|',
|
||||
('user_ids', '=', False),
|
||||
'&',
|
||||
('planned_date_start', '=', False),
|
||||
('date_deadline', '=', False),
|
||||
]
|
||||
</attribute>
|
||||
</filter>
|
||||
<filter name="my_tasks" position="after">
|
||||
<filter
|
||||
name="is_parent_task"
|
||||
string="Parent Task"
|
||||
domain="[('parent_id', '=', False)]"
|
||||
/>
|
||||
</filter>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
2
bemade_fsm/wizard/__init__.py
Normal file
2
bemade_fsm/wizard/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from . import new_task_from_template
|
||||
from . import res_config_settings
|
||||
57
bemade_fsm/wizard/new_task_from_template.py
Normal file
57
bemade_fsm/wizard/new_task_from_template.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
from odoo import models, fields
|
||||
|
||||
|
||||
class NewTaskFromTemplateWizard(models.TransientModel):
|
||||
_name = "project.task.from.template.wizard"
|
||||
_description = "Create Task from Template Wizard"
|
||||
|
||||
project_id = fields.Many2one(
|
||||
comodel_name="project.project",
|
||||
string="Project",
|
||||
help="The project the new task should be created in.",
|
||||
required=True,
|
||||
)
|
||||
|
||||
task_template_id = fields.Many2one(
|
||||
comodel_name="project.task.template",
|
||||
string="Task Template",
|
||||
help="The template to use when creating the new task.",
|
||||
required=True,
|
||||
)
|
||||
|
||||
new_task_title = fields.Char(
|
||||
help=(
|
||||
"The title (name) for the newly created task. If left blank, the name of"
|
||||
" the template will be used."
|
||||
),
|
||||
)
|
||||
|
||||
def default_get(self, fields_list):
|
||||
res = super().default_get(fields_list)
|
||||
active_id = self.env.context.get("active_id", False)
|
||||
active_model = self.env.context.get("active_model", False)
|
||||
if not active_model:
|
||||
params = self.env.context.get("params", False)
|
||||
active_model = params and params.get("model", False)
|
||||
if (
|
||||
active_model == "project.task.template"
|
||||
and active_id
|
||||
and "task_template_id" in fields_list
|
||||
):
|
||||
res.update({"task_template_id": active_id})
|
||||
if active_model == "project.task" and "project_id" in fields_list:
|
||||
res.update({"project_id": self.env.ref("industry_fsm.fsm_project").id})
|
||||
return res
|
||||
|
||||
def action_create_task_from_template(self):
|
||||
self.ensure_one()
|
||||
task = self.task_template_id.create_task_from_self(
|
||||
self.project_id, self.new_task_title
|
||||
)
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "project.task",
|
||||
"res_id": task.id,
|
||||
"view_mode": "form",
|
||||
"target": "current",
|
||||
}
|
||||
29
bemade_fsm/wizard/new_task_from_template.xml
Normal file
29
bemade_fsm/wizard/new_task_from_template.xml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="project_task_from_template_wizard_view_form" model="ir.ui.view">
|
||||
<field name="name">project.task.from.template.wizard.view.form</field>
|
||||
<field name="model">project.task.from.template.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="task_template_id"/>
|
||||
<field name="new_task_title"/>
|
||||
<field name="project_id"/>
|
||||
</group>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button class='btn btn-primary' name="action_create_task_from_template" type="object" string="Create Task"/>
|
||||
<button class='btn btn-secondary' special="cancel" string="Cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record model="ir.actions.act_window" id="action_new_task_from_template">
|
||||
<field name="name">New Task from Template</field>
|
||||
<field name="res_model">project.task.from.template.wizard</field>
|
||||
<field name="binding_model_id" ref="bemade_fsm.model_project_task_template"/>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
</odoo>
|
||||
22
bemade_fsm/wizard/res_config_settings.py
Normal file
22
bemade_fsm/wizard/res_config_settings.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
from odoo import models, fields
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = "res.config.settings"
|
||||
|
||||
company_id = fields.Many2one(
|
||||
comodel_name="res.company",
|
||||
default=lambda self: self.env.company or self.env.user.company_id,
|
||||
)
|
||||
|
||||
separate_time_on_work_orders = fields.Boolean(
|
||||
string="Separate Time from Materials on Work Order",
|
||||
related="company_id.split_time_from_materials_on_service_work_orders",
|
||||
readonly=False,
|
||||
)
|
||||
|
||||
create_default_fsm_visit = fields.Boolean(
|
||||
string="Create Default Visit for FSM Sales Orders",
|
||||
related="company_id.create_default_fsm_visit",
|
||||
readonly=False,
|
||||
)
|
||||
24
bemade_fsm/wizard/res_config_settings.xml
Normal file
24
bemade_fsm/wizard/res_config_settings.xml
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="res_config_settings_view_form" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.inherit.bemade.fsm</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="industry_fsm.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<setting name="module_industry_fsm_report" position="after">
|
||||
<setting
|
||||
name="separate_time_on_work_orders"
|
||||
help="Separate blocks for materials and time on work order reports."
|
||||
company_dependent="1">
|
||||
<field name="separate_time_on_work_orders"/>
|
||||
</setting>
|
||||
<setting
|
||||
name="create_default_fsm_visit"
|
||||
help="Create a default FSM visit if a service SO doesn't already contain one at confirmation."
|
||||
company_dependent="1">
|
||||
<field name="create_default_fsm_visit"/>
|
||||
</setting>
|
||||
</setting>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Loading…
Reference in a new issue