diff --git a/.eslintrc.yml b/.eslintrc.yml new file mode 100644 index 0000000..fed88d7 --- /dev/null +++ b/.eslintrc.yml @@ -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 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a31e807..97adf52 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -89,7 +89,7 @@ repos: args: [--fix, --exit-non-zero-on-fix] - id: ruff-format - repo: https://github.com/OCA/pylint-odoo - rev: v8.0.19 + rev: v9.1.2 hooks: - id: pylint_odoo name: pylint with optional checks diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..ab3a58c --- /dev/null +++ b/.pylintrc @@ -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 diff --git a/.pylintrc-mandatory b/.pylintrc-mandatory new file mode 100644 index 0000000..ab3a58c --- /dev/null +++ b/.pylintrc-mandatory @@ -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 diff --git a/bemade_fsm/__manifest__.py b/bemade_fsm/__manifest__.py index 9b36b31..f1d2daa 100644 --- a/bemade_fsm/__manifest__.py +++ b/bemade_fsm/__manifest__.py @@ -19,53 +19,52 @@ # ######################################################################################## { - 'name': 'Improved Field Service Management', - 'version': '17.0.0.3.0', - 'summary': 'Adds functionality necessary for managing field service operations at Durpro.', - 'description': 'Adds functionality necessary for managing field service operations at Durpro.', - 'category': 'Services/Field Service', - 'author': 'Bemade Inc.', - 'website': 'http://www.bemade.org', - 'license': 'OPL-1', - 'depends': [ - 'sale_stock', - 'sale_project', - 'account', - 'project_enterprise', - 'industry_fsm_stock', - 'industry_fsm_report', - 'industry_fsm_sale_report', - 'bemade_partner_root_ancestor', - 'mail', + "name": "Improved Field Service Management", + "version": "17.0.0.3.0", + "summary": ( + "Adds functionality necessary for managing field service operations at Durpro." + ), + "category": "Services/Field Service", + "author": "Bemade Inc.", + "website": "http://www.bemade.org", + "license": "LGPL-3", + "depends": [ + "sale_stock", + "sale_project", + "account", + "project_enterprise", + "industry_fsm_stock", + "industry_fsm_report", + "industry_fsm_sale_report", + "bemade_partner_root_ancestor", + "mail", ], - 'data': [ - 'data/fsm_data.xml', - 'views/task_template_views.xml', - 'views/equipment.xml', - 'security/ir.model.access.csv', - 'views/product_views.xml', - 'views/res_partner.xml', - 'views/menus.xml', - 'views/task_views.xml', - 'views/sale_order_views.xml', - 'reports/worksheet_custom_report_templates.xml', - 'reports/worksheet_custom_reports.xml', - 'wizard/new_task_from_template.xml', - 'wizard/res_config_settings.xml', + "data": [ + "data/fsm_data.xml", + "views/task_template_views.xml", + "views/equipment.xml", + "security/ir.model.access.csv", + "views/product_views.xml", + "views/res_partner.xml", + "views/menus.xml", + "views/task_views.xml", + "views/sale_order_views.xml", + "reports/worksheet_custom_report_templates.xml", + "reports/worksheet_custom_reports.xml", + "wizard/new_task_from_template.xml", + "wizard/res_config_settings.xml", ], - 'assets': { - 'web.report_assets_common': [ - 'bemade_fsm/static/src/scss/bemade_fsm.scss' - ], - 'web.assets_backend': [ + "assets": { + "web.report_assets_common": ["bemade_fsm/static/src/scss/bemade_fsm.scss"], + "web.assets_backend": [ # BV: need to readd these files # 'bemade_fsm/static/src/js/kanban_view.js', # 'bemade_fsm/static/src/js/list_view.js', ], - 'web.assets_qweb': [ - 'bemade_fsm/static/src/xml/project_view_buttons.xml', - ] + "web.assets_qweb": [ + "bemade_fsm/static/src/xml/project_view_buttons.xml", + ], }, - 'installable': True, - 'auto_install': False + "installable": True, + "auto_install": False, } diff --git a/bemade_fsm/data/fsm_data.xml b/bemade_fsm/data/fsm_data.xml index 75ca4be..b79fca6 100644 --- a/bemade_fsm/data/fsm_data.xml +++ b/bemade_fsm/data/fsm_data.xml @@ -1,30 +1,28 @@ - 2 Waiting on Parts - + - + 15 Work Executed - + - + 19 Exception - + - + - - \ No newline at end of file + diff --git a/bemade_fsm/models/equipment.py b/bemade_fsm/models/equipment.py index 648e018..8743b55 100644 --- a/bemade_fsm/models/equipment.py +++ b/bemade_fsm/models/equipment.py @@ -2,75 +2,86 @@ from odoo import api, fields, models class Equipment(models.Model): - _name = 'bemade_fsm.equipment' - _rec_name = 'complete_name' - _description = 'Field service equipment' - _inherit = ['mail.thread', 'mail.activity.mixin'] + _name = "bemade_fsm.equipment" + _rec_name = "complete_name" + _description = "Field service equipment" + _inherit = ["mail.thread", "mail.activity.mixin"] pid_tag = fields.Char(string="P&ID Tag", tracking=True) - name = fields.Char(string="Name", tracking=True, required=True) + name = fields.Char(tracking=True, required=True) - complete_name = fields.Char(string="Equipment Name", compute="_compute_complete_name", store=True) - - tag_ids = fields.Many2many( - comodel_name='bemade_fsm.equipment.tag', - string='Application', - help="Classify and analyze your equipment categories like: Boiler, Laboratory, Waste water, Pure water" + complete_name = fields.Char( + string="Equipment Name", compute="_compute_complete_name", store=True ) - description = fields.Text(string="Description", tracking=True) + tag_ids = fields.Many2many( + comodel_name="bemade_fsm.equipment.tag", + string="Application", + help=( + "Classify and analyze your equipment categories like: Boiler, Laboratory," + " Waste water, Pure water" + ), + ) + + description = fields.Text(tracking=True) partner_location_id = fields.Many2one( - comodel_name='res.partner', + comodel_name="res.partner", string="Physical Address", tracking=True, - ondelete='cascade' + ondelete="cascade", ) location_notes = fields.Text(string="Physical Location Notes", tracking=True) task_ids = fields.Many2many( - comodel_name='project.task', + comodel_name="project.task", relation="bemade_fsm_task_equipment_rel", column1="equipment_id", column2="task_id", - string='Interventions' + string="Interventions", ) active = fields.Boolean(default=True) - @api.depends('partner_location_id') + @api.depends("partner_location_id") def _compute_partner(self): for rec in self: - rec.partner_id = rec.partner_location_id and rec.partner_location_id.root_ancestor + rec.partner_id = ( + rec.partner_location_id and rec.partner_location_id.root_ancestor + ) @api.model - def name_search(self, name='', args=None, operator='ilike', limit=100): + def name_search(self, name="", args=None, operator="ilike", limit=100): args = args or [] if name: - equipments = self.search([ - '|', '|', - ('pid_tag', operator, name), - ('name', operator, name), - ('partner_location_id.name', operator, name)], - limit=limit) + equipments = self.search( + [ + "|", + "|", + ("pid_tag", operator, name), + ("name", operator, name), + ("partner_location_id.name", operator, name), + ], + limit=limit, + ) else: equipments = self.search(args, limit=limit) return [(equipment.id, equipment.display_name) for equipment in equipments] def action_view_equipment(self): return { - 'name': 'Equipment', - 'view_type': 'form', - 'view_mode': 'form', - 'res_id': self.id, - 'context': self.env.context, - 'res_model': 'bemade_fsm.equipment', - 'type': 'ir.actions.act_window', + "name": "Equipment", + "view_type": "form", + "view_mode": "form", + "res_id": self.id, + "context": self.env.context, + "res_model": "bemade_fsm.equipment", + "type": "ir.actions.act_window", } - @api.depends('pid_tag', 'name') + @api.depends("pid_tag", "name") def _compute_complete_name(self): for rec in self: tag_part = "[%s] " % rec.pid_tag if rec.pid_tag else "" diff --git a/bemade_fsm/models/equipment_tag.py b/bemade_fsm/models/equipment_tag.py index 3f9a82f..8603901 100644 --- a/bemade_fsm/models/equipment_tag.py +++ b/bemade_fsm/models/equipment_tag.py @@ -1,13 +1,13 @@ -from odoo import api, fields, models +from odoo import fields, models class EquipmentTag(models.Model): _name = "bemade_fsm.equipment.tag" - _description = 'Field service equipment category' + _description = "Field service equipment category" - name = fields.Char('Name', required=True, translate=True) - color = fields.Integer('Color Index', default=10) + name = fields.Char(required=True, translate=True) + color = fields.Integer("Color Index", default=10) _sql_constraints = [ - ('name_uniq', 'unique (name)', "Tag name already exists !"), + ("name_uniq", "unique (name)", "Tag name already exists !"), ] diff --git a/bemade_fsm/models/equipment_type.py b/bemade_fsm/models/equipment_type.py index 8cd6623..ccee230 100644 --- a/bemade_fsm/models/equipment_type.py +++ b/bemade_fsm/models/equipment_type.py @@ -1,9 +1,9 @@ -from odoo import api, fields, models +from odoo import fields, models class EquipmentType(models.Model): - _name = 'bemade_fsm.equipment.type' - _description = 'Field service equipment type' - _order = 'id' + _name = "bemade_fsm.equipment.type" + _description = "Field service equipment type" + _order = "id" - name = fields.Char(string='Intervention Name', required=True, translate=True) + name = fields.Char(string="Intervention Name", required=True, translate=True) diff --git a/bemade_fsm/models/fsm_visit.py b/bemade_fsm/models/fsm_visit.py index 7274988..b10eeae 100644 --- a/bemade_fsm/models/fsm_visit.py +++ b/bemade_fsm/models/fsm_visit.py @@ -1,19 +1,28 @@ -from odoo import models, fields, api, _ +from odoo import models, fields, api class FSMVisit(models.Model): _name = "bemade_fsm.visit" - _description = 'Represents a single visit by assigned service personnel.' + _description = "Represents a single visit by assigned service personnel." - label = fields.Text(string="Label", required=True, related='so_section_id.name', readonly=False, copy=True) + label = fields.Text( + string="Label", + required=True, + related="so_section_id.name", + readonly=False, + copy=True, + ) - approx_date = fields.Date(string='Approximate Date', copy=False) + approx_date = fields.Date(string="Approximate Date", copy=False) so_section_id = fields.Many2one( comodel_name="sale.order.line", string="Sale Order Section", - help="The section on the sale order that represents the labour and parts for this visit", - ondelete="cascade" + help=( + "The section on the sale order that represents the labour and parts for" + " this visit" + ), + ondelete="cascade", ) sale_order_id = fields.Many2one( @@ -22,14 +31,18 @@ class FSMVisit(models.Model): readonly=True, ) - is_completed = fields.Boolean(string="Completed", related="so_section_id.is_fully_delivered") + is_completed = fields.Boolean( + string="Completed", related="so_section_id.is_fully_delivered" + ) - is_invoiced = fields.Boolean(string="Invoiced", related="so_section_id.is_fully_delivered_and_invoiced") + is_invoiced = fields.Boolean( + string="Invoiced", related="so_section_id.is_fully_delivered_and_invoiced" + ) summarized_equipment_ids = fields.Many2many( comodel_name="bemade_fsm.equipment", string="Equipment to Service", - compute="_compute_summarized_equipment_ids" + compute="_compute_summarized_equipment_ids", ) task_id = fields.Many2one( @@ -38,21 +51,18 @@ class FSMVisit(models.Model): string="Service Visit", ) - task_ids = fields.One2many( - comodel_name="project.task", - inverse_name="visit_id" - ) + task_ids = fields.One2many(comodel_name="project.task", inverse_name="visit_id") visit_no = fields.Integer( compute="_compute_visit_no", ) - @api.depends('task_ids') + @api.depends("task_ids") def _compute_task_id(self): for rec in self: rec.task_id = rec.task_ids and rec.task_ids[0] - @api.depends('so_section_id', 'sale_order_id.summary_equipment_ids') + @api.depends("so_section_id", "sale_order_id.summary_equipment_ids") def _compute_summarized_equipment_ids(self): for rec in self: lines = rec.so_section_id.get_section_line_ids() @@ -66,23 +76,27 @@ class FSMVisit(models.Model): def create(self, vals_list): recs = super().create(vals_list) for i, rec in enumerate(recs.filtered(lambda visit: not visit.so_section_id)): - rec.so_section_id = rec.env['sale.order.line'].create({ - 'order_id': rec.sale_order_id.id, - 'display_type': 'line_section', - 'name': vals_list[i].get('label', False), - }) + rec.so_section_id = rec.env["sale.order.line"].create( + { + "order_id": rec.sale_order_id.id, + "display_type": "line_section", + "name": vals_list[i].get("label", False), + } + ) return recs @api.depends( - 'so_section_id', - 'sale_order_id', - 'sale_order_id.visit_ids', - 'sale_order_id.visit_ids.so_section_id', - 'sale_order_id.visit_ids.so_section_id.sequence' + "so_section_id", + "sale_order_id", + "sale_order_id.visit_ids", + "sale_order_id.visit_ids.so_section_id", + "sale_order_id.visit_ids.so_section_id.sequence", ) def _compute_visit_no(self): for rec in self: - ordered_visit_lines = self.sale_order_id.visit_ids.so_section_id.sorted("sequence") + ordered_visit_lines = self.sale_order_id.visit_ids.so_section_id.sorted( + "sequence" + ) # Just a straight O(n) search here since n will always be relatively small for index, line in enumerate(ordered_visit_lines): if line == rec.so_section_id: diff --git a/bemade_fsm/models/product_template.py b/bemade_fsm/models/product_template.py index 9ea7dbb..9da9706 100644 --- a/bemade_fsm/models/product_template.py +++ b/bemade_fsm/models/product_template.py @@ -1,16 +1,19 @@ -from odoo import fields, models, api +from odoo import fields, models class ProductTemplate(models.Model): - _inherit = 'product.template' + _inherit = "product.template" task_template_id = fields.Many2one( comodel_name="project.task.template", string="Task Template", - ondelete='restrict' + ondelete="restrict", ) is_field_service = fields.Boolean( string="Plan as field service", - help="Products planned as field service will have travel time considered in planning." + help=( + "Products planned as field service will have travel time considered in" + " planning." + ), ) diff --git a/bemade_fsm/models/project.py b/bemade_fsm/models/project.py index c9cd7bd..79b539f 100644 --- a/bemade_fsm/models/project.py +++ b/bemade_fsm/models/project.py @@ -4,7 +4,9 @@ from odoo import models class Project(models.Model): _inherit = "project.project" - def _fetch_sale_order_item_ids(self, domain_per_model=None, limit=None, offset=None): + def _fetch_sale_order_item_ids( + self, domain_per_model=None, limit=None, offset=None + ): # Override to flush the ORM cache to the database prior to running the query # Temporary fix until Odoo fixes this method (PR #160067 submitted for this) self.env.flush_all() diff --git a/bemade_fsm/models/res_partner.py b/bemade_fsm/models/res_partner.py index 8842d54..60bdb4a 100644 --- a/bemade_fsm/models/res_partner.py +++ b/bemade_fsm/models/res_partner.py @@ -1,86 +1,83 @@ -from odoo import api, fields, models, Command +from odoo import api, fields, models class Partner(models.Model): - _inherit = 'res.partner' + _inherit = "res.partner" - equipment_count = fields.Integer(compute='_compute_equipment_count', string='Equipment Count') + equipment_count = fields.Integer(compute="_compute_equipment_count") owned_equipment_ids = fields.One2many( comodel_name="bemade_fsm.equipment", compute="_compute_owned_equipment_ids", - string="Owned Equipments" + string="Owned Equipments", ) equipment_ids = fields.One2many( - comodel_name='bemade_fsm.equipment', - inverse_name='partner_location_id', - string='Site Equipment' + comodel_name="bemade_fsm.equipment", + inverse_name="partner_location_id", + string="Site Equipment", ) is_site_contact = fields.Boolean( - string='Is a site contact', + string="Is a site contact", compute="_compute_is_site_contact", search="_search_is_site_contact", ) site_ids = fields.Many2many( - string='Work Sites', - comodel_name='res.partner', - relation='res_partner_site_contact_rel', - column1='site_contact_id', - column2='site_id', - tracking=True + string="Work Sites", + comodel_name="res.partner", + relation="res_partner_site_contact_rel", + column1="site_contact_id", + column2="site_id", + tracking=True, ) site_contacts = fields.Many2many( - string='Site Contacts', - comodel_name='res.partner', - relation='res_partner_site_contact_rel', - column1='site_id', - column2='site_contact_id', - domain=[('is_company', '=', False)], - tracking=True + comodel_name="res.partner", + relation="res_partner_site_contact_rel", + column1="site_id", + column2="site_contact_id", + domain=[("is_company", "=", False)], + tracking=True, ) work_order_contacts = fields.Many2many( - string='Work Order Recipients', - comodel_name='res.partner', - relation='res_partner_work_order_contacts_rel', - column1='res_partner_id', - column2='work_order_contact_id', - domain=[('is_company', '=', False)], - tracking=True + string="Work Order Recipients", + comodel_name="res.partner", + relation="res_partner_work_order_contacts_rel", + column1="res_partner_id", + column2="work_order_contact_id", + domain=[("is_company", "=", False)], + tracking=True, ) is_service_site = fields.Boolean( compute="_compute_is_service_site", ) - @api.depends( - 'equipment_ids', - 'child_ids.company_type', - 'child_ids.equipment_ids' - ) + + @api.depends("equipment_ids", "child_ids.company_type", "child_ids.equipment_ids") def _compute_owned_equipment_ids(self): for rec in self: - ids = rec.equipment_ids | rec.child_ids.mapped('equipment_ids') + ids = rec.equipment_ids | rec.child_ids.mapped("equipment_ids") rec.owned_equipment_ids = ids or False - @api.depends('site_ids') + @api.depends("site_ids") def _compute_is_site_contact(self): for rec in self: rec.is_site_contact = rec.site_ids is not False - @api.depends('equipment_ids') + @api.depends("equipment_ids") def _compute_equipment_count(self): for rec in self: - all_equipment_ids = self.env['bemade_fsm.equipment'].search( - [('partner_location_id', '=', rec.id)]) + all_equipment_ids = self.env["bemade_fsm.equipment"].search( + [("partner_location_id", "=", rec.id)] + ) rec.equipment_count = len(all_equipment_ids) @api.model def _search_is_site_contact(self, operator, value): - return [('site_contacts', '!=', False)] + return [("site_contacts", "!=", False)] def _compute_is_service_site(self): for rec in self: diff --git a/bemade_fsm/models/sale_order.py b/bemade_fsm/models/sale_order.py index b6c0ac2..db45a40 100644 --- a/bemade_fsm/models/sale_order.py +++ b/bemade_fsm/models/sale_order.py @@ -1,15 +1,11 @@ from odoo import fields, models, api, _, Command -from odoo.exceptions import ValidationError -from odoo.tools import float_round -import re class SaleOrder(models.Model): - _inherit = 'sale.order' + _inherit = "sale.order" valid_equipment_ids = fields.One2many( - comodel_name="bemade_fsm.equipment", - related="partner_id.owned_equipment_ids" + comodel_name="bemade_fsm.equipment", related="partner_id.owned_equipment_ids" ) default_equipment_ids = fields.Many2many( @@ -18,66 +14,67 @@ class SaleOrder(models.Model): help="The default equipment to service for new sale order lines.", compute="_compute_default_equipment", inverse="_inverse_default_equipment", - store=True + store=True, ) summary_equipment_ids = fields.Many2many( comodel_name="bemade_fsm.equipment", string="Equipment Being Serviced", - compute="_compute_summary_equipment_ids") + compute="_compute_summary_equipment_ids", + ) site_contacts = fields.Many2many( - comodel_name='res.partner', + comodel_name="res.partner", relation="sale_order_site_contacts_rel", compute="_compute_default_contacts", inverse="_inverse_default_contacts", - string='Site Contacts', - store=True - ) - - work_order_contacts = fields.Many2many( - comodel_name='res.partner', - relation='sale_order_work_order_contacts_rel', - compute='_compute_default_contacts', - inverse='_inverse_default_contacts', - string='Work Order Recipients', - store=True - ) - - visit_ids = fields.One2many( - comodel_name='bemade_fsm.visit', - inverse_name="sale_order_id", - readonly=False - ) - - is_fsm = fields.Boolean( - compute='_compute_is_fsm', - string='Is FSM', store=True, ) - @api.depends('order_line.task_id') + work_order_contacts = fields.Many2many( + comodel_name="res.partner", + relation="sale_order_work_order_contacts_rel", + compute="_compute_default_contacts", + inverse="_inverse_default_contacts", + string="Work Order Recipients", + store=True, + ) + + visit_ids = fields.One2many( + comodel_name="bemade_fsm.visit", inverse_name="sale_order_id", readonly=False + ) + + is_fsm = fields.Boolean( + compute="_compute_is_fsm", + string="Is FSM", + store=True, + ) + + @api.depends("order_line.task_id") def get_relevant_order_lines(self, task_id): self.ensure_one() - linked_lines = self.order_line.filtered(lambda l: l.task_id == task_id - or l == task_id.visit_id.so_section_id) - visit_lines = linked_lines.filtered(lambda l: l.visit_id) + linked_lines = self.order_line.filtered( + lambda line: line.task_id == task_id + or line == task_id.visit_id.so_section_id + ) + visit_lines = linked_lines.filtered(lambda line: line.visit_id) for line in visit_lines: linked_lines |= line.get_section_line_ids() return linked_lines - @api.depends('order_line.equipment_ids') + @api.depends("order_line.equipment_ids") def _compute_summary_equipment_ids(self): for rec in self: - rec.summary_equipment_ids = rec.order_line.mapped('equipment_ids') + rec.summary_equipment_ids = rec.order_line.mapped("equipment_ids") - @api.onchange('partner_shipping_id') + @api.onchange("partner_shipping_id") def _onchange_partner_shipping_id(self): - super()._onchange_partner_shipping_id() + res = super()._onchange_partner_shipping_id() self._compute_default_equipment() self._compute_default_contacts() + return res - @api.depends('partner_shipping_id') + @api.depends("partner_shipping_id") def _compute_default_contacts(self): for rec in self: rec.site_contacts = rec.partner_shipping_id.site_contacts @@ -87,10 +84,10 @@ class SaleOrder(models.Model): pass @api.depends( - 'partner_id', - 'partner_shipping_id', - 'partner_shipping_id.equipment_ids', - 'partner_id.owned_equipment_ids' + "partner_id", + "partner_shipping_id", + "partner_shipping_id.equipment_ids", + "partner_id.owned_equipment_ids", ) def _compute_default_equipment(self): for rec in self: @@ -109,36 +106,46 @@ class SaleOrder(models.Model): return rec def _create_default_visit(self): - """ Called when an order is confirmed with lines that will create an FSM task, in order to make sure there is + """Called when an order is confirmed with lines that will create an FSM task, in order to make sure there is a visit line grouping all the service being done.""" self.ensure_one() - visit = self.env['bemade_fsm.visit'].create({ - 'label': _('Service Visit'), - 'sale_order_id': self.id, - }) + visit = self.env["bemade_fsm.visit"].create( + { + "label": _("Service Visit"), + "sale_order_id": self.id, + } + ) # Make sure it goes to the top of the list visit.so_section_id.sequence = 0 def _create_or_organize_visits_if_needed(self): - """ Adds a visit line to the top of the order if there are not already visit lines for an order with lines that + """Adds a visit line to the top of the order if there are not already visit lines for an order with lines that will create an FSM task.""" for order in self.filtered("company_id.create_default_fsm_visit"): if not order.visit_ids and order.is_fsm: order._create_default_visit() if order.is_fsm: # Make sure that all the lines producing FSM tasks are under a visit - visit_line_ids = order.mapped('visit_ids').mapped('so_section_id').mapped('section_line_ids') - if any([ - True for line in - order.order_line.filtered(lambda line: not line.display_type) - if line not in visit_line_ids - ]): + visit_line_ids = ( + order.mapped("visit_ids") + .mapped("so_section_id") + .mapped("section_line_ids") + ) + if any( + [ + True + for line in order.order_line.filtered( + lambda line: not line.display_type + ) + if line not in visit_line_ids + ] + ): # If not, promote the first visit to the top of the order items list for line in order.order_line: line.sequence += 1 - order.mapped('visit_ids').mapped('so_section_id')[0].sequence = 0 + order.mapped("visit_ids").mapped("so_section_id")[0].sequence = 0 - @api.depends('order_line.is_fsm') + @api.depends("order_line.is_fsm") def _compute_is_fsm(self): for rec in self: rec.is_fsm = any([line.is_fsm for line in rec.order_line]) diff --git a/bemade_fsm/models/sale_order_line.py b/bemade_fsm/models/sale_order_line.py index 838f723..e052786 100644 --- a/bemade_fsm/models/sale_order_line.py +++ b/bemade_fsm/models/sale_order_line.py @@ -1,14 +1,10 @@ from odoo import fields, models, api, _, Command -from odoo.exceptions import ValidationError -from odoo.tools import float_round -import re class SaleOrderLine(models.Model): - _inherit = 'sale.order.line' + _inherit = "sale.order.line" valid_equipment_ids = fields.One2many( - comodel_name="bemade_fsm.equipment", - related="order_id.valid_equipment_ids" + comodel_name="bemade_fsm.equipment", related="order_id.valid_equipment_ids" ) visit_ids = fields.One2many( @@ -22,20 +18,26 @@ class SaleOrderLine(models.Model): comodel_name="bemade_fsm.visit", compute="_compute_visit_id", string="Visit", - ondelete='cascade', + ondelete="cascade", store=True, ) is_fully_delivered = fields.Boolean( string="Fully Delivered", compute="_compute_is_fully_delivered", - help="Indicates whether a line or all the lines in a section have been entirely delivered." + help=( + "Indicates whether a line or all the lines in a section have been entirely" + " delivered." + ), ) is_fully_delivered_and_invoiced = fields.Boolean( string="Fully Invoiced", compute="_compute_is_fully_invoiced", - help="Indicates whether a line or all the lines in a section have been entirely delivered and invoiced." + help=( + "Indicates whether a line or all the lines in a section have been entirely" + " delivered and invoiced." + ), ) equipment_ids = fields.Many2many( @@ -43,32 +45,28 @@ class SaleOrderLine(models.Model): comodel_name="bemade_fsm.equipment", relation="bemade_fsm_equipment_sale_order_line_rel", column1="sale_order_line_id", - column2="equipment_id" + column2="equipment_id", ) - is_field_service = fields.Boolean( - string="Is Field Service", - compute="_compute_is_field_service", - store=True - ) + is_field_service = fields.Boolean(compute="_compute_is_field_service", store=True) is_fsm = fields.Boolean( - string='Is FSM', - compute='_compute_is_fsm', + string="Is FSM", + compute="_compute_is_fsm", store=True, ) section_line_ids = fields.One2many( - comodel_name='sale.order.line', - compute='_compute_section_line_ids', + comodel_name="sale.order.line", + compute="_compute_section_line_ids", ) - @api.depends('visit_ids') + @api.depends("visit_ids") def _compute_visit_id(self): for rec in self: rec.visit_id = rec.visit_ids and rec.visit_ids[0] - @api.depends('product_id') + @api.depends("product_id") def _compute_is_field_service(self): for rec in self: rec.is_field_service = rec.product_id.is_field_service @@ -84,38 +82,49 @@ class SaleOrderLine(models.Model): def copy_data(self, default=None): if default is None: default = {} - if 'visit_ids' not in default: - default['visit_ids'] = [(0, 0, visit.copy_data()[0]) for visit in self.visit_ids] + if "visit_ids" not in default: + default["visit_ids"] = [ + (0, 0, visit.copy_data()[0]) for visit in self.visit_ids + ] return super().copy_data(default) def _timesheet_create_task(self, project): - """ Generate task for the given so line, and link it. - :param project: record of project.project in which the task should be created - :return task: record of the created task + """Generate task for the given so line, and link it. + :param project: record of project.project in which the task should be created + :return task: record of the created task - Override to add the logic needed to implement task templates and equipment linkages.""" + Override to add the logic needed to implement task templates and equipment linkages. + """ def _create_task_from_template(project, template, parent): - """ Recursively generates the task and any subtasks from a project.task.template. + """Recursively generates the task and any subtasks from a project.task.template. :param project: project.project record to set on the task's project_id field. :param template: project.task.template to use to create the task. :param parent: project.task to set as the parent to this task. """ - values = _timesheet_create_task_prepare_values_from_template(project, template, parent) - task = self.env['project.task'].sudo().create(values) + values = _timesheet_create_task_prepare_values_from_template( + project, template, parent + ) + task = self.env["project.task"].sudo().create(values) subtasks = [] for t in template.subtasks: subtask = _create_task_from_template(project, t, task) subtasks.append(subtask) - task.write({'child_ids': [Command.set([t.id for t in subtasks])]}) + task.write({"child_ids": [Command.set([t.id for t in subtasks])]}) # We don't want to see the sub-tasks on the SO - task.child_ids.write({'sale_order_id': None, 'sale_line_id': None, }) + task.child_ids.write( + { + "sale_order_id": None, + "sale_line_id": None, + } + ) return task - def _timesheet_create_task_prepare_values_from_template(project, template, - parent): - """ Copies the values from a project.task.template over to the set of values used to create a project.task. + def _timesheet_create_task_prepare_values_from_template( + project, template, parent + ): + """Copies the values from a project.task.template over to the set of values used to create a project.task. :param project: project.project record to set on the task's project_id field. Pass the project.project model or an empty recordset to leave task project_id blank. @@ -124,15 +133,17 @@ class SaleOrderLine(models.Model): :param parent: project.task to set as the parent to this task. """ vals = self._timesheet_create_task_prepare_values(project) - vals['name'] = template.name - vals['description'] = template.description or '' if parent else vals['description'] - vals['parent_id'] = parent and parent.id - vals['user_ids'] = template.assignees.ids - vals['tag_ids'] = template.tags.ids - vals['allocated_hours'] = template.planned_hours - vals['sequence'] = template.sequence + vals["name"] = template.name + vals["description"] = ( + template.description or "" if parent else vals["description"] + ) + vals["parent_id"] = parent and parent.id + vals["user_ids"] = template.assignees.ids + vals["tag_ids"] = template.tags.ids + vals["allocated_hours"] = template.planned_hours + vals["sequence"] = template.sequence if template.equipment_ids: - vals['equipment_ids'] = template.equipment_ids.ids + vals["equipment_ids"] = template.equipment_ids.ids return vals tmpl = self.product_id.task_template_id @@ -140,11 +151,16 @@ class SaleOrderLine(models.Model): task = super()._timesheet_create_task(project) else: task = _create_task_from_template(project, tmpl, None) - self.write({'task_id': task.id}) + self.write({"task_id": task.id}) # post message on task task_msg = _( - "This task has been created from: %s (%s)") % ( - self.order_id.id, self.order_id.name, self.product_id.name) + "This task has been created from: %(so_name)s (%(product_name)s)" + ) % { + "so_id": self.order_id.id, + "so_name": self.order_id.name, + "product_name": self.product_id.name, + } task.message_post(body=task_msg) if not task.equipment_ids and self.equipment_ids: task.equipment_ids = self.equipment_ids.ids @@ -152,9 +168,9 @@ class SaleOrderLine(models.Model): def _timesheet_service_generation(self): super()._timesheet_service_generation() - visit_lines = self.filtered(lambda l: l.visit_id) + visit_lines = self.filtered(lambda line: line.visit_id) for index, line in enumerate(visit_lines): - task_ids = line.get_section_line_ids().mapped('task_id') + task_ids = line.get_section_line_ids().mapped("task_id") if not task_ids: continue if len(set([task.project_id for task in task_ids])) > 1: @@ -162,72 +178,89 @@ class SaleOrderLine(models.Model): return project_id = task_ids[0].project_id line.visit_id.task_id = line._generate_task_for_visit_line( - project_id, index + 1, - sum(task_ids.mapped("allocated_hours")) + project_id, index + 1, sum(task_ids.mapped("allocated_hours")) ) - task_ids.write({'parent_id': line.visit_id.task_id.id}) - (self.mapped('task_id') | self.visit_ids.task_id).filtered("is_fsm").synchronize_name_fsm() + task_ids.write({"parent_id": line.visit_id.task_id.id}) + (self.mapped("task_id") | self.visit_ids.task_id).filtered( + "is_fsm" + ).synchronize_name_fsm() - def _generate_task_for_visit_line(self, project, visit_no: int, allocated_hours: int): + def _generate_task_for_visit_line( + self, project, visit_no: int, allocated_hours: int + ): self.ensure_one() - allocated_hours = sum(self.get_section_line_ids().task_id.mapped('allocated_hours')) - task = self.env['project.task'].create({ - 'name': f"{self.order_id.name} - " + _("Visit") + f" {visit_no} - {self.name}", - 'project_id': project.id, - 'equipment_ids': self.get_section_line_ids().mapped('equipment_ids').ids, - 'sale_order_id': self.order_id.id, - 'partner_id': self.order_id.partner_shipping_id.id, - 'visit_id': self.visit_id.id, - 'allocated_hours': allocated_hours, - 'date_deadline': self.visit_id.approx_date, - 'user_ids': False, # Force to empty or it uses the current user - }) + allocated_hours = sum( + self.get_section_line_ids().task_id.mapped("allocated_hours") + ) + task = self.env["project.task"].create( + { + "name": ( + f"{self.order_id.name} - " + + _("Visit") + + f" {visit_no} - {self.name}" + ), + "project_id": project.id, + "equipment_ids": ( + self.get_section_line_ids().mapped("equipment_ids").ids + ), + "sale_order_id": self.order_id.id, + "partner_id": self.order_id.partner_shipping_id.id, + "visit_id": self.visit_id.id, + "allocated_hours": allocated_hours, + "date_deadline": self.visit_id.approx_date, + "user_ids": False, # Force to empty or it uses the current user + } + ) return task @api.depends( - 'order_id.order_line', - 'display_type', - 'qty_to_deliver', - 'order_id.order_line.qty_to_deliver', - 'order_id.order_line.display_type' + "order_id.order_line", + "display_type", + "qty_to_deliver", + "order_id.order_line.qty_to_deliver", + "order_id.order_line.display_type", ) def _compute_is_fully_delivered(self): for rec in self: rec.is_fully_delivered = rec._iterate_items_compute_bool( - lambda l: l.qty_to_deliver == 0) + lambda line: line.qty_to_deliver == 0 + ) - @api.depends('is_fully_delivered') + @api.depends("is_fully_delivered") def _compute_is_fully_invoiced(self): for rec in self: if not rec.is_fully_delivered: rec.is_fully_delivered_and_invoiced = False return rec.is_fully_delivered_and_invoiced = rec._iterate_items_compute_bool( - lambda l: l.qty_to_invoice == 0) + lambda line: line.qty_to_invoice == 0 + ) def get_section_line_ids(self): - """ Return a recordset of sale.order.line records that are in this sale order section. """ + """Return a recordset of sale.order.line records that are in this sale order section.""" self.ensure_one() - assert self.display_type == 'line_section', "Cannot get section lines for a non-section." + assert ( + self.display_type == "line_section" + ), "Cannot get section lines for a non-section." found = False lines = [] - for line in self.order_id.order_line.sorted(lambda l: l.sequence): + for line in self.order_id.order_line.sorted(lambda line: line.sequence): if line == self: found = True continue if not found: continue - if line.display_type == 'line_section': # Stop when we hit the next section + if line.display_type == "line_section": # Stop when we hit the next section break else: lines.append(line) - return self.env['sale.order.line'].union(*lines) + return self.env["sale.order.line"].union(*lines) - @api.depends('display_type', 'order_id.order_line') + @api.depends("display_type", "order_id.order_line") def _compute_section_line_ids(self): for rec in self: - if rec.display_type == 'line_section': + if rec.display_type == "line_section": rec.section_line_ids = [Command.set(rec.get_section_line_ids().ids)] else: rec.section_line_ids = False @@ -235,7 +268,7 @@ class SaleOrderLine(models.Model): def _iterate_items_compute_bool(self, single_line_func): if not self.display_type: return single_line_func(self) - elif self.display_type == 'line_note': + elif self.display_type == "line_note": return True else: for line in self.order_id.order_line: @@ -244,15 +277,17 @@ class SaleOrderLine(models.Model): found = True if not found: continue - if found and line.display_type == 'line_section': + if found and line.display_type == "line_section": return True val = single_line_func(self) if not val: return val return True - @api.depends('product_id.detailed_type', 'product_id.service_tracking') + @api.depends("product_id.detailed_type", "product_id.service_tracking") def _compute_is_fsm(self): for rec in self: - rec.is_fsm = (rec.product_id.detailed_type == 'service' - and rec.product_id.service_tracking == 'task_global_project') + rec.is_fsm = ( + rec.product_id.detailed_type == "service" + and rec.product_id.service_tracking == "task_global_project" + ) diff --git a/bemade_fsm/models/task.py b/bemade_fsm/models/task.py index 80843b2..29f7112 100644 --- a/bemade_fsm/models/task.py +++ b/bemade_fsm/models/task.py @@ -1,4 +1,4 @@ -from odoo import fields, models, api, Command, _ +from odoo import fields, models, api, Command from odoo.addons.project.models.project_task import CLOSED_STATES import re @@ -34,25 +34,24 @@ class Task(models.Model): string="Can be billed", related=False, compute="_compute_allow_billable", - store=True + store=True, ) visit_id = fields.Many2one( - comodel_name='bemade_fsm.visit', + comodel_name="bemade_fsm.visit", string="Visit", ) relevant_order_lines = fields.Many2many( - comodel_name='sale.order.line', + comodel_name="sale.order.line", store=False, - compute='_compute_relevant_order_lines', + compute="_compute_relevant_order_lines", ) work_order_number = fields.Char(readonly=True) propagate_assignment = fields.Boolean( - string='Propagate Assignment', - help='Propagate assignment of this task to all subtasks.', + help="Propagate assignment of this task to all subtasks.", default=False, ) @@ -76,18 +75,29 @@ class Task(models.Model): rec.site_contacts = rec.parent_id.site_contacts if rec.sale_order_id: seq = 1 - prev_seqs = self.sale_order_id.tasks_ids and \ - self.sale_order_id.tasks_ids.mapped('work_order_number') + prev_seqs = ( + self.sale_order_id.tasks_ids + and self.sale_order_id.tasks_ids.mapped("work_order_number") + ) if prev_seqs: pattern = re.compile(r"(\d+)$") matches = map(lambda n: pattern.search(n), prev_seqs) seq += max(map(lambda n: int(n.group(1)) if n else 0, matches)) - rec.work_order_number = rec.sale_order_id.name.replace('SO', 'SVR', 1) \ - + f"-{seq}" + rec.work_order_number = ( + rec.sale_order_id.name.replace("SO", "SVR", 1) + f"-{seq}" + ) # If the task is linked to a sales order and has no parent, it should inherit SO work order contacts - if not rec.parent_id and not rec.work_order_contacts and rec.sale_order_id.work_order_contacts: + if ( + not rec.parent_id + and not rec.work_order_contacts + and rec.sale_order_id.work_order_contacts + ): rec.work_order_contacts = rec.sale_order_id.work_order_contacts - if not rec.parent_id and not rec.site_contacts and rec.sale_order_id.site_contacts: + if ( + not rec.parent_id + and not rec.site_contacts + and rec.sale_order_id.site_contacts + ): rec.site_contacts = rec.sale_order_id.site_contacts return res @@ -95,45 +105,57 @@ class Task(models.Model): res = super().write(vals) if not self: # End recursion on empty RecordSet return res - if 'propagate_assignment' in vals: + if "propagate_assignment" in vals: # When a user sets propagate assignment, it should propagate that setting all the way down the chain - self.child_ids.write({'propagate_assignment': vals['propagate_assignment']}) - if 'user_ids' in vals: + self.child_ids.write({"propagate_assignment": vals["propagate_assignment"]}) + if "user_ids" in vals: to_propagate = self.filtered(lambda task: task.propagate_assignment) # Here we use child_ids instead of _get_all_subtasks() so as to allow for setting propagate_assignment # to false on a child task. - to_propagate.child_ids.write({'user_ids': vals['user_ids']}) - if 'site_contacts' in vals and self.child_ids: - self._get_all_subtasks().write({'site_contacts': [Command.set(self.site_contacts.ids)]}) - if 'work_order_contacts' in vals and self.child_ids: - self._get_all_subtasks().write({'work_order_contacts': [Command.set(self.work_order_contacts.ids)]}) + to_propagate.child_ids.write({"user_ids": vals["user_ids"]}) + if "site_contacts" in vals and self.child_ids: + self._get_all_subtasks().write( + {"site_contacts": [Command.set(self.site_contacts.ids)]} + ) + if "work_order_contacts" in vals and self.child_ids: + self._get_all_subtasks().write( + {"work_order_contacts": [Command.set(self.work_order_contacts.ids)]} + ) return res - @api.depends('sale_order_id') + @api.depends("sale_order_id") def _compute_relevant_order_lines(self): for rec in self: rec.relevant_order_lines = ( - rec.sale_order_id and rec.sale_order_id.get_relevant_order_lines( - rec) or False) + rec.sale_order_id + and rec.sale_order_id.get_relevant_order_lines(rec) + or False + ) def _get_closed_stage_by_project(self): - """ Gets the stage representing completed tasks for each project in + """Gets the stage representing completed tasks for each project in self.project_id. Copied from industry_fsm/.../project.py:217-221 for consistency. :returns: Dict of project.project -> project.task.type""" return { - project: + project: ( project.type_ids.filtered(lambda stage: stage.is_closed)[:1] or project.type_ids[-1:] + ) for project in self.project_id } - @api.depends('parent_id.visit_id', 'project_id.is_fsm', 'project_id.allow_billable') + @api.depends("parent_id.visit_id", "project_id.is_fsm", "project_id.allow_billable") def _compute_allow_billable(self): for rec in self: # If an FSM task has a parent that is linked to an SO line, then the parent is the billable one - if rec.parent_id and not rec.parent_id.visit_id and rec.project_id and rec.project_id.is_fsm: + if ( + rec.parent_id + and not rec.parent_id.visit_id + and rec.project_id + and rec.project_id.is_fsm + ): rec.allow_billable = False else: rec.allow_billable = rec.project_id.allow_billable @@ -157,7 +179,7 @@ class Task(models.Model): return self def synchronize_name_fsm(self): - """ Applies naming to the entire task tree for tasks that are part of this + """Applies naming to the entire task tree for tasks that are part of this recordset. Root tasks are named: Partner Shipping Name - Sale Line Name (Template Name) @@ -176,7 +198,7 @@ class Task(models.Model): if template: title = template.name elif rec.sale_line_id: - name_parts = rec.sale_line_id.name.split('\n') + name_parts = rec.sale_line_id.name.split("\n") title = name_parts and name_parts[0] or rec.sale_line_id.product_id.name elif rec.visit_id: title = rec.visit_id.label @@ -187,7 +209,7 @@ class Task(models.Model): client_name = rec.sale_order_id.partner_shipping_id.name if not rec.parent_id: - rec.name = f"{rec.work_order_number} - {client_name} - " f"{title}" + rec.name = f"{rec.work_order_number} - {client_name} - {title}" else: rec.name = title diff --git a/bemade_fsm/models/task_template.py b/bemade_fsm/models/task_template.py index 0d4512d..0a90459 100644 --- a/bemade_fsm/models/task_template.py +++ b/bemade_fsm/models/task_template.py @@ -1,8 +1,8 @@ -from odoo import models, fields, api, _, Command +from odoo import models, fields, api, Command class TaskTemplate(models.Model): - _name = 'project.task.template' + _name = "project.task.template" _description = "Template for new project tasks" @api.model @@ -11,50 +11,47 @@ class TaskTemplate(models.Model): name = fields.Char(string="Task Title", required=True) - description = fields.Html(string="Description") + description = fields.Html() assignees = fields.Many2many( comodel_name="res.users", string="Default Assignees", - help="Employees assigned to tasks created from this template." + help="Employees assigned to tasks created from this template.", ) customer = fields.Many2one( comodel_name="res.partner", string="Default Customer", - help="Default customer for tasks created from this template." + help="Default customer for tasks created from this template.", ) project = fields.Many2one( comodel_name="project.project", string="Default Project", - help="Default project for tasks created from this template." + help="Default project for tasks created from this template.", ) tags = fields.Many2many( comodel_name="project.tags", string="Default Tags", - help="Default tags for tasks created from this template." + help="Default tags for tasks created from this template.", ) parent = fields.Many2one( comodel_name="project.task.template", string="Parent Task Template", - ondelete='cascade' + ondelete="cascade", ) subtasks = fields.One2many( comodel_name="project.task.template", inverse_name="parent", - string="Subtask Templates" + string="Subtask Templates", ) - sequence = fields.Integer(string="Sequence") + sequence = fields.Integer() company_id = fields.Many2one( - comodel_name="res.company", - string="Company", - index=1, - default=_current_company + comodel_name="res.company", string="Company", index=1, default=_current_company ) planned_hours = fields.Float("Initially Planned Hours") @@ -69,50 +66,53 @@ class TaskTemplate(models.Model): def action_open_task(self): return { - 'view_mode': 'form', - 'res_model': 'project.task.template', - 'res_id': self.id, - 'type': 'ir.actions.act_window', - 'context': self._context + "view_mode": "form", + "res_model": "project.task.template", + "res_id": self.id, + "type": "ir.actions.act_window", + "context": self._context, } - @api.onchange('customer') + @api.onchange("customer") def _onchange_customer(self): for rec in self: - new_equipment_ids = [eq.id for eq in rec.equipment_ids if eq.partner_location_id == rec.customer] - rec.write({'equipment_ids': [Command.set(new_equipment_ids)]}) + new_equipment_ids = [ + eq.id + for eq in rec.equipment_ids + if eq.partner_location_id == rec.customer + ] + rec.write({"equipment_ids": [Command.set(new_equipment_ids)]}) def _prepare_new_task_values_from_self(self, project, name=False, parent_id=False): vals = { - 'project_id': project.id, - 'name': name or self.name, - 'description': self.description, - 'parent_id': parent_id, - 'user_ids': self.assignees.ids, - 'tag_ids': self.tags.ids, - 'allocated_hours': self.planned_hours, - 'sequence': self.sequence, - 'equipment_ids': [Command.set(self.equipment_ids.ids)] if self.equipment_ids else False, - 'partner_id': project.partner_id and project.partner_id.id, - 'company_id': self.company_id.id, + "project_id": project.id, + "name": name or self.name, + "description": self.description, + "parent_id": parent_id, + "user_ids": self.assignees.ids, + "tag_ids": self.tags.ids, + "allocated_hours": self.planned_hours, + "sequence": self.sequence, + "equipment_ids": ( + [Command.set(self.equipment_ids.ids)] if self.equipment_ids else False + ), + "partner_id": project.partner_id and project.partner_id.id, + "company_id": self.company_id.id, } return vals def create_task_from_self(self, project, name=False, parent_id=False): - """ Create a project.task from this template and return it. Can be called on a RecordSet of multiple templates. + """Create a project.task from this template and return it. Can be called on a RecordSet of multiple templates. :param project: project.project record the task should be added to :param name: name for the new task (defaults to template name) :param parent_id: parent task for the new task (none by default) :return: project.task record created from this template """ - tasks = self.env['project.task'] + tasks = self.env["project.task"] for rec in self: vals = rec._prepare_new_task_values_from_self(project, name, parent_id) - task = rec.env['project.task'].create(vals) + task = rec.env["project.task"].create(vals) rec.subtasks.create_task_from_self(project, name=False, parent_id=task.id) tasks |= task return tasks - - - diff --git a/bemade_fsm/reports/worksheet_custom_report_templates.xml b/bemade_fsm/reports/worksheet_custom_report_templates.xml index 7323cf6..cddad25 100644 --- a/bemade_fsm/reports/worksheet_custom_report_templates.xml +++ b/bemade_fsm/reports/worksheet_custom_report_templates.xml @@ -1,421 +1,519 @@ - - + diff --git a/bemade_fsm/reports/worksheet_custom_reports.py b/bemade_fsm/reports/worksheet_custom_reports.py index a6e1c8b..88d3bac 100644 --- a/bemade_fsm/reports/worksheet_custom_reports.py +++ b/bemade_fsm/reports/worksheet_custom_reports.py @@ -6,6 +6,8 @@ class TaskCustomReport(models.AbstractModel): def _get_report_values(self, docids, data=None): vals = super()._get_report_values(docids, data) - split_time_materials = self.env.company.split_time_from_materials_on_service_work_orders + split_time_materials = ( + self.env.company.split_time_from_materials_on_service_work_orders + ) vals.update({"split_time_materials": split_time_materials}) return vals diff --git a/bemade_fsm/reports/worksheet_custom_reports.xml b/bemade_fsm/reports/worksheet_custom_reports.xml index 5dd1bd7..bde5bc8 100644 --- a/bemade_fsm/reports/worksheet_custom_reports.xml +++ b/bemade_fsm/reports/worksheet_custom_reports.xml @@ -1,12 +1,11 @@ - - - '%s %s' % ( - object.planned_date_begin.strftime('%Y-%m-%d') if object.planned_date_begin else time.strftime('%Y-%m-%d'), - object.name - ) - - - + + '%s %s' % ( + object.planned_date_begin.strftime( + '%Y-%m-%d') if object.planned_date_begin else time.strftime('%Y-%m-%d'), + object.name + ) + + diff --git a/bemade_fsm/tests/test_bemade_fsm_common.py b/bemade_fsm/tests/test_bemade_fsm_common.py index 4a6059e..21cfca0 100644 --- a/bemade_fsm/tests/test_bemade_fsm_common.py +++ b/bemade_fsm/tests/test_bemade_fsm_common.py @@ -7,8 +7,8 @@ class BemadeFSMBaseTest(TransactionCase): @classmethod def _generate_project_manager_user(cls, name, login): group_ids = cls.__get_user_groups() - user_group_project_manager = cls.env.ref('project.group_project_manager') - user_group_fsm_manager = cls.env.ref('industry_fsm.group_fsm_manager') + user_group_project_manager = cls.env.ref("project.group_project_manager") + user_group_fsm_manager = cls.env.ref("industry_fsm.group_fsm_manager") group_ids.append(user_group_fsm_manager.id) group_ids.append(user_group_project_manager.id) @@ -21,118 +21,170 @@ class BemadeFSMBaseTest(TransactionCase): @classmethod def __generate_user(cls, name, login, group_ids): - return cls.env['res.users'].with_context({'no_reset_password': True}).create({ - 'name': name, - 'login': login, - 'password': login, - 'email': f"{login}@test.co", - 'groups_id': [Command.set(group_ids)] - }) + return ( + cls.env["res.users"] + .with_context(no_reset_password=True) + .create( + { + "name": name, + "login": login, + "password": login, + "email": f"{login}@test.co", + "groups_id": [Command.set(group_ids)], + } + ) + ) @classmethod def __get_user_groups(cls): - user_group_employee = cls.env.ref('base.group_user') - user_group_project_user = cls.env.ref('project.group_project_user') - user_group_fsm_user = cls.env.ref('industry_fsm.group_fsm_user') - user_group_sales_user = cls.env.ref('sales_team.group_sale_salesman') - user_group_sales_manager = cls.env.ref('sales_team.group_sale_manager') + user_group_employee = cls.env.ref("base.group_user") + user_group_project_user = cls.env.ref("project.group_project_user") + user_group_fsm_user = cls.env.ref("industry_fsm.group_fsm_user") + user_group_sales_user = cls.env.ref("sales_team.group_sale_salesman") + user_group_sales_manager = cls.env.ref("sales_team.group_sale_manager") user_product_customer = cls.env.ref( - 'customer_product_code.group_product_customer_code_user', - raise_if_not_found=False + "customer_product_code.group_product_customer_code_user", + raise_if_not_found=False, ) - group_ids = [user_group_employee.id, - user_group_project_user.id, - user_group_fsm_user.id, - user_group_sales_manager.id, - user_group_sales_user.id, ] + group_ids = [ + user_group_employee.id, + user_group_project_user.id, + user_group_fsm_user.id, + user_group_sales_manager.id, + user_group_sales_user.id, + ] if user_product_customer: group_ids.append(user_product_customer.id) return group_ids @classmethod - def _generate_partner(cls, name: str = 'Test Company', company_type: str = 'company', parent=None, - location_type='other'): - """ Generates a partner with basic address filled in. + def _generate_partner( + cls, + name: str = "Test Company", + company_type: str = "company", + parent=None, + location_type="other", + ): + """Generates a partner with basic address filled in. :param name: The partner's name. - :param company_type: The type of partner, either 'company' or 'person' are accepted.""" - return cls.env['res.partner'].create({ - 'name': name, - 'company_type': company_type, - 'street': '123 Street St.', - 'city': 'Montreal', - 'state_id': cls.env.ref('base.state_ca_qc').id, - 'country_id': cls.env.ref('base.ca').id, - 'parent_id': parent and parent.id or False, - 'type': location_type, - }) + :param company_type: The type of partner, either 'company' or 'person' are accepted. + """ + return cls.env["res.partner"].create( + { + "name": name, + "company_type": company_type, + "street": "123 Street St.", + "city": "Montreal", + "state_id": cls.env.ref("base.state_ca_qc").id, + "country_id": cls.env.ref("base.ca").id, + "parent_id": parent and parent.id or False, + "type": location_type, + } + ) @classmethod - def _generate_sale_order(cls, partner=None, client_order_ref='Test Order', equipment=None, shipping_location=None): + def _generate_sale_order( + cls, + partner=None, + client_order_ref="Test Order", + equipment=None, + shipping_location=None, + ): partner = partner or cls._generate_partner() - vals = {'partner_id': partner.id, - 'client_order_ref': client_order_ref} + vals = {"partner_id": partner.id, "client_order_ref": client_order_ref} if equipment: - vals.update({'default_equipment_ids': [Command.set([equipment.id])]}) + vals.update({"default_equipment_ids": [Command.set([equipment.id])]}) if shipping_location: - vals.update({'partner_shipping_id': shipping_location.id}) - return cls.env['sale.order'].create(vals) + vals.update({"partner_shipping_id": shipping_location.id}) + return cls.env["sale.order"].create(vals) @classmethod - def _generate_sale_order_line(cls, sale_order, product=None, qty=1.0, uom=None, price=100.0, tax_id=False): + def _generate_sale_order_line( + cls, sale_order, product=None, qty=1.0, uom=None, price=100.0, tax_id=False + ): if not product: product = cls._generate_product() - return cls.env['sale.order.line'].create({ - 'order_id': sale_order.id, - 'product_id': product.id, - 'product_uom_qty': qty, - 'product_uom': uom and uom.id or cls.env.ref("uom.product_uom_hour").id, - 'price_unit': price, - 'tax_id': tax_id, - }) + return cls.env["sale.order.line"].create( + { + "order_id": sale_order.id, + "product_id": product.id, + "product_uom_qty": qty, + "product_uom": uom and uom.id or cls.env.ref("uom.product_uom_hour").id, + "price_unit": price, + "tax_id": tax_id, + } + ) @classmethod - def _generate_equipment(cls, name='test equipment', partner=None): - return cls.env['bemade_fsm.equipment'].create({ - 'name': name, - 'partner_location_id': partner and partner.id or False, - }) + def _generate_equipment(cls, name="test equipment", partner=None): + return cls.env["bemade_fsm.equipment"].create( + { + "name": name, + "partner_location_id": partner and partner.id or False, + } + ) @classmethod - def _generate_product(cls, name='Test Product', product_type='service', service_tracking='task_global_project', - project=None, task_template=None, service_policy='delivered_manual', uom=None): - if 'project' in service_tracking and not project: + def _generate_product( + cls, + name="Test Product", + product_type="service", + service_tracking="task_global_project", + project=None, + task_template=None, + service_policy="delivered_manual", + uom=None, + ): + if "project" in service_tracking and not project: project = cls.env.ref("industry_fsm.fsm_project") uom_id = uom and uom.id or cls.env.ref("uom.product_uom_hour").id or False - return cls.env['product.product'].create({ - 'name': name, - 'type': product_type, - 'service_tracking': service_tracking, - 'service_type': 'timesheet', - 'project_id': service_tracking in ('task_global_project', 'project_only') and project.id or False, - 'project_template_id': service_tracking == 'task_in_project' and project.id or False, - 'task_template_id': task_template and task_template.id or False, - 'service_policy': service_policy, - 'uom_id': uom_id, - 'uom_po_id': uom_id, - }) + return cls.env["product.product"].create( + { + "name": name, + "type": product_type, + "service_tracking": service_tracking, + "service_type": "timesheet", + "project_id": ( + service_tracking in ("task_global_project", "project_only") + and project.id + or False + ), + "project_template_id": ( + service_tracking == "task_in_project" and project.id or False + ), + "task_template_id": task_template and task_template.id or False, + "service_policy": service_policy, + "uom_id": uom_id, + "uom_po_id": uom_id, + } + ) @classmethod - def _generate_fsm_project(cls, name='Test Project'): - return cls.env['project.project'].create({ - 'name': name, - 'allow_material': True, - 'allow_timesheets': True, - 'allow_quotations': True, - 'allow_worksheets': True, - 'is_fsm': True, - }) + def _generate_fsm_project(cls, name="Test Project"): + return cls.env["project.project"].create( + { + "name": name, + "allow_material": True, + "allow_timesheets": True, + "allow_quotations": True, + "allow_worksheets": True, + "is_fsm": True, + } + ) @classmethod - def _generate_task_template(cls, parent=None, structure=None, names=None, planned_hours=1, - equipment=None, customer=None): - """ Generates a task template with the specified structure and naming. + def _generate_task_template( + cls, + parent=None, + structure=None, + names=None, + planned_hours=1, + equipment=None, + customer=None, + ): + """Generates a task template with the specified structure and naming. :param parent: The parent task template for the top-level task template being generated :param structure: A list of integers describing the number of tasks for each level of descendants. An empty @@ -142,48 +194,64 @@ class BemadeFSMBaseTest(TransactionCase): by a sequential integer for its level. Child 1, Child 2, Grandchild 1, etc. If no names argument is passed, a default ['Task Template'] argument will be used. :param planned_hours: The number of planned hours for the top-level task template being generated. - :param equipment: The equipment to add as linked equipment to the task template.""" + :param equipment: The equipment to add as linked equipment to the task template. + """ if not names: - names = ['Task Template'] + names = ["Task Template"] if not structure: structure = [] if len(structure) != len(names) - 1: - raise ValueError("The length of the structure argument must contain exactly one element less than the " - "names argument.") + raise ValueError( + "The length of the structure argument must contain exactly one element" + " less than the names argument." + ) name = names.pop(0) - template = cls.env['project.task.template'].create({ - 'name': name, - 'parent': parent and parent.id or False, - 'planned_hours': planned_hours, - 'equipment_ids': [Command.set(equipment and [equipment.id] or [])], - 'customer': customer and customer.id or False, - }) + template = cls.env["project.task.template"].create( + { + "name": name, + "parent": parent and parent.id or False, + "planned_hours": planned_hours, + "equipment_ids": [Command.set(equipment and [equipment.id] or [])], + "customer": customer and customer.id or False, + } + ) parent = template while structure: subtasks = [] for i in range(0, structure[0]): - subtasks.append(cls.env['project.task.template'].create({ - 'parent': parent.id, - 'name': names[0] + f" {i}", - })) + subtasks.append( + cls.env["project.task.template"].create( + { + "parent": parent.id, + "name": names[0] + f" {i}", + } + ) + ) structure.pop(0) names.pop(0) parent = subtasks[0] return template def _invoice_sale_order(self, so): - wiz = self.env['sale.advance.payment.inv'].with_context( - {'active_ids': [so.id]}).create({}) + wiz = ( + self.env["sale.advance.payment.inv"] + .with_context(active_ids=[so.id]) + .create({}) + ) wiz.create_invoices() inv = so.invoice_ids[-1] inv.action_post() return inv def _generate_visit(self, sale_order, label="Test Label"): - return self.env['bemade_fsm.visit'].create([{ - 'sale_order_id': sale_order.id, - 'label': label, - }]) + return self.env["bemade_fsm.visit"].create( + [ + { + "sale_order_id": sale_order.id, + "label": label, + } + ] + ) def _generate_so_with_one_visit_two_lines(self): so = self._generate_sale_order() @@ -198,8 +266,10 @@ class BemadeFSMBaseTest(TransactionCase): def _generate_so_with_one_visit_two_lines_and_descendants(self): so = self._generate_sale_order() visit = self._generate_visit(sale_order=so) - task_template = self._generate_task_template(structure=[2, 2, 2], - names=['Parent', 'Child', 'Grandchild', 'Great-grandchild']) + task_template = self._generate_task_template( + structure=[2, 2, 2], + names=["Parent", "Child", "Grandchild", "Great-grandchild"], + ) product = self._generate_product(task_template=task_template) sol1 = self._generate_sale_order_line(sale_order=so, product=product) sol2 = self._generate_sale_order_line(sale_order=so, product=product) diff --git a/bemade_fsm/tests/test_equipment.py b/bemade_fsm/tests/test_equipment.py index 16e06e2..8aa68e7 100644 --- a/bemade_fsm/tests/test_equipment.py +++ b/bemade_fsm/tests/test_equipment.py @@ -1,4 +1,4 @@ -from odoo.tests.common import HttpCase, tagged +from odoo.tests.common import tagged from .test_bemade_fsm_common import BemadeFSMBaseTest from odoo import Command from odoo.exceptions import MissingError @@ -8,14 +8,14 @@ from odoo.exceptions import MissingError class TestEquipment(BemadeFSMBaseTest): def test_crud(self): partner_company = self._generate_partner() - partner_contact = self._generate_partner('Site Contact', 'person', partner_company) - equipment = self._generate_equipment('Test Equipment 1', partner_company) + self._generate_partner("Site Contact", "person", partner_company) + equipment = self._generate_equipment("Test Equipment 1", partner_company) # Just make sure the basic ORM stuff is OK self.assertTrue(equipment in partner_company.equipment_ids) self.assertTrue(len(partner_company.equipment_ids) == 1) # Delete should cascade - partner_company.write({'equipment_ids': [Command.set([])]}) + partner_company.write({"equipment_ids": [Command.set([])]}) with self.assertRaises(MissingError): - equipment.name + _ = equipment.name diff --git a/bemade_fsm/tests/test_fsm_contact_setting.py b/bemade_fsm/tests/test_fsm_contact_setting.py index 8920f88..2344b10 100644 --- a/bemade_fsm/tests/test_fsm_contact_setting.py +++ b/bemade_fsm/tests/test_fsm_contact_setting.py @@ -1,34 +1,38 @@ -from odoo.tests import TransactionCase, HttpCase, tagged, Form +from odoo.tests import tagged, Form from odoo import Command from .test_bemade_fsm_common import BemadeFSMBaseTest @tagged("-at_install", "post_install") class SaleOrderFSMContactsCase(BemadeFSMBaseTest): - def test_site_contacts(self): - parent_co = self._generate_partner('Parent Co') - contact_1 = self._generate_partner('Contact 1', 'person', parent_co) - contact_2 = self._generate_partner('Contact 2', 'person', parent_co) + parent_co = self._generate_partner("Parent Co") + contact_1 = self._generate_partner("Contact 1", "person", parent_co) + contact_2 = self._generate_partner("Contact 2", "person", parent_co) # Make sure the SO pulls the defaults from the partner correctly - parent_co.write({'site_contacts': [Command.set([contact_1.id, contact_2.id])]}) + parent_co.write({"site_contacts": [Command.set([contact_1.id, contact_2.id])]}) so = self._generate_sale_order(parent_co) self.assertTrue(so.site_contacts == parent_co.site_contacts) # Make sure updating the site contacts on the SO doesn't feed back to the partner - so.write({'site_contacts': [Command.set([contact_1.id])]}) + so.write({"site_contacts": [Command.set([contact_1.id])]}) self.assertTrue(contact_1 in so.site_contacts) self.assertTrue(contact_2 not in so.site_contacts) - self.assertTrue(so.site_contacts != so.partner_id.site_contacts and len(so.partner_id.site_contacts) == 2) + self.assertTrue( + so.site_contacts != so.partner_id.site_contacts + and len(so.partner_id.site_contacts) == 2 + ) def test_default_workorder_contacts(self): - parent_co = self._generate_partner('Parent Co') - contact_1 = self._generate_partner('Contact 1', 'person', parent_co) - contact_2 = self._generate_partner('Contact 2', 'person', parent_co) + parent_co = self._generate_partner("Parent Co") + contact_1 = self._generate_partner("Contact 1", "person", parent_co) + contact_2 = self._generate_partner("Contact 2", "person", parent_co) # Make sure the SO pulls the defaults from the partner correctly - parent_co.write({'work_order_contacts': [Command.set([contact_1.id, contact_2.id])]}) + parent_co.write( + {"work_order_contacts": [Command.set([contact_1.id, contact_2.id])]} + ) so = self._generate_sale_order(parent_co) self.assertTrue(contact_1 in parent_co.work_order_contacts) self.assertTrue(contact_2 in parent_co.work_order_contacts) @@ -36,42 +40,74 @@ class SaleOrderFSMContactsCase(BemadeFSMBaseTest): self.assertTrue(contact_2 in so.work_order_contacts) # Make sure setting the work order contacts on the SO doesn't feed back to the partner - so.write({'work_order_contacts': [Command.set([contact_1.id])]}) + so.write({"work_order_contacts": [Command.set([contact_1.id])]}) self.assertTrue(contact_1 in so.work_order_contacts) self.assertTrue(contact_2 not in so.work_order_contacts) self.assertTrue( - so.work_order_contacts != so.partner_id.work_order_contacts and len(so.partner_id.work_order_contacts) == 2) + so.work_order_contacts != so.partner_id.work_order_contacts + and len(so.partner_id.work_order_contacts) == 2 + ) def test_multilayer_site_contacts(self): - parent_co = self._generate_partner('Parent Co') - shipping_location = self._generate_partner('Shipping Location', 'company', parent_co, 'delivery') - wo_contact_1 = self._generate_partner('WO Contact 1', 'person', shipping_location) - wo_contact_2 = self._generate_partner('WO Contact 2', 'person', shipping_location) - site_contact_1 = self._generate_partner('Site Contact 1', 'person', shipping_location) - site_contact_2 = self._generate_partner('Site Contact 2', 'person', shipping_location) - shipping_location.write({ - 'work_order_contacts': [Command.set([wo_contact_1.id, wo_contact_2.id])], - 'site_contacts': [Command.set([site_contact_1.id, site_contact_2.id])] - }) + parent_co = self._generate_partner("Parent Co") + shipping_location = self._generate_partner( + "Shipping Location", "company", parent_co, "delivery" + ) + wo_contact_1 = self._generate_partner( + "WO Contact 1", "person", shipping_location + ) + wo_contact_2 = self._generate_partner( + "WO Contact 2", "person", shipping_location + ) + site_contact_1 = self._generate_partner( + "Site Contact 1", "person", shipping_location + ) + site_contact_2 = self._generate_partner( + "Site Contact 2", "person", shipping_location + ) + shipping_location.write( + { + "work_order_contacts": [ + Command.set([wo_contact_1.id, wo_contact_2.id]) + ], + "site_contacts": [Command.set([site_contact_1.id, site_contact_2.id])], + } + ) so = self._generate_sale_order(parent_co) - so.write({'partner_shipping_id': shipping_location.id}) + so.write({"partner_shipping_id": shipping_location.id}) self.assertEqual(so.site_contacts, shipping_location.site_contacts) self.assertEqual(so.work_order_contacts, shipping_location.work_order_contacts) def test_onchange_shipping_address(self): - self.env.user.groups_id += self.env.ref('account.group_delivery_invoice_address') - parent_co = self._generate_partner('Parent Co') - shipping_location = self._generate_partner('Shipping Location', 'company', parent_co, 'delivery') - wo_contact_1 = self._generate_partner('WO Contact 1', 'person', shipping_location) - wo_contact_2 = self._generate_partner('WO Contact 2', 'person', shipping_location) - site_contact_1 = self._generate_partner('Site Contact 1', 'person', shipping_location) - site_contact_2 = self._generate_partner('Site Contact 2', 'person', shipping_location) - shipping_location.write({ - 'work_order_contacts': [Command.set([wo_contact_1.id, wo_contact_2.id])], - 'site_contacts': [Command.set([site_contact_1.id, site_contact_2.id])] - }) + self.env.user.groups_id += self.env.ref( + "account.group_delivery_invoice_address" + ) + parent_co = self._generate_partner("Parent Co") + shipping_location = self._generate_partner( + "Shipping Location", "company", parent_co, "delivery" + ) + wo_contact_1 = self._generate_partner( + "WO Contact 1", "person", shipping_location + ) + wo_contact_2 = self._generate_partner( + "WO Contact 2", "person", shipping_location + ) + site_contact_1 = self._generate_partner( + "Site Contact 1", "person", shipping_location + ) + site_contact_2 = self._generate_partner( + "Site Contact 2", "person", shipping_location + ) + shipping_location.write( + { + "work_order_contacts": [ + Command.set([wo_contact_1.id, wo_contact_2.id]) + ], + "site_contacts": [Command.set([site_contact_1.id, site_contact_2.id])], + } + ) so = self._generate_sale_order(parent_co) diff --git a/bemade_fsm/tests/test_fsm_visit.py b/bemade_fsm/tests/test_fsm_visit.py index 5f9a693..ee76d7c 100644 --- a/bemade_fsm/tests/test_fsm_visit.py +++ b/bemade_fsm/tests/test_fsm_visit.py @@ -1,11 +1,10 @@ -from odoo.tests import TransactionCase, tagged, Form +from odoo.tests import tagged from .test_bemade_fsm_common import BemadeFSMBaseTest from datetime import date, timedelta -@tagged('-at_install', 'post_install') +@tagged("-at_install", "post_install") class FSMVisitTest(BemadeFSMBaseTest): - def test_create_visit_sets_name_on_section(self): so = self._generate_sale_order() self._generate_sale_order_line(sale_order=so) @@ -38,7 +37,7 @@ class FSMVisitTest(BemadeFSMBaseTest): visit = self._generate_visit(so) self._generate_sale_order_line(so) so.action_confirm() - task = so.order_line.filtered(lambda l: l.task_id).task_id + task = so.order_line.filtered(lambda line: line.task_id).task_id task.action_fsm_validate() @@ -49,7 +48,7 @@ class FSMVisitTest(BemadeFSMBaseTest): visit = self._generate_visit(so) self._generate_sale_order_line(so) so.action_confirm() - task = so.order_line.filtered(lambda l: l.task_id).task_id + task = so.order_line.filtered(lambda line: line.task_id).task_id task.action_fsm_validate() self._invoice_sale_order(so) @@ -65,7 +64,10 @@ class FSMVisitTest(BemadeFSMBaseTest): self.assertTrue(visit_task) visit_subtasks = visit_task.child_ids self.assertTrue( - visit_subtasks and sol1.task_id in visit_subtasks and sol2.task_id in visit_subtasks) + visit_subtasks + and sol1.task_id in visit_subtasks + and sol2.task_id in visit_subtasks + ) def test_visit_task_gets_correct_due_date_on_confirmation(self): so, visit, sol1, sol2 = self._generate_so_with_one_visit_two_lines() @@ -94,7 +96,7 @@ class FSMVisitTest(BemadeFSMBaseTest): self.assertEqual(visit_task.allocated_hours, 8.0) def test_adding_visit_creates_one_sale_order_line(self): - partner = self._generate_partner() + self._generate_partner() so = self._generate_sale_order() self._generate_sale_order_line(sale_order=so) self._generate_sale_order_line(sale_order=so) @@ -104,7 +106,12 @@ class FSMVisitTest(BemadeFSMBaseTest): self.assertEqual(len(so.order_line), 3) def test_marking_visit_task_done_completes_descendants(self): - so, visit, sol1, sol2 = self._generate_so_with_one_visit_two_lines_and_descendants() + ( + so, + visit, + sol1, + sol2, + ) = self._generate_so_with_one_visit_two_lines_and_descendants() so.action_confirm() parent = visit.task_id @@ -113,7 +120,7 @@ class FSMVisitTest(BemadeFSMBaseTest): self._assert_is_done(parent) def _assert_is_done(self, task): - """ Recursively assert all tasks in a hierarchy are complete """ + """Recursively assert all tasks in a hierarchy are complete""" self.assertTrue(task.is_closed) for child in task.child_ids: self._assert_is_done(child) @@ -127,11 +134,11 @@ class FSMVisitTest(BemadeFSMBaseTest): self.assertEqual(len(so.order_line), 3) def test_confirming_so_names_visit_properly(self): - """ Visits should be named - Visit - """ + """Visits should be named - Visit - """ so, visit, sol1, sol2 = self._generate_so_with_one_visit_two_lines() so.name = "SO12345" so.action_confirm() task = visit.task_id - supposed_name = f"SVR12345-1 - Test Company - Test Label" + supposed_name = "SVR12345-1 - Test Company - Test Label" self.assertEqual(task.name, supposed_name) diff --git a/bemade_fsm/tests/test_sale_order.py b/bemade_fsm/tests/test_sale_order.py index fd5d620..a8869a9 100644 --- a/bemade_fsm/tests/test_sale_order.py +++ b/bemade_fsm/tests/test_sale_order.py @@ -1,13 +1,14 @@ from .test_task_template import BemadeFSMBaseTest -from odoo.tests.common import tagged, HttpCase, Form +from odoo.tests.common import tagged, Form from odoo import Command @tagged("-at_install", "post_install") class TestSalesOrder(BemadeFSMBaseTest): - @tagged('-at_install', 'post_install') + @tagged("-at_install", "post_install") def test_order_confirmation_simple_template(self): - """ Confirming the order should create a task in the global project based on the task template. """ + """Confirming the order should create a task in the global project based on the + task template.""" partner = self._generate_partner() so = self._generate_sale_order(partner=partner) task_template = self._generate_task_template(planned_hours=8) @@ -24,10 +25,10 @@ class TestSalesOrder(BemadeFSMBaseTest): def test_task_template_tree_order_confirmation(self): partner = self._generate_partner() so = self._generate_sale_order(partner=partner) - parent_template = self._generate_task_template(structure=[2, 1], - names=['Parent Template', - 'Child Template', - 'Grandchild Template']) + parent_template = self._generate_task_template( + structure=[2, 1], + names=["Parent Template", "Child Template", "Grandchild Template"], + ) child_template_1 = parent_template.subtasks[0] child_template_2 = parent_template.subtasks[1] grandchild_template = parent_template.subtasks[0].subtasks[0] @@ -47,7 +48,7 @@ class TestSalesOrder(BemadeFSMBaseTest): self.assertEqual(grandchild_template.name, gc.name) def test_order_confirmation_single_equipment(self): - """ The equipment selected on the SO should transfer to the task.""" + """The equipment selected on the SO should transfer to the task.""" partner = self._generate_partner() equipment = self._generate_equipment(partner=partner) so = self._generate_sale_order(partner=partner, equipment=equipment) @@ -65,18 +66,22 @@ class TestSalesOrder(BemadeFSMBaseTest): self.assertEqual(task2.equipment_ids[0], equipment) def test_order_confirmation_multiple_equipment(self): - """ All equipment items should flow from the sale order line to the final task """ + """All equipment items should flow from the sale order line to the final task""" partner = self._generate_partner() for i in range(5): self._generate_equipment(partner=partner) sale_order = self._generate_sale_order( - partner=partner) # No default equipment since more than 3 on partner - sol1, sol2, sol3 = [self._generate_sale_order_line(sale_order=sale_order) for i - in range(3)] + partner=partner + ) # No default equipment since more than 3 on partner + sol1, sol2, sol3 = [ + self._generate_sale_order_line(sale_order=sale_order) for _ in range(3) + ] sol1.equipment_ids = [ - Command.set([partner.equipment_ids[i].id for i in range(2)])] + Command.set([partner.equipment_ids[i].id for i in range(2)]) + ] sol3.equipment_ids = [ - Command.set([partner.equipment_ids[i].id for i in range(2, 5)])] + Command.set([partner.equipment_ids[i].id for i in range(2, 5)]) + ] sale_order.action_confirm() @@ -85,7 +90,8 @@ class TestSalesOrder(BemadeFSMBaseTest): self.assertEqual(sol3.equipment_ids, sol3.task_id.equipment_ids) def test_task_template_with_equipment_flow(self): - """ The equipment selected on a task template should flow down to the task created on SO confirmation.""" + """The equipment selected on a task template should flow down to the task + created on SO confirmation.""" partner = self._generate_partner() equipment = self._generate_equipment(partner=partner) so = self._generate_sale_order(partner=partner) @@ -98,7 +104,8 @@ class TestSalesOrder(BemadeFSMBaseTest): self.assertEqual(sol.task_id.equipment_ids[0], equipment) def test_sale_order_line_gets_default_equipment(self): - """ Sale order lines created on an SO with default equipment set should inherit that default equipment. """ + """Sale order lines created on a SO with default equipment set should inherit + that default equipment.""" partner = self._generate_partner() self._generate_equipment(partner=partner) sale_order = self._generate_sale_order(partner=partner) @@ -108,7 +115,7 @@ class TestSalesOrder(BemadeFSMBaseTest): self.assertEqual(sol.equipment_ids, partner.equipment_ids) def test_sale_order_gets_correct_default_equipment_from_partner(self): - """ Should pick up equipment from the partner.""" + """Should pick up equipment from the partner.""" partner = self._generate_partner() self._generate_equipment(partner=partner) @@ -126,7 +133,9 @@ class TestSalesOrder(BemadeFSMBaseTest): self.assertEqual(sale_order.default_equipment_ids, parent.owned_equipment_ids) - def test_sale_order_no_default_equipment_with_more_than_three_owned_on_partner(self): + def test_sale_order_no_default_equipment_with_more_than_three_owned_on_partner( + self, + ): parent = self._generate_partner() child = self._generate_partner(parent=parent) for i in range(4): @@ -150,7 +159,7 @@ class TestSalesOrder(BemadeFSMBaseTest): def test_sale_order_prioritize_shipping_location_equipments(self): parent = self._generate_partner() - child = self._generate_partner(parent=parent, location_type='delivery') + child = self._generate_partner(parent=parent, location_type="delivery") self._generate_equipment(partner=parent) self._generate_equipment(partner=child) @@ -171,11 +180,13 @@ class TestSalesOrder(BemadeFSMBaseTest): self.assertEqual(line.equipment_ids, partner.equipment_ids) def test_task_mark_done(self): - """ Marking the task linked to an SO line should mark the line delivered. Marking sub-tasks done should not.""" + """Marking the task linked to a SO line should mark the line delivered. + Marking sub-tasks done should not.""" partner = self._generate_partner() so = self._generate_sale_order(partner=partner) - task_template = self._generate_task_template(structure=[2], - names=["Parent Task", "Subtask"]) + task_template = self._generate_task_template( + structure=[2], names=["Parent Task", "Subtask"] + ) product = self._generate_product(task_template=task_template) sol = self._generate_sale_order_line(so, product=product) so.action_confirm() @@ -186,20 +197,24 @@ class TestSalesOrder(BemadeFSMBaseTest): subtasks.action_fsm_validate(True) self.assertEqual(sol.qty_delivered, 0) - # Marking the top-level tasks done should set the delivered quantity to some non-zero value based on the UOM + # Marking the top-level tasks done should set the delivered quantity to some + # non-zero value based on the UOM parent_task.action_fsm_validate(True) self.assertTrue(sol.qty_delivered != 0) def test_task_contacts_through_sale_order(self): - """ Make sure the site contacts and work order contacts transfer correctly from the SO to the task.""" + """Make sure the site contacts and work order contacts transfer correctly + from the SO to the task.""" partner = self._generate_partner() - contact1 = self._generate_partner('Site contact', 'person', partner) - contact2 = self._generate_partner('Work order contact', 'person', partner) - partner.write({ - 'site_contacts': [Command.set([contact1.id])], - 'work_order_contacts': [Command.set([contact2.id])], - }) + contact1 = self._generate_partner("Site contact", "person", partner) + contact2 = self._generate_partner("Work order contact", "person", partner) + partner.write( + { + "site_contacts": [Command.set([contact1.id])], + "work_order_contacts": [Command.set([contact2.id])], + } + ) so = self._generate_sale_order(partner) product = self._generate_product() sol = self._generate_sale_order_line(sale_order=so, product=product) @@ -213,7 +228,7 @@ class TestSalesOrder(BemadeFSMBaseTest): def test_tasks_created_at_order_confirmation_have_no_assignees(self): so, visit, sol1, sol2 = self._generate_so_with_one_visit_two_lines() - user = self._generate_project_user(name="User", login='login') + user = self._generate_project_user(name="User", login="login") # We test as a specific user since testing as root may not produce the error so.with_user(user).action_confirm() @@ -224,13 +239,15 @@ class TestSalesOrder(BemadeFSMBaseTest): self.assertFalse(visit_task.user_ids) self.assertFalse(subtask1.user_ids) self.assertFalse(subtask2.user_ids) - + def test_long_line_name_overflows_to_task_description(self): so = self._generate_sale_order() product = self._generate_product() - product.description_sale = "This is a long product description.\n" \ - "It even spans multiple lines.\n" \ - "One could find this annoying in a task name." + product.description_sale = ( + "This is a long product description.\n" + "It even spans multiple lines.\n" + "One could find this annoying in a task name." + ) sol = self._generate_sale_order_line(sale_order=so, product=product) @@ -241,12 +258,15 @@ class TestSalesOrder(BemadeFSMBaseTest): self.assertFalse("It even spans multiple lines." in task.name) self.assertFalse("One could find this annoying in a task name." in task.name) self.assertTrue("It even spans multiple lines." in task.description) - self.assertTrue("One could find this annoying in a task name." - in task.description) + self.assertTrue( + "One could find this annoying in a task name." in task.description + ) def test_subtask_templates_no_description_if_blank_on_template(self): so = self._generate_sale_order() - template = self._generate_task_template(structure=[5], names=['Parent', 'Child']) + template = self._generate_task_template( + structure=[5], names=["Parent", "Child"] + ) template.description = "" template.subtasks[0].description = "Some fixed description" for t in template.subtasks[1:]: @@ -257,13 +277,15 @@ class TestSalesOrder(BemadeFSMBaseTest): so.action_confirm() task = sol.task_id - self.assertEqual(task.child_ids[0].description, template.subtasks[0].description) + self.assertEqual( + task.child_ids[0].description, template.subtasks[0].description + ) for t in task.child_ids[1:]: self.assertFalse(t.description) def test_duplicate_sale_order_duplicates_visits(self): - """ Duplicated sales orders should have visits tied to their SO lines as in the original. The copied visits - should not have approximate dates set, however.""" + """Duplicated sales orders should have visits tied to their SO lines as in the + original. The copied visits should not have approximate dates set, however.""" so, visit, line1, line2 = self._generate_so_with_one_visit_two_lines() so2 = so.copy() @@ -278,19 +300,19 @@ class TestSalesOrder(BemadeFSMBaseTest): so = self._generate_sale_order() so.company_id.create_default_fsm_visit = True product = self._generate_product() - sol = self._generate_sale_order_line(so, product) + self._generate_sale_order_line(so, product) so.action_confirm() - visit_line = so.order_line.sorted('sequence')[0] + visit_line = so.order_line.sorted("sequence")[0] self.assertTrue(so.visit_ids) self.assertEqual(visit_line.visit_id, so.visit_ids) def test_confirming_sale_order_with_visit_creates_no_new_lines(self): so = self._generate_sale_order() so.company_id.create_default_fsm_visit = True - product = self._generate_product() - visit = self._generate_visit(so) + self._generate_product() + self._generate_visit(so) so.action_confirm() @@ -300,9 +322,8 @@ class TestSalesOrder(BemadeFSMBaseTest): so = self._generate_sale_order() so.company_id.create_default_fsm_visit = False product = self._generate_product() - sol = self._generate_sale_order_line(so, product) + self._generate_sale_order_line(so, product) so.action_confirm() - visit_line = so.order_line.sorted('sequence')[0] self.assertFalse(so.visit_ids) diff --git a/bemade_fsm/tests/test_settings.py b/bemade_fsm/tests/test_settings.py index e07a8d0..0ee4ba8 100644 --- a/bemade_fsm/tests/test_settings.py +++ b/bemade_fsm/tests/test_settings.py @@ -1,54 +1,48 @@ from odoo.tests import TransactionCase, Form, tagged + @tagged("-at_install", "post_install") class TestSettings(TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() - cls.test_partner_co = cls.env['res.partner'].create({ - 'name': 'Test Co', - }) - cls.test_co = cls.env['res.company'].create({ - 'name': 'Test Co', - 'country_id': cls.env.ref('base.ca').id, - - }) + cls.test_partner_co = cls.env["res.partner"].create( + { + "name": "Test Co", + } + ) + cls.test_co = cls.env["res.company"].create( + { + "name": "Test Co", + "country_id": cls.env.ref("base.ca").id, + } + ) cls.env.user.company_id = cls.test_co def test_enabling_separate_time_on_work_orders(self): - wizard = self.env['res.config.settings'].create({}) - self.assertFalse( - self.test_co.split_time_from_materials_on_service_work_orders - ) + wizard = self.env["res.config.settings"].create({}) + self.assertFalse(self.test_co.split_time_from_materials_on_service_work_orders) with Form(wizard) as form: form.separate_time_on_work_orders = True - self.assertTrue( - self.test_co.split_time_from_materials_on_service_work_orders - ) + self.assertTrue(self.test_co.split_time_from_materials_on_service_work_orders) def test_disabling_separate_time_on_work_orders(self): - wizard = self.env['res.config.settings'].create({}) + wizard = self.env["res.config.settings"].create({}) self.test_co.split_time_from_materials_on_service_work_orders = True with Form(wizard) as form: form.separate_time_on_work_orders = False - self.assertFalse( - self.test_co.split_time_from_materials_on_service_work_orders - ) + self.assertFalse(self.test_co.split_time_from_materials_on_service_work_orders) def test_enabling_create_default_fsm_visit(self): - wizard = self.env['res.config.settings'].create({}) + wizard = self.env["res.config.settings"].create({}) self.test_co.create_default_fsm_visit = False with Form(wizard) as form: form.create_default_fsm_visit = True - self.assertTrue( - self.test_co.create_default_fsm_visit - ) + self.assertTrue(self.test_co.create_default_fsm_visit) def test_disabling_create_default_fsm_visit(self): - wizard = self.env['res.config.settings'].create({}) + wizard = self.env["res.config.settings"].create({}) self.test_co.create_default_fsm_visit = True with Form(wizard) as form: form.create_default_fsm_visit = False - self.assertFalse( - self.test_co.create_default_fsm_visit - ) + self.assertFalse(self.test_co.create_default_fsm_visit) diff --git a/bemade_fsm/tests/test_task.py b/bemade_fsm/tests/test_task.py index 8d5a3ef..156acbe 100644 --- a/bemade_fsm/tests/test_task.py +++ b/bemade_fsm/tests/test_task.py @@ -3,18 +3,19 @@ from odoo.tests.common import tagged, Form from odoo import Command -@tagged('post_install', '-at_install') +@tagged("post_install", "-at_install") class TaskTest(BemadeFSMBaseTest): - @classmethod def setUpClass(cls): # Chose to set up all tests the same way since this code was becoming very redundant super().setUpClass() - cls.user = cls._generate_project_manager_user('Bob', 'Bob') + cls.user = cls._generate_project_manager_user("Bob", "Bob") def _generate_so_with_multilevel_task_template(self): so = self._generate_sale_order() - template = self._generate_task_template(names=['Parent', 'Child', 'Grandchild'], structure=[2, 1]) + template = self._generate_task_template( + names=["Parent", "Child", "Grandchild"], structure=[2, 1] + ) product = self._generate_product(task_template=template) sol = self._generate_sale_order_line(sale_order=so, product=product) return so, sol @@ -25,21 +26,27 @@ class TaskTest(BemadeFSMBaseTest): task = sol.task_id task.propagate_assignment = True - task.write({ - 'user_ids': [Command.set([self.user.id])], - 'propagate_assignment': True, - }) + task.write( + { + "user_ids": [Command.set([self.user.id])], + "propagate_assignment": True, + } + ) - self.assertTrue(all([t.user_ids == self.user for t in task | task._get_all_subtasks()])) + self.assertTrue( + all([t.user_ids == self.user for t in task | task._get_all_subtasks()]) + ) def test_reassigning_task_doesnt_propagate_by_default(self): so, sol = self._generate_so_with_multilevel_task_template() so.action_confirm() task = sol.task_id - task.write({ - 'user_ids': [Command.set([self.user.id])], - }) + task.write( + { + "user_ids": [Command.set([self.user.id])], + } + ) self.assertFalse(any([t.user_ids for t in task.child_ids.child_ids])) @@ -49,24 +56,25 @@ class TaskTest(BemadeFSMBaseTest): task = sol.task_id # First, set propagation and assign task.propagate_assignment = True - task.write({ - 'user_ids': [Command.set([self.user.id])] - }) + task.write({"user_ids": [Command.set([self.user.id])]}) # Then, unset propagation for the children and re-set assignment - task.child_ids.write({'propagate_assignment': False}) - self.assertFalse(any([t.propagate_assignment for t in task._get_all_subtasks()])) + task.child_ids.write({"propagate_assignment": False}) + self.assertFalse( + any([t.propagate_assignment for t in task._get_all_subtasks()]) + ) # Then, test that assigning the parent only assigns its children, not its grandchildren - task.write({ - 'user_ids': [Command.set([])] - }) + task.write({"user_ids": [Command.set([])]}) self.assertTrue(all([not t.user_ids for t in task | task.child_ids])) - self.assertTrue(all([t.user_ids == self.user for t in task.child_ids.child_ids])) + self.assertTrue( + all([t.user_ids == self.user for t in task.child_ids.child_ids]) + ) def test_task_gets_work_order_contacts_from_sale_order(self): so, sol = self._generate_so_with_multilevel_task_template() - work_order_contacts = self._generate_partner(parent=so.partner_id) | self._generate_partner( - parent=so.partner_id) - so.write({'work_order_contacts': [(6, 0, work_order_contacts.ids)]}) + work_order_contacts = self._generate_partner( + parent=so.partner_id + ) | self._generate_partner(parent=so.partner_id) + so.write({"work_order_contacts": [(6, 0, work_order_contacts.ids)]}) so.action_confirm() task = sol.task_id @@ -80,8 +88,10 @@ class TaskTest(BemadeFSMBaseTest): def test_task_gets_site_contacts_from_sale_order(self): so, sol = self._generate_so_with_multilevel_task_template() - site_contacts = self._generate_partner(parent=so.partner_id) | self._generate_partner(parent=so.partner_id) - so.write({'site_contacts': [(6, 0, site_contacts.ids)]}) + site_contacts = self._generate_partner( + parent=so.partner_id + ) | self._generate_partner(parent=so.partner_id) + so.write({"site_contacts": [(6, 0, site_contacts.ids)]}) so.action_confirm() task = sol.task_id @@ -95,12 +105,20 @@ class TaskTest(BemadeFSMBaseTest): def test_task_gets_work_order_contacts_from_parent(self): so, sol = self._generate_so_with_multilevel_task_template() - work_order_contacts = self._generate_partner(parent=so.partner_id) | self._generate_partner(parent=so.partner_id) - so.write({'work_order_contacts': [(6, 0, work_order_contacts.ids)]}) + work_order_contacts = self._generate_partner( + parent=so.partner_id + ) | self._generate_partner(parent=so.partner_id) + so.write({"work_order_contacts": [(6, 0, work_order_contacts.ids)]}) so.action_confirm() task = sol.task_id - task.write({'work_order_contacts': [Command.link(self._generate_partner(parent=so.partner_id).id)]}) + task.write( + { + "work_order_contacts": [ + Command.link(self._generate_partner(parent=so.partner_id).id) + ] + } + ) for subtask in task._get_all_subtasks(): self.assertEqual(subtask.work_order_contacts, task.work_order_contacts) with Form(task) as task_form: @@ -111,12 +129,20 @@ class TaskTest(BemadeFSMBaseTest): def test_task_gets_site_contacts_from_parent(self): so, sol = self._generate_so_with_multilevel_task_template() - site_contacts = self._generate_partner(parent=so.partner_id) | self._generate_partner(parent=so.partner_id) - so.write({'site_contacts': [(6, 0, site_contacts.ids)]}) + site_contacts = self._generate_partner( + parent=so.partner_id + ) | self._generate_partner(parent=so.partner_id) + so.write({"site_contacts": [(6, 0, site_contacts.ids)]}) so.action_confirm() task = sol.task_id - task.write({'site_contacts': [Command.link(self._generate_partner(parent=so.partner_id).id)]}) + task.write( + { + "site_contacts": [ + Command.link(self._generate_partner(parent=so.partner_id).id) + ] + } + ) for subtask in task._get_all_subtasks(): self.assertEqual(subtask.site_contacts, task.site_contacts) with Form(task) as task_form: diff --git a/bemade_fsm/tests/test_task_report.py b/bemade_fsm/tests/test_task_report.py index c12295f..fab5521 100644 --- a/bemade_fsm/tests/test_task_report.py +++ b/bemade_fsm/tests/test_task_report.py @@ -4,34 +4,36 @@ from odoo.tests import Form class TestTaskReport(BemadeFSMBaseTest): def test_split_time_materials_setting(self): - with Form(self.env['res.config.settings']) as settings: + with Form(self.env["res.config.settings"]) as settings: settings.separate_time_on_work_orders = True - with Form(self.env['res.config.settings']) as new_settings: + with Form(self.env["res.config.settings"]): self.assertTrue(settings.separate_time_on_work_orders) so = self._generate_sale_order() service_product = self._generate_product() material_product = self._generate_product( name="Material Product", - product_type='product', - service_tracking='no', + product_type="product", + service_tracking="no", ) visit = self._generate_visit(sale_order=so) - sol = self._generate_sale_order_line(sale_order=so, product=service_product) - sol2 = self._generate_sale_order_line(sale_order=so, product=material_product) + self._generate_sale_order_line(sale_order=so, product=service_product) + self._generate_sale_order_line(sale_order=so, product=material_product) so.action_confirm() task = visit.task_id - html_content = self.env['ir.actions.report']._render( - 'industry_fsm_report.worksheet_custom', - [task.id], - )[0].decode('utf-8').split('\n') + html_content = ( + self.env["ir.actions.report"] + ._render( + "industry_fsm_report.worksheet_custom", + [task.id], + )[0] + .decode("utf-8") + .split("\n") + ) - strings_to_find = [ - "

