bemade_fsm: code style cleanup

This commit is contained in:
Marc Durepos 2024-06-11 21:26:26 -04:00
parent 94ec52ff99
commit 3d8d648fb7
38 changed files with 2474 additions and 1622 deletions

188
.eslintrc.yml Normal file
View file

@ -0,0 +1,188 @@
env:
browser: true
es6: true
# See https://github.com/OCA/odoo-community.org/issues/37#issuecomment-470686449
parserOptions:
ecmaVersion: 2019
overrides:
- files:
- "**/*.esm.js"
parserOptions:
sourceType: module
# Globals available in Odoo that shouldn't produce errorings
globals:
_: readonly
$: readonly
fuzzy: readonly
jQuery: readonly
moment: readonly
odoo: readonly
openerp: readonly
owl: readonly
luxon: readonly
# Styling is handled by Prettier, so we only need to enable AST rules;
# see https://github.com/OCA/maintainer-quality-tools/pull/618#issuecomment-558576890
rules:
accessor-pairs: warn
array-callback-return: warn
callback-return: warn
capitalized-comments:
- warn
- always
- ignoreConsecutiveComments: true
ignoreInlineComments: true
complexity:
- warn
- 15
constructor-super: warn
dot-notation: warn
eqeqeq: warn
global-require: warn
handle-callback-err: warn
id-blacklist: warn
id-match: warn
init-declarations: error
max-depth: warn
max-nested-callbacks: warn
max-statements-per-line: warn
no-alert: warn
no-array-constructor: warn
no-caller: warn
no-case-declarations: warn
no-class-assign: warn
no-cond-assign: error
no-const-assign: error
no-constant-condition: warn
no-control-regex: warn
no-debugger: error
no-delete-var: warn
no-div-regex: warn
no-dupe-args: error
no-dupe-class-members: error
no-dupe-keys: error
no-duplicate-case: error
no-duplicate-imports: error
no-else-return: warn
no-empty-character-class: warn
no-empty-function: error
no-empty-pattern: error
no-empty: warn
no-eq-null: error
no-eval: error
no-ex-assign: error
no-extend-native: warn
no-extra-bind: warn
no-extra-boolean-cast: warn
no-extra-label: warn
no-fallthrough: warn
no-func-assign: error
no-global-assign: error
no-implicit-coercion:
- warn
- allow: ["~"]
no-implicit-globals: warn
no-implied-eval: warn
no-inline-comments: warn
no-inner-declarations: warn
no-invalid-regexp: warn
no-irregular-whitespace: warn
no-iterator: warn
no-label-var: warn
no-labels: warn
no-lone-blocks: warn
no-lonely-if: error
no-mixed-requires: error
no-multi-str: warn
no-native-reassign: error
no-negated-condition: warn
no-negated-in-lhs: error
no-new-func: warn
no-new-object: warn
no-new-require: warn
no-new-symbol: warn
no-new-wrappers: warn
no-new: warn
no-obj-calls: warn
no-octal-escape: warn
no-octal: warn
no-param-reassign: warn
no-path-concat: warn
no-process-env: warn
no-process-exit: warn
no-proto: warn
no-prototype-builtins: warn
no-redeclare: warn
no-regex-spaces: warn
no-restricted-globals: warn
no-restricted-imports: warn
no-restricted-modules: warn
no-restricted-syntax: warn
no-return-assign: error
no-script-url: warn
no-self-assign: warn
no-self-compare: warn
no-sequences: warn
no-shadow-restricted-names: warn
no-shadow: warn
no-sparse-arrays: warn
no-sync: warn
no-this-before-super: warn
no-throw-literal: warn
no-undef-init: warn
no-undef: error
no-unmodified-loop-condition: warn
no-unneeded-ternary: error
no-unreachable: error
no-unsafe-finally: error
no-unused-expressions: error
no-unused-labels: error
no-unused-vars: error
no-use-before-define: error
no-useless-call: warn
no-useless-computed-key: warn
no-useless-concat: warn
no-useless-constructor: warn
no-useless-escape: warn
no-useless-rename: warn
no-void: warn
no-with: warn
operator-assignment: [error, always]
prefer-const: warn
radix: warn
require-yield: warn
sort-imports: warn
spaced-comment: [error, always]
strict: [error, function]
use-isnan: error
valid-jsdoc:
- warn
- prefer:
arg: param
argument: param
augments: extends
constructor: class
exception: throws
func: function
method: function
prop: property
return: returns
virtual: abstract
yield: yields
preferType:
array: Array
bool: Boolean
boolean: Boolean
number: Number
object: Object
str: String
string: String
requireParamDescription: false
requireReturn: false
requireReturnDescription: false
requireReturnType: false
valid-typeof: warn
yoda: warn

View file

@ -89,7 +89,7 @@ repos:
args: [--fix, --exit-non-zero-on-fix]
- id: ruff-format
- repo: https://github.com/OCA/pylint-odoo
rev: v8.0.19
rev: v9.1.2
hooks:
- id: pylint_odoo
name: pylint with optional checks

110
.pylintrc Normal file
View file

@ -0,0 +1,110 @@
[MASTER]
load-plugins=pylint_odoo
score=n
[MESSAGES CONTROL]
disable=all
# This .pylintrc contains optional AND mandatory checks and is meant to be
# loaded in an IDE to have it check everything, in the hope this will make
# optional checks more visible to contributors who otherwise never look at a
# green travis to see optional checks that failed.
# .pylintrc-mandatory containing only mandatory checks is used the pre-commit
# config as a blocking check.
enable=anomalous-backslash-in-string,
api-one-deprecated,
api-one-multi-together,
assignment-from-none,
attribute-deprecated,
class-camelcase,
dangerous-default-value,
dangerous-view-replace-wo-priority,
development-status-allowed,
duplicate-id-csv,
duplicate-key,
duplicate-xml-fields,
duplicate-xml-record-id,
eval-referenced,
eval-used,
incoherent-interpreter-exec-perm,
license-allowed,
manifest-author-string,
manifest-deprecated-key,
manifest-required-key,
manifest-version-format,
method-compute,
method-inverse,
method-required-super,
method-search,
openerp-exception-warning,
pointless-statement,
pointless-string-statement,
print-used,
redundant-keyword-arg,
redundant-modulename-xml,
reimported,
relative-import,
return-in-init,
rst-syntax-error,
sql-injection,
too-few-format-args,
translation-field,
translation-required,
unreachable,
use-vim-comment,
wrong-tabs-instead-of-spaces,
xml-syntax-error,
attribute-string-redundant,
character-not-valid-in-resource-link,
consider-merging-classes-inherited,
context-overridden,
create-user-wo-reset-password,
dangerous-filter-wo-user,
dangerous-qweb-replace-wo-priority,
deprecated-data-xml-node,
deprecated-openerp-xml-node,
duplicate-po-message-definition,
except-pass,
file-not-used,
invalid-commit,
manifest-maintainers-list,
missing-newline-extrafiles,
missing-return,
odoo-addons-relative-import,
old-api7-method-defined,
po-msgstr-variables,
po-syntax-error,
renamed-field-parameter,
resource-not-exist,
str-format-used,
test-folder-imported,
translation-contains-variable,
translation-positional-used,
unnecessary-utf8-coding-comment,
website-manifest-key-not-valid-uri,
xml-attribute-translatable,
xml-deprecated-qweb-directive,
xml-deprecated-tree-attribute,
external-request-timeout,
# messages that do not cause the lint step to fail
consider-merging-classes-inherited,
create-user-wo-reset-password,
dangerous-filter-wo-user,
deprecated-module,
file-not-used,
invalid-commit,
missing-manifest-dependency,
missing-newline-extrafiles,
no-utf8-coding-comment,
odoo-addons-relative-import,
old-api7-method-defined,
redefined-builtin,
too-complex,
unnecessary-utf8-coding-comment
[REPORTS]
msg-template={path}:{line}: [{msg_id}({symbol}), {obj}] {msg}
output-format=colorized
reports=no

110
.pylintrc-mandatory Normal file
View file

@ -0,0 +1,110 @@
[MASTER]
load-plugins=pylint_odoo
score=n
[MESSAGES CONTROL]
disable=all
# This .pylintrc contains optional AND mandatory checks and is meant to be
# loaded in an IDE to have it check everything, in the hope this will make
# optional checks more visible to contributors who otherwise never look at a
# green travis to see optional checks that failed.
# .pylintrc-mandatory containing only mandatory checks is used the pre-commit
# config as a blocking check.
enable=anomalous-backslash-in-string,
api-one-deprecated,
api-one-multi-together,
assignment-from-none,
attribute-deprecated,
class-camelcase,
dangerous-default-value,
dangerous-view-replace-wo-priority,
development-status-allowed,
duplicate-id-csv,
duplicate-key,
duplicate-xml-fields,
duplicate-xml-record-id,
eval-referenced,
eval-used,
incoherent-interpreter-exec-perm,
license-allowed,
manifest-author-string,
manifest-deprecated-key,
manifest-required-key,
manifest-version-format,
method-compute,
method-inverse,
method-required-super,
method-search,
openerp-exception-warning,
pointless-statement,
pointless-string-statement,
print-used,
redundant-keyword-arg,
redundant-modulename-xml,
reimported,
relative-import,
return-in-init,
rst-syntax-error,
sql-injection,
too-few-format-args,
translation-field,
translation-required,
unreachable,
use-vim-comment,
wrong-tabs-instead-of-spaces,
xml-syntax-error,
attribute-string-redundant,
character-not-valid-in-resource-link,
consider-merging-classes-inherited,
context-overridden,
create-user-wo-reset-password,
dangerous-filter-wo-user,
dangerous-qweb-replace-wo-priority,
deprecated-data-xml-node,
deprecated-openerp-xml-node,
duplicate-po-message-definition,
except-pass,
file-not-used,
invalid-commit,
manifest-maintainers-list,
missing-newline-extrafiles,
missing-return,
odoo-addons-relative-import,
old-api7-method-defined,
po-msgstr-variables,
po-syntax-error,
renamed-field-parameter,
resource-not-exist,
str-format-used,
test-folder-imported,
translation-contains-variable,
translation-positional-used,
unnecessary-utf8-coding-comment,
website-manifest-key-not-valid-uri,
xml-attribute-translatable,
xml-deprecated-qweb-directive,
xml-deprecated-tree-attribute,
external-request-timeout,
# messages that do not cause the lint step to fail
consider-merging-classes-inherited,
create-user-wo-reset-password,
dangerous-filter-wo-user,
deprecated-module,
file-not-used,
invalid-commit,
missing-manifest-dependency,
missing-newline-extrafiles,
no-utf8-coding-comment,
odoo-addons-relative-import,
old-api7-method-defined,
redefined-builtin,
too-complex,
unnecessary-utf8-coding-comment
[REPORTS]
msg-template={path}:{line}: [{msg_id}({symbol}), {obj}] {msg}
output-format=colorized
reports=no

View file

@ -19,53 +19,52 @@
#
########################################################################################
{
'name': 'Improved Field Service Management',
'version': '17.0.0.3.0',
'summary': 'Adds functionality necessary for managing field service operations at Durpro.',
'description': 'Adds functionality necessary for managing field service operations at Durpro.',
'category': 'Services/Field Service',
'author': 'Bemade Inc.',
'website': 'http://www.bemade.org',
'license': 'OPL-1',
'depends': [
'sale_stock',
'sale_project',
'account',
'project_enterprise',
'industry_fsm_stock',
'industry_fsm_report',
'industry_fsm_sale_report',
'bemade_partner_root_ancestor',
'mail',
"name": "Improved Field Service Management",
"version": "17.0.0.3.0",
"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",
"bemade_partner_root_ancestor",
"mail",
],
'data': [
'data/fsm_data.xml',
'views/task_template_views.xml',
'views/equipment.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',
"data": [
"data/fsm_data.xml",
"views/task_template_views.xml",
"views/equipment.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': [
"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',
]
"web.assets_qweb": [
"bemade_fsm/static/src/xml/project_view_buttons.xml",
],
},
'installable': True,
'auto_install': False
"installable": True,
"auto_install": False,
}

View file

@ -1,30 +1,28 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<data>
<record id="planning_project_stage_waiting_parts" model="project.task.type">
<field name="sequence">2</field>
<field name="name">Waiting on Parts</field>
<!-- BV: legend_blocked n'existe plus -->
<!-- BV: <field name="legend_blocked">Blocked</field>-->
<field name="fold" eval="False"/>
<field name="fold" eval="False" />
<!-- <field name="is_closed" eval="False"/>-->
<field name="project_ids" eval="[(4,ref('industry_fsm.fsm_project'))]"/>
<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>
<!-- BV: <field name="legend_blocked">Blocked</field>-->
<field name="fold" eval="False"/>
<field name="fold" eval="False" />
<!-- <field name="is_closed" eval="False"/>-->
<field name="project_ids" eval="[(4,ref('industry_fsm.fsm_project'))]"/>
<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>
<!-- BV: <field name="legend_blocked">Blocked</field>-->
<field name="fold" eval="False"/>
<field name="fold" eval="False" />
<!-- <field name="is_closed" eval="False"/>-->
<field name="project_ids" eval="[(4,ref('industry_fsm.fsm_project'))]"/>
<field name="project_ids" eval="[(4,ref('industry_fsm.fsm_project'))]" />
</record>
</data>
</odoo>
</odoo>

View file

@ -2,75 +2,86 @@ from odoo import api, fields, models
class Equipment(models.Model):
_name = 'bemade_fsm.equipment'
_rec_name = 'complete_name'
_description = 'Field service equipment'
_inherit = ['mail.thread', 'mail.activity.mixin']
_name = "bemade_fsm.equipment"
_rec_name = "complete_name"
_description = "Field service equipment"
_inherit = ["mail.thread", "mail.activity.mixin"]
pid_tag = fields.Char(string="P&ID Tag", tracking=True)
name = fields.Char(string="Name", tracking=True, required=True)
name = fields.Char(tracking=True, required=True)
complete_name = fields.Char(string="Equipment Name", compute="_compute_complete_name", store=True)
tag_ids = fields.Many2many(
comodel_name='bemade_fsm.equipment.tag',
string='Application',
help="Classify and analyze your equipment categories like: Boiler, Laboratory, Waste water, Pure water"
complete_name = fields.Char(
string="Equipment Name", compute="_compute_complete_name", store=True
)
description = fields.Text(string="Description", tracking=True)
tag_ids = fields.Many2many(
comodel_name="bemade_fsm.equipment.tag",
string="Application",
help=(
"Classify and analyze your equipment categories like: Boiler, Laboratory,"
" Waste water, Pure water"
),
)
description = fields.Text(tracking=True)
partner_location_id = fields.Many2one(
comodel_name='res.partner',
comodel_name="res.partner",
string="Physical Address",
tracking=True,
ondelete='cascade'
ondelete="cascade",
)
location_notes = fields.Text(string="Physical Location Notes", tracking=True)
task_ids = fields.Many2many(
comodel_name='project.task',
comodel_name="project.task",
relation="bemade_fsm_task_equipment_rel",
column1="equipment_id",
column2="task_id",
string='Interventions'
string="Interventions",
)
active = fields.Boolean(default=True)
@api.depends('partner_location_id')
@api.depends("partner_location_id")
def _compute_partner(self):
for rec in self:
rec.partner_id = rec.partner_location_id and rec.partner_location_id.root_ancestor
rec.partner_id = (
rec.partner_location_id and rec.partner_location_id.root_ancestor
)
@api.model
def name_search(self, name='', args=None, operator='ilike', limit=100):
def name_search(self, name="", args=None, operator="ilike", limit=100):
args = args or []
if name:
equipments = self.search([
'|', '|',
('pid_tag', operator, name),
('name', operator, name),
('partner_location_id.name', operator, name)],
limit=limit)
equipments = self.search(
[
"|",
"|",
("pid_tag", operator, name),
("name", operator, name),
("partner_location_id.name", operator, name),
],
limit=limit,
)
else:
equipments = self.search(args, limit=limit)
return [(equipment.id, equipment.display_name) for equipment in equipments]
def action_view_equipment(self):
return {
'name': 'Equipment',
'view_type': 'form',
'view_mode': 'form',
'res_id': self.id,
'context': self.env.context,
'res_model': 'bemade_fsm.equipment',
'type': 'ir.actions.act_window',
"name": "Equipment",
"view_type": "form",
"view_mode": "form",
"res_id": self.id,
"context": self.env.context,
"res_model": "bemade_fsm.equipment",
"type": "ir.actions.act_window",
}
@api.depends('pid_tag', 'name')
@api.depends("pid_tag", "name")
def _compute_complete_name(self):
for rec in self:
tag_part = "[%s] " % rec.pid_tag if rec.pid_tag else ""

View file

@ -1,13 +1,13 @@
from odoo import api, fields, models
from odoo import fields, models
class EquipmentTag(models.Model):
_name = "bemade_fsm.equipment.tag"
_description = 'Field service equipment category'
_description = "Field service equipment category"
name = fields.Char('Name', required=True, translate=True)
color = fields.Integer('Color Index', default=10)
name = fields.Char(required=True, translate=True)
color = fields.Integer("Color Index", default=10)
_sql_constraints = [
('name_uniq', 'unique (name)', "Tag name already exists !"),
("name_uniq", "unique (name)", "Tag name already exists !"),
]

View file

@ -1,9 +1,9 @@
from odoo import api, fields, models
from odoo import fields, models
class EquipmentType(models.Model):
_name = 'bemade_fsm.equipment.type'
_description = 'Field service equipment type'
_order = 'id'
_name = "bemade_fsm.equipment.type"
_description = "Field service equipment type"
_order = "id"
name = fields.Char(string='Intervention Name', required=True, translate=True)
name = fields.Char(string="Intervention Name", required=True, translate=True)

View file

@ -1,19 +1,28 @@
from odoo import models, fields, api, _
from odoo import models, fields, api
class FSMVisit(models.Model):
_name = "bemade_fsm.visit"
_description = 'Represents a single visit by assigned service personnel.'
_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)
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)
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"
help=(
"The section on the sale order that represents the labour and parts for"
" this visit"
),
ondelete="cascade",
)
sale_order_id = fields.Many2one(
@ -22,14 +31,18 @@ class FSMVisit(models.Model):
readonly=True,
)
is_completed = fields.Boolean(string="Completed", related="so_section_id.is_fully_delivered")
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")
is_invoiced = fields.Boolean(
string="Invoiced", related="so_section_id.is_fully_delivered_and_invoiced"
)
summarized_equipment_ids = fields.Many2many(
comodel_name="bemade_fsm.equipment",
string="Equipment to Service",
compute="_compute_summarized_equipment_ids"
compute="_compute_summarized_equipment_ids",
)
task_id = fields.Many2one(
@ -38,21 +51,18 @@ class FSMVisit(models.Model):
string="Service Visit",
)
task_ids = fields.One2many(
comodel_name="project.task",
inverse_name="visit_id"
)
task_ids = fields.One2many(comodel_name="project.task", inverse_name="visit_id")
visit_no = fields.Integer(
compute="_compute_visit_no",
)
@api.depends('task_ids')
@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')
@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()
@ -66,23 +76,27 @@ class FSMVisit(models.Model):
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),
})
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'
"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")
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:

View file

@ -1,16 +1,19 @@
from odoo import fields, models, api
from odoo import fields, models
class ProductTemplate(models.Model):
_inherit = 'product.template'
_inherit = "product.template"
task_template_id = fields.Many2one(
comodel_name="project.task.template",
string="Task Template",
ondelete='restrict'
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."
help=(
"Products planned as field service will have travel time considered in"
" planning."
),
)

View file

@ -4,7 +4,9 @@ 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):
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()

View file

@ -1,86 +1,83 @@
from odoo import api, fields, models, Command
from odoo import api, fields, models
class Partner(models.Model):
_inherit = 'res.partner'
_inherit = "res.partner"
equipment_count = fields.Integer(compute='_compute_equipment_count', string='Equipment Count')
equipment_count = fields.Integer(compute="_compute_equipment_count")
owned_equipment_ids = fields.One2many(
comodel_name="bemade_fsm.equipment",
compute="_compute_owned_equipment_ids",
string="Owned Equipments"
string="Owned Equipments",
)
equipment_ids = fields.One2many(
comodel_name='bemade_fsm.equipment',
inverse_name='partner_location_id',
string='Site Equipment'
comodel_name="bemade_fsm.equipment",
inverse_name="partner_location_id",
string="Site Equipment",
)
is_site_contact = fields.Boolean(
string='Is a site contact',
string="Is a site contact",
compute="_compute_is_site_contact",
search="_search_is_site_contact",
)
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
string="Work Sites",
comodel_name="res.partner",
relation="res_partner_site_contact_rel",
column1="site_contact_id",
column2="site_id",
tracking=True,
)
site_contacts = fields.Many2many(
string='Site Contacts',
comodel_name='res.partner',
relation='res_partner_site_contact_rel',
column1='site_id',
column2='site_contact_id',
domain=[('is_company', '=', False)],
tracking=True
comodel_name="res.partner",
relation="res_partner_site_contact_rel",
column1="site_id",
column2="site_contact_id",
domain=[("is_company", "=", 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_company', '=', False)],
tracking=True
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_company", "=", False)],
tracking=True,
)
is_service_site = fields.Boolean(
compute="_compute_is_service_site",
)
@api.depends(
'equipment_ids',
'child_ids.company_type',
'child_ids.equipment_ids'
)
@api.depends("equipment_ids", "child_ids.company_type", "child_ids.equipment_ids")
def _compute_owned_equipment_ids(self):
for rec in self:
ids = rec.equipment_ids | rec.child_ids.mapped('equipment_ids')
ids = rec.equipment_ids | rec.child_ids.mapped("equipment_ids")
rec.owned_equipment_ids = ids or False
@api.depends('site_ids')
@api.depends("site_ids")
def _compute_is_site_contact(self):
for rec in self:
rec.is_site_contact = rec.site_ids is not False
@api.depends('equipment_ids')
@api.depends("equipment_ids")
def _compute_equipment_count(self):
for rec in self:
all_equipment_ids = self.env['bemade_fsm.equipment'].search(
[('partner_location_id', '=', rec.id)])
all_equipment_ids = self.env["bemade_fsm.equipment"].search(
[("partner_location_id", "=", rec.id)]
)
rec.equipment_count = len(all_equipment_ids)
@api.model
def _search_is_site_contact(self, operator, value):
return [('site_contacts', '!=', False)]
return [("site_contacts", "!=", False)]
def _compute_is_service_site(self):
for rec in self:

View file

@ -1,15 +1,11 @@
from odoo import fields, models, api, _, Command
from odoo.exceptions import ValidationError
from odoo.tools import float_round
import re
class SaleOrder(models.Model):
_inherit = 'sale.order'
_inherit = "sale.order"
valid_equipment_ids = fields.One2many(
comodel_name="bemade_fsm.equipment",
related="partner_id.owned_equipment_ids"
comodel_name="bemade_fsm.equipment", related="partner_id.owned_equipment_ids"
)
default_equipment_ids = fields.Many2many(
@ -18,66 +14,67 @@ class SaleOrder(models.Model):
help="The default equipment to service for new sale order lines.",
compute="_compute_default_equipment",
inverse="_inverse_default_equipment",
store=True
store=True,
)
summary_equipment_ids = fields.Many2many(
comodel_name="bemade_fsm.equipment",
string="Equipment Being Serviced",
compute="_compute_summary_equipment_ids")
compute="_compute_summary_equipment_ids",
)
site_contacts = fields.Many2many(
comodel_name='res.partner',
comodel_name="res.partner",
relation="sale_order_site_contacts_rel",
compute="_compute_default_contacts",
inverse="_inverse_default_contacts",
string='Site 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,
)
@api.depends('order_line.task_id')
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,
)
@api.depends("order_line.task_id")
def get_relevant_order_lines(self, task_id):
self.ensure_one()
linked_lines = self.order_line.filtered(lambda l: l.task_id == task_id
or l == task_id.visit_id.so_section_id)
visit_lines = linked_lines.filtered(lambda l: l.visit_id)
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')
@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')
rec.summary_equipment_ids = rec.order_line.mapped("equipment_ids")
@api.onchange('partner_shipping_id')
@api.onchange("partner_shipping_id")
def _onchange_partner_shipping_id(self):
super()._onchange_partner_shipping_id()
res = super()._onchange_partner_shipping_id()
self._compute_default_equipment()
self._compute_default_contacts()
return res
@api.depends('partner_shipping_id')
@api.depends("partner_shipping_id")
def _compute_default_contacts(self):
for rec in self:
rec.site_contacts = rec.partner_shipping_id.site_contacts
@ -87,10 +84,10 @@ class SaleOrder(models.Model):
pass
@api.depends(
'partner_id',
'partner_shipping_id',
'partner_shipping_id.equipment_ids',
'partner_id.owned_equipment_ids'
"partner_id",
"partner_shipping_id",
"partner_shipping_id.equipment_ids",
"partner_id.owned_equipment_ids",
)
def _compute_default_equipment(self):
for rec in self:
@ -109,36 +106,46 @@ class SaleOrder(models.Model):
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
"""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,
})
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
"""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
]):
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
order.mapped("visit_ids").mapped("so_section_id")[0].sequence = 0
@api.depends('order_line.is_fsm')
@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])

