[FIX] web: fix translations for duplicate records

This commit ensures that changes in translated fields on a freshly
duplicated record will apply to all translation even when the user
language is not en_US.

Steps to reproduce:

-go to accounting -> configuration -> taxes in other language than en_US
-open any record in form view
-duplicate the record
-change the name of the record and save
-ensure that the name is the same in all languages

task-3339736

closes odoo/odoo#134481

Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
Julien Carion (juca)
2023-09-27 01:50:52 +00:00
committed by Jorge Pinna Puissant
parent aa592f69de
commit 2a2f53f7ce
8 changed files with 143 additions and 12 deletions
@@ -5,6 +5,7 @@ import { FormController } from "@web/views/form/form_controller";
patch(FormController.prototype, {
onWillLoadRoot(nextConfiguration) {
super.onWillLoadRoot(...arguments);
const isSameThread =
this.model.root?.resId === nextConfiguration.resId &&
this.model.root?.resModel === nextConfiguration.resModel;
+18
View File
@@ -1091,6 +1091,24 @@ class Base(models.AbstractModel):
return result
def web_override_translations(self, values):
"""
This method is used to override all the modal translations of the given fields
with the provided value for each field.
:param values: dictionary of the translations to apply for each field name
ex: { "field_name": "new_value" }
"""
self.ensure_one()
for field_name in values:
field = self._fields[field_name]
if field.translate is True:
translations = {lang: False for lang, _ in self.env['res.lang'].get_installed()}
translations['en_US'] = values[field_name]
translations[self.env.lang or 'en_US'] = values[field_name]
self.update_field_translations(field_name, translations)
class ResCompany(models.Model):
_inherit = 'res.company'
@@ -833,7 +833,11 @@ export class Record extends DataPoint {
}
async _save({ reload = true, onError, nextId } = {}) {
const creation = !this.resId;
if (nextId) {
if (creation) {
throw new Error("Cannot set nextId on a new record");
}
reload = true;
}
// before saving, abandon new invalid, untouched records in x2manys
@@ -847,7 +851,6 @@ export class Record extends DataPoint {
return false;
}
const changes = this._getChanges();
const creation = !this.resId;
delete changes.id; // id never changes, and should not be written
if (!creation && !Object.keys(changes).length) {
return true;
@@ -886,19 +889,23 @@ export class Record extends DataPoint {
}
throw e;
}
if ((creation || nextId) && records.length) {
if (reload && !records.length) {
throw new FetchRecordError(nextId || this.resId);
}
if (creation) {
const resId = records[0].id;
const resIds = this.resIds.includes(resId) ? this.resIds : this.resIds.concat([resId]);
const resIds = this.resIds.concat([resId]);
this.model._updateConfig(this.config, { resId, resIds }, { reload: false });
}
await this.model.hooks.onRecordSaved(this, changes);
if (reload) {
if (!records.length) {
throw new FetchRecordError(records.map((r) => r.id));
}
applyProperties(records, this.config.activeFields, this.config.fields);
if (this.resId) {
this.model._updateSimilarRecords(this, records[0]);
}
if (nextId) {
this.model._updateConfig(this.config, { resId: nextId }, { reload: false });
}
if (this.config.isRoot) {
this.model.hooks.onWillLoadRoot(this.config);
}
@@ -912,7 +919,6 @@ export class Record extends DataPoint {
this.data = { ...this._values };
this.dirty = false;
}
await this.model.hooks.onRecordSaved(this, changes);
return true;
}
@@ -114,6 +114,7 @@ export class FormController extends Component {
this.evaluateBooleanExpr = evaluateBooleanExpr;
this.dialogService = useService("dialog");
this.router = useService("router");
this.orm = useService("orm");
this.user = useService("user");
this.viewService = useService("view");
this.ui = useService("ui");
@@ -123,6 +124,7 @@ export class FormController extends Component {
const { create, edit } = this.archInfo.activeActions;
this.canCreate = create && !this.props.preventCreate;
this.canEdit = edit && !this.props.preventEdit;
this.duplicateId = false;
this.display = { ...this.props.display };
if (this.env.inDialog) {
@@ -280,7 +282,9 @@ export class FormController extends Component {
* data necessary for the root record datapoint. Note that this.model.root
* may not exist yet at this point, if this is the first load.
*/
onWillLoadRoot() {}
onWillLoadRoot() {
this.duplicateId = undefined;
}
/**
* onRecordSaved is a callBack that will be executed after the save
@@ -288,7 +292,22 @@ export class FormController extends Component {
* is invalid or if a server error is thrown.
* @param {Record} record
*/
async onRecordSaved(record) {}
async onRecordSaved(record, changes) {
if (this.duplicateId === record.id) {
const translationChanges = {};
for (const fieldName in changes) {
if (record.fields[fieldName].translate) {
translationChanges[fieldName] = changes[fieldName];
}
}
if (Object.keys(translationChanges).length) {
await this.orm.call(this.model.root.resModel, "web_override_translations", [
[this.model.root.resId],
translationChanges,
]);
}
}
}
/**
* onWillSaveRecord is a callBack that will be executed before the
@@ -296,7 +315,7 @@ export class FormController extends Component {
* If it returns false, it will prevent the save.
* @param {Record} record
*/
async onWillSaveRecord(record) {}
async onWillSaveRecord() {}
async onSaveError(error, { discard }) {
const proceed = await new Promise((resolve) => {
@@ -438,6 +457,7 @@ export class FormController extends Component {
async duplicateRecord() {
await this.model.root.duplicate();
this.duplicateId = this.model.root.id;
}
get deleteConfirmationDialogProps() {
@@ -32,7 +32,7 @@
<CogMenu
getActiveIds="() => model.root.isNew ? [] : [model.root.resId]"
context="props.context"
items="props.info.actionMenus ? actionMenuItems : {}"
items="props.info.actionMenus ? this.actionMenuItems : {}"
isDomainSelected="model.root.isDomainSelected"
resModel="model.root.resModel"
domain="props.domain"
@@ -68,7 +68,7 @@ QUnit.module("Views", (hooks) => {
models: {
partner: {
fields: {
display_name: { string: "Displayed name", type: "char" },
display_name: { string: "Displayed name", type: "char", translate: true },
foo: { string: "Foo", type: "char", default: "My little Foo Value" },
bar: { string: "Bar", type: "boolean" },
int_field: { string: "int_field", type: "integer", sortable: true },
@@ -4637,6 +4637,49 @@ QUnit.module("Views", (hooks) => {
assert.verifySteps(["web_save"]);
});
QUnit.test(
"editing a translatable field in a duplicate record overrides translations",
async function (assert) {
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: '<form><field name="display_name"/></form>',
resId: 1,
actionMenus: {},
async mockRPC(route, args) {
if (args.method === "web_save") {
assert.step("web_save");
}
if (args.method === "web_override_translations") {
assert.deepEqual(args.args[1], { display_name: "first record (test)" });
assert.step("web_override_translations");
return true;
}
},
});
assert.strictEqual(
target.querySelector(".o_control_panel .o_breadcrumb").textContent,
"first record",
"should have the display name of the record as title"
);
await toggleActionMenu(target);
await toggleMenuItem(target, "Duplicate");
assert.strictEqual(
target.querySelector(".o_control_panel .o_breadcrumb").textContent,
"first record (copy)",
"should have duplicated the record"
);
assert.containsOnce(target, ".o_form_editable");
await editInput(target, ".o_field_char input", "first record (test)");
await click(target, ".o_form_button_save");
assert.verifySteps(["web_save", "web_override_translations"]);
}
);
QUnit.test("clicking on stat buttons in edit mode", async function (assert) {
let count = 0;
const fakeActionService = {
+1
View File
@@ -18,6 +18,7 @@ from . import test_assets_xml
from . import test_login
from . import test_web_search_read
from . import test_domain
from . import test_translate
from . import test_web_redirect
from . import test_res_users
from . import test_webmanifest
+42
View File
@@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests.common import TransactionCase
class TestTranslationOverride(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.category = cls.env['res.partner.category'].create({'name': 'Reblochon'})
cls.custom = cls.env['ir.model.fields'].create({
'name': 'x_html_test',
'ttype': 'html',
'model_id': cls.category.id,
'translate': True,
})
def test_web_override_translations(self):
self.env['res.lang']._activate_lang('fr_FR')
categoryEN = self.category.with_context(lang='en_US')
categoryFR = self.category.with_context(lang='fr_FR')
customEN = self.custom.with_context(lang='en_US')
customFR = self.custom.with_context(lang='fr_FR')
self.category.web_override_translations({'name': 'commonName'})
self.assertEqual(categoryEN.name, 'commonName')
self.assertEqual(categoryFR.name, 'commonName')
# cannot void translations (incluiding en_US)
self.category.web_override_translations({'name': False})
self.assertEqual(categoryEN.name, 'commonName')
self.assertEqual(categoryFR.name, 'commonName')
# empty str is a valid translation
self.category.web_override_translations({'name': ''})
self.assertEqual(categoryEN.name, '')
self.assertEqual(categoryFR.name, '')
# translated html fields are not changed
self.custom.web_override_translations({'name': '<div>dont</div><div>change</div>'})
self.assertEqual(customEN.name, 'x_html_test')
self.assertEqual(customFR.name, 'x_html_test')