17.0 vanilla

This commit is contained in:
Ernad Husremovic 2025-10-03 18:05:14 +02:00
parent 2e65bf056a
commit df627a6bba
328 changed files with 578149 additions and 759311 deletions

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import ast
import base64
import contextlib
import io
@ -23,8 +24,13 @@ class BaseLanguageExport(models.TransientModel):
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')],
string='File Format', required=True, default='po')
export_type = fields.Selection([('module', 'Module'), ('model', 'Model')],
string='Export Type', required=True, default='module')
modules = fields.Many2many('ir.module.module', 'rel_modules_langexport', 'wiz_id', 'module_id',
string='Apps To Export', domain=[('state','=','installed')])
model_id = fields.Many2one('ir.model', string='Model to Export', domain=[('transient', '=', False)])
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
default='choose')
@ -32,15 +38,21 @@ class BaseLanguageExport(models.TransientModel):
def act_getfile(self):
this = self[0]
lang = this.lang if this.lang != NEW_LANG_KEY else False
mods = sorted(this.mapped('modules.name')) or ['all']
with contextlib.closing(io.BytesIO()) as buf:
tools.trans_export(lang, mods, buf, this.format, self._cr)
if this.export_type == 'model':
ids = self.env[this.model_name].search(ast.literal_eval(this.domain)).ids
tools.trans_export_records(lang, this.model_name, ids, buf, this.format, self._cr)
else:
mods = sorted(this.mapped('modules.name')) or ['all']
tools.trans_export(lang, mods, buf, this.format, self._cr)
out = 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 len(mods) == 1:
filename = mods[0]
extension = this.format

View file

@ -8,16 +8,20 @@
<form string="Export Translations">
<field invisible="1" name="state"/>
<field name="name" invisible="1"/>
<group states="choose" string="Export Settings">
<group invisible="state != 'choose'" string="Export Settings">
<field name="lang"/>
<field name="format"/>
<field name="modules" widget="many2many_tags" options="{'no_create': True}"/>
<field name="export_type"/>
<field name="modules" widget="many2many_tags" options="{'no_create': True}" invisible="export_type == 'model'"/>
<field name="model_id" options="{'no_create': True}" invisible="export_type == 'module'" required="export_type == 'model'"/>
<field name="model_name" invisible="1"/>
<field name="domain" widget="domain" options="{'model': 'model_name'}" invisible="export_type == 'module'"/>
</group>
<div states="get">
<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>
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,
@ -30,12 +34,12 @@
<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>
<footer states="choose">
<footer invisible="state != 'choose'">
<button name="act_getfile" string="Export" type="object" class="btn-primary" data-hotkey="q"/>
<button special="cancel" data-hotkey="z" string="Cancel" type="object" class="btn-secondary"/>
<button special="cancel" data-hotkey="x" string="Cancel" type="object" class="btn-secondary"/>
</footer>
<footer states="get">
<button special="cancel" data-hotkey="z" string="Close" type="object" class="btn-primary"/>
<footer invisible="state != 'get'">
<button special="cancel" data-hotkey="x" string="Close" type="object" class="btn-primary"/>
</footer>
</form>
</field>
@ -43,7 +47,6 @@
<record id="action_wizard_lang_export" model="ir.actions.act_window">
<field name="name">Export Translation</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">base.language.export</field>
<field name="view_mode">form</field>
<field name="target">new</field>

View file

@ -44,8 +44,8 @@ class BaseLanguageImport(models.TransientModel):
_logger.warning('Could not import the file due to a format mismatch or it being malformed.')
raise UserError(
_('File %r not imported due to format mismatch or a malformed file.'
' (Valid formats are .csv, .po, .pot)\n\nTechnical Details:\n%s') % \
(base_lang_import.filename, tools.ustr(e))
' (Valid formats are .csv, .po)\n\nTechnical Details:\n%s',
base_lang_import.filename, tools.ustr(e))
)
translation_importer.save(overwrite=overwrite)
return True

View file