View file

@ -1,14 +1,10 @@
from odoo import fields, models, api, _, Command
from odoo.exceptions import ValidationError
from odoo.tools import float_round
import re
class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'
_inherit = "sale.order.line"
valid_equipment_ids = fields.One2many(
comodel_name="bemade_fsm.equipment",
related="order_id.valid_equipment_ids"
comodel_name="bemade_fsm.equipment", related="order_id.valid_equipment_ids"
)
visit_ids = fields.One2many(
@ -22,20 +18,26 @@ class SaleOrderLine(models.Model):
comodel_name="bemade_fsm.visit",
compute="_compute_visit_id",
string="Visit",
ondelete='cascade',
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."
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."
help=(
"Indicates whether a line or all the lines in a section have been entirely"
" delivered and invoiced."
),
)
equipment_ids = fields.Many2many(
@ -43,32 +45,28 @@ class SaleOrderLine(models.Model):
comodel_name="bemade_fsm.equipment",
relation="bemade_fsm_equipment_sale_order_line_rel",
column1="sale_order_line_id",
column2="equipment_id"
column2="equipment_id",
)
is_field_service = fields.Boolean(
string="Is Field Service",
compute="_compute_is_field_service",
store=True
)
is_field_service = fields.Boolean(compute="_compute_is_field_service", store=True)
is_fsm = fields.Boolean(
string='Is FSM',
compute='_compute_is_fsm',
string="Is FSM",
compute="_compute_is_fsm",
store=True,
)
section_line_ids = fields.One2many(
comodel_name='sale.order.line',
compute='_compute_section_line_ids',
comodel_name="sale.order.line",
compute="_compute_section_line_ids",
)
@api.depends('visit_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')
@api.depends("product_id")
def _compute_is_field_service(self):
for rec in self:
rec.is_field_service = rec.product_id.is_field_service
@ -84,38 +82,49 @@ class SaleOrderLine(models.Model):
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]
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
"""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."""
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.
"""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)
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)
task.write({'child_ids': [Command.set([t.id for t in subtasks])]})
task.write({"child_ids": [Command.set([t.id for t in subtasks])]})
# We don't want to see the sub-tasks on the SO
task.child_ids.write({'sale_order_id': None, 'sale_line_id': None, })
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.
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.
@ -124,15 +133,17 @@ class SaleOrderLine(models.Model):
: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
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
if template.equipment_ids:
vals['equipment_ids'] = template.equipment_ids.ids
vals["equipment_ids"] = template.equipment_ids.ids
return vals
tmpl = self.product_id.task_template_id
@ -140,11 +151,16 @@ class SaleOrderLine(models.Model):
task = super()._timesheet_create_task(project)
else:
task = _create_task_from_template(project, tmpl, None)
self.write({'task_id': task.id})
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=%d>%s</a> (%s)") % (
self.order_id.id, self.order_id.name, self.product_id.name)
"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
@ -152,9 +168,9 @@ class SaleOrderLine(models.Model):
def _timesheet_service_generation(self):
super()._timesheet_service_generation()
visit_lines = self.filtered(lambda l: l.visit_id)
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')
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:
@ -162,72 +178,89 @@ class SaleOrderLine(models.Model):
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"))
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()
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):
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
})
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'
"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 l: l.qty_to_deliver == 0)
lambda line: line.qty_to_deliver == 0
)
@api.depends('is_fully_delivered')
@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 l: l.qty_to_invoice == 0)
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. """
"""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."
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 l: l.sequence):
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
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)
return self.env["sale.order.line"].union(*lines)
@api.depends('display_type', 'order_id.order_line')
@api.depends("display_type", "order_id.order_line")
def _compute_section_line_ids(self):
for rec in self:
if rec.display_type == 'line_section':
if rec.display_type == "line_section":
rec.section_line_ids = [Command.set(rec.get_section_line_ids().ids)]
else:
rec.section_line_ids = False
@ -235,7 +268,7 @@ class SaleOrderLine(models.Model):
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':
elif self.display_type == "line_note":
return True
else:
for line in self.order_id.order_line:
@ -244,15 +277,17 @@ class SaleOrderLine(models.Model):
found = True
if not found:
continue
if found and line.display_type == 'line_section':
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.detailed_type', 'product_id.service_tracking')
@api.depends("product_id.detailed_type", "product_id.service_tracking")
def _compute_is_fsm(self):
for rec in self:
rec.is_fsm = (rec.product_id.detailed_type == 'service'
and rec.product_id.service_tracking == 'task_global_project')
rec.is_fsm = (
rec.product_id.detailed_type == "service"
and rec.product_id.service_tracking == "task_global_project"
)

View file

@ -1,4 +1,4 @@
from odoo import fields, models, api, Command, _
from odoo import fields, models, api, Command
from odoo.addons.project.models.project_task import CLOSED_STATES
import re
@ -34,25 +34,24 @@ class Task(models.Model):
string="Can be billed",
related=False,
compute="_compute_allow_billable",
store=True
store=True,
)
visit_id = fields.Many2one(
comodel_name='bemade_fsm.visit',
comodel_name="bemade_fsm.visit",
string="Visit",
)
relevant_order_lines = fields.Many2many(
comodel_name='sale.order.line',
comodel_name="sale.order.line",
store=False,
compute='_compute_relevant_order_lines',
compute="_compute_relevant_order_lines",
)
work_order_number = fields.Char(readonly=True)
propagate_assignment = fields.Boolean(
string='Propagate Assignment',
help='Propagate assignment of this task to all subtasks.',
help="Propagate assignment of this task to all subtasks.",
default=False,
)
@ -76,18 +75,29 @@ class Task(models.Model):
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')
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), 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}"
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:
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:
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
@ -95,45 +105,57 @@ class Task(models.Model):
res = super().write(vals)
if not self: # End recursion on empty RecordSet
return res
if 'propagate_assignment' in vals:
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:
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']})
if 'site_contacts' in vals and self.child_ids:
self._get_all_subtasks().write({'site_contacts': [Command.set(self.site_contacts.ids)]})
if 'work_order_contacts' in vals and self.child_ids:
self._get_all_subtasks().write({'work_order_contacts': [Command.set(self.work_order_contacts.ids)]})
to_propagate.child_ids.write({"user_ids": vals["user_ids"]})
if "site_contacts" in vals and self.child_ids:
self._get_all_subtasks().write(
{"site_contacts": [Command.set(self.site_contacts.ids)]}
)
if "work_order_contacts" in vals and self.child_ids:
self._get_all_subtasks().write(
{"work_order_contacts": [Command.set(self.work_order_contacts.ids)]}
)
return res
@api.depends('sale_order_id')
@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)
rec.sale_order_id
and rec.sale_order_id.get_relevant_order_lines(rec)
or False
)
def _get_closed_stage_by_project(self):
""" Gets the stage representing completed tasks for each project in
"""Gets the stage representing completed tasks for each project in
self.project_id. Copied from industry_fsm/.../project.py:217-221
for consistency.
:returns: Dict of project.project -> project.task.type"""
return {
project:
project: (
project.type_ids.filtered(lambda stage: stage.is_closed)[:1]
or project.type_ids[-1:]
)
for project in self.project_id
}
@api.depends('parent_id.visit_id', 'project_id.is_fsm', 'project_id.allow_billable')
@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:
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
@ -157,7 +179,7 @@ class Task(models.Model):
return self
def synchronize_name_fsm(self):
""" Applies naming to the entire task tree for tasks that are part of this
"""Applies naming to the entire task tree for tasks that are part of this
recordset. Root tasks are named:
Partner Shipping Name - Sale Line Name (Template Name)
@ -176,7 +198,7 @@ class Task(models.Model):
if template:
title = template.name
elif rec.sale_line_id:
name_parts = rec.sale_line_id.name.split('\n')
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
@ -187,7 +209,7 @@ class Task(models.Model):
client_name = rec.sale_order_id.partner_shipping_id.name
if not rec.parent_id:
rec.name = f"{rec.work_order_number} - {client_name} - " f"{title}"
rec.name = f"{rec.work_order_number} - {client_name} - {title}"
else:
rec.name = title

View file

@ -1,8 +1,8 @@
from odoo import models, fields, api, _, Command
from odoo import models, fields, api, Command
class TaskTemplate(models.Model):
_name = 'project.task.template'
_name = "project.task.template"
_description = "Template for new project tasks"
@api.model
@ -11,50 +11,47 @@ class TaskTemplate(models.Model):
name = fields.Char(string="Task Title", required=True)
description = fields.Html(string="Description")
description = fields.Html()
assignees = fields.Many2many(
comodel_name="res.users",
string="Default Assignees",
help="Employees assigned to tasks created from this template."
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."
help="Default customer for tasks created from this template.",
)
project = fields.Many2one(
comodel_name="project.project",
string="Default Project",
help="Default project for tasks created from this template."
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."
help="Default tags for tasks created from this template.",
)
parent = fields.Many2one(
comodel_name="project.task.template",
string="Parent Task Template",
ondelete='cascade'
ondelete="cascade",
)
subtasks = fields.One2many(
comodel_name="project.task.template",
inverse_name="parent",
string="Subtask Templates"
string="Subtask Templates",
)
sequence = fields.Integer(string="Sequence")
sequence = fields.Integer()
company_id = fields.Many2one(
comodel_name="res.company",
string="Company",
index=1,
default=_current_company
comodel_name="res.company", string="Company", index=1, default=_current_company
)
planned_hours = fields.Float("Initially Planned Hours")
@ -69,50 +66,53 @@ class TaskTemplate(models.Model):
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
"view_mode": "form",
"res_model": "project.task.template",
"res_id": self.id,
"type": "ir.actions.act_window",
"context": self._context,
}
@api.onchange('customer')
@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_location_id == rec.customer]
rec.write({'equipment_ids': [Command.set(new_equipment_ids)]})
new_equipment_ids = [
eq.id
for eq in rec.equipment_ids
if eq.partner_location_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):
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': project.partner_id and project.partner_id.id,
'company_id': self.company_id.id,
"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": project.partner_id and project.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.
"""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']
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)
task = rec.env["project.task"].create(vals)
rec.subtasks.create_task_from_self(project, name=False, parent_id=task.id)
tasks |= task
return tasks

File diff suppressed because it is too large Load diff

View file

@ -6,6 +6,8 @@ class TaskCustomReport(models.AbstractModel):
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
split_time_materials = (
self.env.company.split_time_from_materials_on_service_work_orders
)
vals.update({"split_time_materials": split_time_materials})
return vals

View file

@ -1,12 +1,11 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<data>
<record id="industry_fsm_report.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>
</data>
<record id="industry_fsm_report.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>

View file

@ -7,8 +7,8 @@ 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')
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)
@ -21,118 +21,170 @@ class BemadeFSMBaseTest(TransactionCase):
@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)]
})
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_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_product_customer = cls.env.ref(
'customer_product_code.group_product_customer_code_user',
raise_if_not_found=False
"customer_product_code.group_product_customer_code_user",
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, ]
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,
]
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.
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,
})
: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):
def _generate_sale_order(
cls,
partner=None,
client_order_ref="Test Order",
equipment=None,
shipping_location=None,
):
partner = partner or cls._generate_partner()
vals = {'partner_id': partner.id,
'client_order_ref': client_order_ref}
vals = {"partner_id": partner.id, "client_order_ref": client_order_ref}
if equipment:
vals.update({'default_equipment_ids': [Command.set([equipment.id])]})
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)
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):
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,
})
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['bemade_fsm.equipment'].create({
'name': name,
'partner_location_id': partner and partner.id or False,
})
def _generate_equipment(cls, name="test equipment", partner=None):
return cls.env["bemade_fsm.equipment"].create(
{
"name": name,
"partner_location_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:
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.id or False,
'project_template_id': service_tracking == 'task_in_project' and project.id or False,
'task_template_id': task_template and task_template.id or False,
'service_policy': service_policy,
'uom_id': uom_id,
'uom_po_id': uom_id,
})
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.id
or False
),
"project_template_id": (
service_tracking == "task_in_project" and project.id or False
),
"task_template_id": task_template and task_template.id or False,
"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,
})
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.
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
@ -142,48 +194,64 @@ class BemadeFSMBaseTest(TransactionCase):
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."""
:param equipment: The equipment to add as linked equipment to the task template.
"""
if not names:
names = ['Task Template']
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.")
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,
})
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}",
}))
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 = (
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,
}])
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()
@ -198,8 +266,10 @@ class BemadeFSMBaseTest(TransactionCase):
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'])
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)

View file

@ -1,4 +1,4 @@
from odoo.tests.common import HttpCase, tagged
from odoo.tests.common import tagged
from .test_bemade_fsm_common import BemadeFSMBaseTest
from odoo import Command
from odoo.exceptions import MissingError
@ -8,14 +8,14 @@ from odoo.exceptions import MissingError
class TestEquipment(BemadeFSMBaseTest):
def test_crud(self):
partner_company = self._generate_partner()
partner_contact = self._generate_partner('Site Contact', 'person', partner_company)
equipment = self._generate_equipment('Test Equipment 1', partner_company)
self._generate_partner("Site Contact", "person", partner_company)
equipment = self._generate_equipment("Test Equipment 1", partner_company)
# Just make sure the basic ORM stuff is OK
self.assertTrue(equipment in partner_company.equipment_ids)
self.assertTrue(len(partner_company.equipment_ids) == 1)
# Delete should cascade
partner_company.write({'equipment_ids': [Command.set([])]})
partner_company.write({"equipment_ids": [Command.set([])]})
with self.assertRaises(MissingError):
equipment.name
_ = equipment.name

View file

@ -1,34 +1,38 @@
from odoo.tests import TransactionCase, HttpCase, tagged, Form
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)
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])]})
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])]})
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)
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)
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])]})
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)
@ -36,42 +40,74 @@ class SaleOrderFSMContactsCase(BemadeFSMBaseTest):
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])]})
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)
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])]
})
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})
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])]
})
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)

View file

@ -1,11 +1,10 @@
from odoo.tests import TransactionCase, tagged, Form
from odoo.tests import tagged
from .test_bemade_fsm_common import BemadeFSMBaseTest
from datetime import date, timedelta
@tagged('-at_install', 'post_install')
@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)
@ -38,7 +37,7 @@ class FSMVisitTest(BemadeFSMBaseTest):
visit = self._generate_visit(so)
self._generate_sale_order_line(so)
so.action_confirm()
task = so.order_line.filtered(lambda l: l.task_id).task_id
task = so.order_line.filtered(lambda line: line.task_id).task_id
task.action_fsm_validate()
@ -49,7 +48,7 @@ class FSMVisitTest(BemadeFSMBaseTest):
visit = self._generate_visit(so)
self._generate_sale_order_line(so)
so.action_confirm()
task = so.order_line.filtered(lambda l: l.task_id).task_id
task = so.order_line.filtered(lambda line: line.task_id).task_id
task.action_fsm_validate()
self._invoice_sale_order(so)
@ -65,7 +64,10 @@ class FSMVisitTest(BemadeFSMBaseTest):
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)
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()
@ -94,7 +96,7 @@ class FSMVisitTest(BemadeFSMBaseTest):
self.assertEqual(visit_task.allocated_hours, 8.0)
def test_adding_visit_creates_one_sale_order_line(self):
partner = self._generate_partner()
self._generate_partner()
so = self._generate_sale_order()
self._generate_sale_order_line(sale_order=so)
self._generate_sale_order_line(sale_order=so)
@ -104,7 +106,12 @@ class FSMVisitTest(BemadeFSMBaseTest):
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,
visit,
sol1,
sol2,
) = self._generate_so_with_one_visit_two_lines_and_descendants()
so.action_confirm()
parent = visit.task_id
@ -113,7 +120,7 @@ class FSMVisitTest(BemadeFSMBaseTest):
self._assert_is_done(parent)
def _assert_is_done(self, task):
""" Recursively assert all tasks in a hierarchy are complete """
"""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)
@ -127,11 +134,11 @@ class FSMVisitTest(BemadeFSMBaseTest):
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>"""
"""Visits should be named <SO NUMBER> - Visit <visit #> - <visit label>"""
so, visit, sol1, sol2 = self._generate_so_with_one_visit_two_lines()
so.name = "SO12345"
so.action_confirm()
task = visit.task_id
supposed_name = f"SVR12345-1 - Test Company - Test Label"
supposed_name = "SVR12345-1 - Test Company - Test Label"
self.assertEqual(task.name, supposed_name)

