mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-21 05:12:02 +02:00
19.0 vanilla
This commit is contained in:
parent
0a7ae8db93
commit
991d2234ca
416 changed files with 646602 additions and 300844 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'">
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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'},
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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'}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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'}
|
||||
|
|
@ -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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue