437 lines
17 KiB
Python
437 lines
17 KiB
Python
# The COPYRIGHT file at the top level of this repository contains the full
|
|
# copyright notices and license terms.
|
|
from lxml import etree
|
|
import json
|
|
|
|
from trytond.transaction import Transaction
|
|
from trytond.pool import Pool
|
|
from trytond.wizard import Wizard, StateView, StateTransition, Button
|
|
from trytond.model import ModelView, ModelSQL, fields, Unique
|
|
from trytond.pyson import Eval, PYSONEncoder
|
|
from trytond.i18n import gettext
|
|
from trytond.exceptions import UserError
|
|
|
|
|
|
__all__ = ['MassEdit', 'MassEditFields', 'MassEditWizardStart',
|
|
'MassEditingWizard']
|
|
|
|
PAGE_FIELDS = 10
|
|
|
|
|
|
class MassEdit(ModelSQL, ModelView):
|
|
'Mass Edit'
|
|
__name__ = 'mass.editing'
|
|
model = fields.Many2One('ir.model', 'Model', required=True)
|
|
model_fields = fields.Many2Many('mass.editing-ir.model.field',
|
|
'mass_edit', 'field', 'Fields',
|
|
domain=[
|
|
('model', '=', Eval('model', 0)),
|
|
],
|
|
depends=['model'])
|
|
keyword = fields.Many2One('ir.action.keyword', 'Keyword', readonly=True)
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(MassEdit, cls).__setup__()
|
|
t = cls.__table__()
|
|
cls._sql_constraints += [
|
|
('model_uniq', Unique(t, t.model),
|
|
'Mass Edit must be unique per model.')
|
|
]
|
|
cls._buttons.update({
|
|
'create_keyword': {
|
|
'invisible': Eval('keyword'),
|
|
},
|
|
'remove_keyword': {
|
|
'invisible': ~Eval('keyword'),
|
|
},
|
|
})
|
|
|
|
@classmethod
|
|
def validate(cls, massedits):
|
|
super(MassEdit, cls).validate(massedits)
|
|
for massedit in massedits:
|
|
Model = Pool().get(massedit.model.model)
|
|
if not issubclass(Model, ModelSQL):
|
|
raise UserError(gettext('massedit.not_modelsql',
|
|
model=massedit.rec_name))
|
|
|
|
def get_rec_name(self, name):
|
|
return '%s' % (self.model.rec_name)
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
def create_keyword(cls, massedits):
|
|
pool = Pool()
|
|
Action = pool.get('ir.action.wizard')
|
|
ModelData = pool.get('ir.model.data')
|
|
Keyword = pool.get('ir.action.keyword')
|
|
|
|
for massedit in massedits:
|
|
if massedit.keyword:
|
|
continue
|
|
action = Action(ModelData.get_id('mass_editing',
|
|
'wizard_mass_editing'))
|
|
keyword = Keyword()
|
|
keyword.keyword = 'form_action'
|
|
keyword.model = '%s,-1' % massedit.model.model
|
|
keyword.action = action.action
|
|
keyword.save()
|
|
massedit.keyword = keyword
|
|
massedit.save()
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
def remove_keyword(cls, massedits):
|
|
pool = Pool()
|
|
Keyword = pool.get('ir.action.keyword')
|
|
Keyword.delete([x.keyword for x in massedits if x.keyword])
|
|
|
|
@classmethod
|
|
def delete(cls, massedits):
|
|
cls.remove_keyword(massedits)
|
|
super(MassEdit, cls).delete(massedits)
|
|
|
|
|
|
class MassEditFields(ModelSQL):
|
|
'Mass Edit Fields'
|
|
__name__ = 'mass.editing-ir.model.field'
|
|
|
|
mass_edit = fields.Many2One('mass.editing', 'Mass', required=True,
|
|
ondelete='CASCADE')
|
|
field = fields.Many2One('ir.model.field', 'Field', required=True,
|
|
ondelete='CASCADE')
|
|
|
|
|
|
class MassEditWizardStart(ModelView):
|
|
'Mass Edit Wizard Start'
|
|
__name__ = 'mass.editing.wizard.start'
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(MassEditWizardStart, cls).__setup__()
|
|
|
|
@classmethod
|
|
def fields_view_get(cls, view_id=None, view_type='form'):
|
|
class Decoder(json.JSONDecoder):
|
|
|
|
def __init__(self, context=None):
|
|
self.__context = context or {}
|
|
super(Decoder, self).__init__(object_hook=self._object_hook)
|
|
|
|
def _object_hook(self, dct):
|
|
if '__class__' in dct:
|
|
return 'REMOVE_CLAUSE'
|
|
return dct
|
|
|
|
pool = Pool()
|
|
MassEdit = pool.get('mass.editing')
|
|
|
|
res = super(MassEditWizardStart, cls).fields_view_get(view_id,
|
|
view_type)
|
|
|
|
if view_type == 'tree':
|
|
return res
|
|
|
|
context = Transaction().context
|
|
model = context.get('active_model', None)
|
|
if not model:
|
|
return res
|
|
EditingModel = pool.get(model)
|
|
edits = MassEdit.search([('model.model', '=', model)], limit=1)
|
|
if not edits:
|
|
return res
|
|
edit, = edits
|
|
fields = res['fields']
|
|
root = etree.fromstring(res['arch'])
|
|
form = root.find('separator').getparent()
|
|
|
|
fields.update(EditingModel.fields_get([f.name for f in
|
|
edit.model_fields]))
|
|
|
|
# add notebook if many fields
|
|
pages = []
|
|
if len(edit.model_fields) > PAGE_FIELDS:
|
|
if root.find('.//notebook') is None:
|
|
field_string = MassEdit.fields_get(['model_fields']
|
|
)['model_fields']['string']
|
|
notebook = etree.SubElement(form, 'notebook', {})
|
|
for x in range(0, (len(edit.model_fields) // PAGE_FIELDS) + 1):
|
|
pages.append(etree.SubElement(notebook, 'page', {
|
|
'string': '%s (%s)' % (field_string, x + 1),
|
|
'id': 'page_%s' % x,
|
|
}))
|
|
else:
|
|
for x in range(0, (len(edit.model_fields) // PAGE_FIELDS) + 1):
|
|
pages.append(root.find('.//page[@id="page_%s"]' % x))
|
|
|
|
for x, field in enumerate(edit.model_fields):
|
|
if fields[field.name].get('states'):
|
|
fields[field.name]['states'] = {}
|
|
|
|
if fields[field.name].get('required'):
|
|
fields[field.name]['required'] = False
|
|
|
|
if fields[field.name].get('on_change'):
|
|
fields[field.name]['on_change'] = []
|
|
if fields[field.name].get('on_change_with'):
|
|
fields[field.name]['on_change_with'] = []
|
|
|
|
if fields[field.name].get('domain'):
|
|
old_domain = Decoder().decode(fields[field.name]['domain'])
|
|
new_domain = []
|
|
for clause in old_domain:
|
|
if 'REMOVE_CLAUSE' not in str(clause):
|
|
new_domain.append(clause)
|
|
fields[field.name]['domain'] = \
|
|
PYSONEncoder().encode(new_domain)
|
|
|
|
if field.ttype in ['many2many', 'one2many', 'dict']:
|
|
selection_vals = [
|
|
('', ''),
|
|
('set', 'Set'),
|
|
('remove_all', 'Remove All'),
|
|
]
|
|
_field = getattr(EditingModel, field.name, None)
|
|
if field.ttype in ('many2many', 'dict') or _field.add_remove:
|
|
selection_vals.append(('add', 'Add'),)
|
|
selection_vals.append(('remove', 'Remove'),)
|
|
else:
|
|
selection_vals = [
|
|
('', ''),
|
|
('set', 'Set'),
|
|
('remove', 'Remove')
|
|
]
|
|
translated_vals = []
|
|
for val in selection_vals:
|
|
if val[0]:
|
|
translated_vals.append((val[0], gettext('mass_editing.%s' %
|
|
val[0])))
|
|
else:
|
|
translated_vals.append((val[0], ''))
|
|
|
|
colspan = '1'
|
|
if field.ttype in ['many2many', 'one2many']:
|
|
colspan = '2'
|
|
|
|
fields['selection_%s' % field.name] = {
|
|
'name': 'selection_%s' % field.name,
|
|
'type': 'selection',
|
|
'string': fields[field.name]['string'],
|
|
'selection': translated_vals,
|
|
'help': '',
|
|
'readonly': fields[field.name]['readonly']
|
|
}
|
|
|
|
xml_parent = form if not pages else pages[x // PAGE_FIELDS]
|
|
xml_group = etree.SubElement(xml_parent, 'group', {
|
|
'col': '2',
|
|
'colspan': '4',
|
|
})
|
|
|
|
to_find = ".//label[@id='label_%s']" % field.name
|
|
if root.find(to_find) is None:
|
|
etree.SubElement(xml_group, 'label', {
|
|
'id': "label_%s" % field.name,
|
|
'string': fields[field.name]['string'],
|
|
'xalign': '0.0',
|
|
'colspan': '4',
|
|
})
|
|
to_find = ".//field[@name='selection_%s']" % field.name
|
|
if root.find(to_find) is None:
|
|
etree.SubElement(xml_group, 'field', {
|
|
'name': "selection_%s" % field.name,
|
|
'colspan': colspan,
|
|
})
|
|
to_find = ".//field[@name='%s']" % field.name
|
|
if root.find(to_find) is None:
|
|
etree.SubElement(xml_group, 'field', {
|
|
'name': field.name,
|
|
'colspan': colspan,
|
|
})
|
|
|
|
res['arch'] = etree.tostring(root).decode('utf-8')
|
|
res['fields'] = fields
|
|
return res
|
|
|
|
@classmethod
|
|
def default_get(cls, fields, with_rec_name=True):
|
|
pool = Pool()
|
|
context = Transaction().context
|
|
model = context.get('active_model', None)
|
|
EditingModel = pool.get(model)
|
|
res = dict.fromkeys([f for f in fields
|
|
if f[:10] == 'selection_'], '')
|
|
res.update(EditingModel.default_get([f for f in fields
|
|
if f[:10] != 'selection_'], with_rec_name))
|
|
return res
|
|
|
|
|
|
class CustomDict(dict):
|
|
|
|
def __getattr__(self, name):
|
|
return {}
|
|
|
|
def __setattr__(self, name, value):
|
|
self[name] = value
|
|
|
|
|
|
class MassEditingWizard(Wizard):
|
|
'Mass Edit Wizard'
|
|
__name__ = 'mass.editing.wizard'
|
|
start = StateView('mass.editing.wizard.start',
|
|
'mass_editing.view_mass_editing_wizard_start', [
|
|
Button('Cancel', 'end', 'tryton-cancel'),
|
|
Button('Apply', 'update', 'tryton-ok', True),
|
|
])
|
|
|
|
update = StateTransition()
|
|
|
|
def __getattribute__(self, name):
|
|
if name == 'start':
|
|
if not hasattr(self, 'start_data'):
|
|
self.start_data = CustomDict()
|
|
name = 'start_data'
|
|
return super(MassEditingWizard, self).__getattribute__(name)
|
|
|
|
def transition_update(self):
|
|
context = Transaction().context
|
|
model = context['active_model']
|
|
pool = Pool()
|
|
EditingModel = pool.get(model)
|
|
|
|
def convert_dict_changes(res):
|
|
instances = EditingModel.browse(Transaction().context.get(
|
|
'active_ids'))
|
|
|
|
changes = []
|
|
for instance in instances:
|
|
instance_changes = {}
|
|
for fieldname, changed_values in res.items():
|
|
_field = getattr(EditingModel, fieldname)
|
|
|
|
if not isinstance(_field, fields.Dict):
|
|
instance_changes[fieldname] = changed_values
|
|
continue
|
|
|
|
current_values = getattr(instance, fieldname)
|
|
|
|
for vals_dict in changed_values:
|
|
if 'remove_all' in vals_dict:
|
|
current_values = {
|
|
k: None for k in current_values.keys()}
|
|
elif 'remove' in vals_dict:
|
|
vals_dict, = vals_dict[1:]
|
|
for k in vals_dict.keys():
|
|
if current_values and k in current_values:
|
|
current_values[k] = None
|
|
elif 'add' in vals_dict:
|
|
vals_dict, = vals_dict[1:]
|
|
for k, v in vals_dict.items():
|
|
if k in current_values:
|
|
continue
|
|
else:
|
|
current_values[k] = vals_dict[k]
|
|
else:
|
|
if vals_dict in current_values:
|
|
current_values[vals_dict] = \
|
|
changed_values[vals_dict]
|
|
instance_changes[fieldname] = current_values
|
|
changes.extend([[instance], instance_changes])
|
|
|
|
# by now return changes record by record
|
|
return changes
|
|
|
|
res = {}
|
|
vals = self.start_data
|
|
with_dict = False
|
|
for field, value in vals.items():
|
|
if field.startswith('selection_'):
|
|
split_key = field.split('_', 1)[1]
|
|
_field = getattr(EditingModel, split_key, None)
|
|
xxx2many = False
|
|
one2many = False
|
|
if isinstance(_field, fields.Function) and _field.setter:
|
|
_field = _field._field # Use original field
|
|
if (isinstance(_field, fields.One2Many)
|
|
or isinstance(_field, fields.Many2Many)):
|
|
xxx2many = True
|
|
if isinstance(_field, fields.One2Many):
|
|
one2many = True
|
|
if value == 'set':
|
|
if xxx2many:
|
|
manyvals = vals.get(split_key, None)
|
|
to_set = []
|
|
to_create = []
|
|
for val in manyvals:
|
|
if isinstance(val, dict):
|
|
# check_xxx2many
|
|
for field_name, field_value in val.items():
|
|
if isinstance(field_value, list):
|
|
val[field_name] = [tuple(['add',
|
|
field_value])]
|
|
to_create.append(val)
|
|
else:
|
|
to_set.append(val)
|
|
to_write = []
|
|
if to_set:
|
|
xxx2m_ids = set()
|
|
records = EditingModel.search([
|
|
('id', 'in',
|
|
Transaction().context.get('active_ids')),
|
|
])
|
|
for record in records:
|
|
xxx2m_ids |= set([r.id for r in getattr(
|
|
record, _field.name)])
|
|
xxx2m_ids = list(
|
|
xxx2m_ids - set(to_set) - set(to_create))
|
|
to_write.append(('remove', xxx2m_ids))
|
|
to_write.append(('add', to_set))
|
|
if to_create:
|
|
to_write.append(('create', to_create),)
|
|
if to_write:
|
|
res.update({split_key: to_write})
|
|
elif isinstance(_field, fields.Dict):
|
|
with_dict = True
|
|
res.update({split_key: vals.get(split_key, None)})
|
|
else:
|
|
res.update({split_key: vals.get(split_key, None)})
|
|
elif value == 'remove':
|
|
if isinstance(_field, fields.Dict):
|
|
with_dict = True
|
|
res.update({split_key: [("remove",
|
|
vals.get(split_key, []))]})
|
|
elif xxx2many:
|
|
res.update({split_key: [
|
|
("remove", vals.get(split_key, []))
|
|
]})
|
|
else:
|
|
res.update({split_key: None})
|
|
elif value == 'remove_all':
|
|
if isinstance(_field, fields.Dict):
|
|
with_dict = True
|
|
res.update({split_key: [("remove_all",
|
|
vals.get(split_key, []))]})
|
|
else:
|
|
xxx2m_ids = set()
|
|
records = EditingModel.search([
|
|
('id', 'in', Transaction().context.get('active_ids'))])
|
|
for record in records:
|
|
xxx2m_ids |= set([r.id for r in getattr(
|
|
record, _field.name)])
|
|
res.update({split_key: [
|
|
('delete' if one2many else 'remove', list(xxx2m_ids))]})
|
|
elif value == 'add':
|
|
if isinstance(_field, fields.Dict):
|
|
with_dict = True
|
|
res.update({split_key: [('add', vals.get(split_key, []))]})
|
|
if res:
|
|
if with_dict:
|
|
changes = convert_dict_changes(res)
|
|
else:
|
|
instances = EditingModel.browse(Transaction().context.get(
|
|
'active_ids'))
|
|
changes = [instances, res]
|
|
EditingModel.write(*changes)
|
|
return 'end'
|