View file

@ -1,13 +1,14 @@
from .test_task_template import BemadeFSMBaseTest
from odoo.tests.common import tagged, HttpCase, Form
from odoo.tests.common import tagged, Form
from odoo import Command
@tagged("-at_install", "post_install")
class TestSalesOrder(BemadeFSMBaseTest):
@tagged('-at_install', 'post_install')
@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. """
"""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)
@ -24,10 +25,10 @@ class TestSalesOrder(BemadeFSMBaseTest):
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'])
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]
@ -47,7 +48,7 @@ class TestSalesOrder(BemadeFSMBaseTest):
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."""
"""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)
@ -65,18 +66,22 @@ class TestSalesOrder(BemadeFSMBaseTest):
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 """
"""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 i
in range(3)]
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)])]
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)])]
Command.set([partner.equipment_ids[i].id for i in range(2, 5)])
]
sale_order.action_confirm()
@ -85,7 +90,8 @@ class TestSalesOrder(BemadeFSMBaseTest):
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."""
"""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)
@ -98,7 +104,8 @@ class TestSalesOrder(BemadeFSMBaseTest):
self.assertEqual(sol.task_id.equipment_ids[0], equipment)
def test_sale_order_line_gets_default_equipment(self):
""" Sale order lines created on an SO with default equipment set should inherit that default equipment. """
"""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)
@ -108,7 +115,7 @@ class TestSalesOrder(BemadeFSMBaseTest):
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."""
"""Should pick up equipment from the partner."""
partner = self._generate_partner()
self._generate_equipment(partner=partner)
@ -126,7 +133,9 @@ class TestSalesOrder(BemadeFSMBaseTest):
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):
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):
@ -150,7 +159,7 @@ class TestSalesOrder(BemadeFSMBaseTest):
def test_sale_order_prioritize_shipping_location_equipments(self):
parent = self._generate_partner()
child = self._generate_partner(parent=parent, location_type='delivery')
child = self._generate_partner(parent=parent, location_type="delivery")
self._generate_equipment(partner=parent)
self._generate_equipment(partner=child)
@ -171,11 +180,13 @@ class TestSalesOrder(BemadeFSMBaseTest):
self.assertEqual(line.equipment_ids, partner.equipment_ids)
def test_task_mark_done(self):
""" Marking the task linked to an SO line should mark the line delivered. Marking sub-tasks done should not."""
"""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"])
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()
@ -186,20 +197,24 @@ class TestSalesOrder(BemadeFSMBaseTest):
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
# 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."""
"""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])],
})
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)
@ -213,7 +228,7 @@ class TestSalesOrder(BemadeFSMBaseTest):
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')
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()
@ -224,13 +239,15 @@ class TestSalesOrder(BemadeFSMBaseTest):
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."
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)
@ -241,12 +258,15 @@ class TestSalesOrder(BemadeFSMBaseTest):
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)
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 = self._generate_task_template(
structure=[5], names=["Parent", "Child"]
)
template.description = ""
template.subtasks[0].description = "Some fixed description"
for t in template.subtasks[1:]:
@ -257,13 +277,15 @@ class TestSalesOrder(BemadeFSMBaseTest):
so.action_confirm()
task = sol.task_id
self.assertEqual(task.child_ids[0].description, template.subtasks[0].description)
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."""
"""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()
@ -278,19 +300,19 @@ class TestSalesOrder(BemadeFSMBaseTest):
so = self._generate_sale_order()
so.company_id.create_default_fsm_visit = True
product = self._generate_product()
sol = self._generate_sale_order_line(so, product)
self._generate_sale_order_line(so, product)
so.action_confirm()
visit_line = so.order_line.sorted('sequence')[0]
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
product = self._generate_product()
visit = self._generate_visit(so)
self._generate_product()
self._generate_visit(so)
so.action_confirm()
@ -300,9 +322,8 @@ class TestSalesOrder(BemadeFSMBaseTest):
so = self._generate_sale_order()
so.company_id.create_default_fsm_visit = False
product = self._generate_product()
sol = self._generate_sale_order_line(so, product)
self._generate_sale_order_line(so, product)
so.action_confirm()
visit_line = so.order_line.sorted('sequence')[0]
self.assertFalse(so.visit_ids)

View file

@ -1,54 +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.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
)
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
)
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({})
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
)
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({})
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
)
self.assertTrue(self.test_co.create_default_fsm_visit)
def test_disabling_create_default_fsm_visit(self):
wizard = self.env['res.config.settings'].create({})
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
)
self.assertFalse(self.test_co.create_default_fsm_visit)

View file

@ -3,18 +3,19 @@ from odoo.tests.common import tagged, Form
from odoo import Command
@tagged('post_install', '-at_install')
@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')
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])
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
@ -25,21 +26,27 @@ class TaskTest(BemadeFSMBaseTest):
task = sol.task_id
task.propagate_assignment = True
task.write({
'user_ids': [Command.set([self.user.id])],
'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()]))
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])],
})
task.write(
{
"user_ids": [Command.set([self.user.id])],
}
)
self.assertFalse(any([t.user_ids for t in task.child_ids.child_ids]))
@ -49,24 +56,25 @@ class TaskTest(BemadeFSMBaseTest):
task = sol.task_id
# First, set propagation and assign
task.propagate_assignment = True
task.write({
'user_ids': [Command.set([self.user.id])]
})
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()]))
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([])]
})
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]))
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)]})
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
@ -80,8 +88,10 @@ class TaskTest(BemadeFSMBaseTest):
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)]})
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
@ -95,12 +105,20 @@ class TaskTest(BemadeFSMBaseTest):
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)]})
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)]})
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:
@ -111,12 +129,20 @@ class TaskTest(BemadeFSMBaseTest):
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)]})
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)]})
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:

View file

@ -4,34 +4,36 @@ from odoo.tests import Form
class TestTaskReport(BemadeFSMBaseTest):
def test_split_time_materials_setting(self):
with Form(self.env['res.config.settings']) as settings:
with Form(self.env["res.config.settings"]) as settings:
settings.separate_time_on_work_orders = True
with Form(self.env['res.config.settings']) as new_settings:
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='product',
service_tracking='no',
product_type="product",
service_tracking="no",
)
visit = self._generate_visit(sale_order=so)
sol = self._generate_sale_order_line(sale_order=so, product=service_product)
sol2 = self._generate_sale_order_line(sale_order=so, product=material_product)
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
html_content = self.env['ir.actions.report']._render(
'industry_fsm_report.worksheet_custom',
[task.id],
)[0].decode('utf-8').split('\n')
html_content = (
self.env["ir.actions.report"]
._render(
"industry_fsm_report.worksheet_custom",
[task.id],
)[0]
.decode("utf-8")
.split("\n")
)
strings_to_find = [
"<h2>Materials</h2>",
"<span>Material Product</span>"
]
strings_to_find = ["<h2>Materials</h2>", "<span>Material Product</span>"]
for line in strings_to_find:
line_found = False

View file

@ -1,35 +1,34 @@
from .test_bemade_fsm_common import BemadeFSMBaseTest
from odoo.tests.common import HttpCase, tagged, Form
from odoo.tests.common import tagged, Form
from odoo.exceptions import MissingError
from odoo import Command
from odoo.tools import mute_logger
from psycopg2.errors import ForeignKeyViolation
import psycopg2
@tagged('-at_install', 'post_install')
@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'])
product = self._generate_product(name="Test Product 1", task_template=task_template)
with self.assertRaises(ForeignKeyViolation):
with mute_logger('odoo.sql_db'):
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'])
"""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):
test = grandchild_task.name
_ = grandchild_task.name
def test_dissociating_customer_resets_equipment_appropriately(self):
partner1 = self._generate_partner()
@ -38,7 +37,8 @@ class TestTaskTemplate(BemadeFSMBaseTest):
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
# Switching the partner should trigger on_change that makes sure equipments are
# linked to the new partner
form.customer = partner2
form.save()
@ -46,7 +46,7 @@ class TestTaskTemplate(BemadeFSMBaseTest):
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'])
template = self._generate_task_template(names=["Task"])
product = self._generate_product(task_template=template)
sol1.name = "Short Name 1"
sol2.name = "Short Name 2"
@ -60,14 +60,21 @@ class TestTaskTemplate(BemadeFSMBaseTest):
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])
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(
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()]))
self.assertTrue(
all([t.project_id == project for t in task | task._get_all_subtasks()])
)

