19.0 vanilla

This commit is contained in:
Ernad Husremovic 2025-10-03 18:07:25 +02:00
parent 0a7ae8db93
commit 991d2234ca
416 changed files with 646602 additions and 300844 deletions

View file

@ -8,3 +8,4 @@ from . import base_module_upgrade
from . import base_module_uninstall
from . import base_export_language
from . import base_partner_merge
from . import wizard_ir_model_menu_create

View file

@ -1,25 +1,25 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import ast
import base64
import io
from odoo import api, fields, models, tools, _
from odoo import api, fields, models, tools
from odoo.tools.translate import trans_export, trans_export_records
NEW_LANG_KEY = '__new__'
class BaseLanguageExport(models.TransientModel):
_name = "base.language.export"
_name = 'base.language.export'
_description = 'Language Export'
@api.model
def _get_languages(self):
langs = self.env['res.lang'].get_installed()
return [(NEW_LANG_KEY, _('New Language (Empty translation template)'))] + \
return [(NEW_LANG_KEY, self.env._('New Language (Empty translation template)'))] + \
langs
name = fields.Char('File Name', readonly=True)
lang = fields.Selection(_get_languages, string='Language', required=True, default=NEW_LANG_KEY)
format = fields.Selection([('csv','CSV File'), ('po','PO File'), ('tgz', 'TGZ Archive')],
@ -32,39 +32,41 @@ class BaseLanguageExport(models.TransientModel):
model_name = fields.Char(string="Model Name", related="model_id.model")
domain = fields.Char(string="Model Domain", default='[]')
data = fields.Binary('File', readonly=True, attachment=False)
state = fields.Selection([('choose', 'choose'), ('get', 'get')], # choose language or get the file
state = fields.Selection([('choose', 'choose'), ('get', 'get')], # choose language or get the file
default='choose')
def act_getfile(self):
this = self[0]
lang = this.lang if this.lang != NEW_LANG_KEY else False
self.ensure_one()
lang = self.lang if self.lang != NEW_LANG_KEY else False
with io.BytesIO() as buf:
if this.export_type == 'model':
ids = self.env[this.model_name].search(ast.literal_eval(this.domain)).ids
trans_export_records(lang, this.model_name, ids, buf, this.format, self._cr)
if self.export_type == 'model':
ids = self.env[self.model_name].search(ast.literal_eval(self.domain)).ids
is_exported = trans_export_records(lang, self.model_name, ids, buf, self.format, self.env)
else:
mods = sorted(this.mapped('modules.name')) or ['all']
trans_export(lang, mods, buf, this.format, self._cr)
out = base64.encodebytes(buf.getvalue())
mods = sorted(self.mapped('modules.name')) or ['all']
is_exported = trans_export(lang, mods, buf, self.format, self.env)
out = is_exported and base64.encodebytes(buf.getvalue())
filename = 'new'
if lang:
filename = tools.get_iso_codes(lang)
elif this.export_type == 'model':
filename = this.model_name.replace('.', '_')
elif self.export_type == 'model':
filename = self.model_name.replace('.', '_')
elif len(mods) == 1:
filename = mods[0]
extension = this.format
extension = self.format
if not lang and extension == 'po':
extension = 'pot'
name = "%s.%s" % (filename, extension)
this.write({'state': 'get', 'data': out, 'name': name})
self.write({'state': 'get', 'data': out, 'name': name})
return {
'name': self.env.ref('base.action_wizard_lang_export').name,
'type': 'ir.actions.act_window',
'res_model': 'base.language.export',
'view_mode': 'form',
'res_id': this.id,
'res_id': self.id,
'views': [(False, 'form')],
'target': 'new',
}

View file

@ -18,23 +18,42 @@
</group>
<div invisible="state != 'get'">
<h2>Export Complete</h2>
<p>Here is the exported translation file: <field name="data" readonly="1" filename="name"/></p>
<p>This file was generated using the universal <strong>Unicode/UTF-8</strong> file encoding, please be sure to view and edit
using the same encoding.</p>
<p>The next step depends on the file format:
<ul>
<li>CSV format: you may edit it directly with your favorite spreadsheet software,
the rightmost column (value) contains the translations</li>
<li>PO(T) format: you should edit it with a PO editor such as
<a href="http://www.poedit.net/" target="_blank">POEdit</a>, or your preferred text editor</li>
<li>TGZ format: bundles multiple PO(T) files as a single archive</li>
</ul>
</p>
<p>For more details about translating Odoo in your language, please refer to the
<a href="https://github.com/odoo/odoo/wiki/Translations" target="_blank">documentation</a>.</p>
<div invisible="data">
<div class="mb-2 rounded-2 overflow-hidden d-grid gap-2" >
<div class="alert alert-warning m-0 p-1 ps-3" role="alert">
<div name="error" style="white-space: pre-wrap;" invisible="export_type == 'module'">
Model
<field name="model_id" readonly="1"/>
does not contain translatable terms.<br/>
</div>
<div name="error" style="white-space: pre-wrap;" invisible="export_type == 'model'">
Modules
<field name="modules" widget="many2many_tags" readonly="1"/>
do not contain translatable terms.<br/>
</div>
</div>
</div>
No file could be exported.
</div>
<div invisible="not data">
<p>Here is the exported translation file: <field name="data" readonly="1" filename="name"/></p>
<p>This file was generated using the universal <strong>Unicode/UTF-8</strong> file encoding, please be sure to view and edit
using the same encoding.</p>
<p>The next step depends on the file format:
<ul>
<li>CSV format: you may edit it directly with your favorite spreadsheet software,
the rightmost column (value) contains the translations</li>
<li>PO(T) format: you should edit it with a PO editor such as
<a href="http://www.poedit.net/" target="_blank">POEdit</a>, or your preferred text editor</li>
<li>TGZ format: bundles multiple PO(T) files as a single archive</li>
</ul>
</p>
<p>For more details about translating Odoo in your language, please refer to the
<a href="https://github.com/odoo/odoo/wiki/Translations" target="_blank">documentation</a>.</p>
</div>
</div>
<footer invisible="state != 'choose'">
<button name="act_getfile" string="Export" type="object" class="btn-primary" data-hotkey="q"/>
<button name="act_getfile" data-hotkey="q" string="Export" type="object" class="btn-primary"/>
<button special="cancel" data-hotkey="x" string="Cancel" type="object" class="btn-secondary"/>
</footer>
<footer invisible="state != 'get'">

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
@ -7,7 +6,7 @@ import operator
from tempfile import TemporaryFile
from os.path import splitext
from odoo import api, fields, models, tools, sql_db, _
from odoo import fields, models, tools
from odoo.exceptions import UserError
from odoo.tools.translate import TranslationImporter
@ -15,7 +14,7 @@ _logger = logging.getLogger(__name__)
class BaseLanguageImport(models.TransientModel):
_name = "base.language.import"
_name = 'base.language.import'
_description = "Language Import"
name = fields.Char('Language Name', required=True)
@ -43,7 +42,7 @@ class BaseLanguageImport(models.TransientModel):
except Exception as e:
_logger.warning('Could not import the file due to a format mismatch or it being malformed.')
raise UserError(
_('File "%(file_name)s" not imported due to format mismatch or a malformed file.'
self.env._('File "%(file_name)s" not imported due to format mismatch or a malformed file.'
' (Valid formats are .csv, .po)\n\nTechnical Details:\n%(error_message)s',
file_name=base_lang_import.filename, error_message=e),
)

View file

@ -1,11 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo import api, fields, models
class BaseLanguageInstall(models.TransientModel):
_name = "base.language.install"
_name = 'base.language.install'
_description = "Install Language"
@api.model
@ -13,8 +12,8 @@ class BaseLanguageInstall(models.TransientModel):
""" Display the selected language when using the 'Update Terms' action
from the language list view
"""
if self._context.get('active_model') == 'res.lang':
return self._context.get('active_ids') or [self._context.get('active_id')]
if self.env.context.get('active_model') == 'res.lang':
return self.env.context.get('active_ids') or [self.env.context.get('active_id')]
return False
# add a context on the field itself, to be sure even inactive langs are displayed
@ -52,11 +51,13 @@ class BaseLanguageInstall(models.TransientModel):
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'context': dict(self._context, active_ids=self.ids),
'context': dict(self.env.context, active_ids=self.ids),
'target': 'new',
'params': {
'message': _("The languages that you selected have been successfully installed.\
Users can choose their favorite language in their preferences."),
'message': self.env._(
"The languages that you selected have been successfully installed. "
"Users can choose their favorite language in their preferences."
),
'type': 'success',
'sticky': False,
'next': {'type': 'ir.actions.act_window_close'},

View file

@ -5,29 +5,29 @@ from odoo import api, fields, models
class BaseModuleUninstall(models.TransientModel):
_name = "base.module.uninstall"
_name = 'base.module.uninstall'
_description = "Module Uninstall"
show_all = fields.Boolean()
module_id = fields.Many2one(
'ir.module.module', string="Module", required=True,
module_ids = fields.Many2many(
'ir.module.module', string="Module(s)", required=True,
domain=[('state', 'in', ['installed', 'to upgrade', 'to install'])],
ondelete='cascade', readonly=True,
)
module_ids = fields.Many2many('ir.module.module', string="Impacted modules",
compute='_compute_module_ids')
impacted_module_ids = fields.Many2many('ir.module.module', string="Impacted modules",
compute='_compute_impacted_module_ids')
model_ids = fields.Many2many('ir.model', string="Impacted data models",
compute='_compute_model_ids')
def _get_modules(self):
""" Return all the modules impacted by self. """
return self.module_id.downstream_dependencies(self.module_id)
return self.module_ids.downstream_dependencies(self.module_ids)
@api.depends('module_id', 'show_all')
def _compute_module_ids(self):
@api.depends('module_ids', 'show_all')
def _compute_impacted_module_ids(self):
for wizard in self:
modules = wizard._get_modules().sorted(lambda m: (not m.application, m.sequence))
wizard.module_ids = modules if wizard.show_all else wizard._modules_to_display(modules)
wizard.impacted_module_ids = modules if wizard.show_all else wizard._modules_to_display(modules)
@api.model
def _modules_to_display(self, modules):
@ -37,12 +37,12 @@ class BaseModuleUninstall(models.TransientModel):
""" Return the models (ir.model) to consider for the impact. """
return self.env['ir.model'].search([('transient', '=', False)])
@api.depends('module_ids')
@api.depends('impacted_module_ids')
def _compute_model_ids(self):
ir_models = self._get_models()
ir_models_xids = ir_models._get_external_ids()
for wizard in self:
if wizard.module_id:
if wizard.module_ids:
module_names = set(wizard._get_modules().mapped('name'))
def lost(model):
@ -50,14 +50,16 @@ class BaseModuleUninstall(models.TransientModel):
return xids and all(xid.split('.')[0] in module_names for xid in xids)
# find the models that have all their XIDs in the given modules
self.model_ids = ir_models.filtered(lost).sorted('name')
wizard.model_ids = ir_models.filtered(lost).sorted('name')
else:
wizard.model_ids = False
@api.onchange('module_id')
def _onchange_module_id(self):
# if we select a technical module, show technical modules by default
if not self.module_id.application:
@api.onchange('module_ids')
def _onchange_module_ids(self):
# if the user selects only technical modules, show technical modules.
if self.module_ids and not any(m.application for m in self.module_ids):
self.show_all = True
def action_uninstall(self):
modules = self.module_id
modules = self.module_ids
return modules.button_immediate_uninstall()

View file

@ -16,7 +16,7 @@
<div class="me-auto p-2 bd-highlight"><h3>Apps to Uninstall</h3></div>
<div class="p-2 bd-highlight"><field name="show_all"/> Show All</div>
</div>
<field name="module_ids" mode="kanban" class="o_modules_field">
<field name="impacted_module_ids" mode="kanban" class="o_modules_field">
<kanban create="false" class="o_modules_kanban">
<field name="state"/>
<templates>

View file

@ -4,7 +4,7 @@ from odoo import api, fields, models
class BaseModuleUpdate(models.TransientModel):
_name = "base.module.update"
_name = 'base.module.update'
_description = "Update Module"
updated = fields.Integer('Number of modules updated', readonly=True)

View file

@ -1,17 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import odoo
from odoo import api, fields, models, _
from odoo import api, fields, models
from odoo.exceptions import UserError
class BaseModuleUpgrade(models.TransientModel):
_name = "base.module.upgrade"
_name = 'base.module.upgrade'
_description = "Upgrade Module"
@api.model
@api.returns('ir.module.module')
def get_module_list(self):
states = ['to upgrade', 'to remove', 'to install']
return self.env['ir.module.module'].search([('state', 'in', states)])
@ -58,15 +56,15 @@ class BaseModuleUpgrade(models.TransientModel):
JOIN ir_module_module_dependency d ON (m.id = d.module_id)
LEFT JOIN ir_module_module m2 ON (d.name = m2.name)
WHERE m.id = any(%s) and (m2.state IS NULL or m2.state = %s) """
self._cr.execute(query, (mods.ids, 'uninstalled'))
unmet_packages = [row[0] for row in self._cr.fetchall()]
self.env.cr.execute(query, (mods.ids, 'uninstalled'))
unmet_packages = [row[0] for row in self.env.cr.fetchall()]
if unmet_packages:
raise UserError(_('The following modules are not installed or unknown: %s', '\n\n' + '\n'.join(unmet_packages)))
raise UserError(self.env._('The following modules are not installed or unknown: %s', '\n\n' + '\n'.join(unmet_packages)))
# terminate transaction before re-creating cursor below
self._cr.commit()
odoo.modules.registry.Registry.new(self._cr.dbname, update_module=True)
self._cr.reset()
self.env.cr.commit()
odoo.modules.registry.Registry.new(self.env.cr.dbname, update_module=True)
self.env.cr.reset()
return {'type': 'ir.actions.act_window_close'}

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from ast import literal_eval
@ -8,16 +7,17 @@ import logging
import psycopg2
import datetime
from odoo import api, fields, models, Command
from odoo import _
from odoo import api, fields, models
from odoo.exceptions import ValidationError, UserError
from odoo.fields import Command
from odoo.tools import mute_logger, SQL
_logger = logging.getLogger('odoo.addons.base.partner.merge')
class MergePartnerLine(models.TransientModel):
class BasePartnerMergeLine(models.TransientModel):
_name = 'base.partner.merge.line'
_description = 'Merge Partner Line'
_order = 'min_id asc'
@ -26,19 +26,18 @@ class MergePartnerLine(models.TransientModel):
aggr_ids = fields.Char('Ids', required=True)
class MergePartnerAutomatic(models.TransientModel):
class BasePartnerMergeAutomaticWizard(models.TransientModel):
"""
The idea behind this wizard is to create a list of potential partners to
merge. We use two objects, the first one is the wizard for the end-user.
And the second will contain the partner list to merge.
"""
_name = 'base.partner.merge.automatic.wizard'
_description = 'Merge Partner Wizard'
@api.model
def default_get(self, fields):
res = super(MergePartnerAutomatic, self).default_get(fields)
res = super().default_get(fields)
active_ids = self.env.context.get('active_ids')
if self.env.context.get('active_model') == 'res.partner' and active_ids:
if 'state' in fields:
@ -96,8 +95,22 @@ class MergePartnerAutomatic(models.TransientModel):
AND att2.attrelid = cl2.oid
AND con.contype = 'f'
"""
self._cr.execute(query, (table,))
return self._cr.fetchall()
self.env.cr.execute(query, (table,))
return self.env.cr.fetchall()
def _has_check_or_unique_constraint(self, table, column):
self.env.cr.execute("""
SELECT 1
FROM pg_constraint c
JOIN pg_class r ON (c.conrelid = r.oid)
CROSS JOIN LATERAL unnest(c.conkey) AS cattr(attnum)
JOIN pg_attribute a ON (a.attrelid = c.conrelid AND a.attnum = cattr.attnum)
WHERE c.contype IN ('c', 'u')
AND r.relname = %s
AND a.attname = %s
LIMIT 1
""", (table, column))
return bool(self.env.cr.rowcount)
@api.model
def _update_foreign_keys_generic(self, model, src_records, dst_record):
@ -119,9 +132,9 @@ class MergePartnerAutomatic(models.TransientModel):
# get list of columns of current table (exept the current fk column)
query = "SELECT column_name FROM information_schema.columns WHERE table_name LIKE '%s'" % (table)
self._cr.execute(query, ())
self.env.cr.execute(query, ())
columns = []
for data in self._cr.fetchall():
for data in self.env.cr.fetchall():
if data[0] != column:
columns.append(data[0])
@ -131,6 +144,12 @@ class MergePartnerAutomatic(models.TransientModel):
'column': column,
'value': columns[0],
}
self.env.cr.execute('SELECT FROM "%(table)s" WHERE "%(column)s" IN %%s LIMIT 1' % query_dic,
(tuple(src_records.ids),))
if self.env.cr.fetchone() is None:
continue # no record
if len(columns) <= 1:
# unique key treated
query = """
@ -146,17 +165,21 @@ class MergePartnerAutomatic(models.TransientModel):
___tu.%(value)s = ___tw.%(value)s
)""" % query_dic
for record in src_records:
self._cr.execute(query, (dst_record.id, record.id, dst_record.id))
self.env.cr.execute(query, (dst_record.id, record.id, dst_record.id))
elif not self._has_check_or_unique_constraint(table, column):
# if there is no CHECK or UNIQUE constraint, we do it without a savepoint
query = 'UPDATE "%(table)s" SET "%(column)s" = %%s WHERE "%(column)s" IN %%s' % query_dic
self.env.cr.execute(query, (dst_record.id, tuple(src_records.ids)))
else:
try:
with mute_logger('odoo.sql_db'), self._cr.savepoint():
with mute_logger('odoo.sql_db'), self.env.cr.savepoint():
query = 'UPDATE "%(table)s" SET "%(column)s" = %%s WHERE "%(column)s" IN %%s' % query_dic
self._cr.execute(query, (dst_record.id, tuple(src_records.ids)))
self.env.cr.execute(query, (dst_record.id, tuple(src_records.ids)))
except psycopg2.Error:
# updating fails, most likely due to a violated unique constraint
# keeping record with nonexistent partner_id is useless, better delete it
query = 'DELETE FROM "%(table)s" WHERE "%(column)s" IN %%s' % query_dic
self._cr.execute(query, (tuple(src_records.ids),))
self.env.cr.execute(query, (tuple(src_records.ids),))
@api.model
def _update_reference_fields_generic(self, referenced_model, src_records, dst_record, additional_update_records=None):
@ -173,8 +196,14 @@ class MergePartnerAutomatic(models.TransientModel):
if Model is None:
return
records = Model.sudo().search([(field_model, '=', referenced_model), (field_id, '=', src.id)])
if not records:
return
if not self._has_check_or_unique_constraint(records._table, field_id):
records.sudo().write({field_id: dst_record.id})
records.env.flush_all()
return
try:
with mute_logger('odoo.sql_db'), self._cr.savepoint():
with mute_logger('odoo.sql_db'), self.env.cr.savepoint():
records.sudo().write({field_id: dst_record.id})
records.env.flush_all()
except psycopg2.Error:
@ -259,34 +288,34 @@ class MergePartnerAutomatic(models.TransientModel):
self.env.flush_all()
# company_dependent fields of merged records
with self._cr.savepoint():
for fname, field in dst_record._fields.items():
if field.company_dependent:
self.env.execute_query(SQL(
# use the specific company dependent value of sources
# to fill the non-specific value of destination. Source
# values for rows with larger id have higher priority
# when aggregated
"""
WITH source AS (
SELECT %(field)s
FROM %(table)s
WHERE id IN %(source_ids)s
ORDER BY id
), source_agg AS (
SELECT jsonb_object_agg(key, value) AS value
FROM source, jsonb_each(%(field)s)
)
UPDATE %(table)s
SET %(field)s = source_agg.value || COALESCE(%(table)s.%(field)s, '{}'::jsonb)
FROM source_agg
WHERE id = %(destination_id)s AND source_agg.value IS NOT NULL
""",
table=SQL.identifier(dst_record._table),
field=SQL.identifier(fname),
destination_id=dst_record.id,
source_ids=tuple(src_records.ids),
))
for fname, field in dst_record._fields.items():
if not field.company_dependent:
continue
self.env.execute_query(SQL(
# use the specific company dependent value of sources
# to fill the non-specific value of destination. Source
# values for rows with larger id have higher priority
# when aggregated
"""
WITH source AS (
SELECT %(field)s
FROM %(table)s
WHERE id IN %(source_ids)s
ORDER BY id
), source_agg AS (
SELECT jsonb_object_agg(key, value) AS value
FROM source, jsonb_each(%(field)s)
)
UPDATE %(table)s
SET %(field)s = source_agg.value || COALESCE(%(table)s.%(field)s, '{}'::jsonb)
FROM source_agg
WHERE id = %(destination_id)s AND source_agg.value IS NOT NULL
""",
table=SQL.identifier(dst_record._table),
field=SQL.identifier(fname),
destination_id=dst_record.id,
source_ids=tuple(src_records.ids),
))
@api.model
def _update_foreign_keys(self, src_partners, dst_partner):
@ -376,9 +405,9 @@ class MergePartnerAutomatic(models.TransientModel):
if duplicate_account:
self._update_foreign_keys_generic('res.partner.bank', src_account, duplicate_account)
self._update_reference_fields_generic('res.partner.bank', src_account, duplicate_account)
src_account.unlink()
src_account.sudo().unlink()
else:
src_account.write({'partner_id': dst_partner.id})
src_account.sudo().write({'partner_id': dst_partner.id})
def _merge(self, partner_ids, dst_partner=None, extra_checks=True):
""" private implementation of merge partner
@ -396,21 +425,21 @@ class MergePartnerAutomatic(models.TransientModel):
return
if len(partner_ids) > 3:
raise UserError(_("For safety reasons, you cannot merge more than 3 contacts together. You can re-open the wizard several times if needed."))
raise UserError(self.env._("For safety reasons, you cannot merge more than 3 contacts together. You can re-open the wizard several times if needed."))
# check if the list of partners to merge contains child/parent relation
child_ids = self.env['res.partner']
for partner_id in partner_ids:
child_ids |= Partner.search([('id', 'child_of', [partner_id.id])]) - partner_id
if partner_ids & child_ids:
raise UserError(_("You cannot merge a contact with one of his parent."))
raise UserError(self.env._("You cannot merge a contact with one of his parent."))
# check if the list of partners to merge are linked to more than one user
if len(partner_ids.with_context(active_test=False).user_ids) > 1:
raise UserError(_("You cannot merge contacts linked to more than one user even if only one is active."))
raise UserError(self.env._("You cannot merge contacts linked to more than one user even if only one is active."))
if extra_checks and len(set(partner.email for partner in partner_ids)) > 1:
raise UserError(_("All contacts must have the same email. Only the Administrator can merge contacts with different emails."))
raise UserError(self.env._("All contacts must have the same email. Only the Administrator can merge contacts with different emails."))
# remove dst_partner from partners to merge
if dst_partner and dst_partner in partner_ids:
@ -441,10 +470,10 @@ class MergePartnerAutomatic(models.TransientModel):
self._log_merge_operation(src_partners, dst_partner)
# delete source partner, since they are merged
src_partners.unlink()
src_partners.sudo().unlink()
def _log_merge_operation(self, src_partners, dst_partner):
_logger.info('(uid = %s) merged the partners %r with %s', self._uid, src_partners.ids, dst_partner.id)
_logger.info('(uid = %s) merged the partners %r with %s', self.env.uid, src_partners.ids, dst_partner.id)
# ----------------------------------------
# Helpers
@ -508,7 +537,7 @@ class MergePartnerAutomatic(models.TransientModel):
groups.append(field_name[len(group_by_prefix):])
if not groups:
raise UserError(_("You have to specify a filter for your selection."))
raise UserError(self.env._("You have to specify a filter for your selection."))
return groups
@ -594,10 +623,10 @@ class MergePartnerAutomatic(models.TransientModel):
model_mapping = self._compute_models()
# group partner query
self._cr.execute(query) # pylint: disable=sql-injection
self.env.cr.execute(query)
counter = 0
for min_id, aggr_ids in self._cr.fetchall():
for min_id, aggr_ids in self.env.cr.fetchall():
# To ensure that the used partners are accessible by the user
partners = self.env['res.partner'].search([('id', 'in', aggr_ids)])
if len(partners) < 2:
@ -625,8 +654,9 @@ class MergePartnerAutomatic(models.TransientModel):
""" Start the process 'Merge with Manual Check'. Fill the wizard according to the group_by and exclude
options, and redirect to the first step (treatment of first wizard line). After, for each subset of
partner to merge, the wizard will be actualized.
- Compute the selected groups (with duplication)
- If the user has selected the 'exclude_xxx' fields, avoid the partners
- If the user has selected the ``exclude_xxx`` fields, avoid the partners
"""
self.ensure_one()
groups = self._compute_selected_groupby()
@ -647,7 +677,7 @@ class MergePartnerAutomatic(models.TransientModel):
partner_ids = literal_eval(line.aggr_ids)
self._merge(partner_ids)
line.unlink()
self._cr.commit() # TODO JEM : explain why
self.env.cr.commit() # TODO JEM : explain why
self.write({'state': 'finished'})
return {
@ -692,11 +722,11 @@ class MergePartnerAutomatic(models.TransientModel):
partner_ids = literal_eval(line.aggr_ids)
self._merge(partner_ids)
line.unlink()
self._cr.commit()
self.env.cr.commit()
self.write({'state': 'finished'})
self._cr.execute("""
self.env.cr.execute("""
UPDATE
res_partner
SET
@ -724,7 +754,7 @@ class MergePartnerAutomatic(models.TransientModel):
wizard.action_start_automatic_process()
# NOTE JEM : no idea if this query is usefull
self._cr.execute("""
self.env.cr.execute("""
UPDATE
res_partner
SET

View file

@ -18,7 +18,7 @@
<h2 colspan="2">There are no more contacts to merge for this request</h2>
<button name="%(action_partner_deduplicate)d" string="Deduplicate the other Contacts" class="oe_highlight" type="action" colspan="2"/>
</group>
<p class="oe_grey" invisible="state != 'option'">
<p class="opacity-50" invisible="state != 'option'">
Select the list of fields used to search for
duplicated records. If you select several fields,
Odoo will propose you to merge only those having
@ -47,7 +47,7 @@
<separator string="Merge the following contacts"
invisible="state in ('option', 'finished')"/>
<group invisible="state in ('option', 'finished')" col="1">
<p class="oe_grey">
<p class="opacity-50">
Selected contacts will be merged together.
All documents linked to one of these contacts
will be redirected to the destination contact.
@ -106,6 +106,6 @@
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="binding_model_id" ref="base.model_res_partner"/>
<field name="binding_view_types">list</field>
<field name="binding_view_types">list,kanban</field>
</record>
</odoo>

View file

@ -0,0 +1,25 @@
from odoo import fields, models
class WizardIrModelMenuCreate(models.TransientModel):
_name = 'wizard.ir.model.menu.create'
_description = 'Create Menu Wizard'
menu_id = fields.Many2one('ir.ui.menu', string='Parent Menu', required=True, ondelete='cascade')
name = fields.Char(string='Menu Name', required=True)
def menu_create(self):
for menu in self:
model = self.env['ir.model'].browse(self.env.context.get('model_id'))
vals = {
'name': menu.name,
'res_model': model.model,
'view_mode': 'list,form',
}
action_id = self.env['ir.actions.act_window'].create(vals)
self.env['ir.ui.menu'].create({
'name': menu.name,
'parent_id': menu.menu_id.id,
'action': 'ir.actions.act_window,%d' % (action_id,)
})
return {'type': 'ir.actions.act_window_close'}

View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- menu_create from model form -->
<record id="view_model_menu_create" model="ir.ui.view">
<field name="name">Create Menu</field>
<field name="model">wizard.ir.model.menu.create</field>
<field name="arch" type="xml">
<form string="Create Menu">
<group>
<field name="name"/>
<field name="menu_id"/>
</group>
<footer>
<button name="menu_create" string="Create Menu" type="object" class="btn-primary" data-hotkey="q"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="x" />
</footer>
</form>
</field>
</record>
<record id="act_menu_create" model="ir.actions.act_window">
<field name="name">Create Menu</field>
<field name="res_model">wizard.ir.model.menu.create</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="context">{'model_id': active_id}</field>
</record>
</odoo>