From 218ad8456a06503dd508e7216edcffdc90b35cac Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Wed, 19 Jul 2023 12:11:17 +0000 Subject: [PATCH] [REF] *: adapt codebase to new RelationalModel This commit adapts the code in addons w.r.t. the introduction of the RelationalModel. Main changes that were requested are: - record datapoints no longer always have an "id" key in their data (they still do if the id field is in the view), so we use record.resId instead - the new model is based on fined-grained reactivity, so several components that previously relied on onWillUpdateProps to update their internal state no longer worked. Typically, using the hook "observeRecord" is the way to go now. - specialdata are no longer handled in the model, so the components needing specialData can use the hook "useSpecialData" - more generally, all overrides of models (RelationalModel or KanbanModel) needed to be reworked. Part of task~3179751 Part-of: odoo/odoo#114024 Co-authored-by: Aaron Bohy Co-authored-by: FrancoisGe Co-authored-by: Jorge Pinna Puissant Co-authored-by: Pierre Rousseau --- .../account_move_form/account_move_form.js | 2 +- .../account_payment_field.js | 2 +- .../mail_attachments/mail_attachments.js | 2 +- .../src/components/tax_totals/tax_totals.js | 8 +- .../analytic_distribution.js | 57 +++++---- .../tests/analytic_distribution_tests.js | 4 +- .../wizard/auth_totp_wizard_views.xml | 2 +- .../tests/basic/float_scannable_field_test.js | 8 +- addons/board/static/tests/board_test.js | 10 +- addons/calendar/models/res_partner.py | 2 +- .../calendar/static/src/js/base_calendar.js | 25 ---- .../calendar_form/calendar_quick_create.js | 2 +- .../src/views/fields/many2many_attendee.js | 35 +++--- .../fields/many2many_attendee_expandable.js | 6 - .../tests/tours/debug_menu_set_defaults.js | 2 +- .../crm/static/src/views/crm_form/crm_form.js | 84 +++++-------- .../src/views/crm_kanban/crm_kanban_model.js | 13 +- .../forecast_kanban/forecast_kanban_model.js | 115 ++++++------------ addons/crm/tests/test_sales_team_ui.py | 1 + .../js/event_booth_configurator_controller.js | 13 +- .../static/src/js/sale_product_field.js | 14 ++- .../src/js/event_configurator_controller.js | 3 +- .../department_chart/department_chart.js | 4 +- .../src/js/standalone_m2o_avatar_employee.js | 59 --------- .../hr/static/src/views/profile_form_view.js | 7 +- .../static/tests/m2x_avatar_employee_tests.js | 36 +++--- addons/hr_expense/static/src/views/list.js | 2 +- .../static/src/leave_stats/leave_stats.js | 31 +++-- .../radio_image_field/radio_image_field.js | 7 +- .../static/tests/leave_stats_tests.js | 2 +- .../static/src/fields/hr_org_chart.js | 2 +- .../im_livechat_chatbot_script_answers_m2m.js | 4 +- addons/mail/static/src/core/common/thread.js | 8 +- .../static/src/core/web/activity_button.js | 2 +- addons/mail/static/src/core/web/chatter.js | 5 +- .../web/activity/activity_arch_parser.js | 21 ---- .../views/web/activity/activity_controller.js | 30 +++-- .../src/views/web/activity/activity_model.js | 18 ++- .../web/fields/assign_user_command_hook.js | 19 ++- .../many2many_tags_email.js | 54 ++++---- .../src/views/web/form/form_controller.js | 14 +++ .../static/src/views/web/list_renderer.js | 18 +++ .../static/src/views/web/relational_model.js | 13 -- .../web/activity/activity_widget_tests.js | 13 +- .../mail/static/tests/web/debug_menu_tests.js | 2 +- .../web/fields/many2many_tags_email_tests.js | 8 +- .../web/fields/onchange_on_keydown_tests.js | 6 +- .../mail/static/tests/web/follower_tests.js | 4 +- .../static/src/js/mailing_m2o_filter.js | 2 +- addons/mrp/static/src/widgets/timer.js | 2 +- addons/mrp_subcontracting/__manifest__.py | 5 +- addons/point_of_sale/__manifest__.py | 1 + addons/project/__manifest__.py | 7 +- .../subtask_kanban_list.js | 9 +- .../views/list/list_renderer.js | 2 +- .../project_project_kanban_header.js | 4 +- .../project_task_kanban_dynamic_group_list.js | 49 -------- .../project_task_kanban_header.js | 9 +- .../project_task_kanban_model.js | 65 ++++++---- .../project_subtask_kanban_list_tests.js | 5 +- .../tests/tours/project_update_tour_tests.js | 3 +- .../views/project_update_templates.xml | 2 +- .../static/src/js/purchase_product_field.js | 21 ++-- .../purchase_order_alternatives_widget.js | 6 +- .../src/js/product_catalog/kanban_model.js | 22 ++-- .../static/src/js/product_discount_field.js | 2 +- .../static/tests/sale_product_field_tests.js | 4 +- .../views/sale_order_views.xml | 2 +- .../product_configurator_dialog.js | 55 +-------- .../static/src/js/sale_product_field.js | 46 +++++-- .../static/src/js/sale_product_field.js | 6 +- .../static/src/widgets/qty_at_date_widget.js | 4 +- .../static/src/list/list_data_source.js | 2 +- .../fields/stock_move_line_x2_many_field.js | 2 +- .../list/inventory_report_list_controller.js | 39 ------ .../views/list/inventory_report_list_model.js | 54 ++++++-- .../picking_form/picking_form_controller.js | 4 +- .../views/picking_form/picking_form_model.js | 7 +- .../static/src/widgets/stock_pick_from.js | 2 +- .../question_page_list_renderer.js | 22 ++-- .../question_page_one2many_field.js | 72 +++++------ .../survey_question_trigger.js | 8 +- .../question_page_one2many_field_tests.js | 9 +- addons/survey/tests/test_survey_ui_backend.py | 1 - .../test_mail/static/tests/activity_tests.js | 6 +- ...event_sale_with_product_configurator_ui.js | 4 +- .../static/src/js/backend/html_field.js | 7 +- .../js/systray_items/forum_forum_add_form.js | 2 +- .../src/slide_category_list_renderer.js | 2 +- .../slide_category_one2many_field_tests.js | 1 - 90 files changed, 584 insertions(+), 792 deletions(-) delete mode 100644 addons/calendar/static/src/js/base_calendar.js delete mode 100644 addons/hr/static/src/js/standalone_m2o_avatar_employee.js create mode 100644 addons/mail/static/src/views/web/form/form_controller.js create mode 100644 addons/mail/static/src/views/web/list_renderer.js delete mode 100644 addons/mail/static/src/views/web/relational_model.js delete mode 100644 addons/project/static/src/views/project_task_kanban/project_task_kanban_dynamic_group_list.js diff --git a/addons/account/static/src/components/account_move_form/account_move_form.js b/addons/account/static/src/components/account_move_form/account_move_form.js index d0189f65f20..b6c12f81881 100644 --- a/addons/account/static/src/components/account_move_form/account_move_form.js +++ b/addons/account/static/src/components/account_move_form/account_move_form.js @@ -37,7 +37,7 @@ AccountMoveFormNotebook.props = { } export class AccountMoveFormRenderer extends FormRenderer { async saveBeforeTabChange() { - if (this.props.record.mode === "edit" && this.props.record.isDirty) { + if (this.props.record.mode === "edit" && await this.props.record.isDirty()) { const contentEl = document.querySelector('.o_content'); const scrollPos = contentEl.scrollTop; await this.props.record.save({ diff --git a/addons/account/static/src/components/account_payment_field/account_payment_field.js b/addons/account/static/src/components/account_payment_field/account_payment_field.js index 5da2d3b00d9..f356c6bd599 100644 --- a/addons/account/static/src/components/account_payment_field/account_payment_field.js +++ b/addons/account/static/src/components/account_payment_field/account_payment_field.js @@ -35,7 +35,7 @@ export class AccountPaymentField extends Component { content: [], outstanding: false, title: "", - move_id: this.props.record.data.id, + move_id: this.props.record.resId, }; for (let [key, value] of Object.entries(info.content)) { value.index = key; diff --git a/addons/account/static/src/components/mail_attachments/mail_attachments.js b/addons/account/static/src/components/mail_attachments/mail_attachments.js index a8d46ac5c56..3e68a8f0111 100644 --- a/addons/account/static/src/components/mail_attachments/mail_attachments.js +++ b/addons/account/static/src/components/mail_attachments/mail_attachments.js @@ -64,7 +64,7 @@ export class MailAttachments extends Component { async onWillUnmount(){ // Unlink added attachments if the wizard is not saved. - if(!this.props.record.data.id){ + if(!this.props.record.resId){ this.getValue().forEach((item) => { if(item.manual){ this.attachmentIdsToUnlink.add(item.id); diff --git a/addons/account/static/src/components/tax_totals/tax_totals.js b/addons/account/static/src/components/tax_totals/tax_totals.js index 282ecb60891..1072dd5277f 100644 --- a/addons/account/static/src/components/tax_totals/tax_totals.js +++ b/addons/account/static/src/components/tax_totals/tax_totals.js @@ -6,7 +6,7 @@ import { standardFieldProps } from "@web/views/fields/standard_field_props"; import { registry } from "@web/core/registry"; import { getCurrency } from "@web/core/currency"; -const { Component, onPatched, onWillUpdateProps, useRef, useState } = owl; +const { Component, onPatched, onWillUpdateProps, onWillRender, toRaw, useRef, useState } = owl; /** A line of some TaxTotalsComponent, giving the values of a tax group. @@ -108,9 +108,7 @@ export class TaxTotalsComponent extends Component { setup() { this.totals = {}; this.formatData(this.props); - onWillUpdateProps((nextProps) => { - this.formatData(nextProps); - }); + onWillRender(() => this.formatData(this.props)); } get readonly() { @@ -143,7 +141,7 @@ export class TaxTotalsComponent extends Component { } formatData(props) { - let totals = JSON.parse(JSON.stringify(props.record.data[this.props.name])); + let totals = JSON.parse(JSON.stringify(toRaw(props.record.data[this.props.name]))); const currencyFmtOpts = { currencyId: props.record.data.currency_id && props.record.data.currency_id[0] }; let amount_untaxed = totals.amount_untaxed; diff --git a/addons/analytic/static/src/components/analytic_distribution/analytic_distribution.js b/addons/analytic/static/src/components/analytic_distribution/analytic_distribution.js index a140b66fde3..51822ebb110 100644 --- a/addons/analytic/static/src/components/analytic_distribution/analytic_distribution.js +++ b/addons/analytic/static/src/components/analytic_distribution/analytic_distribution.js @@ -10,6 +10,7 @@ import { shallowEqual } from "@web/core/utils/arrays"; import { sprintf } from "@web/core/utils/strings"; import { _lt } from "@web/core/l10n/translation"; import { AutoComplete } from "@web/core/autocomplete/autocomplete"; +import { useRecordObserver } from "@web/model/relational_model/utils"; import { standardFieldProps } from "@web/views/fields/standard_field_props"; import { TagsList } from "@web/core/tags_list/tags_list"; @@ -18,7 +19,7 @@ import { parseFloat as oParseFloat } from "@web/views/fields/parsers"; import { formatPercentage } from "@web/views/fields/formatters"; import { SelectCreateDialog } from "@web/views/view_dialogs/select_create_dialog"; -const { Component, useState, useRef, useExternalListener, onWillUpdateProps, onWillStart, onPatched } = owl; +const { Component, useState, useRef, useExternalListener, onWillStart, onPatched } = owl; const PLAN_APPLICABILITY = { mandatory: _lt("Mandatory"), @@ -50,7 +51,7 @@ export class AnalyticDistribution extends Component { this.currentValue = this.props.record.data[this.props.name]; onWillStart(this.willStart); - onWillUpdateProps(this.willUpdate); + useRecordObserver(this.willUpdateRecord.bind(this)); onPatched(this.patched); useExternalListener(window, "click", this.onWindowClick, true); @@ -83,43 +84,44 @@ export class AnalyticDistribution extends Component { // Lifecycle async willStart() { if (this.editingRecord) { - await this.fetchAllPlans(this.props); + await this.fetchAllPlans(); } - await this.formatData(this.props); + await this.formatData(); } - async willUpdate(nextProps) { + async willUpdateRecord(record) { // Unless force_applicability, Plans need to be retrieved again as the product or account might have changed // and thus different applicabilities apply // or a model applies that contains unavailable plans // This should only execute when these fields have changed, therefore we use the `_field` props. const valueChanged = JSON.stringify(this.currentValue) !== - JSON.stringify(nextProps.record.data[nextProps.name]); - const currentAccount = this.props.account_field && this.props.record.data[this.props.account_field] || false; - const currentProduct = this.props.product_field && this.props.record.data[this.props.product_field] || false; + JSON.stringify(record.data[this.props.name]); + const currentAccount = this.props.account_field && record.data[this.props.account_field] || false; + const currentProduct = this.props.product_field && record.data[this.props.product_field] || false; const accountChanged = !shallowEqual(this.lastAccount, currentAccount); const productChanged = !shallowEqual(this.lastProduct, currentProduct); if (valueChanged || accountChanged || productChanged) { if (!this.props.force_applicability) { - await this.fetchAllPlans(nextProps); + await this.fetchAllPlans(); } this.lastAccount = accountChanged && currentAccount || this.lastAccount; this.lastProduct = productChanged && currentProduct || this.lastProduct; - await this.formatData(nextProps); + await this.formatData(); } - this.currentValue = nextProps.record.data[nextProps.name]; + this.currentValue = record.data[this.props.name]; } patched() { this.focusToSelector(); } - async formatData(nextProps) { - const data = nextProps.record.data[nextProps.name]; + async formatData() { + const { record, name } = this.props; + const data = record.data[name]; const analytic_account_ids = Object.keys(data).map((id) => parseInt(id)); const records = analytic_account_ids.length ? await this.fetchAnalyticAccounts([["id", "in", analytic_account_ids]]) : []; - let widgetData = Object.assign({}, ...this.allPlans.map((plan) => ({[plan.id]: {...plan, distribution: []}}))); + const widgetData = Object.assign({}, ...this.allPlans.map((plan) => ({ [plan.id]: { ...plan, distribution: [] } }))); records.map((record) => { if (!widgetData[record.root_plan_id[0]]) { // plans might not have been retrieved @@ -144,36 +146,37 @@ export class AnalyticDistribution extends Component { } // ORM - fetchPlansArgs(nextProps) { + fetchPlansArgs() { + const { record, name } = this.props; let args = {}; if (this.props.business_domain_compute) { - args['business_domain'] = evaluateExpr(this.props.business_domain_compute, this.props.record.evalContext); + args['business_domain'] = evaluateExpr(this.props.business_domain_compute, record.evalContext); } if (this.props.business_domain) { args['business_domain'] = this.props.business_domain; } - if (this.props.product_field && this.props.record.data[this.props.product_field]) { - args['product'] = this.props.record.data[this.props.product_field][0]; + if (this.props.product_field && record.data[this.props.product_field]) { + args['product'] = record.data[this.props.product_field][0]; } - if (this.props.account_field && this.props.record.data[this.props.account_field]) { - args['account'] = this.props.record.data[this.props.account_field][0]; + if (this.props.account_field && record.data[this.props.account_field]) { + args['account'] = record.data[this.props.account_field][0]; } if (this.props.force_applicability) { args['applicability'] = this.props.force_applicability; } - const existing_account_ids = Object.keys(nextProps.record.data[nextProps.name]).map((i) => parseInt(i)); + const existing_account_ids = Object.keys(record.data[name]).map((i) => parseInt(i)); if (existing_account_ids.length) { args['existing_account_ids'] = existing_account_ids; } - if (this.props.record.data.company_id) { - args['company_id'] = this.props.record.data.company_id[0]; + if (record.data.company_id) { + args['company_id'] = record.data.company_id[0]; } return args; } - async fetchAllPlans(nextProps) { + async fetchAllPlans() { // TODO: Optimize to execute once for all records when `force_applicability` is set - const argsPlan = this.fetchPlansArgs(nextProps); + const argsPlan = this.fetchPlansArgs(); this.allPlans = await this.orm.call("account.analytic.plan", "get_relevant_plans", [], argsPlan); } @@ -530,8 +533,8 @@ export class AnalyticDistribution extends Component { async openAnalyticEditor() { if (!this.allPlans.length) { - await this.fetchAllPlans(this.props); - await this.formatData(this.props); + await this.fetchAllPlans(); + await this.formatData(); } this.autoFill(); const incompletePlan = this.firstIncompletePlanId; diff --git a/addons/analytic/static/tests/analytic_distribution_tests.js b/addons/analytic/static/tests/analytic_distribution_tests.js index 54ac36a70d2..46f7ad38b21 100644 --- a/addons/analytic/static/tests/analytic_distribution_tests.js +++ b/addons/analytic/static/tests/analytic_distribution_tests.js @@ -74,6 +74,7 @@ QUnit.module("Analytic", (hooks) => { amount: { string: "Amount", type: "float" }, analytic_distribution: { string: "Analytic", type: "json" }, move_id: { string: "Account Move", type: "many2one", relation: "move" }, + analytic_precision: { string: "Analytic Precision", type: "integer" }, }, records: [ { id: 1, label: "Developer Time", amount: 100.00, analytic_distribution: {"1": 30.3, "3": 69.7}}, @@ -140,7 +141,7 @@ QUnit.module("Analytic", (hooks) => { `, - mockRPC(route, { kwargs, method, model }) { + mockRPC(route, { method, model }) { if (method === "get_relevant_plans" && model === "account.analytic.plan") { return Promise.resolve( serverData.models['plan'].records.filter((r) => !r.parent_id && r.applicability !== "unavailable") @@ -333,6 +334,7 @@ QUnit.module("Analytic", (hooks) => { // apply the changes to both move lines triggerHotkey("Escape"); await nextTick(); + await nextTick(); await click(target.querySelector(".modal-dialog .btn-primary")); assert.containsN(target, ".badge", 10, "should contain 2 rows of 5 tags each"); diff --git a/addons/auth_totp/wizard/auth_totp_wizard_views.xml b/addons/auth_totp/wizard/auth_totp_wizard_views.xml index 76b967acf21..2c11bb26bdc 100644 --- a/addons/auth_totp/wizard/auth_totp_wizard_views.xml +++ b/addons/auth_totp/wizard/auth_totp_wizard_views.xml @@ -40,7 +40,7 @@
- +