@ -10,13 +10,13 @@
<group>
<field name="name" placeholder="e.g. English"/>
<field name="code" string="Code" placeholder="e.g. en_US"/>
<field name="data" filename="filename"/>
<field name="data" filename="filename" options="{'accepted_file_extensions': '.csv,.po'}"/>
<field name="filename" invisible="1"/>
<field name="overwrite" groups="base.group_no_one"/>
</group>
<footer>
<button name="import_lang" string="Import" type="object" class="btn-primary" data-hotkey="q"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z" />
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="x" />
</footer>
</form>
</field>
@ -24,7 +24,6 @@
<record id="action_view_base_import_language" model="ir.actions.act_window">
<field name="name">Import Translation</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">base.language.import</field>
<field name="view_mode">form</field>
<field name="target">new</field>

View file

@ -36,8 +36,8 @@
<field name="overwrite" groups="base.group_no_one"/>
</group>
<footer>
<button name="lang_install" string="Add" type="object" class="btn-primary"/>
<button special="cancel" data-hotkey="z" string="Cancel" class="btn-secondary"/>
<button name="lang_install" string="Add" data-hotkey="q" type="object" class="btn-primary"/>
<button special="cancel" data-hotkey="x" string="Cancel" class="btn-secondary"/>
</footer>
</form>
</field>
@ -45,7 +45,6 @@
<record id="action_view_base_language_install" model="ir.actions.act_window">
<field name="name">Add Languages</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">base.language.install</field>
<field name="view_mode">form</field>
<field name="target">new</field>

View file

@ -27,7 +27,11 @@ class BaseModuleUninstall(models.TransientModel):
def _compute_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 modules.filtered('application')
wizard.module_ids = modules if wizard.show_all else wizard._modules_to_display(modules)
@api.model
def _modules_to_display(self, modules):
return modules.filtered('application')
def _get_models(self):
""" Return the models (ir.model) to consider for the impact. """

View file

@ -51,7 +51,7 @@
</field>
<footer>
<button string="Uninstall" class="btn-secondary" type="object" name="action_uninstall" data-hotkey="q"/>
<button string="Discard" class="btn-primary" special="cancel" data-hotkey="z"/>
<button string="Discard" class="btn-primary" special="cancel" data-hotkey="x"/>
</footer>
</form>
</field>

View file

@ -8,22 +8,22 @@
<field name="arch" type="xml">
<form string="Update Module List">
<field name="state" invisible="1"/>
<separator string="Module Update Result" states="done"/>
<group states="init">
<separator string="Module Update Result" invisible="state != 'done'"/>
<group invisible="state != 'init'">
<span class="o_form_label" colspan="2">Click on Update below to start the process...</span>
</group>
<group states="done" >
<group invisible="state != 'done'" >
<field name="updated"/>
<field name="added" />
</group>
<footer>
<div states="init">
<button name="update_module" string="Update" type="object" class="btn-primary"/>
<button special="cancel" data-hotkey="z" string="Cancel" class="btn-secondary"/>
<div invisible="state != 'init'" class="d-flex gap-1">
<button name="update_module" string="Update" type="object" class="btn-primary" data-hotkey="q"/>
<button special="cancel" data-hotkey="x" string="Cancel" class="btn-secondary"/>
</div>
<div states="done">
<div invisible="state != 'done'" class="d-flex gap-1">
<button name="action_module_open" string="Open Apps" type="object" class="btn-primary" data-hotkey="q"/>
<button special="cancel" data-hotkey="z" string="Close" class="btn-secondary"/>
<button special="cancel" data-hotkey="x" string="Close" class="btn-secondary"/>
</div>
</footer>
</form>
@ -32,7 +32,6 @@
<record id="action_view_base_module_update" model="ir.actions.act_window">
<field name="name">Module Update</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">base.module.update</field>
<field name="view_mode">form</field>
<field name="target">new</field>

View file

