diff --git a/field_rrule/README.rst b/field_rrule/README.rst new file mode 100644 index 000000000..2374a93f4 --- /dev/null +++ b/field_rrule/README.rst @@ -0,0 +1,73 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +================ +Repetition rules +================ + +This module was written to offer a field type that holds rrules according to RFC 2445. + +Usage +===== + +To use this module, you need to: + +* depend on it +* say ``from openerp.addons.field_rrule import FieldRRule`` +* use ``FieldRRule`` like any other field +* on forms, use ``widget="rrule"`` +* have a look at ``demo/res_partner.*`` + +Technically, this is a wrapper around serialized fields. The value always will be a subclass of dateutil's ``rruleset``. For technical reasons, this class overrides ``__iter__``, so if you need a proper ``rruleset``, call the value: ``my_browse_record.my_field_of_type_rrule()`` - this gives you a vanilla ``rruleset``. + +If you want to pass a default, use the internal representation you'll find in the database - a list of dictionaries with the keyword arguments to be passed to rrule's constructor and a `type` field that for now can only be `rrule`: A context of ``{"default_rrule": [{"count": 1, "freq": 1, "type": "rrule", "interval": 1, "bymonthday": [1]}]}`` would give you a default for the field ``rrule`` which occurs one time at the first of the month. + +In case you work with defaults and want to dumb down the UI a bit, use ``{'no_add_rule': true}``. + +Further, as this is a serialized field, a value of `not set` will be represented in the database as ``'null'`` if the value was set and unset afterwards, or a database ``null`` if the value was never set - this is then also what you have to search for when you need records with your field unset. + +Known issues / Roadmap +====================== + +* support the unimplemented features of rrules +* support rdates, exdates, exrules +* consider multiple widgets with different feature sets + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smashing it by providing a detailed and welcomed feedback. + +Credits +======= + +Images +------ + +* Odoo Community Association: `Icon `_. + +Contributors +------------ + +* Holger Brunn + +Do not contact contributors directly about help with questions or problems concerning this addon, but use the `community mailing list `_ or the `appropriate specialized mailinglist `_ for help, and the bug tracker linked in `Bug Tracker`_ above for technical issues. + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/field_rrule/__init__.py b/field_rrule/__init__.py new file mode 100644 index 000000000..3f00adc1b --- /dev/null +++ b/field_rrule/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# © 2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from .field_rrule import FieldRRule +from .hooks import post_load_hook diff --git a/field_rrule/__openerp__.py b/field_rrule/__openerp__.py new file mode 100644 index 000000000..59625aa2b --- /dev/null +++ b/field_rrule/__openerp__.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# © 2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +{ + "name": "Repetition Rules", + "version": "8.0.1.0.0", + "author": "Therp BV,Odoo Community Association (OCA)", + "license": "AGPL-3", + "category": "Hidden/Dependency", + "summary": "Provides a field and widget for RRules according to RFC 2445", + "depends": [ + 'web', + ], + "data": [ + 'views/templates.xml', + ], + # this will be activated in the module's post_load_hook if we run on oca's + # runbot + "demo_deactivated": [ + 'demo/res_partner.xml' + ], + "post_load": "post_load_hook", + "qweb": [ + 'static/src/xml/field_rrule.xml', + ], +} diff --git a/field_rrule/demo/__init__.py b/field_rrule/demo/__init__.py new file mode 100644 index 000000000..c946d3886 --- /dev/null +++ b/field_rrule/demo/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# © 2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from . import res_partner diff --git a/field_rrule/demo/res_partner.py b/field_rrule/demo/res_partner.py new file mode 100644 index 000000000..2b19fb836 --- /dev/null +++ b/field_rrule/demo/res_partner.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# © 2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from dateutil.rrule import YEARLY, rrule +from openerp import api, fields, models +from openerp.addons.field_rrule.field_rrule import FieldRRule,\ + SerializableRRuleSet + + +class ResPartner(models.Model): + _inherit = 'res.partner' + + @api.depends('rrule') + def _compute_rrule_representation(self): + for this in self: + if not this.rrule: + this.rrule_representation = 'You did not fill in rules yet' + continue + this.rrule_representation = 'First 5 dates: %s\n%s' % ( + ', '.join(map(str, this.rrule[:5])), + this.rrule, + ) + + rrule = FieldRRule('RRule', default=SerializableRRuleSet(rrule(YEARLY))) + rrule_representation = fields.Text( + string='RRule representation', compute=_compute_rrule_representation) diff --git a/field_rrule/demo/res_partner.xml b/field_rrule/demo/res_partner.xml new file mode 100644 index 000000000..2a61e2317 --- /dev/null +++ b/field_rrule/demo/res_partner.xml @@ -0,0 +1,15 @@ + + + + + res.partner + + + + + + + + + + diff --git a/field_rrule/field_rrule.py b/field_rrule/field_rrule.py new file mode 100644 index 000000000..1ec65d653 --- /dev/null +++ b/field_rrule/field_rrule.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +# © 2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from dateutil.rrule import rrule, rruleset +from openerp import fields, models +_DATETIME_FIELDS = ['_until', '_dtstart'] +_SCALAR_FIELDS = [ + '_wkst', '_cache', '_until', '_dtstart', '_count', '_freq', '_interval', +] +_ZERO_IS_NOT_NONE = ['_freq'] + + +class SerializableRRuleSet(rruleset, list): + """Getting our rule set json stringified is tricky, because we can't + just inject our own json encoder. Now we pretend our set is a list, + then json.dumps will try to iterate, which is why we can do our specific + stuff in __iter__""" + def __init__(self, *args): + self._rrule = [] + super(SerializableRRuleSet, self).__init__(self) + for arg in args: + self.rrule(arg) + + def __iter__(self): + for rule in self._rrule: + yield dict(type='rrule', **{ + key[1:]: + fields.Datetime.to_string(getattr(rule, key)) + if key in _DATETIME_FIELDS + else + [] if getattr(rule, key) is None and key not in _SCALAR_FIELDS + else + list(getattr(rule, key)) if key not in _SCALAR_FIELDS + else getattr(rule, key) + for key in [ + '_byhour', '_wkst', '_bysecond', '_bymonthday', + '_byweekno', '_bysetpos', '_cache', '_bymonth', + '_byyearday', '_byweekday', '_byminute', + '_until', '_dtstart', '_count', '_freq', '_interval', + '_byeaster', + ] + }) + # TODO: implement rdate, exrule, exdate + + def __call__(self, default_self=None): + """convert self to a proper rruleset for iteration. + If we use defaults on our field, this will be called too with + and empty recordset as parameter. In this case, we need self""" + if isinstance(default_self, models.BaseModel): + return self + result = rruleset() + result._rrule = self._rrule + result._rdate = self._rdate + result._exrule = self._exrule + result._exdate = self._exdate + return result + + def __nonzero__(self): + return bool(self._rrule) + + def __repr__(self): + return ', '.join(str(a) for a in self) + + def __getitem__(self, key): + return rruleset.__getitem__(self(), key) + + def __getslice__(self, i, j): + return rruleset.__getitem__(self(), slice(i, j)) + + def __ne__(self, o): + return not self.__eq__(o) + + def __eq__(self, o): + return self.__repr__() == o.__repr__() + + def between(self, after, before, inc=False): + return self().between(after, before, inc=inc) + + def after(self, dt, inc=False): + return self().after(dt, inc=inc) + + def before(self, dt, inc=False): + return self().before(dt, inc=inc) + + def count(self): + return self().count() + + +class FieldRRule(fields.Serialized): + def convert_to_cache(self, value, record, validate=True): + result = SerializableRRuleSet() + if not value: + return result + if isinstance(value, SerializableRRuleSet): + return value + assert isinstance(value, list), 'An RRULE\'s content must be a list' + for data in value: + assert isinstance(data, dict), 'The list must contain dictionaries' + assert 'type' in data, 'The dictionary must contain a type' + data_type = data['type'] + data = { + key: fields.Datetime.from_string(value) + if '_%s' % key in _DATETIME_FIELDS + else map(int, value) + if value and '_%s' % key not in _SCALAR_FIELDS + else int(value) if value + else None if not value and '_%s' % key not in _ZERO_IS_NOT_NONE + else value + for key, value in data.iteritems() + if key != 'type' + } + if data_type == 'rrule': + result.rrule(rrule(**data)) + # TODO: implement rdate, exrule, exdate + else: + raise ValueError('Unknown type given') + return result diff --git a/field_rrule/hooks.py b/field_rrule/hooks.py new file mode 100644 index 000000000..23d223986 --- /dev/null +++ b/field_rrule/hooks.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# © 2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +import socket +import inspect + + +def post_load_hook(): + """do some trickery to have demo data/model on runbot, but nowhere else""" + if socket.getfqdn().endswith('odoo-community.org'): # pragma: nocover + from . import demo # flake8: noqa + for frame, filename, lineno, funcname, line, index in inspect.stack(): + if 'package' in frame.f_locals: + frame.f_locals['package'].info['demo'] =\ + frame.f_locals['package'].info['demo_deactivated'] + break diff --git a/field_rrule/i18n/nl.po b/field_rrule/i18n/nl.po new file mode 100644 index 000000000..80e79af41 --- /dev/null +++ b/field_rrule/i18n/nl.po @@ -0,0 +1,216 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * field_rrule +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-07-19 09:53+0000\n" +"PO-Revision-Date: 2016-07-19 09:53+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: field_rrule +#. openerp-web +#: code:addons/field_rrule/static/src/js/field_rrule.js:307 +#, python-format +msgid "%s occurrences" +msgstr "%s herhalingen" + +#. module: field_rrule +#. openerp-web +#: code:addons/field_rrule/static/src/xml/field_rrule.xml:4 +#, python-format +msgid "Add instance" +msgstr "Regel toevoegen" + +#. module: field_rrule +#. openerp-web +#: code:addons/field_rrule/static/src/xml/field_rrule.xml:33 +#, python-format +msgid "After an amount of occurrences" +msgstr "Na een aantal herhalingen" + +#. module: field_rrule +#. openerp-web +#: code:addons/field_rrule/static/src/xml/field_rrule.xml:30 +#, python-format +msgid "At a date" +msgstr "Op een datum" + +#. module: field_rrule +#. openerp-web +#: code:addons/field_rrule/static/src/xml/field_rrule.xml:14 +#, python-format +msgid "Begin" +msgstr "Begin" + +#. module: field_rrule +#. openerp-web +#: code:addons/field_rrule/static/src/js/field_rrule.js:287 +#, python-format +msgid "Daily" +msgstr "Dagelijks" + +#. module: field_rrule +#. openerp-web +#: code:addons/field_rrule/static/src/xml/field_rrule.xml:76 +#, python-format +msgid "Day of month" +msgstr "Dag in de maand" + +#. module: field_rrule +#. openerp-web +#: code:addons/field_rrule/static/src/xml/field_rrule.xml:63 +#, python-format +msgid "Day of year" +msgstr "Daf in het jaar" + +#. module: field_rrule +#. openerp-web +#: code:addons/field_rrule/static/src/xml/field_rrule.xml:23 +#, python-format +msgid "End" +msgstr "Einde" + +#. module: field_rrule +#. openerp-web +#: code:addons/field_rrule/static/src/js/field_rrule.js:275 +#, python-format +msgid "Friday" +msgstr "Vrijdag" + +#. module: field_rrule +#. openerp-web +#: code:addons/field_rrule/static/src/js/field_rrule.js:288 +#, python-format +msgid "Hourly" +msgstr "Uurlijks" + +#. module: field_rrule +#. openerp-web +#: code:addons/field_rrule/static/src/js/field_rrule.js:305 +#, python-format +msgid "Indefinitely" +msgstr "Oneindig" + +#. module: field_rrule +#. openerp-web +#: code:addons/field_rrule/static/src/xml/field_rrule.xml:54 +#, python-format +msgid "Interval" +msgstr "Interval" + +#. module: field_rrule +#. openerp-web +#: code:addons/field_rrule/static/src/xml/field_rrule.xml:37 +#, python-format +msgid "Leave empty for no limit" +msgstr "Laat leeg voor geen limiet" + +#. module: field_rrule +#. openerp-web +#: code:addons/field_rrule/static/src/js/field_rrule.js:289 +#, python-format +msgid "Minutely" +msgstr "Minutelijks" + +#. module: field_rrule +#. openerp-web +#: code:addons/field_rrule/static/src/js/field_rrule.js:271 +#, python-format +msgid "Monday" +msgstr "Maandag" + +#. module: field_rrule +#. openerp-web +#: code:addons/field_rrule/static/src/js/field_rrule.js:285 +#: code:addons/field_rrule/static/src/xml/field_rrule.xml:48 +#, python-format +msgid "Monthly" +msgstr "Maandelijks" + +#. module: field_rrule +#. openerp-web +#: code:addons/field_rrule/static/src/xml/field_rrule.xml:41 +#, python-format +msgid "Recurrence" +msgstr "Herhaling" + +#. module: field_rrule +#. openerp-web +#: code:addons/field_rrule/static/src/xml/field_rrule.xml:10 +#, python-format +msgid "Remove this rule" +msgstr "Verwijder deze regel" + +#. module: field_rrule +#. openerp-web +#: code:addons/field_rrule/static/src/js/field_rrule.js:276 +#, python-format +msgid "Saturday" +msgstr "Zaterdag" + +#. module: field_rrule +#. openerp-web +#: code:addons/field_rrule/static/src/js/field_rrule.js:290 +#, python-format +msgid "Secondly" +msgstr "Secondelijks" + +#. module: field_rrule +#. openerp-web +#: code:addons/field_rrule/static/src/js/field_rrule.js:277 +#, python-format +msgid "Sunday" +msgstr "Zondag" + +#. module: field_rrule +#. openerp-web +#: code:addons/field_rrule/static/src/js/field_rrule.js:274 +#, python-format +msgid "Thursday" +msgstr "Donderdag" + +#. module: field_rrule +#. openerp-web +#: code:addons/field_rrule/static/src/js/field_rrule.js:272 +#, python-format +msgid "Tuesday" +msgstr "Dinsdag" + +#. module: field_rrule +#. openerp-web +#: code:addons/field_rrule/static/src/js/field_rrule.js:273 +#, python-format +msgid "Wednesday" +msgstr "Woensdag" + +#. module: field_rrule +#. openerp-web +#: code:addons/field_rrule/static/src/xml/field_rrule.xml:89 +#, python-format +msgid "Weekday" +msgstr "Weekdag" + +#. module: field_rrule +#. openerp-web +#: code:addons/field_rrule/static/src/js/field_rrule.js:286 +#: code:addons/field_rrule/static/src/xml/field_rrule.xml:47 +#, python-format +msgid "Weekly" +msgstr "Wekelijks" + +#. module: field_rrule +#. openerp-web +#: code:addons/field_rrule/static/src/js/field_rrule.js:284 +#: code:addons/field_rrule/static/src/xml/field_rrule.xml:49 +#, python-format +msgid "Yearly" +msgstr "Jaarlijks" + diff --git a/field_rrule/static/description/icon.png b/field_rrule/static/description/icon.png new file mode 100644 index 000000000..3a0328b51 Binary files /dev/null and b/field_rrule/static/description/icon.png differ diff --git a/field_rrule/static/src/css/field_rrule.css b/field_rrule/static/src/css/field_rrule.css new file mode 100644 index 000000000..2a4cd1c34 --- /dev/null +++ b/field_rrule/static/src/css/field_rrule.css @@ -0,0 +1,22 @@ +.oe_form_field_rrule .rule_item label +{ + white-space: nowrap; +} +.oe_form_field_rrule .rule_header +{ + text-align: right; + padding-right: 8px; +} +.oe_form_field_rrule th +{ + vertical-align: top; + padding-right: 8px; +} +.oe_form_editable .oe_form_field_rrule form.rule_item:hover .rule_header +{ + background: #7c7bad; +} +.oe_form_editable .oe_form_field_rrule form.rule_item:hover .rule_header .oe_link +{ + color: white; +} diff --git a/field_rrule/static/src/js/field_rrule.js b/field_rrule/static/src/js/field_rrule.js new file mode 100644 index 000000000..605d01b9e --- /dev/null +++ b/field_rrule/static/src/js/field_rrule.js @@ -0,0 +1,311 @@ +//-*- coding: utf-8 -*- +//© 2016 Therp BV +//License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +openerp.field_rrule = function(instance) +{ + instance.field_rrule.FieldRRule = instance.web.form.AbstractField + .extend(instance.web.form.ReinitializeFieldMixin, { + template: 'FieldRRule', + events: { + 'click a.add_rule': 'add_rule', + 'click a.rule_remove': 'remove_rule', + 'change select': 'frequency_changed', + 'change input:not(.rule_ignore_input)': 'input_changed', + 'change input[name="recurrence_type"]': 'toggle_recurrence_type', + }, + set_value: function(val) + { + var result = this._super(jQuery.extend([], val)); + _.each(this.get('value'), function(rule) + { + rule.__id = _.uniqueId(); + }); + this.reinitialize(); + return result; + }, + get_value: function() + { + var result = jQuery.extend( + true, [], this._super.apply(this, arguments)); + _.each(result, function(rule) + { + delete rule.__id; + }); + return result; + }, + initialize_content: function() + { + var self = this; + this.$('select[name="freq"]').trigger('change', true); + this.$('input[name="recurrence_type"]:checked') + .trigger('change', true); + this.$('input[type="datetime"]').each(function() + { + var input = jQuery(this), + current_value = input.val(); + input.datetimepicker({ + closeOnDateSelect: true, + value: current_value ? instance.web.str_to_datetime( + instance.web.parse_value( + input.val(), {type: 'datetime'})) : new Date(), + dateFormat: self.datetimepicker_format( + instance.web._t.database.parameters.date_format), + timeFormat: self.datetimepicker_format( + instance.web._t.database.parameters.time_format), + changeMonth: true, + changeYear: true, + showWeek: true, + showButtonPanel: true, + firstDay: Date.CultureInfo.firstDayOfWeek, + }); + }); + }, + datetimepicker_format: function(odoo_format) + { + var result = '', + map = { + '%a': 'D', + '%A': 'DD', + '%b': 'M', + '%B': 'MM', + '%c': '', // locale's datetime representation + '%d': 'dd', + '%H': 'hh', + '%T': 'hh:mm:ss', + '%I': 'h', + '%j': '', // day of the year unsuported + '%m': 'mm', + '%M': 'mm', + '%p': 't', + '%S': 'ss', + '%U': '', // weeknumber unsupported + '%w': '', // weekday unsupported + '%W': '', // weeknumber unsupported + '%x': '', // locale's date representation + '%X': '', // locale's time representation + '%y': 'y', + '%Y': 'yy', + '%Z': 'z', + '%%': '%', + }; + + for(var i=0; i < odoo_format.length; i++) + { + if(map[odoo_format.substring(i, i+2)]) + { + result += map[odoo_format.substring(i, i+2)]; + i++; + } + else + { + result += odoo_format[i]; + } + } + return result; + }, + frequency_changed: function(e, noreset) + { + var frequency = jQuery(e.currentTarget), + current_item = frequency + .parentsUntil('form', 'table.rule_item'); + current_item.find('[data-visible-freq]').each(function() + { + var node = jQuery(this); + node.toggle( + String(node.data('visible-freq')).split(',') + .indexOf(frequency.val()) >= 0 + ); + }); + this.input_changed(e, noreset); + }, + input_changed: function(e, noreset) + { + var input = jQuery(e.currentTarget), + current_item = input + .parentsUntil('form', 'table.rule_item'), + all_values = this.get('value') || [], + old_values = jQuery.extend(true, [], all_values); + value = _.findWhere(all_values, { + '__id': String(current_item.data('id')), + }); + if(jQuery.isArray(value[input.attr('name')])) + { + var input_value = parseInt(input.val()); + value[input.attr('name')] = _.filter( + value[input.attr('name')], function(x) { + return x != input_value; + }); + if(input.is(':checked')) + { + value[input.attr('name')].push(input_value); + } + } + else if(input.attr('type') == 'datetime') + { + value[input.attr('name')] = instance.web.parse_value( + input.val(), {type: 'datetime'}); + } + else + { + value[input.attr('name')] = input.val() || undefined; + } + if(!noreset) + { + this.reset_fields(value, current_item, input.attr('name')); + this.trigger("change:value", this, { + oldValue: old_values, + newValue: all_values, + }); + } + }, + toggle_recurrence_type: function(e, noreset) + { + var type = jQuery(e.currentTarget), + current_item = type.parentsUntil('tr', 'td'); + current_item.find('input:not(.rule_ignore_input)').each(function() + { + var input = jQuery(this), + visible = input.attr('name') == type.attr('value'); + input.toggle(visible); + if(visible) + { + input.trigger('change', noreset); + } + }); + }, + reset_fields: function(rule, current_item, field) + { + // for some fields, we should reset some other fields when they + // were changed + if(field == 'freq') + { + rule.byweekday = []; + rule.bymonthday = []; + rule.byyearday = []; + current_item.find( + '[name="byweekday"], [name="bymonthday"], ' + + '[name="byyearday"]' + ).prop('checked', false); + } + if(field == 'dtstart') + { + rule.byhour = []; + rule.byminute = []; + rule.bysecond = []; + current_item.find( + '[name="byhour"], [name="bysecond"], [name="byminute"]' + ).prop('checked', false); + } + if(field == 'until') + { + rule.count = undefined; + current_item.find('[name="count"]').val('0'); + } + if(field == 'count') + { + rule.until = undefined; + current_item.find('[name="until"]').val(''); + } + }, + add_rule: function(e) + { + var value = this.get('value') || []; + value.push(this.get_default_rrule()); + this.set('value', value); + this.reinitialize(); + }, + remove_rule: function(e) + { + var value = this.get('value') || [], + old_value = jQuery.extend(true, [], value), + current_item = jQuery(e.currentTarget) + .parentsUntil('form', 'table.rule_item'), + current_id = String(current_item.data('id')); + + for(var i = 0; i < value.length; i++) + { + if(value[i].__id == current_id) + { + value.splice(i, 1); + i--; + } + } + this.trigger("change:value", this, { + oldValue: old_value, + newValue: value, + }); + this.reinitialize(); + }, + get_default_rrule: function() + { + return { + __id: _.uniqueId(), + type: 'rrule', + freq: 1, + count: undefined, + interval: 1, + dtstart: instance.datetime_to_str(new Date()), + byweekday: [], + bymonthday: [], + byyearday: [], + }; + }, + on_timepicker_select: function(datestring, input) + { + return this.format_timepicker_value(input.input || input.$input); + }, + on_timepicker_month_year: function(year, month, input) + { + return this.format_timepicker_value(input.input || input.$input); + }, + format_timepicker_value: function(input) + { + input.val(instance.web.format_value( + input.datetimepicker('getDate'), {type: 'datetime'})); + }, + format_field_weekday: function(weekday) + { + switch(parseInt(weekday)) + { + case 0: return instance.web._t('Monday'); + case 1: return instance.web._t('Tuesday'); + case 2: return instance.web._t('Wednesday'); + case 3: return instance.web._t('Thursday'); + case 4: return instance.web._t('Friday'); + case 5: return instance.web._t('Saturday'); + case 6: return instance.web._t('Sunday'); + } + }, + format_field_freq: function(frequency) + { + switch(parseInt(frequency)) + { + case 0: return instance.web._t('Yearly'); + case 1: return instance.web._t('Monthly'); + case 2: return instance.web._t('Weekly'); + case 3: return instance.web._t('Daily'); + case 4: return instance.web._t('Hourly'); + case 5: return instance.web._t('Minutely'); + case 6: return instance.web._t('Secondly'); + } + }, + format_field_dtstart: function(dtstart) + { + return instance.web.format_value(dtstart, {type: 'datetime'}); + }, + format_field_until: function(until) + { + return instance.web.format_value(until, {type: 'datetime'}); + }, + format_field_count: function(count) + { + if(!count) + { + return instance.web._t('Indefinitely'); + } + return _.str.sprintf(instance.web._t('%s occurrences'), count); + }, + }); + instance.web.form.widgets.add('rrule', 'instance.field_rrule.FieldRRule'); +}; diff --git a/field_rrule/static/src/xml/field_rrule.xml b/field_rrule/static/src/xml/field_rrule.xml new file mode 100644 index 000000000..c76f34bdb --- /dev/null +++ b/field_rrule/static/src/xml/field_rrule.xml @@ -0,0 +1,105 @@ + diff --git a/field_rrule/tests/__init__.py b/field_rrule/tests/__init__.py new file mode 100644 index 000000000..ba9245bc9 --- /dev/null +++ b/field_rrule/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# © 2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from . import test_field_rrule diff --git a/field_rrule/tests/test_field_rrule.py b/field_rrule/tests/test_field_rrule.py new file mode 100644 index 000000000..87c97aec9 --- /dev/null +++ b/field_rrule/tests/test_field_rrule.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# © 2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from psycopg2.extensions import AsIs +from dateutil.rrule import MONTHLY, rrule +from openerp import SUPERUSER_ID, fields, models +from openerp.tests.common import TransactionCase +from openerp.addons.field_rrule import FieldRRule +from openerp.addons.field_rrule.field_rrule import SerializableRRuleSet + + +class RRuleTest(models.TransientModel): + _name = 'test.field.rrule' + + # either use a default in object notation + rrule_with_default = FieldRRule(default=[{ + "type": "rrule", + "dtstart": '2016-01-02 00:00:00', + "count": 1, + "freq": MONTHLY, + "interval": 1, + "bymonthday": [1], + }]) + # or pass a SerializableRRuleSet. + # Rember that this class is callable, so passing it directly as default + # would actually pass an rruleset, because odoo uses the result of the + # callable. But in __call__, we check for this case, so nothing to do + rrule_with_default2 = FieldRRule(default=SerializableRRuleSet( + rrule( + dtstart=fields.Datetime.from_string('2016-01-02 00:00:00'), + interval=1, + freq=MONTHLY, + count=1, + bymonthday=[1], + ))) + # also fiddle with an empty one + rrule = FieldRRule() + + +class TestFieldRrule(TransactionCase): + def test_field_rrule(self): + model = RRuleTest._build_model(self.registry, self.cr) + model._prepare_setup(self.cr, SUPERUSER_ID, False) + model._setup_base(self.cr, SUPERUSER_ID, False) + model._setup_fields(self.cr, SUPERUSER_ID) + model._auto_init(self.cr) + record_id = model.create(self.cr, SUPERUSER_ID, {'rrule': None}) + self.cr.execute( + 'select rrule, rrule_with_default, rrule_with_default2 from ' + '%s where id=%s', (AsIs(model._table), record_id)) + data = self.cr.fetchall()[0] + self.assertEqual(data[0], 'null') + self.assertEqual(data[1], data[2]) + record = model.browse(self.cr, SUPERUSER_ID, record_id) + self.assertFalse(record.rrule) + self.assertTrue(record.rrule_with_default) + self.assertTrue(record.rrule_with_default2) + self.assertEqual(record.rrule_with_default, record.rrule_with_default2) + self.assertEqual(record.rrule_with_default.count(), 1) + self.assertFalse(record.rrule_with_default.after( + fields.Datetime.from_string('2016-02-01 00:00:00'))) + self.assertTrue(record.rrule_with_default.after( + fields.Datetime.from_string('2016-02-01 00:00:00'), inc=True)) diff --git a/field_rrule/views/templates.xml b/field_rrule/views/templates.xml new file mode 100644 index 000000000..578223e4f --- /dev/null +++ b/field_rrule/views/templates.xml @@ -0,0 +1,11 @@ + + + + + +