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] args: [--fix, --exit-non-zero-on-fix]
- id: ruff-format - id: ruff-format
- repo: https://github.com/OCA/pylint-odoo - repo: https://github.com/OCA/pylint-odoo
rev: v8.0.19 rev: v9.1.2
hooks: hooks:
- id: pylint_odoo - id: pylint_odoo
name: pylint with optional checks 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', "name": "Improved Field Service Management",
'version': '17.0.0.3.0', "version": "17.0.0.3.0",
'summary': 'Adds functionality necessary for managing field service operations at Durpro.', "summary": (
'description': 'Adds functionality necessary for managing field service operations at Durpro.', "Adds functionality necessary for managing field service operations at Durpro."
'category': 'Services/Field Service', ),
'author': 'Bemade Inc.', "category": "Services/Field Service",
'website': 'http://www.bemade.org', "author": "Bemade Inc.",
'license': 'OPL-1', "website": "http://www.bemade.org",
'depends': [ "license": "LGPL-3",
'sale_stock', "depends": [
'sale_project', "sale_stock",
'account', "sale_project",
'project_enterprise', "account",
'industry_fsm_stock', "project_enterprise",
'industry_fsm_report', "industry_fsm_stock",
'industry_fsm_sale_report', "industry_fsm_report",
'bemade_partner_root_ancestor', "industry_fsm_sale_report",
'mail', "bemade_partner_root_ancestor",
"mail",
], ],
'data': [ "data": [
'data/fsm_data.xml', "data/fsm_data.xml",
'views/task_template_views.xml', "views/task_template_views.xml",
'views/equipment.xml', "views/equipment.xml",
'security/ir.model.access.csv', "security/ir.model.access.csv",
'views/product_views.xml', "views/product_views.xml",
'views/res_partner.xml', "views/res_partner.xml",
'views/menus.xml', "views/menus.xml",
'views/task_views.xml', "views/task_views.xml",
'views/sale_order_views.xml', "views/sale_order_views.xml",
'reports/worksheet_custom_report_templates.xml', "reports/worksheet_custom_report_templates.xml",
'reports/worksheet_custom_reports.xml', "reports/worksheet_custom_reports.xml",
'wizard/new_task_from_template.xml', "wizard/new_task_from_template.xml",
'wizard/res_config_settings.xml', "wizard/res_config_settings.xml",
], ],
'assets': { "assets": {
'web.report_assets_common': [ "web.report_assets_common": ["bemade_fsm/static/src/scss/bemade_fsm.scss"],
'bemade_fsm/static/src/scss/bemade_fsm.scss' "web.assets_backend": [
],
'web.assets_backend': [
# BV: need to readd these files # BV: need to readd these files
# 'bemade_fsm/static/src/js/kanban_view.js', # 'bemade_fsm/static/src/js/kanban_view.js',
# 'bemade_fsm/static/src/js/list_view.js', # 'bemade_fsm/static/src/js/list_view.js',
], ],
'web.assets_qweb': [ "web.assets_qweb": [
'bemade_fsm/static/src/xml/project_view_buttons.xml', "bemade_fsm/static/src/xml/project_view_buttons.xml",
] ],
}, },
'installable': True, "installable": True,
'auto_install': False "auto_install": False,
} }

View file

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?> <?xml version="1.0" encoding="UTF-8" ?>
<odoo> <odoo>
<data>
<record id="planning_project_stage_waiting_parts" model="project.task.type"> <record id="planning_project_stage_waiting_parts" model="project.task.type">
<field name="sequence">2</field> <field name="sequence">2</field>
<field name="name">Waiting on Parts</field> <field name="name">Waiting on Parts</field>
@ -26,5 +25,4 @@
<!-- <field name="is_closed" 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>
</data>
</odoo> </odoo>

View file

@ -2,75 +2,86 @@ from odoo import api, fields, models
class Equipment(models.Model): class Equipment(models.Model):
_name = 'bemade_fsm.equipment' _name = "bemade_fsm.equipment"
_rec_name = 'complete_name' _rec_name = "complete_name"
_description = 'Field service equipment' _description = "Field service equipment"
_inherit = ['mail.thread', 'mail.activity.mixin'] _inherit = ["mail.thread", "mail.activity.mixin"]
pid_tag = fields.Char(string="P&ID Tag", tracking=True) 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) 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"
) )
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( partner_location_id = fields.Many2one(
comodel_name='res.partner', comodel_name="res.partner",
string="Physical Address", string="Physical Address",
tracking=True, tracking=True,
ondelete='cascade' ondelete="cascade",
) )
location_notes = fields.Text(string="Physical Location Notes", tracking=True) location_notes = fields.Text(string="Physical Location Notes", tracking=True)
task_ids = fields.Many2many( task_ids = fields.Many2many(
comodel_name='project.task', comodel_name="project.task",
relation="bemade_fsm_task_equipment_rel", relation="bemade_fsm_task_equipment_rel",
column1="equipment_id", column1="equipment_id",
column2="task_id", column2="task_id",
string='Interventions' string="Interventions",
) )
active = fields.Boolean(default=True) active = fields.Boolean(default=True)
@api.depends('partner_location_id') @api.depends("partner_location_id")
def _compute_partner(self): def _compute_partner(self):
for rec in 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 @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 [] args = args or []
if name: if name:
equipments = self.search([ equipments = self.search(
'|', '|', [
('pid_tag', operator, name), "|",
('name', operator, name), "|",
('partner_location_id.name', operator, name)], ("pid_tag", operator, name),
limit=limit) ("name", operator, name),
("partner_location_id.name", operator, name),
],
limit=limit,
)
else: else:
equipments = self.search(args, limit=limit) equipments = self.search(args, limit=limit)
return [(equipment.id, equipment.display_name) for equipment in equipments] return [(equipment.id, equipment.display_name) for equipment in equipments]
def action_view_equipment(self): def action_view_equipment(self):
return { return {
'name': 'Equipment', "name": "Equipment",
'view_type': 'form', "view_type": "form",
'view_mode': 'form', "view_mode": "form",
'res_id': self.id, "res_id": self.id,
'context': self.env.context, "context": self.env.context,
'res_model': 'bemade_fsm.equipment', "res_model": "bemade_fsm.equipment",
'type': 'ir.actions.act_window', "type": "ir.actions.act_window",
} }
@api.depends('pid_tag', 'name') @api.depends("pid_tag", "name")
def _compute_complete_name(self): def _compute_complete_name(self):
for rec in self: for rec in self:
tag_part = "[%s] " % rec.pid_tag if rec.pid_tag else "" 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): class EquipmentTag(models.Model):
_name = "bemade_fsm.equipment.tag" _name = "bemade_fsm.equipment.tag"
_description = 'Field service equipment category' _description = "Field service equipment category"
name = fields.Char('Name', required=True, translate=True) name = fields.Char(required=True, translate=True)
color = fields.Integer('Color Index', default=10) color = fields.Integer("Color Index", default=10)
_sql_constraints = [ _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): class EquipmentType(models.Model):
_name = 'bemade_fsm.equipment.type' _name = "bemade_fsm.equipment.type"
_description = 'Field service equipment type' _description = "Field service equipment type"
_order = 'id' _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): class FSMVisit(models.Model):
_name = "bemade_fsm.visit" _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( so_section_id = fields.Many2one(
comodel_name="sale.order.line", comodel_name="sale.order.line",
string="Sale Order Section", string="Sale Order Section",
help="The section on the sale order that represents the labour and parts for this visit", help=(
ondelete="cascade" "The section on the sale order that represents the labour and parts for"
" this visit"
),
ondelete="cascade",
) )
sale_order_id = fields.Many2one( sale_order_id = fields.Many2one(
@ -22,14 +31,18 @@ class FSMVisit(models.Model):
readonly=True, 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( summarized_equipment_ids = fields.Many2many(
comodel_name="bemade_fsm.equipment", comodel_name="bemade_fsm.equipment",
string="Equipment to Service", string="Equipment to Service",
compute="_compute_summarized_equipment_ids" compute="_compute_summarized_equipment_ids",
) )
task_id = fields.Many2one( task_id = fields.Many2one(
@ -38,21 +51,18 @@ class FSMVisit(models.Model):
string="Service Visit", string="Service Visit",
) )
task_ids = fields.One2many( task_ids = fields.One2many(comodel_name="project.task", inverse_name="visit_id")
comodel_name="project.task",
inverse_name="visit_id"
)
visit_no = fields.Integer( visit_no = fields.Integer(
compute="_compute_visit_no", compute="_compute_visit_no",
) )
@api.depends('task_ids') @api.depends("task_ids")
def _compute_task_id(self): def _compute_task_id(self):
for rec in self: for rec in self:
rec.task_id = rec.task_ids and rec.task_ids[0] 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): def _compute_summarized_equipment_ids(self):
for rec in self: for rec in self:
lines = rec.so_section_id.get_section_line_ids() lines = rec.so_section_id.get_section_line_ids()
@ -66,23 +76,27 @@ class FSMVisit(models.Model):
def create(self, vals_list): def create(self, vals_list):
recs = super().create(vals_list) recs = super().create(vals_list)
for i, rec in enumerate(recs.filtered(lambda visit: not visit.so_section_id)): for i, rec in enumerate(recs.filtered(lambda visit: not visit.so_section_id)):
rec.so_section_id = rec.env['sale.order.line'].create({ rec.so_section_id = rec.env["sale.order.line"].create(
'order_id': rec.sale_order_id.id, {
'display_type': 'line_section', "order_id": rec.sale_order_id.id,
'name': vals_list[i].get('label', False), "display_type": "line_section",
}) "name": vals_list[i].get("label", False),
}
)
return recs return recs
@api.depends( @api.depends(
'so_section_id', "so_section_id",
'sale_order_id', "sale_order_id",
'sale_order_id.visit_ids', "sale_order_id.visit_ids",
'sale_order_id.visit_ids.so_section_id', "sale_order_id.visit_ids.so_section_id",
'sale_order_id.visit_ids.so_section_id.sequence' "sale_order_id.visit_ids.so_section_id.sequence",
) )
def _compute_visit_no(self): def _compute_visit_no(self):
for rec in 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 # Just a straight O(n) search here since n will always be relatively small
for index, line in enumerate(ordered_visit_lines): for index, line in enumerate(ordered_visit_lines):
if line == rec.so_section_id: 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): class ProductTemplate(models.Model):
_inherit = 'product.template' _inherit = "product.template"
task_template_id = fields.Many2one( task_template_id = fields.Many2one(
comodel_name="project.task.template", comodel_name="project.task.template",
string="Task Template", string="Task Template",
ondelete='restrict' ondelete="restrict",
) )
is_field_service = fields.Boolean( is_field_service = fields.Boolean(
string="Plan as field service", 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): class Project(models.Model):
_inherit = "project.project" _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 # 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) # Temporary fix until Odoo fixes this method (PR #160067 submitted for this)
self.env.flush_all() 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): 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( owned_equipment_ids = fields.One2many(
comodel_name="bemade_fsm.equipment", comodel_name="bemade_fsm.equipment",
compute="_compute_owned_equipment_ids", compute="_compute_owned_equipment_ids",
string="Owned Equipments" string="Owned Equipments",
) )
equipment_ids = fields.One2many( equipment_ids = fields.One2many(
comodel_name='bemade_fsm.equipment', comodel_name="bemade_fsm.equipment",
inverse_name='partner_location_id', inverse_name="partner_location_id",
string='Site Equipment' string="Site Equipment",
) )
is_site_contact = fields.Boolean( is_site_contact = fields.Boolean(
string='Is a site contact', string="Is a site contact",
compute="_compute_is_site_contact", compute="_compute_is_site_contact",
search="_search_is_site_contact", search="_search_is_site_contact",
) )
site_ids = fields.Many2many( site_ids = fields.Many2many(
string='Work Sites', string="Work Sites",
comodel_name='res.partner', comodel_name="res.partner",
relation='res_partner_site_contact_rel', relation="res_partner_site_contact_rel",
column1='site_contact_id', column1="site_contact_id",
column2='site_id', column2="site_id",
tracking=True tracking=True,
) )
site_contacts = fields.Many2many( site_contacts = fields.Many2many(
string='Site Contacts', comodel_name="res.partner",
comodel_name='res.partner', relation="res_partner_site_contact_rel",
relation='res_partner_site_contact_rel', column1="site_id",
column1='site_id', column2="site_contact_id",
column2='site_contact_id', domain=[("is_company", "=", False)],
domain=[('is_company', '=', False)], tracking=True,
tracking=True
) )
work_order_contacts = fields.Many2many( work_order_contacts = fields.Many2many(
string='Work Order Recipients', string="Work Order Recipients",
comodel_name='res.partner', comodel_name="res.partner",
relation='res_partner_work_order_contacts_rel', relation="res_partner_work_order_contacts_rel",
column1='res_partner_id', column1="res_partner_id",
column2='work_order_contact_id', column2="work_order_contact_id",
domain=[('is_company', '=', False)], domain=[("is_company", "=", False)],
tracking=True tracking=True,
) )
is_service_site = fields.Boolean( is_service_site = fields.Boolean(
compute="_compute_is_service_site", compute="_compute_is_service_site",
) )
@api.depends(
'equipment_ids', @api.depends("equipment_ids", "child_ids.company_type", "child_ids.equipment_ids")
'child_ids.company_type',
'child_ids.equipment_ids'
)
def _compute_owned_equipment_ids(self): def _compute_owned_equipment_ids(self):
for rec in 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 rec.owned_equipment_ids = ids or False
@api.depends('site_ids') @api.depends("site_ids")
def _compute_is_site_contact(self): def _compute_is_site_contact(self):
for rec in self: for rec in self:
rec.is_site_contact = rec.site_ids is not False rec.is_site_contact = rec.site_ids is not False
@api.depends('equipment_ids') @api.depends("equipment_ids")
def _compute_equipment_count(self): def _compute_equipment_count(self):
for rec in self: for rec in self:
all_equipment_ids = self.env['bemade_fsm.equipment'].search( all_equipment_ids = self.env["bemade_fsm.equipment"].search(
[('partner_location_id', '=', rec.id)]) [("partner_location_id", "=", rec.id)]
)
rec.equipment_count = len(all_equipment_ids) rec.equipment_count = len(all_equipment_ids)
@api.model @api.model
def _search_is_site_contact(self, operator, value): def _search_is_site_contact(self, operator, value):
return [('site_contacts', '!=', False)] return [("site_contacts", "!=", False)]
def _compute_is_service_site(self): def _compute_is_service_site(self):
for rec in self: for rec in self:

View file

@ -1,15 +1,11 @@
from odoo import fields, models, api, _, Command from odoo import fields, models, api, _, Command
from odoo.exceptions import ValidationError
from odoo.tools import float_round
import re
class SaleOrder(models.Model): class SaleOrder(models.Model):
_inherit = 'sale.order' _inherit = "sale.order"
valid_equipment_ids = fields.One2many( valid_equipment_ids = fields.One2many(
comodel_name="bemade_fsm.equipment", comodel_name="bemade_fsm.equipment", related="partner_id.owned_equipment_ids"
related="partner_id.owned_equipment_ids"
) )
default_equipment_ids = fields.Many2many( default_equipment_ids = fields.Many2many(
@ -18,66 +14,67 @@ class SaleOrder(models.Model):
help="The default equipment to service for new sale order lines.", help="The default equipment to service for new sale order lines.",
compute="_compute_default_equipment", compute="_compute_default_equipment",
inverse="_inverse_default_equipment", inverse="_inverse_default_equipment",
store=True store=True,
) )
summary_equipment_ids = fields.Many2many( summary_equipment_ids = fields.Many2many(
comodel_name="bemade_fsm.equipment", comodel_name="bemade_fsm.equipment",
string="Equipment Being Serviced", string="Equipment Being Serviced",
compute="_compute_summary_equipment_ids") compute="_compute_summary_equipment_ids",
)
site_contacts = fields.Many2many( site_contacts = fields.Many2many(
comodel_name='res.partner', comodel_name="res.partner",
relation="sale_order_site_contacts_rel", relation="sale_order_site_contacts_rel",
compute="_compute_default_contacts", compute="_compute_default_contacts",
inverse="_inverse_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, 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): def get_relevant_order_lines(self, task_id):
self.ensure_one() self.ensure_one()
linked_lines = self.order_line.filtered(lambda l: l.task_id == task_id linked_lines = self.order_line.filtered(
or l == task_id.visit_id.so_section_id) lambda line: line.task_id == task_id
visit_lines = linked_lines.filtered(lambda l: l.visit_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: for line in visit_lines:
linked_lines |= line.get_section_line_ids() linked_lines |= line.get_section_line_ids()
return linked_lines return linked_lines
@api.depends('order_line.equipment_ids') @api.depends("order_line.equipment_ids")
def _compute_summary_equipment_ids(self): def _compute_summary_equipment_ids(self):
for rec in 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): def _onchange_partner_shipping_id(self):
super()._onchange_partner_shipping_id() res = super()._onchange_partner_shipping_id()
self._compute_default_equipment() self._compute_default_equipment()
self._compute_default_contacts() self._compute_default_contacts()
return res
@api.depends('partner_shipping_id') @api.depends("partner_shipping_id")
def _compute_default_contacts(self): def _compute_default_contacts(self):
for rec in self: for rec in self:
rec.site_contacts = rec.partner_shipping_id.site_contacts rec.site_contacts = rec.partner_shipping_id.site_contacts
@ -87,10 +84,10 @@ class SaleOrder(models.Model):
pass pass
@api.depends( @api.depends(
'partner_id', "partner_id",
'partner_shipping_id', "partner_shipping_id",
'partner_shipping_id.equipment_ids', "partner_shipping_id.equipment_ids",
'partner_id.owned_equipment_ids' "partner_id.owned_equipment_ids",
) )
def _compute_default_equipment(self): def _compute_default_equipment(self):
for rec in self: for rec in self:
@ -112,10 +109,12 @@ class SaleOrder(models.Model):
"""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.""" a visit line grouping all the service being done."""
self.ensure_one() self.ensure_one()
visit = self.env['bemade_fsm.visit'].create({ visit = self.env["bemade_fsm.visit"].create(
'label': _('Service Visit'), {
'sale_order_id': self.id, "label": _("Service Visit"),
}) "sale_order_id": self.id,
}
)
# Make sure it goes to the top of the list # Make sure it goes to the top of the list
visit.so_section_id.sequence = 0 visit.so_section_id.sequence = 0
@ -127,18 +126,26 @@ class SaleOrder(models.Model):
order._create_default_visit() order._create_default_visit()
if order.is_fsm: if order.is_fsm:
# Make sure that all the lines producing FSM tasks are under a visit # 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') visit_line_ids = (
if any([ order.mapped("visit_ids")
True for line in .mapped("so_section_id")
order.order_line.filtered(lambda line: not line.display_type) .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 line not in visit_line_ids
]): ]
):
# If not, promote the first visit to the top of the order items list # If not, promote the first visit to the top of the order items list
for line in order.order_line: for line in order.order_line:
line.sequence += 1 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): def _compute_is_fsm(self):
for rec in self: for rec in self:
rec.is_fsm = any([line.is_fsm for line in rec.order_line]) 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 import fields, models, api, _, Command
from odoo.exceptions import ValidationError
from odoo.tools import float_round
import re
class SaleOrderLine(models.Model): class SaleOrderLine(models.Model):
_inherit = 'sale.order.line' _inherit = "sale.order.line"
valid_equipment_ids = fields.One2many( valid_equipment_ids = fields.One2many(
comodel_name="bemade_fsm.equipment", comodel_name="bemade_fsm.equipment", related="order_id.valid_equipment_ids"
related="order_id.valid_equipment_ids"
) )
visit_ids = fields.One2many( visit_ids = fields.One2many(
@ -22,20 +18,26 @@ class SaleOrderLine(models.Model):
comodel_name="bemade_fsm.visit", comodel_name="bemade_fsm.visit",
compute="_compute_visit_id", compute="_compute_visit_id",
string="Visit", string="Visit",
ondelete='cascade', ondelete="cascade",
store=True, store=True,
) )
is_fully_delivered = fields.Boolean( is_fully_delivered = fields.Boolean(
string="Fully Delivered", string="Fully Delivered",
compute="_compute_is_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( is_fully_delivered_and_invoiced = fields.Boolean(
string="Fully Invoiced", string="Fully Invoiced",
compute="_compute_is_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( equipment_ids = fields.Many2many(
@ -43,32 +45,28 @@ class SaleOrderLine(models.Model):
comodel_name="bemade_fsm.equipment", comodel_name="bemade_fsm.equipment",
relation="bemade_fsm_equipment_sale_order_line_rel", relation="bemade_fsm_equipment_sale_order_line_rel",
column1="sale_order_line_id", column1="sale_order_line_id",
column2="equipment_id" column2="equipment_id",
) )
is_field_service = fields.Boolean( is_field_service = fields.Boolean(compute="_compute_is_field_service", store=True)
string="Is Field Service",
compute="_compute_is_field_service",
store=True
)
is_fsm = fields.Boolean( is_fsm = fields.Boolean(
string='Is FSM', string="Is FSM",
compute='_compute_is_fsm', compute="_compute_is_fsm",
store=True, store=True,
) )
section_line_ids = fields.One2many( section_line_ids = fields.One2many(
comodel_name='sale.order.line', comodel_name="sale.order.line",
compute='_compute_section_line_ids', compute="_compute_section_line_ids",
) )
@api.depends('visit_ids') @api.depends("visit_ids")
def _compute_visit_id(self): def _compute_visit_id(self):
for rec in self: for rec in self:
rec.visit_id = rec.visit_ids and rec.visit_ids[0] 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): def _compute_is_field_service(self):
for rec in self: for rec in self:
rec.is_field_service = rec.product_id.is_field_service rec.is_field_service = rec.product_id.is_field_service
@ -84,8 +82,10 @@ class SaleOrderLine(models.Model):
def copy_data(self, default=None): def copy_data(self, default=None):
if default is None: if default is None:
default = {} default = {}
if 'visit_ids' not in default: if "visit_ids" not in default:
default['visit_ids'] = [(0, 0, visit.copy_data()[0]) for visit in self.visit_ids] default["visit_ids"] = [
(0, 0, visit.copy_data()[0]) for visit in self.visit_ids
]
return super().copy_data(default) return super().copy_data(default)
def _timesheet_create_task(self, project): def _timesheet_create_task(self, project):
@ -93,7 +93,8 @@ class SaleOrderLine(models.Model):
:param project: record of project.project in which the task should be created :param project: record of project.project in which the task should be created
:return task: record of the created task :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): 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.
@ -102,19 +103,27 @@ class SaleOrderLine(models.Model):
:param template: project.task.template to use to create the task. :param template: project.task.template to use to create the task.
:param parent: project.task to set as the parent to this task. :param parent: project.task to set as the parent to this task.
""" """
values = _timesheet_create_task_prepare_values_from_template(project, template, parent) values = _timesheet_create_task_prepare_values_from_template(
task = self.env['project.task'].sudo().create(values) project, template, parent
)
task = self.env["project.task"].sudo().create(values)
subtasks = [] subtasks = []
for t in template.subtasks: for t in template.subtasks:
subtask = _create_task_from_template(project, t, task) subtask = _create_task_from_template(project, t, task)
subtasks.append(subtask) 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 # 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 return task
def _timesheet_create_task_prepare_values_from_template(project, template, def _timesheet_create_task_prepare_values_from_template(
parent): project, template, parent
):
"""Copies the values from a project.task.template over to the set of values used to create a project.task. """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. :param project: project.project record to set on the task's project_id field.
@ -124,15 +133,17 @@ class SaleOrderLine(models.Model):
:param parent: project.task to set as the parent to this task. :param parent: project.task to set as the parent to this task.
""" """
vals = self._timesheet_create_task_prepare_values(project) vals = self._timesheet_create_task_prepare_values(project)
vals['name'] = template.name vals["name"] = template.name
vals['description'] = template.description or '' if parent else vals['description'] vals["description"] = (
vals['parent_id'] = parent and parent.id template.description or "" if parent else vals["description"]
vals['user_ids'] = template.assignees.ids )
vals['tag_ids'] = template.tags.ids vals["parent_id"] = parent and parent.id
vals['allocated_hours'] = template.planned_hours vals["user_ids"] = template.assignees.ids
vals['sequence'] = template.sequence vals["tag_ids"] = template.tags.ids
vals["allocated_hours"] = template.planned_hours
vals["sequence"] = template.sequence
if template.equipment_ids: if template.equipment_ids:
vals['equipment_ids'] = template.equipment_ids.ids vals["equipment_ids"] = template.equipment_ids.ids
return vals return vals
tmpl = self.product_id.task_template_id tmpl = self.product_id.task_template_id
@ -140,11 +151,16 @@ class SaleOrderLine(models.Model):
task = super()._timesheet_create_task(project) task = super()._timesheet_create_task(project)
else: else:
task = _create_task_from_template(project, tmpl, None) task = _create_task_from_template(project, tmpl, None)
self.write({'task_id': task.id}) self.write({"task_id": task.id})
# post message on task # post message on task
task_msg = _( task_msg = _(
"This task has been created from: <a href=# data-oe-model=sale.order data-oe-id=%d>%s</a> (%s)") % ( "This task has been created from: <a href=# data-oe-model=sale.order"
self.order_id.id, self.order_id.name, self.product_id.name) " 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) task.message_post(body=task_msg)
if not task.equipment_ids and self.equipment_ids: if not task.equipment_ids and self.equipment_ids:
task.equipment_ids = self.equipment_ids.ids task.equipment_ids = self.equipment_ids.ids
@ -152,9 +168,9 @@ class SaleOrderLine(models.Model):
def _timesheet_service_generation(self): def _timesheet_service_generation(self):
super()._timesheet_service_generation() 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): 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: if not task_ids:
continue continue
if len(set([task.project_id for task in task_ids])) > 1: if len(set([task.project_id for task in task_ids])) > 1:
@ -162,72 +178,89 @@ class SaleOrderLine(models.Model):
return return
project_id = task_ids[0].project_id project_id = task_ids[0].project_id
line.visit_id.task_id = line._generate_task_for_visit_line( line.visit_id.task_id = line._generate_task_for_visit_line(
project_id, index + 1, project_id, index + 1, sum(task_ids.mapped("allocated_hours"))
sum(task_ids.mapped("allocated_hours"))
) )
task_ids.write({'parent_id': line.visit_id.task_id.id}) 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() (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() self.ensure_one()
allocated_hours = sum(self.get_section_line_ids().task_id.mapped('allocated_hours')) allocated_hours = sum(
task = self.env['project.task'].create({ self.get_section_line_ids().task_id.mapped("allocated_hours")
'name': f"{self.order_id.name} - " + _("Visit") + f" {visit_no} - {self.name}", )
'project_id': project.id, task = self.env["project.task"].create(
'equipment_ids': self.get_section_line_ids().mapped('equipment_ids').ids, {
'sale_order_id': self.order_id.id, "name": (
'partner_id': self.order_id.partner_shipping_id.id, f"{self.order_id.name} - "
'visit_id': self.visit_id.id, + _("Visit")
'allocated_hours': allocated_hours, + f" {visit_no} - {self.name}"
'date_deadline': self.visit_id.approx_date, ),
'user_ids': False, # Force to empty or it uses the current user "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 return task
@api.depends( @api.depends(
'order_id.order_line', "order_id.order_line",
'display_type', "display_type",
'qty_to_deliver', "qty_to_deliver",
'order_id.order_line.qty_to_deliver', "order_id.order_line.qty_to_deliver",
'order_id.order_line.display_type' "order_id.order_line.display_type",
) )
def _compute_is_fully_delivered(self): def _compute_is_fully_delivered(self):
for rec in self: for rec in self:
rec.is_fully_delivered = rec._iterate_items_compute_bool( 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): def _compute_is_fully_invoiced(self):
for rec in self: for rec in self:
if not rec.is_fully_delivered: if not rec.is_fully_delivered:
rec.is_fully_delivered_and_invoiced = False rec.is_fully_delivered_and_invoiced = False
return return
rec.is_fully_delivered_and_invoiced = rec._iterate_items_compute_bool( 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): 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() 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 found = False
lines = [] 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: if line == self:
found = True found = True
continue continue
if not found: if not found:
continue 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 break
else: else:
lines.append(line) 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): def _compute_section_line_ids(self):
for rec in 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)] rec.section_line_ids = [Command.set(rec.get_section_line_ids().ids)]
else: else:
rec.section_line_ids = False rec.section_line_ids = False
@ -235,7 +268,7 @@ class SaleOrderLine(models.Model):
def _iterate_items_compute_bool(self, single_line_func): def _iterate_items_compute_bool(self, single_line_func):
if not self.display_type: if not self.display_type:
return single_line_func(self) return single_line_func(self)
elif self.display_type == 'line_note': elif self.display_type == "line_note":
return True return True
else: else:
for line in self.order_id.order_line: for line in self.order_id.order_line:
@ -244,15 +277,17 @@ class SaleOrderLine(models.Model):
found = True found = True
if not found: if not found:
continue continue
if found and line.display_type == 'line_section': if found and line.display_type == "line_section":
return True return True
val = single_line_func(self) val = single_line_func(self)
if not val: if not val:
return val return val
return True 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): def _compute_is_fsm(self):
for rec in self: for rec in self:
rec.is_fsm = (rec.product_id.detailed_type == 'service' rec.is_fsm = (
and rec.product_id.service_tracking == 'task_global_project') 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 from odoo.addons.project.models.project_task import CLOSED_STATES
import re import re
@ -34,25 +34,24 @@ class Task(models.Model):
string="Can be billed", string="Can be billed",
related=False, related=False,
compute="_compute_allow_billable", compute="_compute_allow_billable",
store=True store=True,
) )
visit_id = fields.Many2one( visit_id = fields.Many2one(
comodel_name='bemade_fsm.visit', comodel_name="bemade_fsm.visit",
string="Visit", string="Visit",
) )
relevant_order_lines = fields.Many2many( relevant_order_lines = fields.Many2many(
comodel_name='sale.order.line', comodel_name="sale.order.line",
store=False, store=False,
compute='_compute_relevant_order_lines', compute="_compute_relevant_order_lines",
) )
work_order_number = fields.Char(readonly=True) work_order_number = fields.Char(readonly=True)
propagate_assignment = fields.Boolean( 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, default=False,
) )
@ -76,18 +75,29 @@ class Task(models.Model):
rec.site_contacts = rec.parent_id.site_contacts rec.site_contacts = rec.parent_id.site_contacts
if rec.sale_order_id: if rec.sale_order_id:
seq = 1 seq = 1
prev_seqs = self.sale_order_id.tasks_ids and \ prev_seqs = (
self.sale_order_id.tasks_ids.mapped('work_order_number') self.sale_order_id.tasks_ids
and self.sale_order_id.tasks_ids.mapped("work_order_number")
)
if prev_seqs: if prev_seqs:
pattern = re.compile(r"(\d+)$") pattern = re.compile(r"(\d+)$")
matches = map(lambda n: pattern.search(n), prev_seqs) matches = map(lambda n: pattern.search(n), prev_seqs)
seq += max(map(lambda n: int(n.group(1)) if n else 0, matches)) 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) \ rec.work_order_number = (
+ f"-{seq}" 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 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 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 rec.site_contacts = rec.sale_order_id.site_contacts
return res return res
@ -95,26 +105,32 @@ class Task(models.Model):
res = super().write(vals) res = super().write(vals)
if not self: # End recursion on empty RecordSet if not self: # End recursion on empty RecordSet
return res 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 # 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']}) self.child_ids.write({"propagate_assignment": vals["propagate_assignment"]})
if 'user_ids' in vals: if "user_ids" in vals:
to_propagate = self.filtered(lambda task: task.propagate_assignment) 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 # 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 false on a child task.
to_propagate.child_ids.write({'user_ids': vals['user_ids']}) to_propagate.child_ids.write({"user_ids": vals["user_ids"]})
if 'site_contacts' in vals and self.child_ids: if "site_contacts" in vals and self.child_ids:
self._get_all_subtasks().write({'site_contacts': [Command.set(self.site_contacts.ids)]}) self._get_all_subtasks().write(
if 'work_order_contacts' in vals and self.child_ids: {"site_contacts": [Command.set(self.site_contacts.ids)]}
self._get_all_subtasks().write({'work_order_contacts': [Command.set(self.work_order_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 return res
@api.depends('sale_order_id') @api.depends("sale_order_id")
def _compute_relevant_order_lines(self): def _compute_relevant_order_lines(self):
for rec in self: for rec in self:
rec.relevant_order_lines = ( rec.relevant_order_lines = (
rec.sale_order_id and rec.sale_order_id.get_relevant_order_lines( rec.sale_order_id
rec) or False) and rec.sale_order_id.get_relevant_order_lines(rec)
or False
)
def _get_closed_stage_by_project(self): 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
@ -123,17 +139,23 @@ class Task(models.Model):
:returns: Dict of project.project -> project.task.type""" :returns: Dict of project.project -> project.task.type"""
return { return {
project: project: (
project.type_ids.filtered(lambda stage: stage.is_closed)[:1] project.type_ids.filtered(lambda stage: stage.is_closed)[:1]
or project.type_ids[-1:] or project.type_ids[-1:]
)
for project in self.project_id 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): def _compute_allow_billable(self):
for rec in 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 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 rec.allow_billable = False
else: else:
rec.allow_billable = rec.project_id.allow_billable rec.allow_billable = rec.project_id.allow_billable
@ -176,7 +198,7 @@ class Task(models.Model):
if template: if template:
title = template.name title = template.name
elif rec.sale_line_id: 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 title = name_parts and name_parts[0] or rec.sale_line_id.product_id.name
elif rec.visit_id: elif rec.visit_id:
title = rec.visit_id.label title = rec.visit_id.label
@ -187,7 +209,7 @@ class Task(models.Model):
client_name = rec.sale_order_id.partner_shipping_id.name client_name = rec.sale_order_id.partner_shipping_id.name
if not rec.parent_id: 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: else:
rec.name = title 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): class TaskTemplate(models.Model):
_name = 'project.task.template' _name = "project.task.template"
_description = "Template for new project tasks" _description = "Template for new project tasks"
@api.model @api.model
@ -11,50 +11,47 @@ class TaskTemplate(models.Model):
name = fields.Char(string="Task Title", required=True) name = fields.Char(string="Task Title", required=True)
description = fields.Html(string="Description") description = fields.Html()
assignees = fields.Many2many( assignees = fields.Many2many(
comodel_name="res.users", comodel_name="res.users",
string="Default Assignees", string="Default Assignees",
help="Employees assigned to tasks created from this template." help="Employees assigned to tasks created from this template.",
) )
customer = fields.Many2one( customer = fields.Many2one(
comodel_name="res.partner", comodel_name="res.partner",
string="Default Customer", string="Default Customer",
help="Default customer for tasks created from this template." help="Default customer for tasks created from this template.",
) )
project = fields.Many2one( project = fields.Many2one(
comodel_name="project.project", comodel_name="project.project",
string="Default 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( tags = fields.Many2many(
comodel_name="project.tags", comodel_name="project.tags",
string="Default 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( parent = fields.Many2one(
comodel_name="project.task.template", comodel_name="project.task.template",
string="Parent Task Template", string="Parent Task Template",
ondelete='cascade' ondelete="cascade",
) )
subtasks = fields.One2many( subtasks = fields.One2many(
comodel_name="project.task.template", comodel_name="project.task.template",
inverse_name="parent", inverse_name="parent",
string="Subtask Templates" string="Subtask Templates",
) )
sequence = fields.Integer(string="Sequence") sequence = fields.Integer()
company_id = fields.Many2one( company_id = fields.Many2one(
comodel_name="res.company", comodel_name="res.company", string="Company", index=1, default=_current_company
string="Company",
index=1,
default=_current_company
) )
planned_hours = fields.Float("Initially Planned Hours") planned_hours = fields.Float("Initially Planned Hours")
@ -69,32 +66,38 @@ class TaskTemplate(models.Model):
def action_open_task(self): def action_open_task(self):
return { return {
'view_mode': 'form', "view_mode": "form",
'res_model': 'project.task.template', "res_model": "project.task.template",
'res_id': self.id, "res_id": self.id,
'type': 'ir.actions.act_window', "type": "ir.actions.act_window",
'context': self._context "context": self._context,
} }
@api.onchange('customer') @api.onchange("customer")
def _onchange_customer(self): def _onchange_customer(self):
for rec in self: for rec in self:
new_equipment_ids = [eq.id for eq in rec.equipment_ids if eq.partner_location_id == rec.customer] new_equipment_ids = [
rec.write({'equipment_ids': [Command.set(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): def _prepare_new_task_values_from_self(self, project, name=False, parent_id=False):
vals = { vals = {
'project_id': project.id, "project_id": project.id,
'name': name or self.name, "name": name or self.name,
'description': self.description, "description": self.description,
'parent_id': parent_id, "parent_id": parent_id,
'user_ids': self.assignees.ids, "user_ids": self.assignees.ids,
'tag_ids': self.tags.ids, "tag_ids": self.tags.ids,
'allocated_hours': self.planned_hours, "allocated_hours": self.planned_hours,
'sequence': self.sequence, "sequence": self.sequence,
'equipment_ids': [Command.set(self.equipment_ids.ids)] if self.equipment_ids else False, "equipment_ids": (
'partner_id': project.partner_id and project.partner_id.id, [Command.set(self.equipment_ids.ids)] if self.equipment_ids else False
'company_id': self.company_id.id, ),
"partner_id": project.partner_id and project.partner_id.id,
"company_id": self.company_id.id,
} }
return vals return vals
@ -106,13 +109,10 @@ class TaskTemplate(models.Model):
:param parent_id: parent task for the new task (none by default) :param parent_id: parent task for the new task (none by default)
:return: project.task record created from this template :return: project.task record created from this template
""" """
tasks = self.env['project.task'] tasks = self.env["project.task"]
for rec in self: for rec in self:
vals = rec._prepare_new_task_values_from_self(project, name, parent_id) 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) rec.subtasks.create_task_from_self(project, name=False, parent_id=task.id)
tasks |= task tasks |= task
return tasks return tasks

View file

@ -1,16 +1,22 @@
<?xml version="1.0" encoding="UTF-8" ?> <?xml version="1.0" encoding="UTF-8" ?>
<odoo> <odoo>
<data>
<template id="workorder_page_materials_table"> <template id="workorder_page_materials_table">
<t t-set="order_lines" <t
t-set="order_lines"
t-value="doc.relevant_order_lines.filtered(lambda l: t-value="doc.relevant_order_lines.filtered(lambda l:
l.product_id.type != 'service' and not l.is_downpayment and not l.visit_id)"/> l.product_id.type != 'service' and not l.is_downpayment and not l.visit_id)"
/>
<t t-if="order_lines"> <t t-if="order_lines">
<t t-set="visit_lines" t-value="order_lines.mapped('visit_id')" /> <t t-set="visit_lines" t-value="order_lines.mapped('visit_id')" />
<t t-set="section_lines" <t
t-value="order_lines.filtered(lambda l: l.display_type == 'line_section')"/> t-set="section_lines"
t-value="order_lines.filtered(lambda l: l.display_type == 'line_section')"
/>
</t> </t>
<div t-if="order_lines.filtered(lambda l: not l.display_type)" style="page-break-inside: avoid;"> <div
t-if="order_lines.filtered(lambda l: not l.display_type)"
style="page-break-inside: avoid;"
>
<h2 t-if="order_lines">Materials</h2> <h2 t-if="order_lines">Materials</h2>
<div t-if="order_lines" class="table-responsive-sm"> <div t-if="order_lines" class="table-responsive-sm">
<table class="table table-sm o_main_table"> <table class="table table-sm o_main_table">
@ -24,18 +30,26 @@
<tbody class="sale_tbody"> <tbody class="sale_tbody">
<t t-foreach="order_lines" t-as="line"> <t t-foreach="order_lines" t-as="line">
<t t-set="is_task" t-value="line == doc.sale_line_id" /> <t t-set="is_task" t-value="line == doc.sale_line_id" />
<tr t-att-class="'bg-200 font-weight-bold o_line_section' if line.display_type == 'line_section' else 'font-italic o_line_note' if line.display_type == 'line_note' else ''"> <tr
<t t-if="not line.display_type and not line.is_downpayment"> t-att-class="'bg-200 font-weight-bold o_line_section' if line.display_type == 'line_section' else 'font-italic o_line_note' if line.display_type == 'line_note' else ''"
>
<t
t-if="not line.display_type and not line.is_downpayment"
>
<td><span t-field="line.name" /></td> <td><span t-field="line.name" /></td>
<td class="text-right"> <td class="text-right">
<span t-field="line.product_uom_qty" /> <span t-field="line.product_uom_qty" />
<span t-field="line.product_uom" <span
groups="uom.group_uom"/> t-field="line.product_uom"
groups="uom.group_uom"
/>
</td> </td>
<td class="text-right"> <td class="text-right">
<span t-field="line.qty_delivered" /> <span t-field="line.qty_delivered" />
<span t-field="line.product_uom" <span
groups="uom.group_uom"/> t-field="line.product_uom"
groups="uom.group_uom"
/>
</td> </td>
</t> </t>
<t t-if="line.display_type == 'line_section'"> <t t-if="line.display_type == 'line_section'">
@ -59,31 +73,45 @@
<t t-set="order_lines" t-value="doc.relevant_order_lines" /> <t t-set="order_lines" t-value="doc.relevant_order_lines" />
<t t-if="order_lines"> <t t-if="order_lines">
<t t-set="visit_lines" t-value="order_lines.mapped('visit_id')" /> <t t-set="visit_lines" t-value="order_lines.mapped('visit_id')" />
<t t-if="visit_lines" t-set="root_tasks" <t
t-value="visit_lines.mapped('task_id')"/> t-if="visit_lines"
<t t-else="" t-set="root_tasks" t-set="root_tasks"
t-value="order_lines.mapped('task_id').filtered(lambda l: not l.parent_id)"/> t-value="visit_lines.mapped('task_id')"
/>
<t
t-else=""
t-set="root_tasks"
t-value="order_lines.mapped('task_id').filtered(lambda l: not l.parent_id)"
/>
<t t-set="final_subtotal" t-value="0" /> <t t-set="final_subtotal" t-value="0" />
<t t-set="final_tax" t-value="0" /> <t t-set="final_tax" t-value="0" />
<t t-set="final_total" t-value="0" /> <t t-set="final_total" t-value="0" />
<t t-set="is_any_total_discount_line" t-value="False" /> <t t-set="is_any_total_discount_line" t-value="False" />
<t t-set="section_lines" <t
t-value="order_lines.filtered(lambda l: l.display_type == 'line_section')"/> t-set="section_lines"
t-value="order_lines.filtered(lambda l: l.display_type == 'line_section')"
/>
<t t-foreach="order_lines" t-as="line"> <t t-foreach="order_lines" t-as="line">
<t t-set="final_subtotal" <t
t-value="final_subtotal + line.delivered_price_subtotal"/> t-set="final_subtotal"
<t t-set="final_total" t-value="final_subtotal + line.delivered_price_subtotal"
t-value="final_total + line.delivered_price_total"/> />
<t t-set="final_tax" <t
t-value="final_tax + line.delivered_price_tax"/> t-set="final_total"
<t t-set="is_any_total_discount_line" t-value="final_total + line.delivered_price_total"
t-value="is_any_total_discount_line or (line.discount and line.price_unit != 0 and line.delivered_price_total == 0)"/> />
<t t-set="final_tax" t-value="final_tax + line.delivered_price_tax" />
<t
t-set="is_any_total_discount_line"
t-value="is_any_total_discount_line or (line.discount and line.price_unit != 0 and line.delivered_price_total == 0)"
/>
</t> </t>
<t t-set="display_discount" <t
t-value="any(line.discount for line in order_lines)"/> t-set="display_discount"
t-value="any(line.discount for line in order_lines)"
/>
</t> </t>
<t t-else="" t-set="root_tasks" <t t-else="" t-set="root_tasks" t-value="doc.root_ancestor" />
t-value="doc.root_ancestor"/>
<div t-if="order_lines.filtered(lambda l: not l.display_type)"> <div t-if="order_lines.filtered(lambda l: not l.display_type)">
<h2 t-if="order_lines">Time &amp; Material</h2> <h2 t-if="order_lines">Time &amp; Material</h2>
<div t-if="order_lines" class="table-responsive-sm"> <div t-if="order_lines" class="table-responsive-sm">
@ -94,14 +122,21 @@
<th class="text-right">Ordered</th> <th class="text-right">Ordered</th>
<th class="text-right">Delivered</th> <th class="text-right">Delivered</th>
<th class="text-right">Unit Price</th> <th class="text-right">Unit Price</th>
<th t-if="display_discount" class="text-right" <th
groups="product.group_discount_per_so_line"> t-if="display_discount"
class="text-right"
groups="product.group_discount_per_so_line"
>
<span>Disc.%</span> <span>Disc.%</span>
</th> </th>
<th class="text-right"> <th class="text-right">
<span groups="account.group_show_line_subtotals_tax_excluded"> <span
groups="account.group_show_line_subtotals_tax_excluded"
>
Amount</span> Amount</span>
<span groups="account.group_show_line_subtotals_tax_included"> <span
groups="account.group_show_line_subtotals_tax_included"
>
Total Price</span> Total Price</span>
</th> </th>
</tr> </tr>
@ -110,33 +145,49 @@
<t t-set="current_subtotal" t-value="0" /> <t t-set="current_subtotal" t-value="0" />
<t t-foreach="order_lines" t-as="line"> <t t-foreach="order_lines" t-as="line">
<t t-set="is_task" t-value="line == doc.sale_line_id" /> <t t-set="is_task" t-value="line == doc.sale_line_id" />
<t t-set="is_total_discount" <t
t-value="line.discount and line.price_unit != 0 and line.delivered_price_total == 0"/> t-set="is_total_discount"
<t t-set="current_subtotal" t-value="line.discount and line.price_unit != 0 and line.delivered_price_total == 0"
/>
<t
t-set="current_subtotal"
t-value="current_subtotal + line.delivered_price_subtotal" t-value="current_subtotal + line.delivered_price_subtotal"
groups="account.group_show_line_subtotals_tax_excluded"/> groups="account.group_show_line_subtotals_tax_excluded"
<t t-set="current_total" />
<t
t-set="current_total"
t-value="current_subtotal + line.delivered_price_total" t-value="current_subtotal + line.delivered_price_total"
groups="account.group_show_line_subtotals_tax_included"/> groups="account.group_show_line_subtotals_tax_included"
<tr t-att-class="'bg-200 font-weight-bold o_line_section' if line.display_type == 'line_section' else 'font-italic o_line_note' if line.display_type == 'line_note' else ''"> />
<t t-if="not line.display_type and not line.is_downpayment"> <tr
t-att-class="'bg-200 font-weight-bold o_line_section' if line.display_type == 'line_section' else 'font-italic o_line_note' if line.display_type == 'line_note' else ''"
>
<t
t-if="not line.display_type and not line.is_downpayment"
>
<td><span t-field="line.name" /></td> <td><span t-field="line.name" /></td>
<td class="text-right"> <td class="text-right">
<span t-field="line.product_uom_qty" /> <span t-field="line.product_uom_qty" />
<span t-field="line.product_uom" <span
groups="uom.group_uom"/> t-field="line.product_uom"
groups="uom.group_uom"
/>
</td> </td>
<td class="text-right"> <td class="text-right">
<span t-field="line.qty_delivered" /> <span t-field="line.qty_delivered" />
<span t-field="line.product_uom" <span
groups="uom.group_uom"/> t-field="line.product_uom"
groups="uom.group_uom"
/>
</td> </td>
<td class="text-right"> <td class="text-right">
<span t-field="line.price_unit" /> <span t-field="line.price_unit" />
</td> </td>
<td t-if="display_discount" <td
t-if="display_discount"
class="text-right" class="text-right"
groups="product.group_discount_per_so_line"> groups="product.group_discount_per_so_line"
>
<span t-field="line.discount" /> <span t-field="line.discount" />
</td> </td>
<td class="text-right o_price_total"> <td class="text-right o_price_total">
@ -156,7 +207,9 @@
</td> </td>
</t> </t>
</tr> </tr>
<t t-if="current_section and len(section_lines) > 1 and (line_last or order_lines[line_index+1].display_type == 'line_section')"> <t
t-if="current_section and len(section_lines) > 1 and (line_last or order_lines[line_index+1].display_type == 'line_section')"
>
<tr class="is-subtotal text-right"> <tr class="is-subtotal text-right">
<td colspan="99"> <td colspan="99">
<strong class="mr16">Section <strong class="mr16">Section
@ -174,28 +227,39 @@
</div> </div>
<div class="container_subtotal" name="so_total_summary"> <div class="container_subtotal" name="so_total_summary">
<div id="total" class="row justify-content-end" name="total"> <div id="total" class="row justify-content-end" name="total">
<div t-attf-class="#{'col-auto' if report_type != 'html' else 'col-sm-2'}"> <div
t-attf-class="#{'col-auto' if report_type != 'html' else 'col-sm-2'}"
>
<table class="table table-sm"> <table class="table table-sm">
<tr t-if="final_tax" class="border-black o_subtotal" <tr
style=""> t-if="final_tax"
class="border-black o_subtotal"
style=""
>
<td><strong>Untaxed amount</strong></td> <td><strong>Untaxed amount</strong></td>
<td class="text-right"> <td class="text-right">
<span t-esc="final_subtotal" <span
t-options='{"widget": "monetary", "display_currency": doc.sale_order_id.pricelist_id.currency_id}'/> t-esc="final_subtotal"
t-options='{"widget": "monetary", "display_currency": doc.sale_order_id.pricelist_id.currency_id}'
/>
</td> </td>
</tr> </tr>
<tr t-if="final_tax" class="border-black o_subtotal"> <tr t-if="final_tax" class="border-black o_subtotal">
<td><strong>Taxes</strong></td> <td><strong>Taxes</strong></td>
<td class="text-right"> <td class="text-right">
<span t-esc="final_tax" <span
t-options='{"widget": "monetary", "display_currency": doc.sale_order_id.pricelist_id.currency_id}'/> t-esc="final_tax"
t-options='{"widget": "monetary", "display_currency": doc.sale_order_id.pricelist_id.currency_id}'
/>
</td> </td>
</tr> </tr>
<tr t-if="final_total" class="border-black o_total"> <tr t-if="final_total" class="border-black o_total">
<td><strong>Total</strong></td> <td><strong>Total</strong></td>
<td class="text-right"> <td class="text-right">
<span t-esc="final_total" <span
t-options='{"widget": "monetary", "display_currency": doc.sale_order_id.pricelist_id.currency_id}'/> t-esc="final_total"
t-options='{"widget": "monetary", "display_currency": doc.sale_order_id.pricelist_id.currency_id}'
/>
</td> </td>
</tr> </tr>
</table> </table>
@ -223,7 +287,9 @@
<h2 t-out="str(intervention_index + 1) + '. ' + intervention.name" /> <h2 t-out="str(intervention_index + 1) + '. ' + intervention.name" />
<h3 t-if="intervention.equipment_ids">Equipment: <h3 t-if="intervention.equipment_ids">Equipment:
<t t-foreach="intervention.equipment_ids" t-as="equipment_id"> <t t-foreach="intervention.equipment_ids" t-as="equipment_id">
<span t-out="equipment_id.complete_name + (', ' if not equipment_id_last else '')"/> <span
t-out="equipment_id.complete_name + (', ' if not equipment_id_last else '')"
/>
</t> </t>
</h3> </h3>
<div> <div>
@ -270,21 +336,29 @@
<h4 t-if="doc.sale_order_id and doc.sale_order_id.client_order_ref"> <h4 t-if="doc.sale_order_id and doc.sale_order_id.client_order_ref">
Purchase Order: <span Purchase Order: <span
name="po_number" name="po_number"
t-out="doc.sale_order_id.client_order_ref"/> t-out="doc.sale_order_id.client_order_ref"
/>
</h4> </h4>
<hr /> <hr />
<div class="row" name="address_and_time"> <div class="row" name="address_and_time">
<div t-attf-class="{{('col-6' if report_type == 'pdf' else 'col-md-6 col-12') + ' mb-3'}}"> <div
t-attf-class="{{('col-6' if report_type == 'pdf' else 'col-md-6 col-12') + ' mb-3'}}"
>
<t t-if="doc.partner_id"> <t t-if="doc.partner_id">
<div><h6>Customer: </h6></div> <div><h6>Customer: </h6></div>
<div t-esc="doc.partner_id" t-options='{ <div
t-esc="doc.partner_id"
t-options='{
"widget": "contact", "widget": "contact",
"fields": ["name", "address",] "fields": ["name", "address",]
}'/> }'
/>
</t> </t>
</div> </div>
<div t-attf-class="{{('col-6' if report_type == 'pdf' else 'col-md-6 col-12') + ' mb-3'}}" <div
t-if="doc.planned_date_begin or doc.date_deadline"> t-attf-class="{{('col-6' if report_type == 'pdf' else 'col-md-6 col-12') + ' mb-3'}}"
t-if="doc.planned_date_begin or doc.date_deadline"
>
<div t-if="doc.planned_date_begin"><h6>Planned start: </h6></div> <div t-if="doc.planned_date_begin"><h6>Planned start: </h6></div>
<div class="mb-3"> <div class="mb-3">
<div t-out="doc.planned_date_begin.strftime('%Y-%m-%d %H:%M')" /> <div t-out="doc.planned_date_begin.strftime('%Y-%m-%d %H:%M')" />
@ -296,32 +370,46 @@
</div> </div>
</div> </div>
<div class="row" name="site_and_work_order_contacts"> <div class="row" name="site_and_work_order_contacts">
<div t-attf-class="{{'col-6' if report_type == 'pdf' else 'col-md-6 col-12'}}"> <div
t-attf-class="{{'col-6' if report_type == 'pdf' else 'col-md-6 col-12'}}"
>
<div t-if="doc.site_contacts"><h6>Site Contacts: </h6></div> <div t-if="doc.site_contacts"><h6>Site Contacts: </h6></div>
<t t-foreach="doc.site_contacts" t-as="contact"> <t t-foreach="doc.site_contacts" t-as="contact">
<div class="mb-3"> <div class="mb-3">
<div t-esc="contact" t-options='{ <div
t-esc="contact"
t-options='{
"widget": "contact", "widget": "contact",
"fields": ["name", "phone", "email"] "fields": ["name", "phone", "email"]
}'/> }'
/>
</div> </div>
</t> </t>
</div> </div>
<div t-attf-class="{{'col-6' if report_type == 'pdf' else 'col-md-6 col-12'}}"> <div
t-attf-class="{{'col-6' if report_type == 'pdf' else 'col-md-6 col-12'}}"
>
<div t-if="doc.work_order_contacts"><h6>Work Order <div t-if="doc.work_order_contacts"><h6>Work Order
Contacts: </h6></div> Contacts: </h6></div>
<t t-foreach="doc.work_order_contacts" t-as="contact"> <t t-foreach="doc.work_order_contacts" t-as="contact">
<div class="mb-3"> <div class="mb-3">
<div t-esc="contact" t-options='{ <div
t-esc="contact"
t-options='{
"widget": "contact", "widget": "contact",
"fields": ["name", "phone", "email"] "fields": ["name", "phone", "email"]
}'/> }'
/>
</div> </div>
</t> </t>
</div> </div>
</div> </div>
<hr /> <hr />
<div t-if="not is_html_empty(doc.description)" class="row" name="visit_description"> <div
t-if="not is_html_empty(doc.description)"
class="row"
name="visit_description"
>
<div class="col-12"> <div class="col-12">
<span t-out="doc.description" /> <span t-out="doc.description" />
</div> </div>
@ -329,8 +417,10 @@
<hr t-if="not is_html_empty(doc.description)" /> <hr t-if="not is_html_empty(doc.description)" />
</template> </template>
<template id="workorder_equipment_summary"> <template id="workorder_equipment_summary">
<div t-if="doc.equipment_ids and len(doc.child_ids) > 1" <div
style="page-break-inside: avoid;"> t-if="doc.equipment_ids and len(doc.child_ids) > 1"
style="page-break-inside: avoid;"
>
<h3>Equipment Serviced</h3> <h3>Equipment Serviced</h3>
<div class="table-responsive-sm"> <div class="table-responsive-sm">
<table class="table table-sm o_main_table overflow-hidden"> <table class="table table-sm o_main_table overflow-hidden">
@ -348,12 +438,14 @@
<td><span t-esc="equipment.pid_tag" /></td> <td><span t-esc="equipment.pid_tag" /></td>
<td><span t-esc="equipment.name" /></td> <td><span t-esc="equipment.name" /></td>
<td> <td>
<t t-foreach="equipment.tag_ids" <t t-foreach="equipment.tag_ids" t-as="application">
t-as="application"> <span
<span t-esc="application.name + (', ' if not application_last else '')"/> t-esc="application.name + (', ' if not application_last else '')"
/>
</t> </t>
</td> </td>
<td t-esc="equipment.partner_location_id" <td
t-esc="equipment.partner_location_id"
t-options='{ t-options='{
"widget": "contact", "widget": "contact",
"fields": ["name", "address"]}' "fields": ["name", "address"]}'
@ -391,7 +483,8 @@
<span t-field="timesheet.name" /> <span t-field="timesheet.name" />
</td> </td>
<td> <td>
<span t-esc="timesheet.unit_amount" <span
t-esc="timesheet.unit_amount"
t-options="{ t-options="{
'widget': 'duration', 'widget': 'duration',
'digital': True, 'digital': True,
@ -408,14 +501,19 @@
</template> </template>
<template id="workorder_page_signature_block"> <template id="workorder_page_signature_block">
<div t-if="doc.worksheet_signature"> <div t-if="doc.worksheet_signature">
<div t-if="report_type == html" class="ribbon" style=" <div
t-if="report_type == html"
class="ribbon"
style="
position: absolute; position: absolute;
right: 0px; top: 0px; right: 0px; top: 0px;
z-index: 1; z-index: 1;
overflow: hidden; overflow: hidden;
width: 75px; height: 75px; width: 75px; height: 75px;
text-align: right;"> text-align: right;"
<span style=" >
<span
style="
font-size: 10px; font-size: 10px;
color: #fff; color: #fff;
text-transform: uppercase; text-transform: uppercase;
@ -426,16 +524,21 @@
background: green; background: green;
position: absolute; position: absolute;
top: 19px; right: -21px; left: auto; top: 19px; right: -21px; left: auto;
padding: 0;"> padding: 0;"
>
Signed Signed
</span> </span>
</div> </div>
<div t-attf-class="#{' col-12 col-lg-3' if report_type != 'html' else ' <div
t-attf-class="#{' col-12 col-lg-3' if report_type != 'html' else '
col-sm-7 col-md-4'} ml-auto text-right" col-sm-7 col-md-4'} ml-auto text-right"
style="page-break-inside: avoid"> style="page-break-inside: avoid"
>
<h5>Signature</h5> <h5>Signature</h5>
<img t-att-src="image_data_uri(doc.worksheet_signature)" <img
style="max-height: 6rem; max-width: 100%; color:black;"/><br/> t-att-src="image_data_uri(doc.worksheet_signature)"
style="max-height: 6rem; max-width: 100%; color:black;"
/><br />
<span t-field="doc.worksheet_signed_by" /> <span t-field="doc.worksheet_signed_by" />
</div> </div>
</div> </div>
@ -448,7 +551,9 @@
<t t-call="bemade_fsm.workorder_page_materials_table" /> <t t-call="bemade_fsm.workorder_page_materials_table" />
</t> </t>
<t t-else=""> <t t-else="">
<t t-call="bemade_fsm.workorder_page_sale_order_table_with_pricing_and_labour"/> <t
t-call="bemade_fsm.workorder_page_sale_order_table_with_pricing_and_labour"
/>
</t> </t>
<t t-call="bemade_fsm.workorder_equipment_summary" /> <t t-call="bemade_fsm.workorder_equipment_summary" />
<t t-call="bemade_fsm.workorder_page_signature_block" /> <t t-call="bemade_fsm.workorder_page_signature_block" />
@ -460,18 +565,24 @@
<t t-foreach="docs" t-as="doc"> <t t-foreach="docs" t-as="doc">
<t t-set="doc" t-value="doc.root_ancestor" t-if="doc.parent_id" /> <t t-set="doc" t-value="doc.root_ancestor" t-if="doc.parent_id" />
<t t-call="web.external_layout"> <t t-call="web.external_layout">
<t t-call="bemade_fsm.work_order_page" <t
t-lang="doc.partner_id.lang"/> t-call="bemade_fsm.work_order_page"
t-lang="doc.partner_id.lang"
/>
</t> </t>
</t> </t>
</t> </t>
</template> </template>
<template id="worksheet_custom" <template
inherit_id="industry_fsm_report.worksheet_custom"> id="worksheet_custom"
<xpath expr="//t[@t-call='industry_fsm_report.worksheet_custom_page']" inherit_id="industry_fsm_report.worksheet_custom"
position="replace"> priority="100"
>
<xpath
expr="//t[@t-call='industry_fsm_report.worksheet_custom_page']"
position="replace"
>
<div t-call="bemade_fsm.work_order_page" /> <div t-call="bemade_fsm.work_order_page" />
</xpath> </xpath>
</template> </template>
</data>
</odoo> </odoo>

View file

@ -6,6 +6,8 @@ class TaskCustomReport(models.AbstractModel):
def _get_report_values(self, docids, data=None): def _get_report_values(self, docids, data=None):
vals = super()._get_report_values(docids, data) 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}) vals.update({"split_time_materials": split_time_materials})
return vals return vals

View file

@ -1,12 +1,11 @@
<?xml version="1.0" encoding="utf-8" ?> <?xml version="1.0" encoding="utf-8" ?>
<odoo> <odoo>
<data>
<record id="industry_fsm_report.task_custom_report" model="ir.actions.report"> <record id="industry_fsm_report.task_custom_report" model="ir.actions.report">
<field name="print_report_name">'%s %s' % ( <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.planned_date_begin.strftime(
'%Y-%m-%d') if object.planned_date_begin else time.strftime('%Y-%m-%d'),
object.name object.name
) )
</field> </field>
</record> </record>
</data>
</odoo> </odoo>

View file

@ -7,8 +7,8 @@ class BemadeFSMBaseTest(TransactionCase):
@classmethod @classmethod
def _generate_project_manager_user(cls, name, login): def _generate_project_manager_user(cls, name, login):
group_ids = cls.__get_user_groups() group_ids = cls.__get_user_groups()
user_group_project_manager = cls.env.ref('project.group_project_manager') 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_fsm_manager = cls.env.ref("industry_fsm.group_fsm_manager")
group_ids.append(user_group_fsm_manager.id) group_ids.append(user_group_fsm_manager.id)
group_ids.append(user_group_project_manager.id) group_ids.append(user_group_project_manager.id)
@ -21,117 +21,169 @@ class BemadeFSMBaseTest(TransactionCase):
@classmethod @classmethod
def __generate_user(cls, name, login, group_ids): def __generate_user(cls, name, login, group_ids):
return cls.env['res.users'].with_context({'no_reset_password': True}).create({ return (
'name': name, cls.env["res.users"]
'login': login, .with_context(no_reset_password=True)
'password': login, .create(
'email': f"{login}@test.co", {
'groups_id': [Command.set(group_ids)] "name": name,
}) "login": login,
"password": login,
"email": f"{login}@test.co",
"groups_id": [Command.set(group_ids)],
}
)
)
@classmethod @classmethod
def __get_user_groups(cls): def __get_user_groups(cls):
user_group_employee = cls.env.ref('base.group_user') user_group_employee = cls.env.ref("base.group_user")
user_group_project_user = cls.env.ref('project.group_project_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_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_user = cls.env.ref("sales_team.group_sale_salesman")
user_group_sales_manager = cls.env.ref('sales_team.group_sale_manager') user_group_sales_manager = cls.env.ref("sales_team.group_sale_manager")
user_product_customer = cls.env.ref( user_product_customer = cls.env.ref(
'customer_product_code.group_product_customer_code_user', "customer_product_code.group_product_customer_code_user",
raise_if_not_found=False raise_if_not_found=False,
) )
group_ids = [user_group_employee.id, group_ids = [
user_group_employee.id,
user_group_project_user.id, user_group_project_user.id,
user_group_fsm_user.id, user_group_fsm_user.id,
user_group_sales_manager.id, user_group_sales_manager.id,
user_group_sales_user.id, ] user_group_sales_user.id,
]
if user_product_customer: if user_product_customer:
group_ids.append(user_product_customer.id) group_ids.append(user_product_customer.id)
return group_ids return group_ids
@classmethod @classmethod
def _generate_partner(cls, name: str = 'Test Company', company_type: str = 'company', parent=None, def _generate_partner(
location_type='other'): cls,
name: str = "Test Company",
company_type: str = "company",
parent=None,
location_type="other",
):
"""Generates a partner with basic address filled in. """Generates a partner with basic address filled in.
:param name: The partner's name. :param name: The partner's name.
:param company_type: The type of partner, either 'company' or 'person' are accepted.""" :param company_type: The type of partner, either 'company' or 'person' are accepted.
return cls.env['res.partner'].create({ """
'name': name, return cls.env["res.partner"].create(
'company_type': company_type, {
'street': '123 Street St.', "name": name,
'city': 'Montreal', "company_type": company_type,
'state_id': cls.env.ref('base.state_ca_qc').id, "street": "123 Street St.",
'country_id': cls.env.ref('base.ca').id, "city": "Montreal",
'parent_id': parent and parent.id or False, "state_id": cls.env.ref("base.state_ca_qc").id,
'type': location_type, "country_id": cls.env.ref("base.ca").id,
}) "parent_id": parent and parent.id or False,
"type": location_type,
}
)
@classmethod @classmethod
def _generate_sale_order(cls, partner=None, client_order_ref='Test Order', equipment=None, shipping_location=None): def _generate_sale_order(
cls,
partner=None,
client_order_ref="Test Order",
equipment=None,
shipping_location=None,
):
partner = partner or cls._generate_partner() partner = partner or cls._generate_partner()
vals = {'partner_id': partner.id, vals = {"partner_id": partner.id, "client_order_ref": client_order_ref}
'client_order_ref': client_order_ref}
if equipment: if equipment:
vals.update({'default_equipment_ids': [Command.set([equipment.id])]}) vals.update({"default_equipment_ids": [Command.set([equipment.id])]})
if shipping_location: if shipping_location:
vals.update({'partner_shipping_id': shipping_location.id}) vals.update({"partner_shipping_id": shipping_location.id})
return cls.env['sale.order'].create(vals) return cls.env["sale.order"].create(vals)
@classmethod @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: if not product:
product = cls._generate_product() product = cls._generate_product()
return cls.env['sale.order.line'].create({ return cls.env["sale.order.line"].create(
'order_id': sale_order.id, {
'product_id': product.id, "order_id": sale_order.id,
'product_uom_qty': qty, "product_id": product.id,
'product_uom': uom and uom.id or cls.env.ref("uom.product_uom_hour").id, "product_uom_qty": qty,
'price_unit': price, "product_uom": uom and uom.id or cls.env.ref("uom.product_uom_hour").id,
'tax_id': tax_id, "price_unit": price,
}) "tax_id": tax_id,
}
)
@classmethod @classmethod
def _generate_equipment(cls, name='test equipment', partner=None): def _generate_equipment(cls, name="test equipment", partner=None):
return cls.env['bemade_fsm.equipment'].create({ return cls.env["bemade_fsm.equipment"].create(
'name': name, {
'partner_location_id': partner and partner.id or False, "name": name,
}) "partner_location_id": partner and partner.id or False,
}
)
@classmethod @classmethod
def _generate_product(cls, name='Test Product', product_type='service', service_tracking='task_global_project', def _generate_product(
project=None, task_template=None, service_policy='delivered_manual', uom=None): cls,
if 'project' in service_tracking and not project: 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") 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 uom_id = uom and uom.id or cls.env.ref("uom.product_uom_hour").id or False
return cls.env['product.product'].create({ return cls.env["product.product"].create(
'name': name, {
'type': product_type, "name": name,
'service_tracking': service_tracking, "type": product_type,
'service_type': 'timesheet', "service_tracking": service_tracking,
'project_id': service_tracking in ('task_global_project', 'project_only') and project.id or False, "service_type": "timesheet",
'project_template_id': service_tracking == 'task_in_project' and project.id or False, "project_id": (
'task_template_id': task_template and task_template.id or False, service_tracking in ("task_global_project", "project_only")
'service_policy': service_policy, and project.id
'uom_id': uom_id, or False
'uom_po_id': uom_id, ),
}) "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 @classmethod
def _generate_fsm_project(cls, name='Test Project'): def _generate_fsm_project(cls, name="Test Project"):
return cls.env['project.project'].create({ return cls.env["project.project"].create(
'name': name, {
'allow_material': True, "name": name,
'allow_timesheets': True, "allow_material": True,
'allow_quotations': True, "allow_timesheets": True,
'allow_worksheets': True, "allow_quotations": True,
'is_fsm': True, "allow_worksheets": True,
}) "is_fsm": True,
}
)
@classmethod @classmethod
def _generate_task_template(cls, parent=None, structure=None, names=None, planned_hours=1, def _generate_task_template(
equipment=None, customer=None): cls,
parent=None,
structure=None,
names=None,
planned_hours=1,
equipment=None,
customer=None,
):
"""Generates a task template with the specified structure and naming. """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 parent: The parent task template for the top-level task template being generated
@ -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 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. 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 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: if not names:
names = ['Task Template'] names = ["Task Template"]
if not structure: if not structure:
structure = [] structure = []
if len(structure) != len(names) - 1: if len(structure) != len(names) - 1:
raise ValueError("The length of the structure argument must contain exactly one element less than the " raise ValueError(
"names argument.") "The length of the structure argument must contain exactly one element"
" less than the names argument."
)
name = names.pop(0) name = names.pop(0)
template = cls.env['project.task.template'].create({ template = cls.env["project.task.template"].create(
'name': name, {
'parent': parent and parent.id or False, "name": name,
'planned_hours': planned_hours, "parent": parent and parent.id or False,
'equipment_ids': [Command.set(equipment and [equipment.id] or [])], "planned_hours": planned_hours,
'customer': customer and customer.id or False, "equipment_ids": [Command.set(equipment and [equipment.id] or [])],
}) "customer": customer and customer.id or False,
}
)
parent = template parent = template
while structure: while structure:
subtasks = [] subtasks = []
for i in range(0, structure[0]): for i in range(0, structure[0]):
subtasks.append(cls.env['project.task.template'].create({ subtasks.append(
'parent': parent.id, cls.env["project.task.template"].create(
'name': names[0] + f" {i}", {
})) "parent": parent.id,
"name": names[0] + f" {i}",
}
)
)
structure.pop(0) structure.pop(0)
names.pop(0) names.pop(0)
parent = subtasks[0] parent = subtasks[0]
return template return template
def _invoice_sale_order(self, so): def _invoice_sale_order(self, so):
wiz = self.env['sale.advance.payment.inv'].with_context( wiz = (
{'active_ids': [so.id]}).create({}) self.env["sale.advance.payment.inv"]
.with_context(active_ids=[so.id])
.create({})
)
wiz.create_invoices() wiz.create_invoices()
inv = so.invoice_ids[-1] inv = so.invoice_ids[-1]
inv.action_post() inv.action_post()
return inv return inv
def _generate_visit(self, sale_order, label="Test Label"): def _generate_visit(self, sale_order, label="Test Label"):
return self.env['bemade_fsm.visit'].create([{ return self.env["bemade_fsm.visit"].create(
'sale_order_id': sale_order.id, [
'label': label, {
}]) "sale_order_id": sale_order.id,
"label": label,
}
]
)
def _generate_so_with_one_visit_two_lines(self): def _generate_so_with_one_visit_two_lines(self):
so = self._generate_sale_order() so = self._generate_sale_order()
@ -198,8 +266,10 @@ class BemadeFSMBaseTest(TransactionCase):
def _generate_so_with_one_visit_two_lines_and_descendants(self): def _generate_so_with_one_visit_two_lines_and_descendants(self):
so = self._generate_sale_order() so = self._generate_sale_order()
visit = self._generate_visit(sale_order=so) visit = self._generate_visit(sale_order=so)
task_template = self._generate_task_template(structure=[2, 2, 2], task_template = self._generate_task_template(
names=['Parent', 'Child', 'Grandchild', 'Great-grandchild']) structure=[2, 2, 2],
names=["Parent", "Child", "Grandchild", "Great-grandchild"],
)
product = self._generate_product(task_template=task_template) product = self._generate_product(task_template=task_template)
sol1 = self._generate_sale_order_line(sale_order=so, product=product) sol1 = self._generate_sale_order_line(sale_order=so, product=product)
sol2 = 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 .test_bemade_fsm_common import BemadeFSMBaseTest
from odoo import Command from odoo import Command
from odoo.exceptions import MissingError from odoo.exceptions import MissingError
@ -8,14 +8,14 @@ from odoo.exceptions import MissingError
class TestEquipment(BemadeFSMBaseTest): class TestEquipment(BemadeFSMBaseTest):
def test_crud(self): def test_crud(self):
partner_company = self._generate_partner() partner_company = self._generate_partner()
partner_contact = self._generate_partner('Site Contact', 'person', partner_company) self._generate_partner("Site Contact", "person", partner_company)
equipment = self._generate_equipment('Test Equipment 1', partner_company) equipment = self._generate_equipment("Test Equipment 1", partner_company)
# Just make sure the basic ORM stuff is OK # Just make sure the basic ORM stuff is OK
self.assertTrue(equipment in partner_company.equipment_ids) self.assertTrue(equipment in partner_company.equipment_ids)
self.assertTrue(len(partner_company.equipment_ids) == 1) self.assertTrue(len(partner_company.equipment_ids) == 1)
# Delete should cascade # Delete should cascade
partner_company.write({'equipment_ids': [Command.set([])]}) partner_company.write({"equipment_ids": [Command.set([])]})
with self.assertRaises(MissingError): 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 odoo import Command
from .test_bemade_fsm_common import BemadeFSMBaseTest from .test_bemade_fsm_common import BemadeFSMBaseTest
@tagged("-at_install", "post_install") @tagged("-at_install", "post_install")
class SaleOrderFSMContactsCase(BemadeFSMBaseTest): class SaleOrderFSMContactsCase(BemadeFSMBaseTest):
def test_site_contacts(self): def test_site_contacts(self):
parent_co = self._generate_partner('Parent Co') parent_co = self._generate_partner("Parent Co")
contact_1 = self._generate_partner('Contact 1', 'person', parent_co) contact_1 = self._generate_partner("Contact 1", "person", parent_co)
contact_2 = self._generate_partner('Contact 2', 'person', parent_co) contact_2 = self._generate_partner("Contact 2", "person", parent_co)
# Make sure the SO pulls the defaults from the partner correctly # 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) so = self._generate_sale_order(parent_co)
self.assertTrue(so.site_contacts == parent_co.site_contacts) 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 # 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_1 in so.site_contacts)
self.assertTrue(contact_2 not 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): def test_default_workorder_contacts(self):
parent_co = self._generate_partner('Parent Co') parent_co = self._generate_partner("Parent Co")
contact_1 = self._generate_partner('Contact 1', 'person', parent_co) contact_1 = self._generate_partner("Contact 1", "person", parent_co)
contact_2 = self._generate_partner('Contact 2', 'person', parent_co) contact_2 = self._generate_partner("Contact 2", "person", parent_co)
# Make sure the SO pulls the defaults from the partner correctly # 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) so = self._generate_sale_order(parent_co)
self.assertTrue(contact_1 in parent_co.work_order_contacts) self.assertTrue(contact_1 in parent_co.work_order_contacts)
self.assertTrue(contact_2 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) 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 # 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_1 in so.work_order_contacts)
self.assertTrue(contact_2 not in so.work_order_contacts) self.assertTrue(contact_2 not in so.work_order_contacts)
self.assertTrue( 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): def test_multilayer_site_contacts(self):
parent_co = self._generate_partner('Parent Co') parent_co = self._generate_partner("Parent Co")
shipping_location = self._generate_partner('Shipping Location', 'company', parent_co, 'delivery') shipping_location = self._generate_partner(
wo_contact_1 = self._generate_partner('WO Contact 1', 'person', shipping_location) "Shipping Location", "company", parent_co, "delivery"
wo_contact_2 = self._generate_partner('WO Contact 2', 'person', shipping_location) )
site_contact_1 = self._generate_partner('Site Contact 1', 'person', shipping_location) wo_contact_1 = self._generate_partner(
site_contact_2 = self._generate_partner('Site Contact 2', 'person', shipping_location) "WO Contact 1", "person", shipping_location
shipping_location.write({ )
'work_order_contacts': [Command.set([wo_contact_1.id, wo_contact_2.id])], wo_contact_2 = self._generate_partner(
'site_contacts': [Command.set([site_contact_1.id, site_contact_2.id])] "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 = 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.site_contacts, shipping_location.site_contacts)
self.assertEqual(so.work_order_contacts, shipping_location.work_order_contacts) self.assertEqual(so.work_order_contacts, shipping_location.work_order_contacts)
def test_onchange_shipping_address(self): def test_onchange_shipping_address(self):
self.env.user.groups_id += self.env.ref('account.group_delivery_invoice_address') self.env.user.groups_id += self.env.ref(
parent_co = self._generate_partner('Parent Co') "account.group_delivery_invoice_address"
shipping_location = self._generate_partner('Shipping Location', 'company', parent_co, 'delivery') )
wo_contact_1 = self._generate_partner('WO Contact 1', 'person', shipping_location) parent_co = self._generate_partner("Parent Co")
wo_contact_2 = self._generate_partner('WO Contact 2', 'person', shipping_location) shipping_location = self._generate_partner(
site_contact_1 = self._generate_partner('Site Contact 1', 'person', shipping_location) "Shipping Location", "company", parent_co, "delivery"
site_contact_2 = self._generate_partner('Site Contact 2', 'person', shipping_location) )
shipping_location.write({ wo_contact_1 = self._generate_partner(
'work_order_contacts': [Command.set([wo_contact_1.id, wo_contact_2.id])], "WO Contact 1", "person", shipping_location
'site_contacts': [Command.set([site_contact_1.id, site_contact_2.id])] )
}) 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 = 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 .test_bemade_fsm_common import BemadeFSMBaseTest
from datetime import date, timedelta from datetime import date, timedelta
@tagged('-at_install', 'post_install') @tagged("-at_install", "post_install")
class FSMVisitTest(BemadeFSMBaseTest): class FSMVisitTest(BemadeFSMBaseTest):
def test_create_visit_sets_name_on_section(self): def test_create_visit_sets_name_on_section(self):
so = self._generate_sale_order() so = self._generate_sale_order()
self._generate_sale_order_line(sale_order=so) self._generate_sale_order_line(sale_order=so)
@ -38,7 +37,7 @@ class FSMVisitTest(BemadeFSMBaseTest):
visit = self._generate_visit(so) visit = self._generate_visit(so)
self._generate_sale_order_line(so) self._generate_sale_order_line(so)
so.action_confirm() so.action_confirm()
task = so.order_line.filtered(lambda l: l.task_id).task_id task = so.order_line.filtered(lambda line: line.task_id).task_id
task.action_fsm_validate() task.action_fsm_validate()
@ -49,7 +48,7 @@ class FSMVisitTest(BemadeFSMBaseTest):
visit = self._generate_visit(so) visit = self._generate_visit(so)
self._generate_sale_order_line(so) self._generate_sale_order_line(so)
so.action_confirm() so.action_confirm()
task = so.order_line.filtered(lambda l: l.task_id).task_id task = so.order_line.filtered(lambda line: line.task_id).task_id
task.action_fsm_validate() task.action_fsm_validate()
self._invoice_sale_order(so) self._invoice_sale_order(so)
@ -65,7 +64,10 @@ class FSMVisitTest(BemadeFSMBaseTest):
self.assertTrue(visit_task) self.assertTrue(visit_task)
visit_subtasks = visit_task.child_ids visit_subtasks = visit_task.child_ids
self.assertTrue( 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): def test_visit_task_gets_correct_due_date_on_confirmation(self):
so, visit, sol1, sol2 = self._generate_so_with_one_visit_two_lines() 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) self.assertEqual(visit_task.allocated_hours, 8.0)
def test_adding_visit_creates_one_sale_order_line(self): def test_adding_visit_creates_one_sale_order_line(self):
partner = self._generate_partner() self._generate_partner()
so = self._generate_sale_order() so = self._generate_sale_order()
self._generate_sale_order_line(sale_order=so) self._generate_sale_order_line(sale_order=so)
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) self.assertEqual(len(so.order_line), 3)
def test_marking_visit_task_done_completes_descendants(self): 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() so.action_confirm()
parent = visit.task_id parent = visit.task_id
@ -133,5 +140,5 @@ class FSMVisitTest(BemadeFSMBaseTest):
so.action_confirm() so.action_confirm()
task = visit.task_id 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) self.assertEqual(task.name, supposed_name)

View file

@ -1,13 +1,14 @@
from .test_task_template import BemadeFSMBaseTest 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 from odoo import Command
@tagged("-at_install", "post_install") @tagged("-at_install", "post_install")
class TestSalesOrder(BemadeFSMBaseTest): class TestSalesOrder(BemadeFSMBaseTest):
@tagged('-at_install', 'post_install') @tagged("-at_install", "post_install")
def test_order_confirmation_simple_template(self): 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() partner = self._generate_partner()
so = self._generate_sale_order(partner=partner) so = self._generate_sale_order(partner=partner)
task_template = self._generate_task_template(planned_hours=8) task_template = self._generate_task_template(planned_hours=8)
@ -24,10 +25,10 @@ class TestSalesOrder(BemadeFSMBaseTest):
def test_task_template_tree_order_confirmation(self): def test_task_template_tree_order_confirmation(self):
partner = self._generate_partner() partner = self._generate_partner()
so = self._generate_sale_order(partner=partner) so = self._generate_sale_order(partner=partner)
parent_template = self._generate_task_template(structure=[2, 1], parent_template = self._generate_task_template(
names=['Parent Template', structure=[2, 1],
'Child Template', names=["Parent Template", "Child Template", "Grandchild Template"],
'Grandchild Template']) )
child_template_1 = parent_template.subtasks[0] child_template_1 = parent_template.subtasks[0]
child_template_2 = parent_template.subtasks[1] child_template_2 = parent_template.subtasks[1]
grandchild_template = parent_template.subtasks[0].subtasks[0] grandchild_template = parent_template.subtasks[0].subtasks[0]
@ -70,13 +71,17 @@ class TestSalesOrder(BemadeFSMBaseTest):
for i in range(5): for i in range(5):
self._generate_equipment(partner=partner) self._generate_equipment(partner=partner)
sale_order = self._generate_sale_order( sale_order = self._generate_sale_order(
partner=partner) # No default equipment since more than 3 on partner partner=partner
sol1, sol2, sol3 = [self._generate_sale_order_line(sale_order=sale_order) for i ) # No default equipment since more than 3 on partner
in range(3)] sol1, sol2, sol3 = [
self._generate_sale_order_line(sale_order=sale_order) for _ in range(3)
]
sol1.equipment_ids = [ 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 = [ 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() sale_order.action_confirm()
@ -85,7 +90,8 @@ class TestSalesOrder(BemadeFSMBaseTest):
self.assertEqual(sol3.equipment_ids, sol3.task_id.equipment_ids) self.assertEqual(sol3.equipment_ids, sol3.task_id.equipment_ids)
def test_task_template_with_equipment_flow(self): 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() partner = self._generate_partner()
equipment = self._generate_equipment(partner=partner) equipment = self._generate_equipment(partner=partner)
so = self._generate_sale_order(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) self.assertEqual(sol.task_id.equipment_ids[0], equipment)
def test_sale_order_line_gets_default_equipment(self): 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() partner = self._generate_partner()
self._generate_equipment(partner=partner) self._generate_equipment(partner=partner)
sale_order = self._generate_sale_order(partner=partner) sale_order = self._generate_sale_order(partner=partner)
@ -126,7 +133,9 @@ class TestSalesOrder(BemadeFSMBaseTest):
self.assertEqual(sale_order.default_equipment_ids, parent.owned_equipment_ids) 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() parent = self._generate_partner()
child = self._generate_partner(parent=parent) child = self._generate_partner(parent=parent)
for i in range(4): for i in range(4):
@ -150,7 +159,7 @@ class TestSalesOrder(BemadeFSMBaseTest):
def test_sale_order_prioritize_shipping_location_equipments(self): def test_sale_order_prioritize_shipping_location_equipments(self):
parent = self._generate_partner() 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=parent)
self._generate_equipment(partner=child) self._generate_equipment(partner=child)
@ -171,11 +180,13 @@ class TestSalesOrder(BemadeFSMBaseTest):
self.assertEqual(line.equipment_ids, partner.equipment_ids) self.assertEqual(line.equipment_ids, partner.equipment_ids)
def test_task_mark_done(self): 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() partner = self._generate_partner()
so = self._generate_sale_order(partner=partner) so = self._generate_sale_order(partner=partner)
task_template = self._generate_task_template(structure=[2], task_template = self._generate_task_template(
names=["Parent Task", "Subtask"]) structure=[2], names=["Parent Task", "Subtask"]
)
product = self._generate_product(task_template=task_template) product = self._generate_product(task_template=task_template)
sol = self._generate_sale_order_line(so, product=product) sol = self._generate_sale_order_line(so, product=product)
so.action_confirm() so.action_confirm()
@ -186,20 +197,24 @@ class TestSalesOrder(BemadeFSMBaseTest):
subtasks.action_fsm_validate(True) subtasks.action_fsm_validate(True)
self.assertEqual(sol.qty_delivered, 0) 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) parent_task.action_fsm_validate(True)
self.assertTrue(sol.qty_delivered != 0) self.assertTrue(sol.qty_delivered != 0)
def test_task_contacts_through_sale_order(self): 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() partner = self._generate_partner()
contact1 = self._generate_partner('Site contact', 'person', partner) contact1 = self._generate_partner("Site contact", "person", partner)
contact2 = self._generate_partner('Work order contact', 'person', partner) contact2 = self._generate_partner("Work order contact", "person", partner)
partner.write({ partner.write(
'site_contacts': [Command.set([contact1.id])], {
'work_order_contacts': [Command.set([contact2.id])], "site_contacts": [Command.set([contact1.id])],
}) "work_order_contacts": [Command.set([contact2.id])],
}
)
so = self._generate_sale_order(partner) so = self._generate_sale_order(partner)
product = self._generate_product() product = self._generate_product()
sol = self._generate_sale_order_line(sale_order=so, product=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): def test_tasks_created_at_order_confirmation_have_no_assignees(self):
so, visit, sol1, sol2 = self._generate_so_with_one_visit_two_lines() 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 # We test as a specific user since testing as root may not produce the error
so.with_user(user).action_confirm() so.with_user(user).action_confirm()
@ -228,9 +243,11 @@ class TestSalesOrder(BemadeFSMBaseTest):
def test_long_line_name_overflows_to_task_description(self): def test_long_line_name_overflows_to_task_description(self):
so = self._generate_sale_order() so = self._generate_sale_order()
product = self._generate_product() product = self._generate_product()
product.description_sale = "This is a long product description.\n" \ product.description_sale = (
"It even spans multiple lines.\n" \ "This is a long product description.\n"
"It even spans multiple lines.\n"
"One could find this annoying in a task name." "One could find this annoying in a task name."
)
sol = self._generate_sale_order_line(sale_order=so, product=product) 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("It even spans multiple lines." in task.name)
self.assertFalse("One could find this annoying in a task name." 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("It even spans multiple lines." in task.description)
self.assertTrue("One could find this annoying in a task name." self.assertTrue(
in task.description) "One could find this annoying in a task name." in task.description
)
def test_subtask_templates_no_description_if_blank_on_template(self): def test_subtask_templates_no_description_if_blank_on_template(self):
so = self._generate_sale_order() 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.description = ""
template.subtasks[0].description = "Some fixed description" template.subtasks[0].description = "Some fixed description"
for t in template.subtasks[1:]: for t in template.subtasks[1:]:
@ -257,13 +277,15 @@ class TestSalesOrder(BemadeFSMBaseTest):
so.action_confirm() so.action_confirm()
task = sol.task_id 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:]: for t in task.child_ids[1:]:
self.assertFalse(t.description) self.assertFalse(t.description)
def test_duplicate_sale_order_duplicates_visits(self): 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 """Duplicated sales orders should have visits tied to their SO lines as in the
should not have approximate dates set, however.""" original. The copied visits should not have approximate dates set, however."""
so, visit, line1, line2 = self._generate_so_with_one_visit_two_lines() so, visit, line1, line2 = self._generate_so_with_one_visit_two_lines()
so2 = so.copy() so2 = so.copy()
@ -278,19 +300,19 @@ class TestSalesOrder(BemadeFSMBaseTest):
so = self._generate_sale_order() so = self._generate_sale_order()
so.company_id.create_default_fsm_visit = True so.company_id.create_default_fsm_visit = True
product = self._generate_product() product = self._generate_product()
sol = self._generate_sale_order_line(so, product) self._generate_sale_order_line(so, product)
so.action_confirm() 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.assertTrue(so.visit_ids)
self.assertEqual(visit_line.visit_id, so.visit_ids) self.assertEqual(visit_line.visit_id, so.visit_ids)
def test_confirming_sale_order_with_visit_creates_no_new_lines(self): def test_confirming_sale_order_with_visit_creates_no_new_lines(self):
so = self._generate_sale_order() so = self._generate_sale_order()
so.company_id.create_default_fsm_visit = True so.company_id.create_default_fsm_visit = True
product = self._generate_product() self._generate_product()
visit = self._generate_visit(so) self._generate_visit(so)
so.action_confirm() so.action_confirm()
@ -300,9 +322,8 @@ class TestSalesOrder(BemadeFSMBaseTest):
so = self._generate_sale_order() so = self._generate_sale_order()
so.company_id.create_default_fsm_visit = False so.company_id.create_default_fsm_visit = False
product = self._generate_product() product = self._generate_product()
sol = self._generate_sale_order_line(so, product) self._generate_sale_order_line(so, product)
so.action_confirm() so.action_confirm()
visit_line = so.order_line.sorted('sequence')[0]
self.assertFalse(so.visit_ids) self.assertFalse(so.visit_ids)

View file

@ -1,54 +1,48 @@
from odoo.tests import TransactionCase, Form, tagged from odoo.tests import TransactionCase, Form, tagged
@tagged("-at_install", "post_install") @tagged("-at_install", "post_install")
class TestSettings(TransactionCase): class TestSettings(TransactionCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super().setUpClass() super().setUpClass()
cls.test_partner_co = cls.env['res.partner'].create({ cls.test_partner_co = cls.env["res.partner"].create(
'name': 'Test Co', {
}) "name": "Test Co",
cls.test_co = cls.env['res.company'].create({ }
'name': 'Test Co', )
'country_id': cls.env.ref('base.ca').id, 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 cls.env.user.company_id = cls.test_co
def test_enabling_separate_time_on_work_orders(self): def test_enabling_separate_time_on_work_orders(self):
wizard = self.env['res.config.settings'].create({}) wizard = self.env["res.config.settings"].create({})
self.assertFalse( self.assertFalse(self.test_co.split_time_from_materials_on_service_work_orders)
self.test_co.split_time_from_materials_on_service_work_orders
)
with Form(wizard) as form: with Form(wizard) as form:
form.separate_time_on_work_orders = True form.separate_time_on_work_orders = True
self.assertTrue( self.assertTrue(self.test_co.split_time_from_materials_on_service_work_orders)
self.test_co.split_time_from_materials_on_service_work_orders
)
def test_disabling_separate_time_on_work_orders(self): 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 self.test_co.split_time_from_materials_on_service_work_orders = True
with Form(wizard) as form: with Form(wizard) as form:
form.separate_time_on_work_orders = False form.separate_time_on_work_orders = False
self.assertFalse( self.assertFalse(self.test_co.split_time_from_materials_on_service_work_orders)
self.test_co.split_time_from_materials_on_service_work_orders
)
def test_enabling_create_default_fsm_visit(self): 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 self.test_co.create_default_fsm_visit = False
with Form(wizard) as form: with Form(wizard) as form:
form.create_default_fsm_visit = True form.create_default_fsm_visit = True
self.assertTrue( self.assertTrue(self.test_co.create_default_fsm_visit)
self.test_co.create_default_fsm_visit
)
def test_disabling_create_default_fsm_visit(self): 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 self.test_co.create_default_fsm_visit = True
with Form(wizard) as form: with Form(wizard) as form:
form.create_default_fsm_visit = False form.create_default_fsm_visit = False
self.assertFalse( self.assertFalse(self.test_co.create_default_fsm_visit)
self.test_co.create_default_fsm_visit
)

View file

@ -3,18 +3,19 @@ from odoo.tests.common import tagged, Form
from odoo import Command from odoo import Command
@tagged('post_install', '-at_install') @tagged("post_install", "-at_install")
class TaskTest(BemadeFSMBaseTest): class TaskTest(BemadeFSMBaseTest):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
# Chose to set up all tests the same way since this code was becoming very redundant # Chose to set up all tests the same way since this code was becoming very redundant
super().setUpClass() 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): def _generate_so_with_multilevel_task_template(self):
so = self._generate_sale_order() 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) product = self._generate_product(task_template=template)
sol = self._generate_sale_order_line(sale_order=so, product=product) sol = self._generate_sale_order_line(sale_order=so, product=product)
return so, sol return so, sol
@ -25,21 +26,27 @@ class TaskTest(BemadeFSMBaseTest):
task = sol.task_id task = sol.task_id
task.propagate_assignment = True task.propagate_assignment = True
task.write({ task.write(
'user_ids': [Command.set([self.user.id])], {
'propagate_assignment': True, "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): def test_reassigning_task_doesnt_propagate_by_default(self):
so, sol = self._generate_so_with_multilevel_task_template() so, sol = self._generate_so_with_multilevel_task_template()
so.action_confirm() so.action_confirm()
task = sol.task_id task = sol.task_id
task.write({ task.write(
'user_ids': [Command.set([self.user.id])], {
}) "user_ids": [Command.set([self.user.id])],
}
)
self.assertFalse(any([t.user_ids for t in task.child_ids.child_ids])) 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 task = sol.task_id
# First, set propagation and assign # First, set propagation and assign
task.propagate_assignment = True task.propagate_assignment = True
task.write({ task.write({"user_ids": [Command.set([self.user.id])]})
'user_ids': [Command.set([self.user.id])]
})
# Then, unset propagation for the children and re-set assignment # Then, unset propagation for the children and re-set assignment
task.child_ids.write({'propagate_assignment': False}) task.child_ids.write({"propagate_assignment": False})
self.assertFalse(any([t.propagate_assignment for t in task._get_all_subtasks()])) 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 # Then, test that assigning the parent only assigns its children, not its grandchildren
task.write({ task.write({"user_ids": [Command.set([])]})
'user_ids': [Command.set([])]
})
self.assertTrue(all([not t.user_ids for t in task | task.child_ids])) 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): def test_task_gets_work_order_contacts_from_sale_order(self):
so, sol = self._generate_so_with_multilevel_task_template() so, sol = self._generate_so_with_multilevel_task_template()
work_order_contacts = self._generate_partner(parent=so.partner_id) | self._generate_partner( work_order_contacts = self._generate_partner(
parent=so.partner_id) parent=so.partner_id
so.write({'work_order_contacts': [(6, 0, work_order_contacts.ids)]}) ) | self._generate_partner(parent=so.partner_id)
so.write({"work_order_contacts": [(6, 0, work_order_contacts.ids)]})
so.action_confirm() so.action_confirm()
task = sol.task_id task = sol.task_id
@ -80,8 +88,10 @@ class TaskTest(BemadeFSMBaseTest):
def test_task_gets_site_contacts_from_sale_order(self): def test_task_gets_site_contacts_from_sale_order(self):
so, sol = self._generate_so_with_multilevel_task_template() 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) site_contacts = self._generate_partner(
so.write({'site_contacts': [(6, 0, site_contacts.ids)]}) parent=so.partner_id
) | self._generate_partner(parent=so.partner_id)
so.write({"site_contacts": [(6, 0, site_contacts.ids)]})
so.action_confirm() so.action_confirm()
task = sol.task_id task = sol.task_id
@ -95,12 +105,20 @@ class TaskTest(BemadeFSMBaseTest):
def test_task_gets_work_order_contacts_from_parent(self): def test_task_gets_work_order_contacts_from_parent(self):
so, sol = self._generate_so_with_multilevel_task_template() 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) work_order_contacts = self._generate_partner(
so.write({'work_order_contacts': [(6, 0, work_order_contacts.ids)]}) 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() so.action_confirm()
task = sol.task_id 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(): for subtask in task._get_all_subtasks():
self.assertEqual(subtask.work_order_contacts, task.work_order_contacts) self.assertEqual(subtask.work_order_contacts, task.work_order_contacts)
with Form(task) as task_form: with Form(task) as task_form:
@ -111,12 +129,20 @@ class TaskTest(BemadeFSMBaseTest):
def test_task_gets_site_contacts_from_parent(self): def test_task_gets_site_contacts_from_parent(self):
so, sol = self._generate_so_with_multilevel_task_template() 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) site_contacts = self._generate_partner(
so.write({'site_contacts': [(6, 0, site_contacts.ids)]}) parent=so.partner_id
) | self._generate_partner(parent=so.partner_id)
so.write({"site_contacts": [(6, 0, site_contacts.ids)]})
so.action_confirm() so.action_confirm()
task = sol.task_id 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(): for subtask in task._get_all_subtasks():
self.assertEqual(subtask.site_contacts, task.site_contacts) self.assertEqual(subtask.site_contacts, task.site_contacts)
with Form(task) as task_form: with Form(task) as task_form:

View file

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

View file

@ -1,35 +1,34 @@
from .test_bemade_fsm_common import BemadeFSMBaseTest 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.exceptions import MissingError
from odoo import Command
from odoo.tools import mute_logger 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): class TestTaskTemplate(BemadeFSMBaseTest):
def test_delete_task_template(self): def test_delete_task_template(self):
"""User should never be able to delete a task template used on a product""" """User should never be able to delete a task template used on a product"""
task_template = self._generate_task_template(names=['Template 1']) task_template = self._generate_task_template(names=["Template 1"])
product = self._generate_product(name="Test Product 1", task_template=task_template) self._generate_product(name="Test Product 1", task_template=task_template)
with self.assertRaises(ForeignKeyViolation): with self.assertRaises(psycopg2.errors.ForeignKeyViolation):
with mute_logger('odoo.sql_db'): with mute_logger("odoo.sql_db"):
task_template.unlink() task_template.unlink()
def test_delete_subtask_template(self): 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 """Deletion of a child task should be OK even if the parent is on a product.
subtask should be deleted.""" Children of the deleted subtask should be deleted."""
parent_task = self._generate_task_template(structure=[2, 1], parent_task = self._generate_task_template(
names=['Parent Template', 'Child Template', structure=[2, 1],
'Grandchild Template']) names=["Parent Template", "Child Template", "Grandchild Template"],
)
grandchild_task = parent_task.subtasks[0].subtasks[0] grandchild_task = parent_task.subtasks[0].subtasks[0]
parent_task.subtasks[0].unlink() parent_task.subtasks[0].unlink()
# Reading deleted child's name field should be impossible # Reading deleted child's name field should be impossible
with self.assertRaises(MissingError): with self.assertRaises(MissingError):
test = grandchild_task.name _ = grandchild_task.name
def test_dissociating_customer_resets_equipment_appropriately(self): def test_dissociating_customer_resets_equipment_appropriately(self):
partner1 = self._generate_partner() partner1 = self._generate_partner()
@ -38,7 +37,8 @@ class TestTaskTemplate(BemadeFSMBaseTest):
task = self._generate_task_template(customer=partner1, equipment=equipment1) task = self._generate_task_template(customer=partner1, equipment=equipment1)
form = Form(task) 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.customer = partner2
form.save() form.save()
@ -46,7 +46,7 @@ class TestTaskTemplate(BemadeFSMBaseTest):
def test_child_task_names_are_short_version(self): def test_child_task_names_are_short_version(self):
so, visit, sol1, sol2 = self._generate_so_with_one_visit_two_lines() 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) product = self._generate_product(task_template=template)
sol1.name = "Short Name 1" sol1.name = "Short Name 1"
sol2.name = "Short Name 2" sol2.name = "Short Name 2"
@ -60,14 +60,21 @@ class TestTaskTemplate(BemadeFSMBaseTest):
def test_task_creation_directly_from_template(self): def test_task_creation_directly_from_template(self):
project = self.env.ref("industry_fsm.fsm_project") 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") 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), len(template.subtasks))
self.assertEqual(len(task.child_ids[0].child_ids), len(template.subtasks[0].subtasks)) self.assertEqual(
self.assertEqual(len(task.child_ids[1].child_ids), len(template.subtasks[1].subtasks)) 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.name, "My new task")
self.assertEqual(task.child_ids[0].name, template.subtasks[0].name) 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

@ -5,7 +5,7 @@
<field name="name">bemade_fsm.equipment.form</field> <field name="name">bemade_fsm.equipment.form</field>
<field name="model">bemade_fsm.equipment</field> <field name="model">bemade_fsm.equipment</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Equipment"> <form>
<sheet> <sheet>
<group> <group>
<group name="left"> <group name="left">
@ -15,11 +15,17 @@
<field name="location_notes" /> <field name="location_notes" />
</group> </group>
<group name="right"> <group name="right">
<field name="partner_location_id" <field
name="partner_location_id"
groups="account.group_delivery_invoice_address" groups="account.group_delivery_invoice_address"
context="{'default_type': 'delivery', 'show_address': 1}" context="{'default_type': 'delivery', 'show_address': 1}"
options='{"always_reload": True}'/> options='{"always_reload": True}'
<field name="tag_ids" widget="many2many_tags" options="{'no_open': False}"/> />
<field
name="tag_ids"
widget="many2many_tags"
options="{'no_open': False}"
/>
</group> </group>
</group> </group>
</sheet> </sheet>
@ -36,11 +42,15 @@
<field name="name">bemade_fsm.equipment.tree</field> <field name="name">bemade_fsm.equipment.tree</field>
<field name="model">bemade_fsm.equipment</field> <field name="model">bemade_fsm.equipment</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree string="Equipment" editable="bottom"> <tree editable="bottom">
<field name="pid_tag" /> <field name="pid_tag" />
<field name="name" /> <field name="name" />
<field name="description" /> <field name="description" />
<field name="tag_ids" widget="many2many_tags" options="{'no_open': False}"/> <field
name="tag_ids"
widget="many2many_tags"
options="{'no_open': False}"
/>
<field name="partner_location_id" /> <field name="partner_location_id" />
</tree> </tree>
</field> </field>

View file

@ -1,32 +1,40 @@
<?xml version="1.0" encoding="UTF-8" ?> <?xml version="1.0" encoding="UTF-8" ?>
<odoo> <odoo>
<data> <menuitem
<menuitem id="project_task_template_menu" id="project_task_template_menu"
name="Task Templates" name="Task Templates"
parent="project.menu_main_pm" parent="project.menu_main_pm"
action="task_template_act_window" action="task_template_act_window"
groups="project.group_project_manager,project.group_project_user"/> groups="project.group_project_manager,project.group_project_user"
<menuitem id="service_task_template_meny" />
<menuitem
id="service_task_template_meny"
name="Task Templates" name="Task Templates"
action="task_template_act_window" action="task_template_act_window"
parent="industry_fsm.fsm_menu_root" parent="industry_fsm.fsm_menu_root"
groups="project.group_project_manager,industry_fsm.group_fsm_manager"/> groups="project.group_project_manager,industry_fsm.group_fsm_manager"
<menuitem id="menu_service_client" />
<menuitem
id="menu_service_client"
name="Clients" name="Clients"
sequence="10" sequence="10"
parent="industry_fsm.fsm_menu_root" parent="industry_fsm.fsm_menu_root"
groups="industry_fsm.group_fsm_user"/> groups="industry_fsm.group_fsm_user"
<menuitem id="menu_service_client_clients" />
<menuitem
id="menu_service_client_clients"
name="Clients" name="Clients"
action="base.action_partner_customer_form" action="base.action_partner_customer_form"
sequence="20" sequence="20"
parent="menu_service_client" parent="menu_service_client"
groups="industry_fsm.group_fsm_user"/> groups="industry_fsm.group_fsm_user"
<menuitem id="menu_service_client_equipment" />
<menuitem
id="menu_service_client_equipment"
name="Client Equipment" name="Client Equipment"
action="action_window_equipment" action="action_window_equipment"
sequence="21" sequence="21"
parent="menu_service_client" parent="menu_service_client"
groups="industry_fsm.group_fsm_user"/> groups="industry_fsm.group_fsm_user"
</data> />
</odoo> </odoo>

View file

@ -1,16 +1,19 @@
<?xml version="1.0" encoding="utf-8" ?> <?xml version="1.0" encoding="utf-8" ?>
<odoo> <odoo>
<data>
<record id="product_template_form_inherit" model="ir.ui.view"> <record id="product_template_form_inherit" model="ir.ui.view">
<field name="name">bemade_fsm.product.template.form</field> <field name="name">bemade_fsm.product.template.form</field>
<field name="model">product.template</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="inherit_id"
ref="sale_project.product_template_form_view_invoice_policy_inherit_sale_project"
/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr="//field[@name='project_id']" position="after"> <xpath expr="//field[@name='project_id']" position="after">
<field name="task_template_id" <field
name="task_template_id"
invisible="service_tracking not in ('task_global_project', 'task_in_project')" invisible="service_tracking not in ('task_global_project', 'task_in_project')"
domain="[('parent', '=', False)]"/> domain="[('parent', '=', False)]"
/>
</xpath> </xpath>
</field> </field>
</record> </record>
@ -26,5 +29,4 @@
<!-- </xpath>--> <!-- </xpath>-->
<!-- </field>--> <!-- </field>-->
<!-- </record>--> <!-- </record>-->
</data>
</odoo> </odoo>

View file

@ -16,7 +16,11 @@
<xpath expr="//page[@name='internal_notes']" position="before"> <xpath expr="//page[@name='internal_notes']" position="before">
<field name="is_site_contact" invisible="True" /> <field name="is_site_contact" invisible="True" />
<field name="is_service_site" invisible="True" /> <field name="is_service_site" invisible="True" />
<page name="field_service" string="Service Site Responsibilities" invisible="is_service_site or is_company"> <page
name="field_service"
string="Service Site Responsibilities"
invisible="is_service_site or is_company"
>
<field name="owned_equipment_ids" invisible="True" /> <field name="owned_equipment_ids" invisible="True" />
<group> <group>
<field name="site_ids"> <field name="site_ids">
@ -26,28 +30,51 @@
</field> </field>
</group> </group>
</page> </page>
<page name="service_equipment" string="Service Site Info" invisible="is_site_contact"> <page
<field name="equipment_ids" 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'}" context="{'tree_view_ref': 'bemade_fsm.fsm_equipment_view_tree'}"
readonly="False"/> readonly="False"
<field name="site_contacts" />
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"/> <field
<field name="work_order_contacts" name="site_contacts"
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"/> context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"
<field name="owned_equipment_ids" />
<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'}" context="{'tree_view_ref': 'bemade_fsm.fsm_equipment_view_tree'}"
readonly="True"/> readonly="True"
/>
</page> </page>
</xpath> </xpath>
<div name="button_box" position="inside"> <div name="button_box" position="inside">
<button class="oe_stat_button" type="action" name="%(bemade_fsm.act_res_partner_2_equipment)d" <button
icon="fa-tachometer"> class="oe_stat_button"
<field string="Equipments" name="equipment_count" widget="statinfo"/> type="action"
name="%(bemade_fsm.act_res_partner_2_equipment)d"
icon="fa-tachometer"
>
<field
string="Equipments"
name="equipment_count"
widget="statinfo"
/>
</button> </button>
</div> </div>
<field name="lang" position="after"> <field name="lang" position="after">
</field> </field>
<xpath expr="/form//field[@name='child_ids']/form//field[@name='comment']" position="after"> <xpath
expr="/form//field[@name='child_ids']/form//field[@name='comment']"
position="after"
>
<field name="is_site_contact" /> <field name="is_site_contact" />
</xpath> </xpath>
</field> </field>
@ -72,10 +99,12 @@
<field name="pid_tag" /> <field name="pid_tag" />
<field name="name" /> <field name="name" />
<field name="location_notes" /> <field name="location_notes" />
<button name="action_view_equipment" <button
name="action_view_equipment"
type="object" type="object"
string="Details" string="Details"
icon="fa-external-link"/> icon="fa-external-link"
/>
</tree> </tree>
</field> </field>
</record> </record>

View file

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8" ?> <?xml version="1.0" encoding="utf-8" ?>
<odoo> <odoo>
<data>
<record id="sale_order_form_inherit" model="ir.ui.view"> <record id="sale_order_form_inherit" model="ir.ui.view">
<field name="name">bemade_fsm.sale_order.form</field> <field name="name">bemade_fsm.sale_order.form</field>
<field name="model">sale.order</field> <field name="model">sale.order</field>
@ -9,33 +8,45 @@
<xpath expr="//page[@name='other_information']" position="before"> <xpath expr="//page[@name='other_information']" position="before">
<page name="field_service" string="Field Service"> <page name="field_service" string="Field Service">
<group name="fsm_visits" string="Service Visits"> <group name="fsm_visits" string="Service Visits">
<field name="visit_ids" <field
context="{'tree_view_ref': 'bemade_fsm.bemade_fsm_visit_tree'}"/> name="visit_ids"
context="{'tree_view_ref': 'bemade_fsm.bemade_fsm_visit_tree'}"
/>
</group> </group>
<group name="field_service_info" string="Contacts and Equipment"> <group name="field_service_info" string="Contacts and Equipment">
<field name="valid_equipment_ids" invisible="1" /> <field name="valid_equipment_ids" invisible="1" />
<field name="summary_equipment_ids" <field
name="summary_equipment_ids"
context="{'default_partner_location_id': partner_shipping_id,}" context="{'default_partner_location_id': partner_shipping_id,}"
widget="many2many_tags" widget="many2many_tags"
groups="account.group_delivery_invoice_address"/> groups="account.group_delivery_invoice_address"
<field name="site_contacts" />
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"/> <field
<field name="work_order_contacts" name="site_contacts"
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"/> context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"
/>
<field
name="work_order_contacts"
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"
/>
<field name="default_equipment_ids" <field
name="default_equipment_ids"
context="{'default_partner_location_id': partner_shipping_id,}" context="{'default_partner_location_id': partner_shipping_id,}"
widget="many2many_tags" widget="many2many_tags"
domain="[('id', 'in', valid_equipment_ids)]" domain="[('id', 'in', valid_equipment_ids)]"
groups="account.group_delivery_invoice_address"/> groups="account.group_delivery_invoice_address"
/>
</group> </group>
</page> </page>
</xpath> </xpath>
<xpath expr="//tree//field[@name='name']" position="after"> <xpath expr="//tree//field[@name='name']" position="after">
<field name="valid_equipment_ids" invisible="1" /> <field name="valid_equipment_ids" invisible="1" />
<field name="equipment_ids" <field
name="equipment_ids"
widget="many2many_tags" widget="many2many_tags"
domain="[('id', 'in', valid_equipment_ids)]"/> domain="[('id', 'in', valid_equipment_ids)]"
/>
</xpath> </xpath>
</field> </field>
</record> </record>
@ -51,5 +62,4 @@
</tree> </tree>
</field> </field>
</record> </record>
</data>
</odoo> </odoo>

View file

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8" ?> <?xml version="1.0" encoding="utf-8" ?>
<odoo> <odoo>
<data>
<record id="task_template_form_view" model="ir.ui.view"> <record id="task_template_form_view" model="ir.ui.view">
<field name="name">bemade_fsm.task_template.form</field> <field name="name">bemade_fsm.task_template.form</field>
<field name="model">project.task.template</field> <field name="model">project.task.template</field>
@ -23,16 +21,22 @@
</group> </group>
<group> <group>
<field name="customer" /> <field name="customer" />
<field name="equipment_ids" <field
name="equipment_ids"
domain="[('partner_location_id', '=', customer)]" domain="[('partner_location_id', '=', customer)]"
context="{'tree_view_ref': 'bemade_fsm.equipment_view_tree'}"/> context="{'tree_view_ref': 'bemade_fsm.equipment_view_tree'}"
/>
<field name="tags" widget="many2many_tags" /> <field name="tags" widget="many2many_tags" />
<field name="company_id" /> <field name="company_id" />
</group> </group>
</group> </group>
<notebook> <notebook>
<page name="description_page" string="Description"> <page name="description_page" string="Description">
<field name="description" type="html" options="{'collaborative': true}"/> <field
name="description"
type="html"
options="{'collaborative': true}"
/>
</page> </page>
<page name="subtasks_page" string="Subtasks"> <page name="subtasks_page" string="Subtasks">
<field name="subtasks"> <field name="subtasks">
@ -40,9 +44,17 @@
<field name="sequence" widget="handle" /> <field name="sequence" widget="handle" />
<field name="name" /> <field name="name" />
<field name="customer" /> <field name="customer" />
<field name="assignees" widget="many2many_avatar_user"/> <field
<button name="action_open_task" type="object" title="View Task" name="assignees"
string="View Task" class="btn btn-link pull-right"/> widget="many2many_avatar_user"
/>
<button
name="action_open_task"
type="object"
title="View Task"
string="View Task"
class="btn btn-link pull-right"
/>
</tree> </tree>
</field> </field>
</page> </page>
@ -56,7 +68,7 @@
<field name="name">project.task_template.tree</field> <field name="name">project.task_template.tree</field>
<field name="model">project.task.template</field> <field name="model">project.task.template</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree string="Task Template"> <tree>
<field name="name" /> <field name="name" />
<field name="assignees" widget="many2many_avatar_user" /> <field name="assignees" widget="many2many_avatar_user" />
<field name="project" /> <field name="project" />
@ -78,12 +90,24 @@
<field name="subtasks" /> <field name="subtasks" />
<field name="planned_hours" /> <field name="planned_hours" />
<group expand="1" string="Group By"> <group expand="1" string="Group By">
<filter string="Project" name="groupby_project" domain="[]" <filter
context="{'group_by':'project'}"/> string="Project"
<filter string="Parent Task" name="groupby_parent" domain="[]" name="groupby_project"
context="{'group_by':'parent'}"/> domain="[]"
<filter string="Customer" name="groupby_customer" domain="[]" context="{'group_by':'project'}"
context="{'group_by':'customer'}"/> />
<filter
string="Parent Task"
name="groupby_parent"
domain="[]"
context="{'group_by':'parent'}"
/>
<filter
string="Customer"
name="groupby_customer"
domain="[]"
context="{'group_by':'customer'}"
/>
</group> </group>
</search> </search>
</field> </field>
@ -100,6 +124,4 @@
</p> </p>
</field> </field>
</record> </record>
</data>
</odoo> </odoo>

View file

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8" ?> <?xml version="1.0" encoding="utf-8" ?>
<odoo> <odoo>
<data>
<record id="bemade_fsm_project_task_form_inherit" model="ir.ui.view"> <record id="bemade_fsm_project_task_form_inherit" model="ir.ui.view">
<field name="name">bemade_fsm.project_task.form</field> <field name="name">bemade_fsm.project_task.form</field>
<field name="model">project.task</field> <field name="model">project.task</field>
@ -9,29 +8,44 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr="//field[@name='name']/.." position="before"> <xpath expr="//field[@name='name']/.." position="before">
<h1 class="d-flex flex-row justify-content-between"> <h1 class="d-flex flex-row justify-content-between">
<field name="work_order_number" invisible="work_order_number == False"/> <field
name="work_order_number"
invisible="work_order_number == False"
/>
</h1> </h1>
</xpath> </xpath>
<xpath expr="//page[@name='extra_info']" position="after"> <xpath expr="//page[@name='extra_info']" position="after">
<page string="Field Service" name="field_service" translate="True"> <page string="Field Service" name="field_service" translate="True">
<group name="equipment_and_contacts"> <group name="equipment_and_contacts">
<field name="equipment_ids" <field
name="equipment_ids"
domain="[('partner_location_id', '=', partner_id)]" domain="[('partner_location_id', '=', partner_id)]"
context="{'tree_view_ref': 'bemade_fsm.equipment_view_tree'}"/> context="{'tree_view_ref': 'bemade_fsm.equipment_view_tree'}"
<field name="site_contacts" />
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"/> <field
<field name="work_order_contacts" name="site_contacts"
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"/> context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"
/>
<field
name="work_order_contacts"
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"
/>
</group> </group>
</page> </page>
</xpath> </xpath>
<button name="action_fsm_validate" class='btn-primary' <button
position="attributes"> name="action_fsm_validate"
class='btn-primary'
position="attributes"
>
<attribute name="string">Mark as Delivered</attribute> <attribute name="string">Mark as Delivered</attribute>
<attribute name="groups">industry_fsm.group_fsm_manager</attribute> <attribute name="groups">industry_fsm.group_fsm_manager</attribute>
</button> </button>
<button name="action_fsm_validate" class='btn-secondary' <button
position="attributes"> name="action_fsm_validate"
class='btn-secondary'
position="attributes"
>
<attribute name="string">Mark as Delivered</attribute> <attribute name="string">Mark as Delivered</attribute>
<attribute name="groups">industry_fsm.group_fsm_manager</attribute> <attribute name="groups">industry_fsm.group_fsm_manager</attribute>
</button> </button>
@ -42,7 +56,10 @@
<field name="model">project.task</field> <field name="model">project.task</field>
<field name="name">bemade_fsm.project_task.form2</field> <field name="name">bemade_fsm.project_task.form2</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr="//field[@name='child_ids']/tree//field[@name='name']" position="after"> <xpath
expr="//field[@name='child_ids']/tree//field[@name='name']"
position="after"
>
<field name="description" string="Description/Comments" /> <field name="description" string="Description/Comments" />
</xpath> </xpath>
<field name="user_ids" position="after"> <field name="user_ids" position="after">
@ -54,47 +71,6 @@
<record id="industry_fsm.project_task_action_fsm" model="ir.actions.act_window"> <record id="industry_fsm.project_task_action_fsm" model="ir.actions.act_window">
<field name="context">{'search_default_is_parent_task': True}</field> <field name="context">{'search_default_is_parent_task': True}</field>
</record> </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>
</record>
<record id="project_task_view_list_fsm_inherit" model="ir.ui.view"> <record id="project_task_view_list_fsm_inherit" model="ir.ui.view">
<field name="name">project.task.view.list.fsm.inherit</field> <field name="name">project.task.view.list.fsm.inherit</field>
<field name="model">project.task</field> <field name="model">project.task</field>
@ -117,32 +93,38 @@
</field> </field>
</field> </field>
</record> </record>
<record id="industry_fsm.project_task_action_fsm_map" <record id="industry_fsm.project_task_action_fsm_map" model="ir.actions.act_window">
model="ir.actions.act_window"> <field name="context">{'search_default_is_parent_task': True}</field>
<field name="domain">[('is_fsm', '=', True), <field name="domain">[('is_fsm', '=', True),
('project_id', '!=', False), ('project_id', '!=', False),
('display_in_project', '=', True), ('display_in_project', '=', True),
('parent_id', '=', False)] ('parent_id', '=', False)]
</field> </field>
</record> </record>
<record id="industry_fsm.project_task_action_to_schedule_fsm" <record
model="ir.actions.act_window"> 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), <field name="domain">[('is_fsm', '=', True),
('project_id', '!=', False), ('project_id', '!=', False),
('display_in_project', '=', True), ('display_in_project', '=', True),
('parent_id', '=', False)] ('parent_id', '=', False)]
</field> </field>
</record> </record>
<record id="industry_fsm.project_task_action_all_fsm" <record id="industry_fsm.project_task_action_all_fsm" model="ir.actions.act_window">
model="ir.actions.act_window"> <field name="context">{'search_default_is_parent_task': True}</field>
<field name="domain">[('is_fsm', '=', True), <field name="domain">[('is_fsm', '=', True),
('project_id', '!=', False), ('project_id', '!=', False),
('display_in_project', '=', True), ('display_in_project', '=', True),
('parent_id', '=', False)] ('parent_id', '=', False)]
</field> </field>
</record> </record>
<record id="industry_fsm_sale.project_task_action_to_invoice_fsm" <record
model="ir.actions.act_window"> 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), <field name="domain">[('is_fsm', '=', True),
('project_id', '!=', False), ('project_id', '!=', False),
('display_in_project', '=', True), ('display_in_project', '=', True),
@ -151,24 +133,33 @@
</field> </field>
</record> </record>
<!-- Add parent_id = false to domain for planning actions as well --> <!-- Add parent_id = false to domain for planning actions as well -->
<record id="industry_fsm.project_task_action_fsm_planning_groupby_user" <record
model="ir.actions.act_window"> 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), <field name="domain">[('is_fsm', '=', True),
('project_id', '!=', False), ('project_id', '!=', False),
('display_in_project', '=', True), ('display_in_project', '=', True),
('parent_id', '=', False)] ('parent_id', '=', False)]
</field> </field>
</record> </record>
<record id="industry_fsm.project_task_action_fsm_planning_groupby_project" <record
model="ir.actions.act_window"> 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), <field name="domain">[('is_fsm', '=', True),
('project_id', '!=', False), ('project_id', '!=', False),
('display_in_project', '=', True), ('display_in_project', '=', True),
('parent_id', '=', False)] ('parent_id', '=', False)]
</field> </field>
</record> </record>
<record id="industry_fsm_report.project_task_action_fsm_planning_groupby_worksheet" <record
model="ir.actions.act_window"> 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), <field name="domain">[('is_fsm', '=', True),
('project_id', '!=', False), ('project_id', '!=', False),
('display_in_project', '=', True), ('display_in_project', '=', True),
@ -188,12 +179,14 @@
<!-- </record>--> <!-- </record>-->
<record id="project_task_view_calendar_fsm_no_worksheet" model="ir.ui.view"> <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="name">bemade_fsm.project_task_view_calendar_no_worksheet</field>
<field name="inherit_id" <field
ref="industry_fsm_report.project_task_view_calendar_fsm_worksheet"/> name="inherit_id"
ref="industry_fsm_report.project_task_view_calendar_fsm_worksheet"
/>
<field name="model">project.task</field> <field name="model">project.task</field>
<field name="priority" eval="100" />
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr="//field[@name='worksheet_template_id']" <xpath expr="//field[@name='worksheet_template_id']" position="replace" />
position="replace"></xpath>
<xpath expr="//calendar" position="attributes"> <xpath expr="//calendar" position="attributes">
<attribute name="color">user_ids</attribute> <attribute name="color">user_ids</attribute>
</xpath> </xpath>
@ -218,11 +211,12 @@
</attribute> </attribute>
</filter> </filter>
<filter name="my_tasks" position="after"> <filter name="my_tasks" position="after">
<filter name="is_parent_task" <filter
name="is_parent_task"
string="Parent Task" string="Parent Task"
domain="[('parent_id', '=', False)]"/> domain="[('parent_id', '=', False)]"
/>
</filter> </filter>
</field> </field>
</record> </record>
</data>
</odoo> </odoo>

View file

@ -1,5 +1,4 @@
from odoo import models, fields, api, _ from odoo import models, fields
from odoo.exceptions import UserError
class NewTaskFromTemplateWizard(models.TransientModel): class NewTaskFromTemplateWizard(models.TransientModel):
@ -7,45 +6,52 @@ class NewTaskFromTemplateWizard(models.TransientModel):
_description = "Create Task from Template Wizard" _description = "Create Task from Template Wizard"
project_id = fields.Many2one( project_id = fields.Many2one(
comodel_name='project.project', comodel_name="project.project",
string='Project', string="Project",
help='The project the new task should be created in.', help="The project the new task should be created in.",
required=True, required=True,
) )
task_template_id = fields.Many2one( task_template_id = fields.Many2one(
comodel_name='project.task.template', comodel_name="project.task.template",
string='Task Template', string="Task Template",
help='The template to use when creating the new task.', help="The template to use when creating the new task.",
required=True, required=True,
) )
new_task_title = fields.Char( 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): def default_get(self, fields_list):
res = super().default_get(fields_list) res = super().default_get(fields_list)
active_id = self.env.context.get('active_id', False) active_id = self.env.context.get("active_id", False)
active_model = self.env.context.get('active_model', False) active_model = self.env.context.get("active_model", False)
if not active_model: if not active_model:
params = self.env.context.get('params', False) params = self.env.context.get("params", False)
active_model = params and params.get('model', 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: if (
res.update({'task_template_id': active_id}) active_model == "project.task.template"
if active_model == 'project.task' and 'project_id' in fields_list: and active_id
res.update({'project_id': self.env.ref('industry_fsm.fsm_project').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 return res
def action_create_task_from_template(self): def action_create_task_from_template(self):
self.ensure_one() 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 { return {
'type': 'ir.actions.act_window', "type": "ir.actions.act_window",
'res_model': 'project.task', "res_model": "project.task",
'res_id': task.id, "res_id": task.id,
'view_mode': 'form', "view_mode": "form",
'target': 'current', "target": "current",
} }

View file

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