[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:
committed by
Jorge Pinna Puissant
parent
aa592f69de
commit
2a2f53f7ce
@@ -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;
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
Reference in New Issue
Block a user