mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 18:52:02 +02:00
18.0 vanilla
This commit is contained in:
parent
d72e748793
commit
0a7ae8db93
337 changed files with 399651 additions and 232598 deletions
|
|
@ -3,10 +3,10 @@
|
|||
|
||||
import ast
|
||||
import base64
|
||||
import contextlib
|
||||
import io
|
||||
|
||||
from odoo import api, fields, models, tools, _
|
||||
from odoo.tools.translate import trans_export, trans_export_records
|
||||
|
||||
NEW_LANG_KEY = '__new__'
|
||||
|
||||
|
|
@ -39,13 +39,13 @@ class BaseLanguageExport(models.TransientModel):
|
|||
this = self[0]
|
||||
lang = this.lang if this.lang != NEW_LANG_KEY else False
|
||||
|
||||
with contextlib.closing(io.BytesIO()) as buf:
|
||||
with io.BytesIO() as buf:
|
||||
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)
|
||||
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)
|
||||
trans_export(lang, mods, buf, this.format, self._cr)
|
||||
out = base64.encodebytes(buf.getvalue())
|
||||
|
||||
filename = 'new'
|
||||
|
|
|
|||
|
|
@ -6,15 +6,14 @@
|
|||
<field name="model">base.language.export</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Export Translations">
|
||||
<field invisible="1" name="state"/>
|
||||
<field name="name" invisible="1"/>
|
||||
<field name="name" invisible="1"/> <!-- The name is needed in the BinaryField component used to download the file -->
|
||||
<group invisible="state != 'choose'" string="Export Settings">
|
||||
<field name="lang"/>
|
||||
<field name="format"/>
|
||||
<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="model_name" invisible="1"/> <!-- The model_name is needed for the option of the domain -->
|
||||
<field name="domain" widget="domain" options="{'model': 'model_name'}" invisible="export_type == 'module'"/>
|
||||
</group>
|
||||
<div invisible="state != 'get'">
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class BaseLanguageImport(models.TransientModel):
|
|||
_description = "Language Import"
|
||||
|
||||
name = fields.Char('Language Name', required=True)
|
||||
code = fields.Char('ISO Code', size=6, required=True,
|
||||
code = fields.Char('ISO Code', required=True,
|
||||
help="ISO Language and Country code, e.g. en_US")
|
||||
data = fields.Binary('File', required=True, attachment=False)
|
||||
filename = fields.Char('File Name', required=True)
|
||||
|
|
@ -43,9 +43,9 @@ 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 %r not imported due to format mismatch or a malformed file.'
|
||||
' (Valid formats are .csv, .po)\n\nTechnical Details:\n%s',
|
||||
base_lang_import.filename, tools.ustr(e))
|
||||
_('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),
|
||||
)
|
||||
translation_importer.save(overwrite=overwrite)
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
<field name="name" placeholder="e.g. English"/>
|
||||
<field name="code" string="Code" placeholder="e.g. en_US"/>
|
||||
<field name="data" filename="filename" options="{'accepted_file_extensions': '.csv,.po'}"/>
|
||||
<field name="filename" invisible="1"/>
|
||||
<field name="filename" invisible="1"/> <!-- The name is needed in the BinaryField component used to upload the file -->
|
||||
<field name="overwrite" groups="base.group_no_one"/>
|
||||
</group>
|
||||
<footer>
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
<field name="overwrite" groups="base.group_no_one"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="lang_install" string="Add" data-hotkey="q" type="object" class="btn-primary"/>
|
||||
<button name="lang_install" block-ui="1" string="Add" data-hotkey="q" type="object" class="btn-primary"/>
|
||||
<button special="cancel" data-hotkey="x" string="Cancel" class="btn-secondary"/>
|
||||
</footer>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -12,42 +12,37 @@
|
|||
Uninstalling modules can be risky, we recommend you to try it on a duplicate or test database first.
|
||||
</p>
|
||||
</div>
|
||||
<field name="module_id" invisible="1"/>
|
||||
<div class="d-flex bd-highlight">
|
||||
<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">
|
||||
<kanban create="false" class="o_modules_kanban">
|
||||
<field name="icon"/>
|
||||
<field name="state"/>
|
||||
<field name="summary"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_module_vignette">
|
||||
<t t-set="installed" t-value="record.state.raw_value == 'installed'" />
|
||||
<img t-attf-src="#{record.icon.value}" class="oe_module_icon" alt="Icon" />
|
||||
<div class="oe_module_desc" t-att-title="record.shortdesc.value">
|
||||
<h4 class="o_kanban_record_title">
|
||||
<field name="shortdesc" />&nbsp;
|
||||
</h4>
|
||||
<p class="oe_module_name">
|
||||
<field groups="!base.group_no_one" name="summary" />
|
||||
<code groups="base.group_no_one">
|
||||
<field name="name" /></code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<t t-name="card" class="flex-row align-items-center">
|
||||
<aside>
|
||||
<field name="icon" widget="image_url" options="{'size': [50, 50]}" alt="Icon"/>
|
||||
</aside>
|
||||
<main t-att-title="record.shortdesc.value">
|
||||
<field class="fw-bold fs-5 mb-0" name="shortdesc" />
|
||||
<p class="text-muted small my-1 lh-sm">
|
||||
<field groups="!base.group_no_one" name="summary" />
|
||||
<code groups="base.group_no_one">
|
||||
<field name="name" />
|
||||
</code>
|
||||
</p>
|
||||
</main>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
<h3>Documents to Delete</h3>
|
||||
<field name="model_ids" string="Models" nolabel="1">
|
||||
<tree string="Models">
|
||||
<list string="Models">
|
||||
<field name="name" string="Document"/>
|
||||
<field name="count"/>
|
||||
</tree>
|
||||
</list>
|
||||
</field>
|
||||
<footer>
|
||||
<button string="Uninstall" class="btn-secondary" type="object" name="action_uninstall" data-hotkey="q"/>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class BaseModuleUpdate(models.TransientModel):
|
|||
res = {
|
||||
'domain': str([]),
|
||||
'name': 'Modules',
|
||||
'view_mode': 'tree,form',
|
||||
'view_mode': 'list,form',
|
||||
'res_model': 'ir.module.module',
|
||||
'view_id': False,
|
||||
'type': 'ir.actions.act_window',
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
<field name="model">base.module.update</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Update Module List">
|
||||
<field name="state" invisible="1"/>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -3,16 +3,15 @@
|
|||
|
||||
from ast import literal_eval
|
||||
from collections import defaultdict
|
||||
import functools
|
||||
import itertools
|
||||
import logging
|
||||
import psycopg2
|
||||
import datetime
|
||||
|
||||
from odoo import api, fields, models, Command
|
||||
from odoo import SUPERUSER_ID, _
|
||||
from odoo import _
|
||||
from odoo.exceptions import ValidationError, UserError
|
||||
from odoo.tools import mute_logger
|
||||
from odoo.tools import mute_logger, SQL
|
||||
|
||||
_logger = logging.getLogger('odoo.addons.base.partner.merge')
|
||||
|
||||
|
|
@ -101,16 +100,15 @@ class MergePartnerAutomatic(models.TransientModel):
|
|||
return self._cr.fetchall()
|
||||
|
||||
@api.model
|
||||
def _update_foreign_keys(self, src_partners, dst_partner):
|
||||
""" Update all foreign key from the src_partner to dst_partner. All many2one fields will be updated.
|
||||
:param src_partners : merge source res.partner recordset (does not include destination one)
|
||||
:param dst_partner : record of destination res.partner
|
||||
def _update_foreign_keys_generic(self, model, src_records, dst_record):
|
||||
""" Update all foreign key from the src_records to dst_record for any model.
|
||||
:param model: model name as a string
|
||||
:param src_records: merge source recordset (does not include destination one)
|
||||
:param dst_record: record of destination
|
||||
"""
|
||||
_logger.debug('_update_foreign_keys for dst_partner: %s for src_partners: %s', dst_partner.id, str(src_partners.ids))
|
||||
_logger.debug('_update_foreign_keys_generic for dst_record: %s for src_records: %s', dst_record.id, str(src_records.ids))
|
||||
|
||||
# find the many2one relation to a partner
|
||||
Partner = self.env['res.partner']
|
||||
relations = self._get_fk_on('res_partner')
|
||||
relations = self._get_fk_on(self.env[model]._table)
|
||||
|
||||
# this guarantees cache consistency
|
||||
self.env.invalidate_all()
|
||||
|
|
@ -147,52 +145,55 @@ class MergePartnerAutomatic(models.TransientModel):
|
|||
"%(column)s" = %%s AND
|
||||
___tu.%(value)s = ___tw.%(value)s
|
||||
)""" % query_dic
|
||||
for partner in src_partners:
|
||||
self._cr.execute(query, (dst_partner.id, partner.id, dst_partner.id))
|
||||
for record in src_records:
|
||||
self._cr.execute(query, (dst_record.id, record.id, dst_record.id))
|
||||
else:
|
||||
try:
|
||||
with mute_logger('odoo.sql_db'), self._cr.savepoint():
|
||||
query = 'UPDATE "%(table)s" SET "%(column)s" = %%s WHERE "%(column)s" IN %%s' % query_dic
|
||||
self._cr.execute(query, (dst_partner.id, tuple(src_partners.ids),))
|
||||
self._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_partners.ids),))
|
||||
self._cr.execute(query, (tuple(src_records.ids),))
|
||||
|
||||
@api.model
|
||||
def _update_reference_fields(self, src_partners, dst_partner):
|
||||
""" Update all reference fields from the src_partner to dst_partner.
|
||||
:param src_partners : merge source res.partner recordset (does not include destination one)
|
||||
:param dst_partner : record of destination res.partner
|
||||
def _update_reference_fields_generic(self, referenced_model, src_records, dst_record, additional_update_records=None):
|
||||
""" Update all reference fields from the src_records to dst_record for any model.
|
||||
:param referenced_model: model name as a string
|
||||
:param src_records: merge source recordset (does not include destination one)
|
||||
:param dst_record: record of destination
|
||||
:param additional_update_records: list of tuples (model, field_model, field_id)
|
||||
"""
|
||||
_logger.debug('_update_reference_fields for dst_partner: %s for src_partners: %r', dst_partner.id, src_partners.ids)
|
||||
_logger.debug('_update_reference_fields_generic for dst_record: %s for src_records: %r', dst_record.id, src_records.ids)
|
||||
|
||||
def update_records(model, src, field_model='model', field_id='res_id'):
|
||||
Model = self.env[model] if model in self.env else None
|
||||
if Model is None:
|
||||
return
|
||||
records = Model.sudo().search([(field_model, '=', 'res.partner'), (field_id, '=', src.id)])
|
||||
records = Model.sudo().search([(field_model, '=', referenced_model), (field_id, '=', src.id)])
|
||||
try:
|
||||
with mute_logger('odoo.sql_db'), self._cr.savepoint():
|
||||
records.sudo().write({field_id: dst_partner.id})
|
||||
records.sudo().write({field_id: dst_record.id})
|
||||
records.env.flush_all()
|
||||
except psycopg2.Error:
|
||||
# updating fails, most likely due to a violated unique constraint
|
||||
# keeping record with nonexistent partner_id is useless, better delete it
|
||||
records.sudo().unlink()
|
||||
|
||||
update_records = functools.partial(update_records)
|
||||
for record in src_records:
|
||||
update_records('ir.attachment', src=record, field_model='res_model')
|
||||
update_records('mail.followers', src=record, field_model='res_model')
|
||||
update_records('mail.activity', src=record, field_model='res_model')
|
||||
update_records('mail.message', src=record)
|
||||
update_records('ir.model.data', src=record)
|
||||
|
||||
for partner in src_partners:
|
||||
update_records('calendar', src=partner, field_model='model_id.model')
|
||||
update_records('ir.attachment', src=partner, field_model='res_model')
|
||||
update_records('mail.followers', src=partner, field_model='res_model')
|
||||
update_records('mail.activity', src=partner, field_model='res_model')
|
||||
update_records('mail.message', src=partner)
|
||||
update_records('ir.model.data', src=partner)
|
||||
additional_update_records = additional_update_records or []
|
||||
for update_record in additional_update_records:
|
||||
update_records(update_record['model'], src=record, field_model=update_record['field_model'])
|
||||
|
||||
records = self.env['ir.model.fields'].sudo().search([('ttype', '=', 'reference')])
|
||||
records = self.env['ir.model.fields'].sudo().search([('ttype', '=', 'reference'), ('store', '=', True)])
|
||||
for record in records:
|
||||
try:
|
||||
Model = self.env[record.model]
|
||||
|
|
@ -204,35 +205,105 @@ class MergePartnerAutomatic(models.TransientModel):
|
|||
if Model._abstract or field.compute is not None:
|
||||
continue
|
||||
|
||||
for partner in src_partners:
|
||||
records_ref = Model.sudo().search([(record.name, '=', 'res.partner,%d' % partner.id)])
|
||||
for src_record in src_records:
|
||||
records_ref = Model.sudo().search([(record.name, '=', '%s,%d' % (referenced_model, src_record.id))])
|
||||
values = {
|
||||
record.name: 'res.partner,%d' % dst_partner.id,
|
||||
record.name: '%s,%d' % (referenced_model, dst_record.id),
|
||||
}
|
||||
records_ref.sudo().write(values)
|
||||
# company_dependent fields referring the merged records
|
||||
for field in self.env.registry.many2one_company_dependents[dst_record._name]:
|
||||
self.env.cr.execute(SQL(
|
||||
"""
|
||||
UPDATE %(table)s
|
||||
SET %(field)s = (
|
||||
SELECT jsonb_object_agg(key,
|
||||
CASE
|
||||
WHEN value::int IN %(src_record_ids)s
|
||||
THEN %(dest_record_id)s
|
||||
ELSE value::int
|
||||
END
|
||||
)
|
||||
FROM jsonb_each_text(%(field)s)
|
||||
)
|
||||
WHERE %(field)s IS NOT NULL
|
||||
""",
|
||||
table=SQL.identifier(self.env[field.model_name]._table),
|
||||
field=SQL.identifier(field.name),
|
||||
src_record_ids=tuple(src_records.ids),
|
||||
dest_record_id=dst_record.id,
|
||||
))
|
||||
|
||||
# merge the fallback values for company dependent many2one fields
|
||||
self.env.cr.execute(SQL(
|
||||
"""
|
||||
UPDATE ir_default
|
||||
SET json_value =
|
||||
CASE
|
||||
WHEN json_value::int IN %(src_record_ids)s
|
||||
THEN %(dest_record_id)s
|
||||
ELSE json_value
|
||||
END
|
||||
FROM ir_model_fields f
|
||||
WHERE f.id = ir_default.field_id
|
||||
AND f.company_dependent
|
||||
AND f.relation = %(model_name)s
|
||||
AND f.ttype = 'many2one'
|
||||
AND json_value ~ '^[0-9]+$';
|
||||
""",
|
||||
src_record_ids=tuple(src_records.ids),
|
||||
dest_record_id=str(dst_record.id),
|
||||
model_name=dst_record._name,
|
||||
))
|
||||
|
||||
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}')
|
||||
# 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),
|
||||
))
|
||||
|
||||
@api.model
|
||||
def _update_foreign_keys(self, src_partners, dst_partner):
|
||||
""" Update all foreign key from the src_partner to dst_partner. All many2one fields will be updated.
|
||||
:param src_partners : merge source res.partner recordset (does not include destination one)
|
||||
:param dst_partner : record of destination res.partner
|
||||
"""
|
||||
self._update_foreign_keys_generic('res.partner', src_partners, dst_partner)
|
||||
|
||||
@api.model
|
||||
def _update_reference_fields(self, src_partners, dst_partner):
|
||||
""" Update all reference fields from the src_partner to dst_partner.
|
||||
:param src_partners : merge source res.partner recordset (does not include destination one)
|
||||
:param dst_partner : record of destination res.partner
|
||||
"""
|
||||
additional_update_records = [{'model': 'calendar', 'field_model': 'model_id.model'}]
|
||||
self._update_reference_fields_generic('res.partner', src_partners, dst_partner, additional_update_records)
|
||||
|
||||
def _get_summable_fields(self):
|
||||
""" Returns the list of fields that should be summed when merging partners
|
||||
|
|
@ -292,6 +363,23 @@ class MergePartnerAutomatic(models.TransientModel):
|
|||
except ValidationError:
|
||||
_logger.info('Skip recursive partner hierarchies for parent_id %s of partner: %s', parent_id, dst_partner.id)
|
||||
|
||||
@api.model
|
||||
def _merge_bank_accounts(self, src_partners, dst_partner):
|
||||
""" Merge bank accounts of src_partners into dst_partner.
|
||||
:param src_partners: merge source res.partner recordset (does not include destination one)
|
||||
:param dst_partner: record of destination res.partner
|
||||
"""
|
||||
all_src_accounts = src_partners.bank_ids
|
||||
|
||||
for src_account in all_src_accounts:
|
||||
duplicate_account = dst_partner.bank_ids.filtered(lambda a: a.sanitized_acc_number == src_account.sanitized_acc_number)
|
||||
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()
|
||||
else:
|
||||
src_account.write({'partner_id': dst_partner.id})
|
||||
|
||||
def _merge(self, partner_ids, dst_partner=None, extra_checks=True):
|
||||
""" private implementation of merge partner
|
||||
:param partner_ids : ids of partner to merge
|
||||
|
|
@ -340,6 +428,9 @@ class MergePartnerAutomatic(models.TransientModel):
|
|||
'company_id': dst_partner.company_id.id
|
||||
})
|
||||
|
||||
# Merge bank accounts before merging partners
|
||||
self._merge_bank_accounts(src_partners, dst_partner)
|
||||
|
||||
# call sub methods to do the merge
|
||||
self._update_foreign_keys(src_partners, dst_partner)
|
||||
self._update_reference_fields(src_partners, dst_partner)
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@
|
|||
all these fields in common. (not one of the fields).
|
||||
</p>
|
||||
<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"
|
||||
|
|
@ -61,14 +60,14 @@
|
|||
context="{'partner_show_db_id': True}"/>
|
||||
</group>
|
||||
<field name="partner_ids" nolabel="1">
|
||||
<tree string="Partners">
|
||||
<list string="Partners">
|
||||
<field name="id" />
|
||||
<field name="display_name" />
|
||||
<field name="email" />
|
||||
<field name="is_company" />
|
||||
<field name="vat" />
|
||||
<field name="country_id" />
|
||||
</tree>
|
||||
</list>
|
||||
</field>
|
||||
</group>
|
||||
</sheet>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue