diff --git a/.gitignore b/.gitignore index 68bc17f..9c283fd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,36 +1,28 @@ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] -*$py.class +/.venv +/.pytest_cache # C extensions *.so # Distribution / packaging .Python +env/ +bin/ build/ develop-eggs/ dist/ -downloads/ eggs/ -.eggs/ -lib/ lib64/ parts/ sdist/ var/ -wheels/ -share/python-wheels/ *.egg-info/ .installed.cfg *.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec +*.eggs # Installer logs pip-log.txt @@ -39,122 +31,44 @@ pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ -.nox/ .coverage -.coverage.* .cache nosetests.xml coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ # Translations *.mo -*.pot + +# Pycharm +.idea + +# Eclipse +.settings + +# Visual Studio cache/options directory +.vs/ +.vscode + +# OSX Files +.DS_Store # Django stuff: *.log -local_settings.py -db.sqlite3 -db.sqlite3-journal -# Flask stuff: -instance/ -.webassets-cache +# Mr Developer +.mr.developer.cfg +.project +.pydevproject -# Scrapy stuff: -.scrapy +# Rope +.ropeproject # Sphinx documentation docs/_build/ -# PyBuilder -.pybuilder/ -target/ +# Backup files +*~ +*.swp -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/#use-with-ide -.pdm.toml - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +# OCA rules +!static/lib/ diff --git a/bemade_fsm/__manifest__.py b/bemade_fsm/__manifest__.py index 0a0e459..a3069fe 100644 --- a/bemade_fsm/__manifest__.py +++ b/bemade_fsm/__manifest__.py @@ -35,6 +35,7 @@ 'industry_fsm', 'industry_fsm_sale', 'bemade_partner_root_ancestor', + 'mail', ], 'data': ['views/task_template_views.xml', 'views/equipment.xml', @@ -43,11 +44,12 @@ 'views/res_partner.xml', 'views/menus.xml', 'views/task_views.xml', + 'views/sale_order_views.xml', ], 'assets': { 'web.assets_tests': [ 'bemade_fsm/static/tests/tours/task_template_tour.js', - 'bemade_fsm/static/tests/tours/task_equipment_tour.js', + 'bemade_fsm/static/tests/tours/equipment_tour.js', ], }, 'installable': True, diff --git a/bemade_fsm/models/equipment.py b/bemade_fsm/models/equipment.py index b82c143..2cbb096 100644 --- a/bemade_fsm/models/equipment.py +++ b/bemade_fsm/models/equipment.py @@ -20,6 +20,7 @@ class EquipmentType(models.Model): name = fields.Char(string='Intervention Name', required=True, translate=True) + class Equipment(models.Model): _name = 'bemade_fsm.equipment' _rec_name = 'complete_name' @@ -39,13 +40,16 @@ class Equipment(models.Model): "Waste water, Pure water") partner_id = fields.Many2one('res.partner', string="Owner", - compute="_compute_partner") + compute="_compute_partner", + search="_search_partner",) + description = fields.Text(string="Description", tracking=True) partner_location_id = fields.Many2one('res.partner', string="Physical Address", - tracking=True,) + tracking=True, + ondelete='cascade') location_notes = fields.Text(string="Physical Location Notes", tracking=True) @@ -58,6 +62,10 @@ class Equipment(models.Model): for rec in self: rec.partner_id = rec.partner_location_id and rec.partner_location_id.root_ancestor + @api.model + def _search_partner(self, operator, value): + return [('partner_location_id.root_ancestor', operator, value)] + @api.depends('pid_tag', 'name') def _compute_complete_name(self): for rec in self: @@ -87,4 +95,4 @@ class Equipment(models.Model): 'context': self.env.context, 'res_model': 'bemade_fsm.equipment', 'type': 'ir.actions.act_window', - } \ No newline at end of file + } diff --git a/bemade_fsm/models/res_partner.py b/bemade_fsm/models/res_partner.py index c9f1877..f5dba22 100644 --- a/bemade_fsm/models/res_partner.py +++ b/bemade_fsm/models/res_partner.py @@ -16,7 +16,8 @@ class Partner(models.Model): string='Site Equipment') is_site_contact = fields.Boolean(string='Is a site contact', - compute="_compute_is_site_contact") + compute="_compute_is_site_contact", + search="_search_is_site_contact",) site_ids = fields.Many2many(string='Work Sites', comodel_name='res.partner', @@ -33,6 +34,14 @@ class Partner(models.Model): 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) + @api.depends('site_ids') def _compute_is_site_contact(self): for rec in self: @@ -41,5 +50,9 @@ class Partner(models.Model): @api.depends('equipment_ids') def _compute_equipment_count(self): for rec in self: - all_equipmemt_ids = self.env['bemade_fsm.equipment'].search([('partner_id', '=', rec.id)]) - rec.equipment_count = len(all_equipmemt_ids) + all_equipment_ids = self.env['bemade_fsm.equipment'].search([('partner_id', '=', rec.id)]) + rec.equipment_count = len(all_equipment_ids) + + @api.model + def _search_is_site_contact(self, operator, value): + return [('site_contacts', '!=', False)] diff --git a/bemade_fsm/models/sale_order.py b/bemade_fsm/models/sale_order.py index 0605e28..d0842b6 100644 --- a/bemade_fsm/models/sale_order.py +++ b/bemade_fsm/models/sale_order.py @@ -1,6 +1,49 @@ from odoo import fields, models, api, _, Command +class SaleOrder(models.Model): + _inherit = 'sale.order' + + site_contacts = fields.Many2many(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) + + equipment_ids = fields.Many2many(comodel_name='bemade_fsm.equipment', + compute='_compute_equipment', + inverse='_inverse_equipment', + string='Equipment to Service', + store=True, + ondelete='restrict') + + @api.depends('partner_id') + def _compute_default_contacts(self): + for rec in self: + rec.site_contacts = rec.partner_shipping_id.site_contacts + rec.work_order_contacts = rec.partner_shipping_id.work_order_contacts + + def _inverse_default_contacts(self): + pass + + @api.depends('partner_id') + def _compute_equipment(self): + for rec in self: + rec.equipment_ids = self.partner_shipping_id.equipment_ids if len( + self.partner_shipping_id.equipment_ids) <= 1 else False + + def _inverse_equipment(self): + pass + + class SaleOrderLine(models.Model): _inherit = 'sale.order.line' diff --git a/bemade_fsm/models/task_template.py b/bemade_fsm/models/task_template.py index f95b3c7..3f70518 100644 --- a/bemade_fsm/models/task_template.py +++ b/bemade_fsm/models/task_template.py @@ -3,7 +3,7 @@ from odoo import models, fields, api, _ class TaskTemplate(models.Model): _name = 'project.task.template' - + _description = "Template for new project tasks" @api.model def _current_company(self): return self.env.company diff --git a/bemade_fsm/static/tests/tours/equipment_tour.js b/bemade_fsm/static/tests/tours/equipment_tour.js new file mode 100644 index 0000000..8a6bf49 --- /dev/null +++ b/bemade_fsm/static/tests/tours/equipment_tour.js @@ -0,0 +1,126 @@ +/** @odoo-module **/ + +import tour from 'web_tour.tour'; + +const TEST_COMPANY = "Test Partner Company"; +const TEST_EQPT1 = "Test Equipment 1"; +const TEST_EQPT2 = "Test Equipment 2"; +tour.register('equipment_base_tour', { + test: true, + url: '/web', + }, /* Create a new "Test Equipment" and link it to "Test Partner Company" */ + [tour.stepUtils.showAppsMenuItem(), { + content: 'Navigate to the Service menu', + trigger: '.o_app[data-menu-xmlid="industry_fsm.fsm_menu_root"]', + }, { + content: 'Navigate to the Clients submenu', + trigger: 'button.dropdown-toggle[data-menu-xmlid="bemade_fsm.menu_service_client"]', + }, { + content: 'Navigate to the Equipment menu', + trigger: '.dropdown-item[data-menu-xmlid="bemade_fsm.menu_service_client_equipment"]', + }, { + content: 'Click the create button', + trigger: '.o_list_button_add', + extra_trigger: 'li.breadcrumb-item.active:has(span:contains(Equipment))', + }, { + content: 'Add a tag', + trigger: 'input[name="pid_tag"]', + run: 'text TestPIDTag', + }, { + content: 'Set the name', + trigger: 'input[name="name"]', + run: `text ${TEST_EQPT2}`, + }, { + content: 'Set the partner', + trigger: 'div[name="partner_location_id"] div div input', + run: 'text Test Partner Company', + }, { + content: 'Click the partner in the dropdown', + trigger: `li a.dropdown-item:contains(${TEST_COMPANY})`, + }, { + content: 'Save equipment', + trigger: 'button.o_form_button_save', + }, { + /* Navigate to the client and make sure that there are two equipments saved (one from the Python test case) */ + content: 'Navigate to the Clients submenu', + trigger: 'button.dropdown-toggle[data-menu-xmlid="bemade_fsm.menu_service_client"]', + }, { + content: 'Click on the clients submenu', + trigger: '.dropdown-item[data-menu-xmlid="bemade_fsm.menu_service_client_clients"]', + }, { + content: 'Search for the Test Partner Company', + trigger: 'input.o_searchview_input', + extra_trigger: 'li.breadcrumb-item.active:has(span:contains(Customers))', + run: `text ${TEST_COMPANY}` + }, { + content: 'Validate Search', + trigger: '.o_menu_item.o_selection_focus', + run: 'click', + }, { + content: 'Open the test client.', + trigger: `div.o_kanban_record:has(span:contains(${TEST_COMPANY}))`, + }, { + content: 'Click the Field Service tab.', + trigger: 'a.nav-link:contains(Field Service)', + extra_trigger: `h1 span.o_field_partner_autocomplete[name="name"]:contains(${TEST_COMPANY})`, + }, { + content: 'Make sure we have a first test equipment', + /*trigger: `div[name="equipment_ids"]:has(td:contains(${TEST_EQPT1}))`,*/ + trigger: `td:contains(${TEST_EQPT1})`, + run: function () { + }, + }, { + content: 'Make sure we have a second test equipment', + trigger: `div[name="equipment_ids"]:has(td:contains(${TEST_EQPT2}))`, + run: function () { + }, + } + ]); + +tour.register('equipment_sale_order_tour', { + test: true, + url: '/web', +}, [tour.stepUtils.showAppsMenuItem(), { + content: 'Navigate to the Sales menu', + trigger: '.o_app[data-menu-xmlid="sale.sale_menu_root"]', +}, { + content: 'Create an order', + trigger: 'button.o_list_button_add', +}, { + content: 'Select the test partner', + trigger: 'div[name="partner_id"] input', + run: `text ${TEST_COMPANY}`, +}, { + content: 'Click the partner in the dropdown', + trigger: `li a.dropdown-item:contains(${TEST_COMPANY})`, +}, { + content: 'Save', + trigger: 'button.o_form_button_save', +}, { + content: 'Navigate to the Field Service tab.', + trigger: 'a.nav-link[role="tab"]:contains(Field Service)' +}, { + content: 'Check that default equipment shows up.', + trigger: `.o_field_cell:contains(${TEST_EQPT1})`, +}, { + content: 'Add test equipment 2', + trigger: `div[name="equipment_ids"] a:contains(Add a line)`, +}, { + content: 'Click create', + trigger: '.modal-footer > button:has(span:contains(Create))', +}, { + content: 'Name the new equipment', + trigger: 'input.o_field_widget[name="name"]', + run: `text ${TEST_EQPT2}`, +}, { + content: 'Save & Close', + trigger: 'button:has(span:contains(Save & Close))', +}, { + content: 'Save', + trigger: 'button.o_form_button_save', +}, { + content: 'Check that the new equipment was added', + trigger: `.o_field_cell:contains(${TEST_EQPT2})`, + run: function () {}, +} +]); \ No newline at end of file diff --git a/bemade_fsm/tests/__init__.py b/bemade_fsm/tests/__init__.py index a66b934..fc3738e 100644 --- a/bemade_fsm/tests/__init__.py +++ b/bemade_fsm/tests/__init__.py @@ -2,3 +2,4 @@ from . import test_bemade_fsm_common from . import test_task_template from . import test_task_template_sale_order from . import test_equipment +from . import test_fsm_contact_setting diff --git a/bemade_fsm/tests/test_bemade_fsm_common.py b/bemade_fsm/tests/test_bemade_fsm_common.py index 47942ac..4fcfe35 100644 --- a/bemade_fsm/tests/test_bemade_fsm_common.py +++ b/bemade_fsm/tests/test_bemade_fsm_common.py @@ -1,6 +1,8 @@ -from odoo.tests.common import TransactionCase +from odoo.tests.common import TransactionCase, tagged from odoo import Command + +@tagged("-at_install", "post_install") class FSMManagerUserTransactionCase(TransactionCase): @classmethod diff --git a/bemade_fsm/tests/test_equipment.py b/bemade_fsm/tests/test_equipment.py index 2ca0485..c2d4b8d 100644 --- a/bemade_fsm/tests/test_equipment.py +++ b/bemade_fsm/tests/test_equipment.py @@ -1,6 +1,10 @@ from odoo.tests.common import HttpCase, tagged from .test_bemade_fsm_common import FSMManagerUserTransactionCase +from odoo import Command +from odoo.exceptions import MissingError + +@tagged("-at_install", "post_install") class TestEquipmentCommon(FSMManagerUserTransactionCase): @classmethod @@ -13,8 +17,8 @@ class TestEquipmentCommon(FSMManagerUserTransactionCase): 'company_type': 'company', 'street': '123 Street St.', 'city': 'Montreal', - 'state_id': cls.env['res.country.state'].search([('name','ilike','Quebec%')]).id, - 'country_id': cls.env['res.country'].search([('name','=','Canada')]).id + 'state_id': cls.env['res.country.state'].search([('name', 'ilike', 'Quebec%')]).id, + 'country_id': cls.env['res.country'].search([('name', '=', 'Canada')]).id }) cls.partner_contact = cls.env['res.partner'].create({ @@ -30,8 +34,26 @@ class TestEquipmentCommon(FSMManagerUserTransactionCase): @tagged('-at_install', 'post_install') -class TestEquipmentTour(HttpCase, TestEquipmentCommon): +class TestEquipmentBase(TestEquipmentCommon): - def test_equipment_tour(self): - self.start_tour('/web', 'task_equipment_tour', + def test_crd(self): + # Just make sure the basic ORM stuff is OK + self.assertTrue(self.equipment in self.partner_company.equipment_ids) + self.assertTrue(len(self.partner_company.equipment_ids) == 1) + self.partner_company.write({'equipment_ids': [Command.set([])]}) + # Delete should cascade + with self.assertRaises(MissingError): + self.equipment.name + + +@tagged('-at_install', 'post_install') +class TestEquipmentTours(HttpCase, TestEquipmentCommon): + + def test_equipment_base_tour(self): + self.start_tour('/web', 'equipment_base_tour', login=self.user.login, ) + + def test_equipment_sale_order_tour(self): + self.start_tour('/web', 'equipment_sale_order_tour', login=self.user.login) + # Make sure the equipment added to the SO in the test tour was added to the partner + self.assertEqual(len(self.partner_company.equipment_ids), 2) diff --git a/bemade_fsm/tests/test_fsm_contact_setting.py b/bemade_fsm/tests/test_fsm_contact_setting.py index a8874c2..7d32ecb 100644 --- a/bemade_fsm/tests/test_fsm_contact_setting.py +++ b/bemade_fsm/tests/test_fsm_contact_setting.py @@ -1,30 +1,56 @@ from odoo.tests import TransactionCase, HttpCase, tagged from odoo import Command -from test_bemade_fsm_common import FSMManagerUserTransactionCase +from .test_bemade_fsm_common import FSMManagerUserTransactionCase + +@tagged("-at_install", "post_install") class SaleOrderFSMContactsCase(FSMManagerUserTransactionCase): - def test_default_site_contacts(self): - # Make sure the SO pulls the defaults from the partner correctly - pass + @classmethod + def setUpClass(cls): + super().setUpClass() + Partner = cls.env['res.partner'] + cls.parent_co = Partner.create({ + 'name': 'Parent Co', + 'company_type': 'company', + }) + cls.contact_1 = Partner.create({ + 'name': 'Contact 1', + 'company_type': 'person', + 'parent_id': cls.parent_co.id, + }) + cls.contact_2 = Partner.create({ + 'name': 'Contact 2', + 'company_type': 'person', + 'parent_id': cls.parent_co.id, + }) - def test_default_billing_contacts(self): + def test_site_contacts(self): # Make sure the SO pulls the defaults from the partner correctly - pass + self.parent_co.write({'site_contacts': [Command.set([self.contact_1.id, self.contact_2.id])]}) + so = self.env['sale.order'].create({ + 'partner_id': self.parent_co.id, + }) + self.assertTrue(so.site_contacts == self.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([self.contact_1.id])]}) + self.assertTrue(self.contact_1 in so.site_contacts) + self.assertTrue(self.contact_2 not in so.site_contacts) + self.assertTrue(so.site_contacts != so.partner_id.site_contacts and len(so.partner_id.site_contacts) == 2) def test_default_workorder_contacts(self): # Make sure the SO pulls the defaults from the partner correctly - pass - - def test_change_site_contacts(self): - # Make sure the SO contacts can be updated without feeding back to the partner - pass - - def test_change_workorder_contacts(self): - # Make sure the SO contacts can be updated without feeding back to the partner - pass - - def test_change_billing_contacts(self): - # Make sure the SO contacts can be updated without feedin back to the partner - pass - + self.parent_co.write({'work_order_contacts': [Command.set([self.contact_1.id, self.contact_2.id])]}) + so = self.env['sale.order'].create({ + 'partner_id': self.parent_co.id, + }) + self.assertTrue(self.contact_1 in self.parent_co.work_order_contacts) + self.assertTrue(self.contact_2 in self.parent_co.work_order_contacts) + self.assertTrue(self.contact_1 in so.work_order_contacts) + self.assertTrue(self.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([self.contact_1.id])]}) + self.assertTrue(self.contact_1 in so.work_order_contacts) + self.assertTrue(self.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) diff --git a/bemade_fsm/tests/test_task_template.py b/bemade_fsm/tests/test_task_template.py index bd46e0a..c0bce51 100644 --- a/bemade_fsm/tests/test_task_template.py +++ b/bemade_fsm/tests/test_task_template.py @@ -5,6 +5,7 @@ from odoo import Command from psycopg2.errors import ForeignKeyViolation +@tagged("-at_install", "post_install") class TestTaskTemplateCommon(FSMManagerUserTransactionCase): @classmethod diff --git a/bemade_fsm/tests/test_task_template_sale_order.py b/bemade_fsm/tests/test_task_template_sale_order.py index 176c222..e2fe05e 100644 --- a/bemade_fsm/tests/test_task_template_sale_order.py +++ b/bemade_fsm/tests/test_task_template_sale_order.py @@ -2,6 +2,7 @@ from .test_task_template import TestTaskTemplateCommon from odoo.tests.common import tagged +@tagged("-at_install", "post_install") class TestTaskTemplateSalesOrder(TestTaskTemplateCommon): @classmethod def setUpClass(cls): diff --git a/bemade_fsm/views/res_partner.xml b/bemade_fsm/views/res_partner.xml index d451cb9..1cd4cf8 100644 --- a/bemade_fsm/views/res_partner.xml +++ b/bemade_fsm/views/res_partner.xml @@ -25,10 +25,18 @@ + + + + + + + + - +