diff --git a/CHANGELOG b/CHANGELOG index 6dfac0a095..4436966d1b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Add option to show revisions * Add a multi selection widget for many2many * Remove auto-refresh * Add support of domain for non-relation field diff --git a/tryton/action/main.py b/tryton/action/main.py index 33ecfd45bf..00ae9d9191 100644 --- a/tryton/action/main.py +++ b/tryton/action/main.py @@ -113,6 +113,7 @@ class Action(object): action_ctx = decoder.decode(action.get('pyson_context') or '{}') ctx.update(action_ctx) ctx.update(context) + action_ctx.update(context) ctx['context'] = ctx decoder = PYSONDecoder(ctx) diff --git a/tryton/common/common.py b/tryton/common/common.py index 29513198f2..d6ced20b15 100644 --- a/tryton/common/common.py +++ b/tryton/common/common.py @@ -168,6 +168,23 @@ class ModelAccess(object): MODELACCESS = ModelAccess() +class ModelHistory(object): + _models = set() + + def load_history(self): + self._models.clear() + try: + self._models.update(rpc.execute('model', 'ir.model', + 'list_history', rpc.CONTEXT)) + except TrytonServerError: + pass + + def __contains__(self, model): + return model in self._models + +MODELHISTORY = ModelHistory() + + class ViewSearch(object): searches = None diff --git a/tryton/common/date_widget.py b/tryton/common/date_widget.py index a94ed66f28..20b4f2314c 100644 --- a/tryton/common/date_widget.py +++ b/tryton/common/date_widget.py @@ -154,7 +154,11 @@ class DateEntry(gtk.Entry): def set_text(self, text): self._interactive_input = False try: + # Force position as _on_insert_text use it + position = self.get_position() + self.set_position(0) gtk.Entry.set_text(self, text) + self.idle_set_position(position) finally: self._interactive_input = True @@ -217,7 +221,7 @@ class DateEntry(gtk.Entry): res = None date = self.compute_date(self.get_text()) try: - res = datetime.datetime(*time.strptime(date, self.format)[:6]) + res = datetime.datetime.strptime(date, self.format) except ValueError: return None if set_text: diff --git a/tryton/gui/main.py b/tryton/gui/main.py index c04933fcff..237989d9ba 100644 --- a/tryton/gui/main.py +++ b/tryton/gui/main.py @@ -855,6 +855,7 @@ class Main(object): for target in ( common.ICONFACTORY.load_icons, common.MODELACCESS.load_models, + common.MODELHISTORY.load_history, common.VIEW_SEARCH.load_searches, ): t = threading.Thread(target=target) diff --git a/tryton/gui/window/attachment.py b/tryton/gui/window/attachment.py index 6cacc8ea82..ea326370cc 100644 --- a/tryton/gui/window/attachment.py +++ b/tryton/gui/window/attachment.py @@ -15,11 +15,12 @@ class Attachment(WinForm): def __init__(self, record, callback=None): self.resource = '%s,%s' % (record.model_name, record.id) self.attachment_callback = callback + context = record.context_get() + context['resource'] = self.resource screen = Screen('ir.attachment', domain=[ ('resource', '=', self.resource), - ], mode=['tree', 'form'], context={ - 'resource': self.resource, - }, exclude_field='resource') + ], mode=['tree', 'form'], context=context, + exclude_field='resource') screen.search_filter() screen.parent = record super(Attachment, self).__init__(screen, self.callback, diff --git a/tryton/gui/window/form.py b/tryton/gui/window/form.py index 4f17443a19..76cadedd3f 100644 --- a/tryton/gui/window/form.py +++ b/tryton/gui/window/form.py @@ -11,11 +11,13 @@ from tryton.gui.window import Window from tryton.gui.window.win_export import WinExport from tryton.gui.window.win_import import WinImport from tryton.gui.window.attachment import Attachment +from tryton.gui.window.revision import Revision from tryton.signal_event import SignalEvent from tryton.common import message, sur, sur_3b, COLOR_SCHEMES, timezoned_date import tryton.common as common from tryton.translate import date_format from tryton.common import RPCExecute, RPCException +from tryton.common.datetime_strftime import datetime_strftime from tryton import plugins from tabcontent import TabContent @@ -61,6 +63,7 @@ class Form(SignalEvent, TabContent): (_('_Next'), 'tryton-go-next', 'sig_next', '/Form/Next'), (_('_Search'), 'tryton-find', 'sig_search', '/Form/Search'), (_('View _Logs...'), None, 'sig_logs', None), + (_('Show revisions...'), 'tryton-clock', 'revision', None), (None,) * 4, (_('_Close Tab'), 'tryton-close', 'sig_win_close', '/Form/Close'), @@ -96,8 +99,6 @@ class Form(SignalEvent, TabContent): domain = [] if view_ids is None: view_ids = [] - if context is None: - context = {} self.model = model self.res_id = res_id @@ -107,7 +108,7 @@ class Form(SignalEvent, TabContent): self.view_ids = view_ids self.dialogs = [] - self.screen = Screen(self.model, mode=mode, context=self.context, + self.screen = Screen(self.model, mode=mode, context=context, view_ids=view_ids, domain=domain, limit=limit, order=order, search_value=search_value, tab_domain=tab_domain) self.screen.widget.show() @@ -117,6 +118,12 @@ class Form(SignalEvent, TabContent): else: self.name = name + if self.model not in common.MODELHISTORY: + self.menu_def = self.menu_def[:] + # Remove callback to revision + self.menu_def[11] = (self.menu_def[11][:2] + (None,) + + self.menu_def[11][3:]) + self.create_tabcontent() self.url_entry = url_entry = gtk.Entry() @@ -154,10 +161,12 @@ class Form(SignalEvent, TabContent): in ('tree', 'graph', 'calendar'): self.screen.search_filter() + self.update_revision() + def get_toolbars(self): try: return RPCExecute('model', self.model, 'view_toolbar_get', - context=self.context) + context=self.screen.context) except RPCException: return {} @@ -174,7 +183,7 @@ class Form(SignalEvent, TabContent): and self.domain == value.domain and self.mode == value.mode and self.view_ids == value.view_ids - and self.context == value.context + and self.screen.context == value.screen.context and self.name == value.name and self.screen.limit == value.screen.limit and self.screen.search_value == value.screen.search_value) @@ -235,7 +244,7 @@ class Form(SignalEvent, TabContent): try: res = RPCExecute('model', self.model, 'read', [obj_id], - [x[0] for x in fields], context=self.context) + [x[0] for x in fields], context=self.screen.context) except RPCException: return message_str = '' @@ -252,6 +261,42 @@ class Form(SignalEvent, TabContent): message(message_str) return True + def revision(self, widget=None): + if not self.modified_save(): + return + current_id = (self.screen.current_record.id + if self.screen.current_record else None) + try: + revisions = RPCExecute('model', self.model, 'history_revisions', + [r.id for r in self.screen.selected_records]) + except RPCException: + return + revision = self.screen.context.get('_datetime') + revision = Revision(revisions, revision).run() + if revision != self.screen.context.get('_datetime'): + self.screen.clear() + # Update root group context that will be propagated + self.screen.group._context['_datetime'] = revision + if self.screen.current_view.view_type != 'form': + self.screen.search_filter( + self.screen.screen_container.get_text()) + else: + # Test if record exist in revisions + self.screen.load([current_id]) + self.screen.display(set_cursor=True) + self.update_revision() + + def update_revision(self): + revision = self.screen.context.get('_datetime') + if revision: + format_ = date_format() + ' %H:%M:%S.%f' + revision = datetime_strftime(revision, format_) + self.title.set_label('%s @ %s' % (self.name, revision)) + else: + self.title.set_label(self.name) + for button in ('new', 'save'): + self.buttons[button].props.sensitive = not revision + def sig_remove(self, widget=None): if not common.MODELACCESS[self.model]['delete']: return @@ -271,7 +316,7 @@ class Form(SignalEvent, TabContent): fields = {} for name, field in self.screen.group.fields.iteritems(): fields[name] = field.attrs - WinImport(self.model, self.context) + WinImport(self.model, self.screen.context) def sig_save_as(self, widget=None): while self.screen.view_to_load: @@ -279,7 +324,7 @@ class Form(SignalEvent, TabContent): fields = {} for name, field in self.screen.group.fields.iteritems(): fields[name] = field.attrs - WinExport(self.model, self.ids_get(), context=self.context) + WinExport(self.model, self.ids_get(), context=self.screen.context) def sig_new(self, widget=None, autosave=True): if not common.MODELACCESS[self.model]['create']: @@ -296,13 +341,7 @@ class Form(SignalEvent, TabContent): return if not self.modified_save(): return - ids = [r.id for r in self.screen.selected_records] - try: - new_ids = RPCExecute('model', self.model, 'copy', ids, {}, - context=self.context) - except RPCException: - return - self.screen.load(new_ids) + self.screen.copy() self.message_info(_('Working now on the duplicated record(s)!'), 'green') @@ -336,11 +375,12 @@ class Form(SignalEvent, TabContent): self.screen.cancel_current() set_cursor = False self.screen.save_tree_state(store=False) + record_id = (self.screen.current_record.id + if self.screen.current_record else None) if self.screen.current_view.view_type != 'form': - obj_id = self.id_get() self.screen.search_filter(self.screen.screen_container.get_text()) for record in self.screen.group: - if record.id == obj_id: + if record.id == record_id: self.screen.current_record = record set_cursor = True break @@ -467,7 +507,7 @@ class Form(SignalEvent, TabContent): 'id': record_id, 'ids': record_ids, } - Action._exec_action(action, data, self.context) + Action._exec_action(action, data, self.screen.context) def activate_save(self): self.buttons['save'].props.sensitive = self.screen.modified() diff --git a/tryton/gui/window/revision.py b/tryton/gui/window/revision.py new file mode 100644 index 0000000000..ec371b8b13 --- /dev/null +++ b/tryton/gui/window/revision.py @@ -0,0 +1,68 @@ +#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 gtk +import gettext + +from tryton.config import TRYTON_ICON +from tryton.common import get_toplevel_window +from tryton.common.date_widget import DateEntry +from tryton.common.datetime_strftime import datetime_strftime +from tryton.translate import date_format + +_ = gettext.gettext + + +class Revision(object): + 'Ask revision' + + def __init__(self, revisions, revision=None): + self.parent = get_toplevel_window() + self.win = gtk.Dialog(_('Revision'), self.parent, + gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OK, gtk.RESPONSE_OK)) + self.win.set_default_response(gtk.RESPONSE_OK) + self.win.set_icon(TRYTON_ICON) + self.win.set_has_separator(True) + self.win.vbox.set_spacing(3) + self.win.vbox.pack_start(gtk.Label( + _('Select a revision')), expand=False, fill=True) + self.win.vbox.pack_start(gtk.HSeparator()) + hbox = gtk.HBox(spacing=3) + hbox.pack_start(gtk.Label(_('Revision:')), expand=True, fill=True) + format_ = date_format() + ' %H:%M:%S.%f' + self.entry = DateEntry(format_) + if revision: + self.entry.set_text(datetime_strftime(revision, format_)) + list_store = gtk.ListStore(str, str) + list_store.append(('', '')) + for revision, id_, name in revisions: + list_store.append((datetime_strftime(revision, format_), name)) + combobox = gtk.ComboBoxEntry(list_store) + combobox.add(self.entry) + cell = gtk.CellRendererText() + combobox.pack_start(cell, True) + combobox.add_attribute(cell, 'text', 1) + hbox.pack_start(combobox, expand=True, fill=True) + combobox.set_entry_text_column(0) + combobox.connect('changed', self.changed) + self.entry.set_property('activates_default', True) + self.win.vbox.pack_start(hbox, expand=True, fill=True) + self.win.show_all() + + def changed(self, combobox): + # set_text must be called because DateEntry doesn't work correctly with + # the default insert/delete text behavior of combobox + model = combobox.get_model() + idx = combobox.get_active() + if idx >= 0: + self.entry.set_text(model[idx][0]) + + def run(self): + response = self.win.run() + revision = None + if response == gtk.RESPONSE_OK: + revision = self.entry.date_get() + self.parent.present() + self.win.destroy() + return revision diff --git a/tryton/gui/window/tabcontent.py b/tryton/gui/window/tabcontent.py index fb473444c8..401c0dcd92 100644 --- a/tryton/gui/window/tabcontent.py +++ b/tryton/gui/window/tabcontent.py @@ -43,7 +43,7 @@ class TabContent(object): self.widget.pack_start(self.scrolledwindow) def make_title_bar(self): - title = gtk.Label() + self.title = title = gtk.Label() title.modify_font(pango.FontDescription("bold 14")) title.set_label(self.name) title.set_padding(10, 4) diff --git a/tryton/gui/window/view_form/model/record.py b/tryton/gui/window/view_form/model/record.py index ad1914290c..c64b0fa093 100644 --- a/tryton/gui/window/view_form/model/record.py +++ b/tryton/gui/window/view_form/model/record.py @@ -602,7 +602,7 @@ class Record(SignalEvent): 'search_count', [ ('resource', '=', '%s,%s' % (self.model_name, self.id)), - ]) + ], context=self.context_get()) except RPCException: return 0 return self.attachment_count diff --git a/tryton/gui/window/view_form/screen/screen.py b/tryton/gui/window/view_form/screen/screen.py index 3708f49c0d..6fcb700b26 100644 --- a/tryton/gui/window/view_form/screen/screen.py +++ b/tryton/gui/window/view_form/screen/screen.py @@ -46,8 +46,6 @@ class Screen(SignalEvent): view_ids = [] if mode is None: mode = ['tree', 'form'] - if context is None: - context = {} if views_preload is None: views_preload = {} if domain is None: @@ -69,7 +67,6 @@ class Screen(SignalEvent): self.size_limit = None self.views_preload = views_preload self.model_name = model_name - self.context = context self.views = [] self.view_ids = view_ids[:] self.parent = None @@ -80,7 +77,7 @@ class Screen(SignalEvent): lambda: collections.defaultdict(lambda: None)) self.tree_states_done = set() self.__group = None - self.new_group() + self.new_group(context or {}) self.__current_record = None self.current_record = None self.screen_container = ScreenContainer(tab_domain) @@ -230,6 +227,10 @@ class Screen(SignalEvent): self.load(ids) return bool(ids) + @property + def context(self): + return self.group.context + def __get_group(self): return self.__group @@ -260,9 +261,10 @@ class Screen(SignalEvent): group = property(__get_group, __set_group) - def new_group(self): + def new_group(self, context=None): + context = context if context is not None else self.context self.group = Group(self.model_name, {}, domain=self.domain, - context=self.context, readonly=self.readonly) + context=context, readonly=self.readonly) def _group_cleared(self, group, signal): for view in self.views: diff --git a/tryton/gui/window/win_form.py b/tryton/gui/window/win_form.py index 98687d1ab7..3f866d337a 100644 --- a/tryton/gui/window/win_form.py +++ b/tryton/gui/window/win_form.py @@ -122,6 +122,7 @@ class WinForm(NoModal): hbox = gtk.HBox(homogeneous=False, spacing=0) tooltips = common.Tooltips() access = common.MODELACCESS[screen.model_name] + readonly = self.screen.group.readonly if domain is not None: self.wid_text = gtk.Entry() @@ -140,7 +141,7 @@ class WinForm(NoModal): self.but_add.add(img_add) self.but_add.set_relief(gtk.RELIEF_NONE) hbox.pack_start(self.but_add, expand=False, fill=False) - if not access['read']: + if not access['read'] or readonly: self.but_add.set_sensitive(False) self.but_remove = gtk.Button() @@ -153,7 +154,7 @@ class WinForm(NoModal): self.but_remove.add(img_remove) self.but_remove.set_relief(gtk.RELIEF_NONE) hbox.pack_start(self.but_remove, expand=False, fill=False) - if not access['read']: + if not access['read'] or readonly: self.but_remove.set_sensitive(False) hbox.pack_start(gtk.VSeparator(), expand=False, fill=True) @@ -167,7 +168,7 @@ class WinForm(NoModal): self.but_new.add(img_new) self.but_new.set_relief(gtk.RELIEF_NONE) hbox.pack_start(self.but_new, expand=False, fill=False) - if not access['create']: + if not access['create'] or readonly: self.but_new.set_sensitive(False) self.but_del = gtk.Button() @@ -180,7 +181,7 @@ class WinForm(NoModal): self.but_del.add(img_del) self.but_del.set_relief(gtk.RELIEF_NONE) hbox.pack_start(self.but_del, expand=False, fill=False) - if not access['delete']: + if not access['delete'] or readonly: self.but_del.set_sensitive(False) self.but_undel = gtk.Button() @@ -194,6 +195,8 @@ class WinForm(NoModal): self.but_undel.add(img_undel) self.but_undel.set_relief(gtk.RELIEF_NONE) hbox.pack_start(self.but_undel, expand=False, fill=False) + if not access['delete'] or readonly: + self.but_undel.set_sensitive(False) hbox.pack_start(gtk.VSeparator(), expand=False, fill=True) @@ -355,9 +358,10 @@ class WinForm(NoModal): def _sig_label(self, screen, signal_data): name = '_' + access = common.MODELACCESS[screen.model_name] + readonly = screen.group.readonly if signal_data[0] >= 1: name = str(signal_data[0]) - self.but_del.set_sensitive(True) if self.domain is not None: self.but_remove.set_sensitive(True) if signal_data[0] < signal_data[1]: @@ -368,8 +372,9 @@ class WinForm(NoModal): self.but_pre.set_sensitive(True) else: self.but_pre.set_sensitive(False) - self.but_del.set_sensitive(True) - self.but_undel.set_sensitive(True) + if access['delete'] and not readonly: + self.but_del.set_sensitive(True) + self.but_undel.set_sensitive(True) else: self.but_del.set_sensitive(False) self.but_undel.set_sensitive(False)