@ -28,15 +28,12 @@ class BaseModuleUpgrade(models.TransientModel):
if view_type != 'form':
return res
if not(self._context.get('active_model') and self._context.get('active_id')):
return res
if not self.get_module_list():
res['arch'] = '''<form string="Upgrade Completed">
<separator string="Upgrade Completed" colspan="4"/>
<footer>
<button name="config" string="Start Configuration" type="object" class="btn-primary" data-hotkey="q"/>
<button special="cancel" data-hotkey="z" string="Close" class="btn-secondary"/>
<button special="cancel" data-hotkey="x" string="Close" class="btn-secondary"/>
</footer>
</form>'''
@ -60,13 +57,11 @@ class BaseModuleUpgrade(models.TransientModel):
FROM ir_module_module m
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 in %s and (m2.state IS NULL or m2.state IN %s) """
self._cr.execute(query, (tuple(mods.ids), ('uninstalled',)))
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()]
if unmet_packages:
raise UserError(_('The following modules are not installed or unknown: %s') % ('\n\n' + '\n'.join(unmet_packages)))
mods.download()
raise UserError(_('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()

View file

@ -14,7 +14,7 @@
<field name="module_info"/>
<footer>
<button name="upgrade_module" string="Confirm" type="object" class="btn-primary" data-hotkey="q"/>
<button string="Cancel" class="btn-secondary" name="upgrade_module_cancel" type="object" data-hotkey="z"/>
<button string="Cancel" class="btn-secondary" name="upgrade_module_cancel" type="object" data-hotkey="x"/>
</footer>
</form>
</field>
@ -22,7 +22,6 @@
<record id="action_view_base_module_upgrade" model="ir.actions.act_window">
<field name="name">Apply Schedule Upgrade</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">base.module.upgrade</field>
<field name="view_mode">form</field>
<field name="target">new</field>
@ -42,11 +41,11 @@
<field name="priority" eval="20"/>
<field name="arch" type="xml">
<form string="Apply Schedule Upgrade">
<div><span class="o_form_label">The selected modules have been updated / installed !</span></div>
<div><span class="o_form_label">The selected modules have been updated/installed!</span></div>
<div><span class="o_form_label">We suggest to reload the menu tab to see the new menus (Ctrl+T then Ctrl+R)."</span></div>
<footer>
<button name="config" string="Start configuration" type="object" class="btn-primary" data-hotkey="q"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="x"/>
</footer>
</form>
</field>
@ -54,7 +53,6 @@
<record id="action_view_base_module_upgrade_install" model="ir.actions.act_window">
<field name="name">Module Upgrade Install</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">base.module.upgrade</field>
<field name="view_mode">form</field>
<field name="view_id" ref="view_base_module_upgrade_install"/>

View file

@ -213,6 +213,27 @@ class MergePartnerAutomatic(models.TransientModel):
self.env.flush_all()
# Company-dependent fields
try:
with self._cr.savepoint():
params = {
'destination_id': f'res.partner,{dst_partner.id}',
'source_ids': tuple(f'res.partner,{src}' for src in src_partners.ids),
}
self._cr.execute("""
UPDATE ir_property AS _ip1
SET res_id = %(destination_id)s
WHERE res_id IN %(source_ids)s
AND NOT EXISTS (
SELECT
FROM ir_property AS _ip2
WHERE _ip2.res_id = %(destination_id)s
AND _ip2.fields_id = _ip1.fields_id
AND _ip2.company_id IS NOT DISTINCT FROM _ip1.company_id
)""", params)
except psycopg2.Error:
_logger.info(f'Could not move ir.property from partners: {src_partners.ids} to partner: {dst_partner.id}')
def _get_summable_fields(self):
""" Returns the list of fields that should be summed when merging partners
"""
@ -243,7 +264,9 @@ class MergePartnerAutomatic(models.TransientModel):
if field.type not in ('many2many', 'one2many') and field.compute is None:
for item in itertools.chain(src_partners, [dst_partner]):
if item[column]:
if column in summable_fields and values.get(column):
if field.type == 'reference':
values[column] = item[column]
elif column in summable_fields and values.get(column):
values[column] += write_serializer(item[column])
else:
values[column] = write_serializer(item[column])
@ -294,6 +317,10 @@ class MergePartnerAutomatic(models.TransientModel):
if partner_ids & child_ids:
raise UserError(_("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."))
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."))

View file

@ -14,22 +14,22 @@
<field name='arch' type='xml'>
<form string='Automatic Merge Wizard' class="o_partner_merge_wizard">
<sheet>
<group attrs="{'invisible': [('state', '!=', 'finished')]}">
<group invisible="state != 'finished'">
<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" attrs="{'invisible': [('state', '!=', ('option'))]}">
<p class="oe_grey" 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
all these fields in common. (not one of the fields).
</p>
<group attrs="{'invisible': ['|', ('state', 'not in', ('selection', 'finished')), ('number_group', '=', 0)]}">
<group invisible="state not in ('selection', 'finished') or number_group == 0">
<field name="state" invisible="1" />
<field name="number_group"/>
</group>
<group string="Search duplicates based on duplicated data in"
attrs="{'invisible': [('state', 'not in', ('option',))]}">
invisible="state not in ('option',)">
<field name='group_by_email' />
<field name='group_by_name' />
<field name='group_by_is_company' />
@ -37,17 +37,17 @@
<field name='group_by_parent_id' />
</group>
<group string="Exclude contacts having"
attrs="{'invisible': [('state', 'not in', ('option',))]}">
invisible="state not in ('option',)">
<field name='exclude_contact' />
<field name='exclude_journal_item' />
</group>
<separator string="Options" attrs="{'invisible': [('state', 'not in', ('option',))]}"/>
<group attrs="{'invisible': [('state', 'not in', ('option','finished'))]}">
<field name='maximum_group' attrs="{'readonly': [('state', 'in', ('finished'))]}"/>
<separator string="Options" invisible="state not in ('option',)"/>
<group invisible="state not in ('option', 'finished')">
<field name='maximum_group' readonly="state == 'finished'"/>
</group>
<separator string="Merge the following contacts"
attrs="{'invisible': [('state', 'in', ('option', 'finished'))]}"/>
<group attrs="{'invisible': [('state', 'in', ('option', 'finished'))]}" col="1">
invisible="state in ('option', 'finished')"/>
<group invisible="state in ('option', 'finished')" col="1">
<p class="oe_grey">
Selected contacts will be merged together.
All documents linked to one of these contacts
@ -57,9 +57,8 @@
<group col="2">
<field name="dst_partner_id"
domain="[('id', 'in', partner_ids or False)]"
attrs="{'required': [('state', '=', 'selection')]}"
context="{'partner_show_db_id': True}"
options="{'always_reload': True}"/>
required="state == 'selection'"
context="{'partner_show_db_id': True}"/>
</group>
<field name="partner_ids" nolabel="1">
<tree string="Partners">
@ -77,26 +76,26 @@
<button name='action_merge' string='Merge Contacts'
class='oe_highlight'
type='object' data-hotkey="q"
attrs="{'invisible': [('state', 'in', ('option', 'finished' ))]}" />
invisible="state in ('option', 'finished')" />
<button name='action_skip' string='Skip these contacts'
type='object'
attrs="{'invisible': [('state', '!=', 'selection')]}" />
invisible="state != 'selection'" />
<button name='action_start_manual_process'
string='Merge with Manual Check' data-hotkey="x"
string='Merge with Manual Check' data-hotkey="w"
type='object' class='oe_highlight'
attrs="{'invisible': [('state', '!=', 'option')]}" />
invisible="state != 'option'" />
<button name='action_start_automatic_process'
string='Merge Automatically' data-hotkey="l"
type='object' class='oe_highlight'
confirm="Are you sure to execute the automatic merge of your contacts ?"
attrs="{'invisible': [('state', '!=', 'option')]}" />
confirm="Are you sure to execute the automatic merge of your contacts?"
invisible="state != 'option'" />
<button name='action_update_all_process'
string='Merge Automatically all process'
type='object' data-hotkey="y"
confirm="Are you sure to execute the list of automatic merges of your contacts ?"
attrs="{'invisible': [('state', '!=', 'option')]}" />
<button special="cancel" data-hotkey="z" string="Cancel" type="object" class="btn btn-secondary oe_inline" attrs="{'invisible': [('state', '=', 'finished')]}"/>
<button special="cancel" data-hotkey="z" string="Close" type="object" class="btn btn-secondary oe_inline" attrs="{'invisible': [('state', '!=', 'finished')]}"/>
confirm="Are you sure to execute the list of automatic merges of your contacts?"
invisible="state != 'option'" />
<button special="cancel" data-hotkey="x" string="Cancel" type="object" class="btn btn-secondary oe_inline" invisible="state == 'finished'"/>
<button special="cancel" data-hotkey="x" string="Close" type="object" class="btn btn-secondary oe_inline" invisible="state != 'finished'"/>
</footer>
</form>
</field>