View file

@ -1,32 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!-- Equipment Form View -->
<record id="equipment_view_form" model="ir.ui.view">
<field name="name">bemade_fsm.equipment.form</field>
<field name="model">bemade_fsm.equipment</field>
<field name="arch" type="xml">
<form string="Equipment">
<form>
<sheet>
<group>
<group name="left">
<field name="pid_tag"/>
<field name="name"/>
<field name="description"/>
<field name="location_notes"/>
<field name="pid_tag" />
<field name="name" />
<field name="description" />
<field name="location_notes" />
</group>
<group name="right">
<field name="partner_location_id"
groups="account.group_delivery_invoice_address"
context="{'default_type': 'delivery', 'show_address': 1}"
options='{"always_reload": True}'/>
<field name="tag_ids" widget="many2many_tags" options="{'no_open': False}"/>
<field
name="partner_location_id"
groups="account.group_delivery_invoice_address"
context="{'default_type': 'delivery', 'show_address': 1}"
options='{"always_reload": True}'
/>
<field
name="tag_ids"
widget="many2many_tags"
options="{'no_open': False}"
/>
</group>
</group>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="activity_ids" widget="mail_activity"/>
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers" />
<field name="activity_ids" widget="mail_activity" />
<field name="message_ids" widget="mail_thread" />
</div>
</form>
</field>
@ -36,12 +42,16 @@
<field name="name">bemade_fsm.equipment.tree</field>
<field name="model">bemade_fsm.equipment</field>
<field name="arch" type="xml">
<tree string="Equipment" editable="bottom">
<field name="pid_tag"/>
<field name="name"/>
<field name="description"/>
<field name="tag_ids" widget="many2many_tags" options="{'no_open': False}"/>
<field name="partner_location_id"/>
<tree editable="bottom">
<field name="pid_tag" />
<field name="name" />
<field name="description" />
<field
name="tag_ids"
widget="many2many_tags"
options="{'no_open': False}"
/>
<field name="partner_location_id" />
</tree>
</field>
</record>

View file

@ -1,32 +1,40 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<data>
<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"/>
<menuitem id="menu_service_client"
name="Clients"
sequence="10"
parent="industry_fsm.fsm_menu_root"
groups="industry_fsm.group_fsm_user"/>
<menuitem id="menu_service_client_clients"
name="Clients"
action="base.action_partner_customer_form"
sequence="20"
parent="menu_service_client"
groups="industry_fsm.group_fsm_user"/>
<menuitem id="menu_service_client_equipment"
name="Client Equipment"
action="action_window_equipment"
sequence="21"
parent="menu_service_client"
groups="industry_fsm.group_fsm_user"/>
</data>
</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"
/>
<menuitem
id="menu_service_client"
name="Clients"
sequence="10"
parent="industry_fsm.fsm_menu_root"
groups="industry_fsm.group_fsm_user"
/>
<menuitem
id="menu_service_client_clients"
name="Clients"
action="base.action_partner_customer_form"
sequence="20"
parent="menu_service_client"
groups="industry_fsm.group_fsm_user"
/>
<menuitem
id="menu_service_client_equipment"
name="Client Equipment"
action="action_window_equipment"
sequence="21"
parent="menu_service_client"
groups="industry_fsm.group_fsm_user"
/>
</odoo>

View file

@ -1,30 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<data>
<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>
<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>-->
</data>
</odoo>
<!-- 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>

View file

@ -1,4 +1,4 @@
<?xml version="1.0"?>
<?xml version="1.0" ?>
<odoo>
<record id="act_res_partner_2_equipment" model="ir.actions.act_window">
<field name="name">Equipments</field>
@ -11,44 +11,71 @@
<record id="partner_equipment_location_view_form" model="ir.ui.view">
<field name="name">bemade_fsm.partner.equipment.location.form</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="inherit_id" ref="base.view_partner_form" />
<field name="arch" type="xml">
<xpath expr="//page[@name='internal_notes']" position="before">
<field name="is_site_contact" invisible="True"/>
<field name="is_service_site" invisible="True"/>
<page name="field_service" string="Service Site Responsibilities" invisible="is_service_site or is_company">
<field name="owned_equipment_ids" invisible="True"/>
<field name="is_site_contact" invisible="True" />
<field name="is_service_site" invisible="True" />
<page
name="field_service"
string="Service Site Responsibilities"
invisible="is_service_site or is_company"
>
<field name="owned_equipment_ids" invisible="True" />
<group>
<field name="site_ids">
<field name="site_ids">
<tree editable="bottom">
<field name="name" widget="res_partner_many2one"/>
<field name="name" widget="res_partner_many2one" />
</tree>
</field>
</group>
</page>
<page name="service_equipment" string="Service Site Info" invisible="is_site_contact">
<field name="equipment_ids"
context="{'tree_view_ref': 'bemade_fsm.fsm_equipment_view_tree'}"
readonly="False"/>
<field name="site_contacts"
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"/>
<field name="work_order_contacts"
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"/>
<field name="owned_equipment_ids"
context="{'tree_view_ref': 'bemade_fsm.fsm_equipment_view_tree'}"
readonly="True"/>
<page
name="service_equipment"
string="Service Site Info"
invisible="is_site_contact"
>
<field
name="equipment_ids"
context="{'tree_view_ref': 'bemade_fsm.fsm_equipment_view_tree'}"
readonly="False"
/>
<field
name="site_contacts"
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"
/>
<field
name="work_order_contacts"
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"
/>
<field
name="owned_equipment_ids"
context="{'tree_view_ref': 'bemade_fsm.fsm_equipment_view_tree'}"
readonly="True"
/>
</page>
</xpath>
<div name="button_box" position="inside">
<button class="oe_stat_button" type="action" name="%(bemade_fsm.act_res_partner_2_equipment)d"
icon="fa-tachometer">
<field string="Equipments" name="equipment_count" widget="statinfo"/>
<button
class="oe_stat_button"
type="action"
name="%(bemade_fsm.act_res_partner_2_equipment)d"
icon="fa-tachometer"
>
<field
string="Equipments"
name="equipment_count"
widget="statinfo"
/>
</button>
</div>
<field name="lang" position="after">
</field>
<xpath expr="/form//field[@name='child_ids']/form//field[@name='comment']" position="after">
<field name="is_site_contact"/>
<xpath
expr="/form//field[@name='child_ids']/form//field[@name='comment']"
position="after"
>
<field name="is_site_contact" />
</xpath>
</field>
</record>
@ -58,9 +85,9 @@
<field name="arch" type="xml">
<tree editable="bottom">
<field name="name" />
<field name="email" widget="email"/>
<field name="phone" widget="phone"/>
<field name="mobile" widget="phone"/>
<field name="email" widget="email" />
<field name="phone" widget="phone" />
<field name="mobile" widget="phone" />
</tree>
</field>
</record>
@ -69,13 +96,15 @@
<field name="model">bemade_fsm.equipment</field>
<field name="arch" type="xml">
<tree>
<field name="pid_tag"/>
<field name="name"/>
<field name="location_notes"/>
<button name="action_view_equipment"
type="object"
string="Details"
icon="fa-external-link"/>
<field name="pid_tag" />
<field name="name" />
<field name="location_notes" />
<button
name="action_view_equipment"
type="object"
string="Details"
icon="fa-external-link"
/>
</tree>
</field>
</record>

