1130 lines
40 KiB
Python
Executable File
1130 lines
40 KiB
Python
Executable File
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
|
# this repository contains the full copyright notices and license terms.
|
|
import os
|
|
from collections import defaultdict
|
|
from functools import partial
|
|
from operator import itemgetter
|
|
|
|
from genshi.template.text import TextTemplate
|
|
|
|
from trytond.cache import Cache, MemoryCache
|
|
from trytond.config import config
|
|
from trytond.i18n import gettext
|
|
from trytond.model import (
|
|
DeactivableMixin, Index, ModelSingleton, ModelSQL, ModelStorage, ModelView,
|
|
fields, sequence_ordered)
|
|
from trytond.model.exceptions import ValidationError
|
|
from trytond.pool import Pool
|
|
from trytond.pyson import PYSON, Eval, PYSONDecoder, PYSONEncoder
|
|
from trytond.rpc import RPC
|
|
from trytond.tools import file_open
|
|
from trytond.transaction import (
|
|
Transaction, inactive_records, without_check_access)
|
|
|
|
if not config.get('html', 'plugins-ir.action.report-report_content_html'):
|
|
config.set(
|
|
'html', 'plugins-ir.action.report-report_content_html', 'fullpage')
|
|
|
|
|
|
class WizardModelError(ValidationError):
|
|
pass
|
|
|
|
|
|
class EmailError(ValidationError):
|
|
pass
|
|
|
|
|
|
class ViewError(ValidationError):
|
|
pass
|
|
|
|
|
|
class DomainError(ValidationError):
|
|
pass
|
|
|
|
|
|
class ContextError(ValidationError):
|
|
pass
|
|
|
|
|
|
EMAIL_REFKEYS = set(('cc', 'to', 'subject'))
|
|
ACTION_SELECTION = [
|
|
('ir.action.report', "Report"),
|
|
('ir.action.act_window', "Window"),
|
|
('ir.action.wizard', "Wizard"),
|
|
('ir.action.url', "URL"),
|
|
]
|
|
|
|
|
|
class Action(DeactivableMixin, ModelSQL, ModelView):
|
|
"Action"
|
|
__name__ = 'ir.action'
|
|
name = fields.Char('Name', required=True, translate=True)
|
|
type = fields.Selection(
|
|
ACTION_SELECTION, "Type", required=True, readonly=True)
|
|
action = fields.Function(
|
|
fields.Reference("Action", selection=ACTION_SELECTION),
|
|
'get_action')
|
|
records = fields.Selection([
|
|
('selected', "Selected"),
|
|
('listed', "Listed"),
|
|
], "Records",
|
|
help="The records on which the action runs.")
|
|
usage = fields.Char('Usage')
|
|
keywords = fields.One2Many('ir.action.keyword', 'action',
|
|
'Keywords')
|
|
icon = fields.Many2One('ir.ui.icon', 'Icon')
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(Action, cls).__setup__()
|
|
cls.__rpc__.update({
|
|
'get_action_value': RPC(instantiate=0, cache=dict(days=1)),
|
|
})
|
|
|
|
def get_action(self, name):
|
|
return f'{self.type},{self.id}'
|
|
|
|
@classmethod
|
|
def default_records(cls):
|
|
return 'selected'
|
|
|
|
@staticmethod
|
|
def default_usage():
|
|
return None
|
|
|
|
@classmethod
|
|
def write(cls, actions, values, *args):
|
|
pool = Pool()
|
|
super(Action, cls).write(actions, values, *args)
|
|
pool.get('ir.action.keyword')._get_keyword_cache.clear()
|
|
|
|
@classmethod
|
|
@inactive_records
|
|
@without_check_access
|
|
def get_action_id(cls, action_id):
|
|
pool = Pool()
|
|
if cls.search([
|
|
('id', '=', action_id),
|
|
]):
|
|
return action_id
|
|
for action_type in (
|
|
'ir.action.report',
|
|
'ir.action.act_window',
|
|
'ir.action.wizard',
|
|
'ir.action.url',
|
|
):
|
|
Action = pool.get(action_type)
|
|
actions = Action.search([
|
|
('id', '=', action_id),
|
|
])
|
|
if actions:
|
|
action, = actions
|
|
return action.action.id
|
|
|
|
@classmethod
|
|
def get_action_values(cls, type_, action_ids, columns=None):
|
|
pool = Pool()
|
|
Action = pool.get(type_)
|
|
if columns is None:
|
|
columns = []
|
|
columns += ['id', 'name', 'type', 'records', 'icon.rec_name']
|
|
if type_ == 'ir.action.report':
|
|
columns += ['report_name', 'direct_print']
|
|
elif type_ == 'ir.action.act_window':
|
|
columns += [
|
|
'views', 'domains', 'res_model', 'limit',
|
|
'context_model', 'context_domain',
|
|
'pyson_domain', 'pyson_context', 'pyson_order',
|
|
'pyson_search_value']
|
|
elif type_ == 'ir.action.wizard':
|
|
columns += ['wiz_name', 'window']
|
|
elif type_ == 'ir.action.url':
|
|
columns += ['url']
|
|
actions = Action.read(action_ids, columns)
|
|
if type_ == 'ir.action.act_window':
|
|
for values in actions:
|
|
if (values['res_model']
|
|
and issubclass(
|
|
pool.get(values['res_model']), ModelSingleton)):
|
|
values['res_id'] = 1
|
|
return actions
|
|
|
|
def get_action_value(self):
|
|
action_id = self.get_action_id(self.id)
|
|
if action_id is not None:
|
|
return self.get_action_values(self.type, [action_id])[0]
|
|
|
|
|
|
class ActionKeyword(ModelSQL, ModelView):
|
|
"Action keyword"
|
|
__name__ = 'ir.action.keyword'
|
|
keyword = fields.Selection([
|
|
('tree_open', 'Open tree'),
|
|
('form_print', 'Print form'),
|
|
('form_action', 'Action form'),
|
|
('form_relate', 'Form relate'),
|
|
('graph_open', 'Open Graph'),
|
|
], string='Keyword', required=True)
|
|
model = fields.Reference('Model', selection='models_get')
|
|
action = fields.Many2One('ir.action', 'Action',
|
|
ondelete='CASCADE')
|
|
_get_keyword_cache = Cache(
|
|
'ir_action_keyword.get_keyword', context=False)
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(ActionKeyword, cls).__setup__()
|
|
cls.__access__.add('action')
|
|
table = cls.__table__()
|
|
|
|
cls.__rpc__.update({
|
|
'get_keyword': RPC(cache=dict(days=1)),
|
|
})
|
|
cls._sql_indexes.add(
|
|
Index(
|
|
table,
|
|
(table.keyword, Index.Equality()),
|
|
(table.model, Index.Equality())))
|
|
|
|
@classmethod
|
|
def validate_fields(cls, actions, field_names):
|
|
super().validate_fields(actions, field_names)
|
|
cls.check_wizard_model(actions, field_names)
|
|
|
|
@classmethod
|
|
def check_wizard_model(cls, actions, field_names=None):
|
|
pool = Pool()
|
|
ActionWizard = pool.get('ir.action.wizard')
|
|
if field_names and not (field_names & {'action', 'model'}):
|
|
return
|
|
for action in actions:
|
|
if action.action.type == 'ir.action.wizard':
|
|
action_wizards = ActionWizard.search([
|
|
('action', '=', action.action.id),
|
|
], limit=1)
|
|
# could be empty when copying an action
|
|
if action_wizards:
|
|
action_wizard, = action_wizards
|
|
if action_wizard.model:
|
|
if not str(action.model).startswith(
|
|
'%s,' % action_wizard.model):
|
|
raise WizardModelError(
|
|
gettext('ir.msg_action_wrong_wizard_model',
|
|
name=action_wizard.rec_name))
|
|
|
|
@staticmethod
|
|
def _convert_vals(vals):
|
|
vals = vals.copy()
|
|
pool = Pool()
|
|
Action = pool.get('ir.action')
|
|
if 'action' in vals:
|
|
vals['action'] = Action.get_action_id(vals['action'])
|
|
return vals
|
|
|
|
@staticmethod
|
|
def models_get():
|
|
pool = Pool()
|
|
Model = pool.get('ir.model')
|
|
return [(None, '')] + Model.get_name_items()
|
|
|
|
@classmethod
|
|
def delete(cls, keywords):
|
|
ModelView._view_toolbar_get_cache.clear()
|
|
cls._get_keyword_cache.clear()
|
|
super(ActionKeyword, cls).delete(keywords)
|
|
|
|
@classmethod
|
|
def create(cls, vlist):
|
|
ModelView._view_toolbar_get_cache.clear()
|
|
cls._get_keyword_cache.clear()
|
|
new_vlist = []
|
|
for vals in vlist:
|
|
new_vlist.append(cls._convert_vals(vals))
|
|
return super(ActionKeyword, cls).create(new_vlist)
|
|
|
|
@classmethod
|
|
def write(cls, keywords, values, *args):
|
|
actions = iter((keywords, values) + args)
|
|
args = []
|
|
for keywords, values in zip(actions, actions):
|
|
args.extend((keywords, cls._convert_vals(values)))
|
|
super(ActionKeyword, cls).write(*args)
|
|
ModelView._view_toolbar_get_cache.clear()
|
|
cls._get_keyword_cache.clear()
|
|
|
|
@classmethod
|
|
def get_keyword(cls, keyword, value):
|
|
pool = Pool()
|
|
Action = pool.get('ir.action')
|
|
Menu = pool.get('ir.ui.menu')
|
|
ModelAccess = pool.get('ir.model.access')
|
|
User = pool.get('res.user')
|
|
groups = User.get_groups()
|
|
key = (Transaction().language, groups, keyword, tuple(value))
|
|
keywords = cls._get_keyword_cache.get(key)
|
|
if keywords is not None:
|
|
return keywords
|
|
keywords = []
|
|
model, record_id = value
|
|
|
|
clause = [
|
|
('keyword', '=', keyword),
|
|
['OR',
|
|
('model', '=', model + ',-1'),
|
|
('model', '=', None),
|
|
],
|
|
]
|
|
if record_id is not None and record_id >= 0:
|
|
clause = ['OR',
|
|
clause,
|
|
[
|
|
('keyword', '=', keyword),
|
|
('model', '=', model + ',' + str(record_id)),
|
|
],
|
|
]
|
|
clause = [clause, ('action.active', '=', True)]
|
|
action_keywords = cls.search(clause, order=[])
|
|
types = defaultdict(list)
|
|
for action_keyword in action_keywords:
|
|
type_ = action_keyword.action.type
|
|
types[type_].append(action_keyword.action.id)
|
|
for type_, action_ids in types.items():
|
|
for value in Action.get_action_values(type_, action_ids):
|
|
if (type_ == 'ir.action.act_window'
|
|
and value['res_model']
|
|
and not ModelAccess.check(
|
|
value['res_model'], raise_exception=False)):
|
|
continue
|
|
value['keyword'] = keyword
|
|
keywords.append(value)
|
|
if (record_id is not None
|
|
and keyword == 'tree_open' and model == Menu.__name__):
|
|
menu = Menu(record_id)
|
|
for value in keywords:
|
|
if value['type'] == 'ir.action.act_window':
|
|
if len(keywords) == 1:
|
|
value['name'] = menu.name
|
|
if menu.parent:
|
|
parent = menu.parent
|
|
if parent.name == value['name']:
|
|
parent = parent.parent
|
|
if parent:
|
|
value['name'] = (
|
|
parent.rec_name + ' / ' + value['name'])
|
|
keywords.sort(key=itemgetter('name'))
|
|
cls._get_keyword_cache.set(key, keywords)
|
|
return keywords
|
|
|
|
|
|
class ActionMixin(ModelSQL):
|
|
_order_name = 'action'
|
|
_action_name = 'name'
|
|
|
|
action = fields.Many2One(
|
|
'ir.action', "Action",
|
|
required=True, readonly=True, ondelete='CASCADE')
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
pool = Pool()
|
|
super(ActionMixin, cls).__setup__()
|
|
cls.__access__.add('action')
|
|
cls.action.domain = [
|
|
('type', '=', cls.__name__),
|
|
]
|
|
Action = pool.get('ir.action')
|
|
for name in dir(Action):
|
|
field = getattr(Action, name)
|
|
if (isinstance(field, fields.Field)
|
|
and not getattr(cls, name, None)):
|
|
setattr(cls, name, fields.Function(field, 'get_action',
|
|
setter='set_action', searcher='search_action'))
|
|
default_func = 'default_' + name
|
|
if getattr(Action, default_func, None):
|
|
setattr(cls, default_func,
|
|
partial(ActionMixin._default_action, name))
|
|
|
|
@staticmethod
|
|
def _default_action(name):
|
|
pool = Pool()
|
|
Action = pool.get('ir.action')
|
|
return getattr(Action, 'default_' + name, None)()
|
|
|
|
@classmethod
|
|
def get_action(cls, ids, names):
|
|
def identical(v):
|
|
return v
|
|
|
|
def list_int(v):
|
|
return list(map(int, v))
|
|
records = cls.browse(ids)
|
|
result = {}
|
|
for name in names:
|
|
result[name] = values = {}
|
|
for record in records:
|
|
value = getattr(record, 'action')
|
|
convert = identical
|
|
if value is not None:
|
|
value = getattr(value, name)
|
|
if isinstance(value, ModelStorage):
|
|
if cls._fields[name]._type == 'reference':
|
|
convert = str
|
|
else:
|
|
convert = int
|
|
elif isinstance(value, (list, tuple)):
|
|
convert = list_int
|
|
values[record.id] = convert(value)
|
|
return result
|
|
|
|
@classmethod
|
|
def set_action(cls, records, name, value):
|
|
pool = Pool()
|
|
Action = pool.get('ir.action')
|
|
Action.write([r.action for r in records], {
|
|
name: value,
|
|
})
|
|
|
|
@classmethod
|
|
def search_action(cls, name, clause):
|
|
return [('action.' + clause[0],) + tuple(clause[1:])]
|
|
|
|
@classmethod
|
|
def create(cls, vlist):
|
|
pool = Pool()
|
|
ModelView._view_toolbar_get_cache.clear()
|
|
Action = pool.get('ir.action')
|
|
ir_action = cls.__table__()
|
|
new_records = []
|
|
to_write = []
|
|
for values in vlist:
|
|
later = {}
|
|
action_values = {}
|
|
values = values.copy()
|
|
for field in values:
|
|
if field in Action._fields:
|
|
action_values[field] = values[field]
|
|
if hasattr(getattr(cls, field), 'set'):
|
|
later[field] = values[field]
|
|
for field in later:
|
|
del values[field]
|
|
action_values['type'] = cls.default_type()
|
|
transaction = Transaction()
|
|
database = transaction.database
|
|
cursor = transaction.connection.cursor()
|
|
if database.nextid(transaction.connection, cls._table):
|
|
database.setnextid(transaction.connection, cls._table,
|
|
database.currid(transaction.connection, Action._table))
|
|
if 'action' not in values:
|
|
action, = Action.create([action_values])
|
|
values['action'] = action.id
|
|
else:
|
|
action = Action(values['action'])
|
|
record, = super(ActionMixin, cls).create([values])
|
|
cursor.execute(*ir_action.update(
|
|
[ir_action.id], [action.id],
|
|
where=ir_action.id == record.id))
|
|
record = cls(action.id)
|
|
new_records.append(record)
|
|
to_write.extend(([record], later))
|
|
if to_write:
|
|
cls.write(*to_write)
|
|
return new_records
|
|
|
|
@classmethod
|
|
def write(cls, records, values, *args):
|
|
pool = Pool()
|
|
ActionKeyword = pool.get('ir.action.keyword')
|
|
super(ActionMixin, cls).write(records, values, *args)
|
|
ModelView._view_toolbar_get_cache.clear()
|
|
ActionKeyword._get_keyword_cache.clear()
|
|
|
|
@classmethod
|
|
def delete(cls, records):
|
|
pool = Pool()
|
|
ModelView._view_toolbar_get_cache.clear()
|
|
Action = pool.get('ir.action')
|
|
actions = [x.action for x in records]
|
|
super(ActionMixin, cls).delete(records)
|
|
Action.delete(actions)
|
|
|
|
@classmethod
|
|
def copy(cls, records, default=None):
|
|
pool = Pool()
|
|
Action = pool.get('ir.action')
|
|
if default is None:
|
|
default = {}
|
|
default = default.copy()
|
|
new_records = []
|
|
for record in records:
|
|
default['action'] = Action.copy([record.action])[0].id
|
|
new_records.extend(super(ActionMixin, cls).copy([record],
|
|
default=default))
|
|
return new_records
|
|
|
|
@classmethod
|
|
def fetch_action(cls, action_id):
|
|
fields = list(cls._fields.keys())
|
|
return cls.search_read(
|
|
[('action', '=', action_id)], fields_names=fields, limit=1)
|
|
|
|
|
|
class ActionReport(
|
|
fields.fmany2one(
|
|
'model_ref', 'model', 'ir.model,model', "Model",
|
|
ondelete='CASCADE'),
|
|
fields.fmany2one(
|
|
'module_ref', 'module', 'ir.module,name', "Module",
|
|
readonly=True, ondelete='CASCADE'),
|
|
ActionMixin, ModelSQL, ModelView):
|
|
"Action report"
|
|
__name__ = 'ir.action.report'
|
|
_action_name = 'report_name'
|
|
model = fields.Char('Model')
|
|
report_name = fields.Char('Internal Name', required=True)
|
|
report = fields.Char(
|
|
"Path",
|
|
states={
|
|
'invisible': Eval('is_custom', False),
|
|
},
|
|
depends=['is_custom'])
|
|
report_content_custom = fields.Binary('Content')
|
|
is_custom = fields.Function(fields.Boolean("Is Custom"), 'get_is_custom')
|
|
report_content = fields.Function(fields.Binary('Content',
|
|
filename='report_content_name'),
|
|
'get_report_content', setter='set_report_content')
|
|
report_content_name = fields.Function(fields.Char('Content Name'),
|
|
'on_change_with_report_content_name')
|
|
report_content_html = fields.Function(fields.Binary(
|
|
"Content HTML",
|
|
states={
|
|
'invisible': ~Eval('template_extension').in_(
|
|
['html', 'xhtml']),
|
|
},
|
|
depends=['template_extension']),
|
|
'get_report_content_html', setter='set_report_content_html')
|
|
direct_print = fields.Boolean('Direct Print')
|
|
single = fields.Boolean("Single",
|
|
help="Check if the template works only for one record.")
|
|
translatable = fields.Boolean("Translatable",
|
|
help="Uncheck to disable translations for this report.")
|
|
template_extension = fields.Selection([
|
|
('odt', 'OpenDocument Text'),
|
|
('odp', 'OpenDocument Presentation'),
|
|
('ods', 'OpenDocument Spreadsheet'),
|
|
('odg', 'OpenDocument Graphics'),
|
|
('txt', 'Plain Text'),
|
|
('xml', 'XML'),
|
|
('html', 'HTML'),
|
|
('xhtml', 'XHTML'),
|
|
], string='Template Extension', required=True,
|
|
translate=False)
|
|
extension = fields.Selection([
|
|
('', ''),
|
|
('bib', 'BibTex'),
|
|
('bmp', 'Windows Bitmap'),
|
|
('csv', 'Text CSV'),
|
|
('dbf', 'dBase'),
|
|
('dif', 'Data Interchange Format'),
|
|
('doc', 'Microsoft Word 97/2000/XP'),
|
|
('doc6', 'Microsoft Word 6.0'),
|
|
('doc95', 'Microsoft Word 95'),
|
|
('docbook', 'DocBook'),
|
|
('docx', 'Microsoft Office Open XML Text'),
|
|
('docx7', 'Microsoft Word 2007 XML'),
|
|
('emf', 'Enhanced Metafile'),
|
|
('eps', 'Encapsulated PostScript'),
|
|
('gif', 'Graphics Interchange Format'),
|
|
('html', 'HTML Document'),
|
|
('jpg', 'Joint Photographic Experts Group'),
|
|
('met', 'OS/2 Metafile'),
|
|
('ooxml', 'Microsoft Office Open XML'),
|
|
('pbm', 'Portable Bitmap'),
|
|
('pct', 'Mac Pict'),
|
|
('pdb', 'AportisDoc (Palm)'),
|
|
('pdf', 'Portable Document Format'),
|
|
('pgm', 'Portable Graymap'),
|
|
('png', 'Portable Network Graphic'),
|
|
('ppm', 'Portable Pixelmap'),
|
|
('ppt', 'Microsoft PowerPoint 97/2000/XP'),
|
|
('psw', 'Pocket Word'),
|
|
('pwp', 'PlaceWare'),
|
|
('pxl', 'Pocket Excel'),
|
|
('ras', 'Sun Raster Image'),
|
|
('rtf', 'Rich Text Format'),
|
|
('latex', 'LaTeX 2e'),
|
|
('sda', 'StarDraw 5.0 (OpenOffice.org Impress)'),
|
|
('sdc', 'StarCalc 5.0'),
|
|
('sdc4', 'StarCalc 4.0'),
|
|
('sdc3', 'StarCalc 3.0'),
|
|
('sdd', 'StarImpress 5.0'),
|
|
('sdd3', 'StarDraw 3.0 (OpenOffice.org Impress)'),
|
|
('sdd4', 'StarImpress 4.0'),
|
|
('sdw', 'StarWriter 5.0'),
|
|
('sdw4', 'StarWriter 4.0'),
|
|
('sdw3', 'StarWriter 3.0'),
|
|
('slk', 'SYLK'),
|
|
('svg', 'Scalable Vector Graphics'),
|
|
('svm', 'StarView Metafile'),
|
|
('swf', 'Macromedia Flash (SWF)'),
|
|
('sxc', 'OpenOffice.org 1.0 Spreadsheet'),
|
|
('sxi', 'OpenOffice.org 1.0 Presentation'),
|
|
('sxd', 'OpenOffice.org 1.0 Drawing'),
|
|
('sxd3', 'StarDraw 3.0'),
|
|
('sxd5', 'StarDraw 5.0'),
|
|
('sxw', 'Open Office.org 1.0 Text Document'),
|
|
('text', 'Text Encoded'),
|
|
('tiff', 'Tagged Image File Format'),
|
|
('txt', 'Plain Text'),
|
|
('wmf', 'Windows Metafile'),
|
|
('xhtml', 'XHTML Document'),
|
|
('xls', 'Microsoft Excel 97/2000/XP'),
|
|
('xls5', 'Microsoft Excel 5.0'),
|
|
('xls95', 'Microsoft Excel 95'),
|
|
('xlsx', 'Microsoft Excel 2007/2010 XML'),
|
|
('xpm', 'X PixMap'),
|
|
], translate=False,
|
|
string='Extension', help='Leave empty for the same as template, '
|
|
'see LibreOffice documentation for compatible format.')
|
|
record_name = fields.Char(
|
|
"Record Name", translate=True,
|
|
help="A Genshi expression to compute the name using 'record'.\n"
|
|
"Leave empty for the default name.",)
|
|
module = fields.Char('Module', readonly=True)
|
|
_template_cache = MemoryCache('ir.action.report.template', context=False)
|
|
|
|
@staticmethod
|
|
def default_type():
|
|
return 'ir.action.report'
|
|
|
|
@staticmethod
|
|
def default_report_content():
|
|
return None
|
|
|
|
@staticmethod
|
|
def default_direct_print():
|
|
return False
|
|
|
|
@classmethod
|
|
def default_single(cls):
|
|
return False
|
|
|
|
@classmethod
|
|
def default_translatable(cls):
|
|
return True
|
|
|
|
@staticmethod
|
|
def default_template_extension():
|
|
return 'odt'
|
|
|
|
@staticmethod
|
|
def default_extension():
|
|
return ''
|
|
|
|
@staticmethod
|
|
def default_module():
|
|
return Transaction().context.get('module')
|
|
|
|
def get_is_custom(self, name):
|
|
return bool(self.report_content_custom)
|
|
|
|
@classmethod
|
|
def get_report_content(cls, reports, name):
|
|
contents = {}
|
|
converter = fields.Binary.cast
|
|
default = None
|
|
format_ = Transaction().context.get(
|
|
'%s.%s' % (cls.__name__, name), '')
|
|
if format_ == 'size':
|
|
converter = len
|
|
default = 0
|
|
for report in reports:
|
|
data = getattr(report, name + '_custom')
|
|
if not data and getattr(report, name[:-8]):
|
|
try:
|
|
with file_open(
|
|
getattr(report, name[:-8]).replace('/', os.sep),
|
|
mode='rb') as fp:
|
|
data = fp.read()
|
|
except Exception:
|
|
data = None
|
|
contents[report.id] = converter(data) if data else default
|
|
return contents
|
|
|
|
@classmethod
|
|
def set_report_content(cls, records, name, value):
|
|
cls.write(records, {'%s_custom' % name: value})
|
|
|
|
@classmethod
|
|
def get_report_content_html(cls, reports, name):
|
|
return cls.get_report_content(reports, name[:-5])
|
|
|
|
@classmethod
|
|
def set_report_content_html(cls, reports, name, value):
|
|
if value is not None:
|
|
value = value.encode('utf-8')
|
|
cls.set_report_content(reports, name[:-5], value)
|
|
|
|
@fields.depends('name', 'template_extension')
|
|
def on_change_with_report_content_name(self, name=None):
|
|
return ''.join(
|
|
filter(None, [self.name, os.extsep, self.template_extension]))
|
|
|
|
@classmethod
|
|
def get_pyson(cls, reports, name):
|
|
pysons = {}
|
|
field = name[6:]
|
|
defaults = {}
|
|
for report in reports:
|
|
pysons[report.id] = (getattr(report, field)
|
|
or defaults.get(field, 'null'))
|
|
return pysons
|
|
|
|
@classmethod
|
|
def copy(cls, reports, default=None):
|
|
if default is None:
|
|
default = {}
|
|
default = default.copy()
|
|
default.setdefault('module', None)
|
|
default.setdefault(
|
|
'report_content_custom',
|
|
lambda o: None if o['report'] else o['report_content_custom'])
|
|
return super().copy(reports, default=default)
|
|
|
|
@classmethod
|
|
def write(cls, reports, values, *args):
|
|
context = Transaction().context
|
|
if 'module' in context:
|
|
actions = iter((reports, values) + args)
|
|
args = []
|
|
for reports, values in zip(actions, actions):
|
|
values = values.copy()
|
|
values['module'] = context['module']
|
|
args.extend((reports, values))
|
|
reports, values = args[:2]
|
|
args = args[2:]
|
|
cls._template_cache.clear()
|
|
super(ActionReport, cls).write(reports, values, *args)
|
|
|
|
def get_template_cached(self):
|
|
return self._template_cache.get(self.id)
|
|
|
|
def set_template_cached(self, template):
|
|
self._template_cache.set(self.id, template)
|
|
|
|
@classmethod
|
|
def validate_fields(cls, reports, field_names):
|
|
super().validate_fields(reports, field_names)
|
|
cls.check_record_name(reports, field_names)
|
|
|
|
@classmethod
|
|
def check_record_name(cls, reports, field_names=None):
|
|
if field_names and 'record_name' not in field_names:
|
|
return
|
|
for report in reports:
|
|
if not report.record_name:
|
|
return
|
|
try:
|
|
TextTemplate(report.record_name)
|
|
except Exception as exception:
|
|
raise ValidationError(gettext(
|
|
'ir.msg_report_invalid_record_name',
|
|
report=report.rec_name,
|
|
exception=exception)) from exception
|
|
|
|
|
|
class ActionActWindow(
|
|
fields.fmany2one(
|
|
'res_model_ref', 'res_model', 'ir.model,model', "Model",
|
|
ondelete='CASCADE'),
|
|
fields.fmany2one(
|
|
'context_model_ref', 'context_model', 'ir.model,model',
|
|
"Context Model", ondelete='CASCADE'),
|
|
ActionMixin, ModelSQL, ModelView):
|
|
"Action act window"
|
|
__name__ = 'ir.action.act_window'
|
|
domain = fields.Char('Domain Value')
|
|
context = fields.Char('Context Value')
|
|
order = fields.Char('Order Value')
|
|
res_model = fields.Char('Model')
|
|
context_model = fields.Char('Context Model')
|
|
context_domain = fields.Char(
|
|
"Context Domain",
|
|
help="Part of the domain that will be evaluated on each refresh.")
|
|
act_window_views = fields.One2Many('ir.action.act_window.view',
|
|
'act_window', 'Views')
|
|
views = fields.Function(fields.Binary('Views'), 'get_views')
|
|
act_window_domains = fields.One2Many('ir.action.act_window.domain',
|
|
'act_window', 'Domains')
|
|
domains = fields.Function(fields.Binary('Domains'), 'get_domains')
|
|
limit = fields.Integer('Limit', help='Default limit for the list view.')
|
|
search_value = fields.Char('Search Criteria',
|
|
help='Default search criteria for the list view.')
|
|
pyson_domain = fields.Function(fields.Char('PySON Domain'), 'get_pyson')
|
|
pyson_context = fields.Function(fields.Char('PySON Context'),
|
|
'get_pyson')
|
|
pyson_order = fields.Function(fields.Char('PySON Order'), 'get_pyson')
|
|
pyson_search_value = fields.Function(fields.Char(
|
|
'PySON Search Criteria'), 'get_pyson')
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(ActionActWindow, cls).__setup__()
|
|
cls.__rpc__.update({
|
|
'get': RPC(cache=dict(days=1)),
|
|
})
|
|
|
|
@staticmethod
|
|
def default_type():
|
|
return 'ir.action.act_window'
|
|
|
|
@staticmethod
|
|
def default_context():
|
|
return '{}'
|
|
|
|
@staticmethod
|
|
def default_search_value():
|
|
return '[]'
|
|
|
|
@classmethod
|
|
def validate(cls, actions):
|
|
super(ActionActWindow, cls).validate(actions)
|
|
cls.check_views(actions)
|
|
|
|
@classmethod
|
|
def validate_fields(cls, actions, field_names):
|
|
super().validate_fields(actions, field_names)
|
|
cls.check_domain(actions, field_names)
|
|
cls.check_context(actions, field_names)
|
|
|
|
@classmethod
|
|
def check_views(cls, actions):
|
|
"Check views"
|
|
for action in actions:
|
|
if action.res_model:
|
|
for act_window_view in action.act_window_views:
|
|
view = act_window_view.view
|
|
if view.model != action.res_model:
|
|
raise ViewError(
|
|
gettext('ir.msg_action_invalid_views',
|
|
view=view.rec_name,
|
|
action=action.rec_name))
|
|
if view.type == 'board':
|
|
raise ViewError(
|
|
gettext('ir.msg_action_invalid_views',
|
|
view=view.rec_name,
|
|
action=action.rec_name))
|
|
else:
|
|
for act_window_view in action.act_window_views:
|
|
view = act_window_view.view
|
|
if view.model:
|
|
raise ViewError(
|
|
gettext('ir.msg_action_invalid_views',
|
|
view=view.rec_name,
|
|
action=action.rec_name))
|
|
if view.type != 'board':
|
|
raise ViewError(
|
|
gettext('ir.msg_action_invalid_views',
|
|
view=view.rec_name,
|
|
action=action.rec_name))
|
|
|
|
@classmethod
|
|
def check_domain(cls, actions, field_names=None):
|
|
"Check domain and search_value"
|
|
if field_names and not (field_names & {'domain', 'search_value'}):
|
|
return
|
|
for action in actions:
|
|
for domain in (action.domain, action.search_value):
|
|
if not domain:
|
|
continue
|
|
try:
|
|
value = PYSONDecoder().decode(domain)
|
|
except Exception as exception:
|
|
raise DomainError(
|
|
gettext('ir.msg_action_invalid_domain',
|
|
domain=domain,
|
|
action=action.rec_name)) from exception
|
|
if isinstance(value, PYSON):
|
|
if not value.types() == set([list]):
|
|
raise DomainError(
|
|
gettext('ir.msg_action_invalid_domain',
|
|
domain=domain,
|
|
action=action.rec_name))
|
|
elif not isinstance(value, list):
|
|
raise DomainError(
|
|
gettext('ir.msg_action_invalid_domain',
|
|
domain=domain,
|
|
action=action.rec_name))
|
|
else:
|
|
try:
|
|
fields.domain_validate(value)
|
|
except Exception as exception:
|
|
raise DomainError(
|
|
gettext('ir.msg_action_invalid_domain',
|
|
domain=domain,
|
|
action=action.rec_name)) from exception
|
|
|
|
@classmethod
|
|
def check_context(cls, actions, field_names=None):
|
|
"Check context"
|
|
if field_names and 'context' not in field_names:
|
|
return
|
|
for action in actions:
|
|
if action.context:
|
|
try:
|
|
value = PYSONDecoder().decode(action.context)
|
|
except Exception as exception:
|
|
raise ContextError(
|
|
gettext('ir.msg_action_invalid_context',
|
|
context=action.context,
|
|
action=action.rec_name)) from exception
|
|
if isinstance(value, PYSON):
|
|
if not value.types() == set([dict]):
|
|
raise ContextError(
|
|
gettext('ir.msg_action_invalid_context',
|
|
context=action.context,
|
|
action=action.rec_name))
|
|
elif not isinstance(value, dict):
|
|
raise ContextError(
|
|
gettext('ir.msg_action_invalid_context',
|
|
context=action.context,
|
|
action=action.rec_name))
|
|
else:
|
|
try:
|
|
fields.context_validate(value)
|
|
except Exception as exception:
|
|
raise ContextError(
|
|
gettext('ir.msg_action_invalid_context',
|
|
context=action.context,
|
|
action=action.rec_name)) from exception
|
|
|
|
def get_views(self, name):
|
|
return [(view.view.id, view.view.type)
|
|
for view in self.act_window_views]
|
|
|
|
def get_domains(self, name):
|
|
return [(domain.name, domain.domain or '[]', domain.count)
|
|
for domain in self.act_window_domains]
|
|
|
|
@classmethod
|
|
def get_pyson(cls, windows, name):
|
|
pool = Pool()
|
|
encoder = PYSONEncoder()
|
|
pysons = {}
|
|
field = name[6:]
|
|
defaults = {
|
|
'domain': '[]',
|
|
'context': '{}',
|
|
'search_value': '[]',
|
|
}
|
|
for window in windows:
|
|
if not window.order and field == 'order':
|
|
if window.res_model:
|
|
defaults['order'] = encoder.encode(
|
|
getattr(pool.get(window.res_model), '_order', 'null'))
|
|
else:
|
|
defaults['order'] = 'null'
|
|
pysons[window.id] = (getattr(window, field)
|
|
or defaults.get(field, 'null'))
|
|
return pysons
|
|
|
|
@classmethod
|
|
def get(cls, xml_id):
|
|
'Get values from XML id or id'
|
|
pool = Pool()
|
|
ModelData = pool.get('ir.model.data')
|
|
Action = pool.get('ir.action')
|
|
if '.' in xml_id:
|
|
action_id = ModelData.get_id(*xml_id.split('.'))
|
|
else:
|
|
action_id = int(xml_id)
|
|
return Action(action_id).get_action_value()
|
|
|
|
|
|
class ActionActWindowView(
|
|
sequence_ordered(), DeactivableMixin, ModelSQL, ModelView):
|
|
"Action act window view"
|
|
__name__ = 'ir.action.act_window.view'
|
|
view = fields.Many2One(
|
|
'ir.ui.view', "View", required=True, ondelete='CASCADE',
|
|
domain=[
|
|
('model', '=', Eval('model', None)),
|
|
])
|
|
act_window = fields.Many2One('ir.action.act_window', 'Action',
|
|
ondelete='CASCADE')
|
|
model = fields.Function(fields.Char("Model"), 'on_change_with_model')
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls.__access__.add('act_window')
|
|
|
|
@classmethod
|
|
def __register__(cls, module_name):
|
|
super().__register__(module_name)
|
|
|
|
table = cls.__table_handler__(module_name)
|
|
|
|
# Migration from 5.0: remove required on sequence
|
|
table.not_null_action('sequence', 'remove')
|
|
|
|
@fields.depends('act_window', '_parent_act_window.res_model')
|
|
def on_change_with_model(self, name=None):
|
|
if self.act_window:
|
|
return self.act_window.res_model
|
|
|
|
@classmethod
|
|
def create(cls, vlist):
|
|
pool = Pool()
|
|
windows = super(ActionActWindowView, cls).create(vlist)
|
|
pool.get('ir.action.keyword')._get_keyword_cache.clear()
|
|
return windows
|
|
|
|
@classmethod
|
|
def write(cls, windows, values, *args):
|
|
pool = Pool()
|
|
super(ActionActWindowView, cls).write(windows, values, *args)
|
|
pool.get('ir.action.keyword')._get_keyword_cache.clear()
|
|
|
|
@classmethod
|
|
def delete(cls, windows):
|
|
pool = Pool()
|
|
super(ActionActWindowView, cls).delete(windows)
|
|
pool.get('ir.action.keyword')._get_keyword_cache.clear()
|
|
|
|
|
|
class ActionActWindowDomain(
|
|
sequence_ordered(), DeactivableMixin, ModelSQL, ModelView):
|
|
"Action act window domain"
|
|
__name__ = 'ir.action.act_window.domain'
|
|
name = fields.Char('Name', translate=True)
|
|
domain = fields.Char('Domain')
|
|
count = fields.Boolean('Count')
|
|
act_window = fields.Many2One('ir.action.act_window', 'Action',
|
|
required=True, ondelete='CASCADE')
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls.__access__.add('act_window')
|
|
|
|
@classmethod
|
|
def __register__(cls, module_name):
|
|
super().__register__(module_name)
|
|
|
|
table = cls.__table_handler__(module_name)
|
|
|
|
# Migration from 5.0: remove required on sequence
|
|
table.not_null_action('sequence', 'remove')
|
|
|
|
@classmethod
|
|
def default_count(cls):
|
|
return False
|
|
|
|
@classmethod
|
|
def validate_fields(cls, actions, field_names):
|
|
super().validate_fields(actions, field_names)
|
|
cls.check_domain(actions, field_names)
|
|
|
|
@classmethod
|
|
def check_domain(cls, actions, field_names=None):
|
|
if field_names and 'domain' not in field_names:
|
|
return
|
|
for action in actions:
|
|
if not action.domain:
|
|
continue
|
|
try:
|
|
value = PYSONDecoder().decode(action.domain)
|
|
except Exception as exception:
|
|
raise DomainError(gettext(
|
|
'ir.msg_action_invalid_domain',
|
|
domain=action.domain,
|
|
action=action.rec_name)) from exception
|
|
if isinstance(value, PYSON):
|
|
if not value.types() == set([list]):
|
|
raise DomainError(gettext(
|
|
'ir.msg_action_invalid_domain',
|
|
domain=action.domain,
|
|
action=action.rec_name))
|
|
elif not isinstance(value, list):
|
|
raise DomainError(gettext(
|
|
'ir.msg_action_invalid_domain',
|
|
domain=action.domain,
|
|
action=action.rec_name))
|
|
else:
|
|
try:
|
|
fields.domain_validate(value)
|
|
except Exception as exception:
|
|
raise DomainError(gettext(
|
|
'ir.msg_action_invalid_domain',
|
|
domain=action.domain,
|
|
action=action.rec_name)) from exception
|
|
|
|
@classmethod
|
|
def create(cls, vlist):
|
|
pool = Pool()
|
|
domains = super(ActionActWindowDomain, cls).create(vlist)
|
|
pool.get('ir.action.keyword')._get_keyword_cache.clear()
|
|
return domains
|
|
|
|
@classmethod
|
|
def write(cls, domains, values, *args):
|
|
pool = Pool()
|
|
super(ActionActWindowDomain, cls).write(domains, values, *args)
|
|
pool.get('ir.action.keyword')._get_keyword_cache.clear()
|
|
|
|
@classmethod
|
|
def delete(cls, domains):
|
|
pool = Pool()
|
|
super(ActionActWindowDomain, cls).delete(domains)
|
|
pool.get('ir.action.keyword')._get_keyword_cache.clear()
|
|
|
|
|
|
class ActionWizard(
|
|
fields.fmany2one(
|
|
'model_ref', 'model', 'ir.model,model', "Model",
|
|
ondelete='CASCADE'),
|
|
ActionMixin, ModelSQL, ModelView):
|
|
"Action wizard"
|
|
__name__ = 'ir.action.wizard'
|
|
_action_name = 'wiz_name'
|
|
wiz_name = fields.Char('Wizard name', required=True)
|
|
model = fields.Char('Model')
|
|
window = fields.Boolean('Window', help='Run wizard in a new window.')
|
|
|
|
@staticmethod
|
|
def default_type():
|
|
return 'ir.action.wizard'
|
|
|
|
@classmethod
|
|
def get_models(cls, name, action_id=None):
|
|
# TODO add cache
|
|
domain = [
|
|
(cls._action_name, '=', name),
|
|
]
|
|
if action_id:
|
|
domain.append(('id', '=', action_id))
|
|
actions = cls.search(domain)
|
|
return {a.model for a in actions if a.model}
|
|
|
|
@classmethod
|
|
def get_name(cls, name, model):
|
|
# TODO add cache
|
|
actions = cls.search([
|
|
(cls._action_name, '=', name),
|
|
('model', '=', model),
|
|
], limit=1)
|
|
if actions:
|
|
action, = actions
|
|
return action.name
|
|
return name
|
|
|
|
|
|
class ActionURL(ActionMixin, ModelSQL, ModelView):
|
|
"Action URL"
|
|
__name__ = 'ir.action.url'
|
|
url = fields.Char('Action Url', required=True)
|
|
|
|
@staticmethod
|
|
def default_type():
|
|
return 'ir.action.url'
|