Materials

", - "Material Product" - ] + strings_to_find = ["

Materials

", "Material Product"] for line in strings_to_find: line_found = False diff --git a/bemade_fsm/tests/test_task_template.py b/bemade_fsm/tests/test_task_template.py index 11479bb..ba6bd70 100644 --- a/bemade_fsm/tests/test_task_template.py +++ b/bemade_fsm/tests/test_task_template.py @@ -1,35 +1,34 @@ from .test_bemade_fsm_common import BemadeFSMBaseTest -from odoo.tests.common import HttpCase, tagged, Form +from odoo.tests.common import tagged, Form from odoo.exceptions import MissingError -from odoo import Command from odoo.tools import mute_logger -from psycopg2.errors import ForeignKeyViolation +import psycopg2 -@tagged('-at_install', 'post_install') +@tagged("-at_install", "post_install") class TestTaskTemplate(BemadeFSMBaseTest): - def test_delete_task_template(self): """User should never be able to delete a task template used on a product""" - task_template = self._generate_task_template(names=['Template 1']) - product = self._generate_product(name="Test Product 1", task_template=task_template) - with self.assertRaises(ForeignKeyViolation): - with mute_logger('odoo.sql_db'): + task_template = self._generate_task_template(names=["Template 1"]) + self._generate_product(name="Test Product 1", task_template=task_template) + with self.assertRaises(psycopg2.errors.ForeignKeyViolation): + with mute_logger("odoo.sql_db"): task_template.unlink() def test_delete_subtask_template(self): - """ Deletion of a child task should be OK even if the parent is on a product. Children of the deleted - subtask should be deleted.""" - parent_task = self._generate_task_template(structure=[2, 1], - names=['Parent Template', 'Child Template', - 'Grandchild Template']) + """Deletion of a child task should be OK even if the parent is on a product. + Children of the deleted subtask should be deleted.""" + parent_task = self._generate_task_template( + structure=[2, 1], + names=["Parent Template", "Child Template", "Grandchild Template"], + ) grandchild_task = parent_task.subtasks[0].subtasks[0] parent_task.subtasks[0].unlink() # Reading deleted child's name field should be impossible with self.assertRaises(MissingError): - test = grandchild_task.name + _ = grandchild_task.name def test_dissociating_customer_resets_equipment_appropriately(self): partner1 = self._generate_partner() @@ -38,7 +37,8 @@ class TestTaskTemplate(BemadeFSMBaseTest): task = self._generate_task_template(customer=partner1, equipment=equipment1) form = Form(task) - # Switching the partner should trigger on_change that makes sure equipments are linked to the new partner + # Switching the partner should trigger on_change that makes sure equipments are + # linked to the new partner form.customer = partner2 form.save() @@ -46,7 +46,7 @@ class TestTaskTemplate(BemadeFSMBaseTest): def test_child_task_names_are_short_version(self): so, visit, sol1, sol2 = self._generate_so_with_one_visit_two_lines() - template = self._generate_task_template(names=['Task']) + template = self._generate_task_template(names=["Task"]) product = self._generate_product(task_template=template) sol1.name = "Short Name 1" sol2.name = "Short Name 2" @@ -60,14 +60,21 @@ class TestTaskTemplate(BemadeFSMBaseTest): def test_task_creation_directly_from_template(self): project = self.env.ref("industry_fsm.fsm_project") - template = self._generate_task_template(names=['Task', 'Child', 'Grandchild'], structure=[2, 1]) + template = self._generate_task_template( + names=["Task", "Child", "Grandchild"], structure=[2, 1] + ) task = template.create_task_from_self(project, "My new task") self.assertEqual(len(task.child_ids), len(template.subtasks)) - self.assertEqual(len(task.child_ids[0].child_ids), len(template.subtasks[0].subtasks)) - self.assertEqual(len(task.child_ids[1].child_ids), len(template.subtasks[1].subtasks)) + self.assertEqual( + len(task.child_ids[0].child_ids), len(template.subtasks[0].subtasks) + ) + self.assertEqual( + len(task.child_ids[1].child_ids), len(template.subtasks[1].subtasks) + ) self.assertEqual(task.name, "My new task") self.assertEqual(task.child_ids[0].name, template.subtasks[0].name) - self.assertTrue(all([t.project_id == project for t in task | task._get_all_subtasks()])) - + self.assertTrue( + all([t.project_id == project for t in task | task._get_all_subtasks()]) + ) diff --git a/bemade_fsm/views/equipment.xml b/bemade_fsm/views/equipment.xml index 3cf6a14..fca156f 100644 --- a/bemade_fsm/views/equipment.xml +++ b/bemade_fsm/views/equipment.xml @@ -1,32 +1,38 @@ - + bemade_fsm.equipment.form bemade_fsm.equipment -
+ - - - - + + + + - - + +
- - - + + +
@@ -36,12 +42,16 @@ bemade_fsm.equipment.tree bemade_fsm.equipment - - - - - - + + + + + +
diff --git a/bemade_fsm/views/menus.xml b/bemade_fsm/views/menus.xml index d5dce9e..5ee0f1c 100644 --- a/bemade_fsm/views/menus.xml +++ b/bemade_fsm/views/menus.xml @@ -1,32 +1,40 @@ - - - - - - - - \ No newline at end of file + + + + + +
diff --git a/bemade_fsm/views/product_views.xml b/bemade_fsm/views/product_views.xml index 56b669d..5efd9f7 100644 --- a/bemade_fsm/views/product_views.xml +++ b/bemade_fsm/views/product_views.xml @@ -1,30 +1,32 @@ - + - + + bemade_fsm.product.template.form + product.template + + + + + + + - - bemade_fsm.product.template.form - product.template - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + diff --git a/bemade_fsm/views/res_partner.xml b/bemade_fsm/views/res_partner.xml index 7cbe295..df3b70e 100644 --- a/bemade_fsm/views/res_partner.xml +++ b/bemade_fsm/views/res_partner.xml @@ -1,4 +1,4 @@ - + Equipments @@ -11,44 +11,71 @@ bemade_fsm.partner.equipment.location.form res.partner - + - - - - + + + + - + - + - - - - - + + + + +
-
- - + +
@@ -58,9 +85,9 @@ - - - + + +
@@ -69,13 +96,15 @@ bemade_fsm.equipment - - - - - + + bemade_fsm.project_task.form + project.task + + + + +

+ +

+
+ + + + + + + + + + + +
+
+ + + project.task + bemade_fsm.project_task.form2 + + + + + + - - - - project.task - bemade_fsm.project_task.form2 - - - - - - - + + + + + {'search_default_is_parent_task': True} + + + project.task.view.list.fsm.inherit + project.task + + + + project_list + + + - - - - {'search_default_is_parent_task': True} - - - {'search_default_is_parent_task': True} - - - {'search_default_is_parent_task': True} - - - {'search_default_is_parent_task': True} - - - {'search_default_is_parent_task': True} - - - {'search_default_is_parent_task': True} - - - {'search_default_is_parent_task': True} - - - {'search_default_is_parent_task': True} - - - bemade_fsm.project_task_view_calendar_no_worksheet - - project.task - - - - user_ids - + + hide - - - project.task.view.list.fsm.inherit - project.task - - - - project_list - - - - - - hide - - - hide - - - hide - + + hide - - - [('is_fsm', '=', True), - ('project_id', '!=', False), - ('display_in_project', '=', True), - ('parent_id', '=', False)] + + hide - - - [('is_fsm', '=', True), - ('project_id', '!=', False), - ('display_in_project', '=', True), - ('parent_id', '=', False)] - - - - [('is_fsm', '=', True), - ('project_id', '!=', False), - ('display_in_project', '=', True), - ('parent_id', '=', False)] - - - - [('is_fsm', '=', True), - ('project_id', '!=', False), - ('display_in_project', '=', True), - ('parent_id', '=', False), - ('invoice_status', '=', 'to invoice')] - - - - - [('is_fsm', '=', True), - ('project_id', '!=', False), - ('display_in_project', '=', True), - ('parent_id', '=', False)] - - - - [('is_fsm', '=', True), - ('project_id', '!=', False), - ('display_in_project', '=', True), - ('parent_id', '=', False)] - - - - [('is_fsm', '=', True), - ('project_id', '!=', False), - ('display_in_project', '=', True), - ('parent_id', '=', False)] - - - - - - - - - - - - - - - bemade_fsm.project_task_view_calendar_no_worksheet - - project.task - - - - user_ids - - - - - project.task.view.search.fsm.inherit - project.task - - - - - [ - '&', - ('fsm_done', '=', False), - '|', - ('user_ids', '=', False), - '&', - ('planned_date_start', '=', False), - ('date_deadline', '=', False), - ] - - - - - - - - +
+ + + {'search_default_is_parent_task': True} + [('is_fsm', '=', True), + ('project_id', '!=', False), + ('display_in_project', '=', True), + ('parent_id', '=', False)] + + + + {'search_default_is_parent_task': True} + [('is_fsm', '=', True), + ('project_id', '!=', False), + ('display_in_project', '=', True), + ('parent_id', '=', False)] + + + + {'search_default_is_parent_task': True} + [('is_fsm', '=', True), + ('project_id', '!=', False), + ('display_in_project', '=', True), + ('parent_id', '=', False)] + + + + {'search_default_is_parent_task': True} + [('is_fsm', '=', True), + ('project_id', '!=', False), + ('display_in_project', '=', True), + ('parent_id', '=', False), + ('invoice_status', '=', 'to invoice')] + + + + + {'search_default_is_parent_task': True} + [('is_fsm', '=', True), + ('project_id', '!=', False), + ('display_in_project', '=', True), + ('parent_id', '=', False)] + + + + {'search_default_is_parent_task': True} + [('is_fsm', '=', True), + ('project_id', '!=', False), + ('display_in_project', '=', True), + ('parent_id', '=', False)] + + + + {'search_default_is_parent_task': True} + [('is_fsm', '=', True), + ('project_id', '!=', False), + ('display_in_project', '=', True), + ('parent_id', '=', False)] + + + + + + + + + + + + + + + bemade_fsm.project_task_view_calendar_no_worksheet + + project.task + + + + + user_ids + + + + + project.task.view.search.fsm.inherit + project.task + + + + + [ + '&', + ('fsm_done', '=', False), + '|', + ('user_ids', '=', False), + '&', + ('planned_date_start', '=', False), + ('date_deadline', '=', False), + ] + + + + + + +
diff --git a/bemade_fsm/wizard/new_task_from_template.py b/bemade_fsm/wizard/new_task_from_template.py index 6c8c5e1..519e1c7 100644 --- a/bemade_fsm/wizard/new_task_from_template.py +++ b/bemade_fsm/wizard/new_task_from_template.py @@ -1,5 +1,4 @@ -from odoo import models, fields, api, _ -from odoo.exceptions import UserError +from odoo import models, fields class NewTaskFromTemplateWizard(models.TransientModel): @@ -7,45 +6,52 @@ class NewTaskFromTemplateWizard(models.TransientModel): _description = "Create Task from Template Wizard" project_id = fields.Many2one( - comodel_name='project.project', - string='Project', - help='The project the new task should be created in.', + comodel_name="project.project", + string="Project", + help="The project the new task should be created in.", required=True, ) task_template_id = fields.Many2one( - comodel_name='project.task.template', - string='Task Template', - help='The template to use when creating the new task.', + comodel_name="project.task.template", + string="Task Template", + help="The template to use when creating the new task.", required=True, ) new_task_title = fields.Char( - help='The title (name) for the newly created task. If left blank, the name of the template will be used.', + help=( + "The title (name) for the newly created task. If left blank, the name of" + " the template will be used." + ), ) def default_get(self, fields_list): res = super().default_get(fields_list) - active_id = self.env.context.get('active_id', False) - active_model = self.env.context.get('active_model', False) + active_id = self.env.context.get("active_id", False) + active_model = self.env.context.get("active_model", False) if not active_model: - params = self.env.context.get('params', False) - active_model = params and params.get('model', False) - if active_model == 'project.task.template' and active_id and 'task_template_id' in fields_list: - res.update({'task_template_id': active_id}) - if active_model == 'project.task' and 'project_id' in fields_list: - res.update({'project_id': self.env.ref('industry_fsm.fsm_project').id}) + params = self.env.context.get("params", False) + active_model = params and params.get("model", False) + if ( + active_model == "project.task.template" + and active_id + and "task_template_id" in fields_list + ): + res.update({"task_template_id": active_id}) + if active_model == "project.task" and "project_id" in fields_list: + res.update({"project_id": self.env.ref("industry_fsm.fsm_project").id}) return res def action_create_task_from_template(self): self.ensure_one() - task = self.task_template_id.create_task_from_self(self.project_id, self.new_task_title) + task = self.task_template_id.create_task_from_self( + self.project_id, self.new_task_title + ) return { - 'type': 'ir.actions.act_window', - 'res_model': 'project.task', - 'res_id': task.id, - 'view_mode': 'form', - 'target': 'current', + "type": "ir.actions.act_window", + "res_model": "project.task", + "res_id": task.id, + "view_mode": "form", + "target": "current", } - - diff --git a/bemade_fsm/wizard/res_config_settings.py b/bemade_fsm/wizard/res_config_settings.py index 458c9b9..b4c371e 100644 --- a/bemade_fsm/wizard/res_config_settings.py +++ b/bemade_fsm/wizard/res_config_settings.py @@ -1,11 +1,11 @@ -from odoo import models, fields, api +from odoo import models, fields class ResConfigSettings(models.TransientModel): _inherit = "res.config.settings" company_id = fields.Many2one( - 'res.company', + "res.company", default=lambda self: self.env.company or self.env.user.company_id, ) separate_time_on_work_orders = fields.Boolean(