View file

@ -1,55 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<data>
<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="{'tree_view_ref': 'bemade_fsm.bemade_fsm_visit_tree'}"/>
</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_location_id': partner_shipping_id,}"
widget="many2many_tags"
groups="account.group_delivery_invoice_address"/>
<field name="site_contacts"
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"/>
<field name="work_order_contacts"
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"/>
<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="{'tree_view_ref': 'bemade_fsm.bemade_fsm_visit_tree'}"
/>
</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_location_id': partner_shipping_id,}"
widget="many2many_tags"
groups="account.group_delivery_invoice_address"
/>
<field
name="site_contacts"
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"
/>
<field
name="work_order_contacts"
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"
/>
<field name="default_equipment_ids"
context="{'default_partner_location_id': partner_shipping_id,}"
widget="many2many_tags"
domain="[('id', 'in', valid_equipment_ids)]"
groups="account.group_delivery_invoice_address"/>
</group>
</page>
</xpath>
<xpath expr="//tree//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_tree" model="ir.ui.view">
<field name="name">bemade_fsm.visit.tree</field>
<field name="model">bemade_fsm.visit</field>
<field name="arch" type="xml">
<tree editable="bottom">
<field name="label"/>
<field name="approx_date"/>
<field name="is_completed" widget="boolean"/>
<field name="is_invoiced" widget="boolean"/>
</tree>
</field>
</record>
</data>
</odoo>
<field
name="default_equipment_ids"
context="{'default_partner_location_id': partner_shipping_id,}"
widget="many2many_tags"
domain="[('id', 'in', valid_equipment_ids)]"
groups="account.group_delivery_invoice_address"
/>
</group>
</page>
</xpath>
<xpath expr="//tree//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_tree" model="ir.ui.view">
<field name="name">bemade_fsm.visit.tree</field>
<field name="model">bemade_fsm.visit</field>
<field name="arch" type="xml">
<tree editable="bottom">
<field name="label" />
<field name="approx_date" />
<field name="is_completed" widget="boolean" />
<field name="is_invoiced" widget="boolean" />
</tree>
</field>
</record>
</odoo>

View file

@ -1,105 +1,127 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<data>
<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>
<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>
<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_location_id', '=', customer)]"
context="{'tree_view_ref': 'bemade_fsm.equipment_view_tree'}"/>
<field name="tags" widget="many2many_tags"/>
<field name="company_id" />
</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_location_id', '=', customer)]"
context="{'tree_view_ref': 'bemade_fsm.equipment_view_tree'}"
/>
<field name="tags" widget="many2many_tags" />
<field name="company_id" />
</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">
<tree 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"/>
</tree>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="task_template_tree_view" model="ir.ui.view">
<field name="name">project.task_template.tree</field>
<field name="model">project.task.template</field>
<field name="arch" type="xml">
<tree string="Task Template">
<field name="name"/>
<field name="assignees" widget="many2many_avatar_user"/>
<field name="project"/>
<field name="parent"/>
<field name="planned_hours"/>
</tree>
</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>
<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">
<tree 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"
/>
</tree>
</field>
</page>
</notebook>
</sheet>
</form>
</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">tree,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>
<record id="task_template_tree_view" model="ir.ui.view">
<field name="name">project.task_template.tree</field>
<field name="model">project.task.template</field>
<field name="arch" type="xml">
<tree>
<field name="name" />
<field name="assignees" widget="many2many_avatar_user" />
<field name="project" />
<field name="parent" />
<field name="planned_hours" />
</tree>
</field>
</record>
</data>
</odoo>
<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">tree,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>

View file

@ -1,228 +1,222 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<data>
<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="Field Service" name="field_service" translate="True">
<group name="equipment_and_contacts">
<field name="equipment_ids"
domain="[('partner_location_id', '=', partner_id)]"
context="{'tree_view_ref': 'bemade_fsm.equipment_view_tree'}"/>
<field name="site_contacts"
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"/>
<field name="work_order_contacts"
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"/>
</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>
<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="Field Service" name="field_service" translate="True">
<group name="equipment_and_contacts">
<field
name="equipment_ids"
domain="[('partner_location_id', '=', partner_id)]"
context="{'tree_view_ref': 'bemade_fsm.equipment_view_tree'}"
/>
<field
name="site_contacts"
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"
/>
<field
name="work_order_contacts"
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"
/>
</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']/tree//field[@name='name']"
position="after"
>
<field name="description" string="Description/Comments" />
</xpath>
<field name="user_ids" position="after">
<field name="propagate_assignment" />
</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']/tree//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">
<tree position="attributes">
<attribute name="js_class">project_list</attribute>
</tree>
<field name="partner_id" position="after">
<field name="work_order_number" optional="show" />
</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="industry_fsm.project_task_action_fsm_map"
model="ir.actions.act_window">
<field name="context">{'search_default_is_parent_task': True}</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>
</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>
</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>
</record>
<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>
</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>
</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>
</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="arch" type="xml">
<xpath expr="//field[@name='worksheet_template_id']"
position="replace"></xpath>
<xpath expr="//calendar" position="attributes">
<attribute name="color">user_ids</attribute>
</xpath>
<field name="company_id" position="attributes">
<attribute name="optional">hide</attribute>
</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">
<tree position="attributes">
<attribute name="js_class">project_list</attribute>
</tree>
<field name="partner_id" position="after">
<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="worksheet_template_id" position="attributes">
<attribute name="optional">hide</attribute>
</field>
</record>
<record id="industry_fsm.project_task_action_fsm_map"
model="ir.actions.act_window">
<field name="domain">[('is_fsm', '=', True),
('project_id', '!=', False),
('display_in_project', '=', True),
('parent_id', '=', False)]
<field name="project_id" position="attributes">
<attribute name="optional">hide</attribute>
</field>
</record>
<record id="industry_fsm.project_task_action_to_schedule_fsm"
model="ir.actions.act_window">
<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="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="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="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="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="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="arch" type="xml">
<xpath expr="//field[@name='worksheet_template_id']"
position="replace"></xpath>
<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">
[
'&amp;',
('fsm_done', '=', False),
'|',
('user_ids', '=', False),
'&amp;',
('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>
</data>
</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">
[
'&amp;',
('fsm_done', '=', False),
'|',
('user_ids', '=', False),
'&amp;',
('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>

View file

@ -1,5 +1,4 @@
from odoo import models, fields, api, _
from odoo.exceptions import UserError
from odoo import models, fields
class NewTaskFromTemplateWizard(models.TransientModel):
@ -7,45 +6,52 @@ class NewTaskFromTemplateWizard(models.TransientModel):
_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.',
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.',
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.',
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)
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})
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)
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',
"type": "ir.actions.act_window",
"res_model": "project.task",
"res_id": task.id,
"view_mode": "form",
"target": "current",
}

View file

@ -1,11 +1,11 @@
from odoo import models, fields, api
from odoo import models, fields
class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"
company_id = fields.Many2one(
'res.company',
"res.company",
default=lambda self: self.env.company or self.env.user.company_id,
)
separate_time_on_work_orders = fields.Boolean(