Browse Source

Merge pull request #480 from hbrunn/8.0-field_rrule

[ADD] field_rrule
pull/465/head
Daniel Reis 8 years ago
committed by GitHub
parent
commit
c527061521
  1. 73
      field_rrule/README.rst
  2. 5
      field_rrule/__init__.py
  3. 26
      field_rrule/__openerp__.py
  4. 4
      field_rrule/demo/__init__.py
  5. 26
      field_rrule/demo/res_partner.py
  6. 15
      field_rrule/demo/res_partner.xml
  7. 117
      field_rrule/field_rrule.py
  8. 16
      field_rrule/hooks.py
  9. 216
      field_rrule/i18n/nl.po
  10. BIN
      field_rrule/static/description/icon.png
  11. 22
      field_rrule/static/src/css/field_rrule.css
  12. 311
      field_rrule/static/src/js/field_rrule.js
  13. 105
      field_rrule/static/src/xml/field_rrule.xml
  14. 4
      field_rrule/tests/__init__.py
  15. 63
      field_rrule/tests/test_field_rrule.py
  16. 11
      field_rrule/views/templates.xml

73
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
<https://github.com/OCA/server-tools/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 <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_.
Contributors
------------
* Holger Brunn <hbrunn@therp.nl>
Do not contact contributors directly about help with questions or problems concerning this addon, but use the `community mailing list <mailto:community@mail.odoo.com>`_ or the `appropriate specialized mailinglist <https://odoo-community.org/groups>`_ 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.

5
field_rrule/__init__.py

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# © 2016 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from .field_rrule import FieldRRule
from .hooks import post_load_hook

26
field_rrule/__openerp__.py

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# © 2016 Therp BV <http://therp.nl>
# 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',
],
}

4
field_rrule/demo/__init__.py

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# © 2016 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from . import res_partner

26
field_rrule/demo/res_partner.py

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# © 2016 Therp BV <http://therp.nl>
# 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)

15
field_rrule/demo/res_partner.xml

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<record id="view_partner_form" model="ir.ui.view">
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form" />
<field name="arch" type="xml">
<field name="website" position="after">
<field name="rrule" widget="rrule" />
<field name="rrule_representation" />
</field>
</field>
</record>
</data>
</openerp>

117
field_rrule/field_rrule.py

@ -0,0 +1,117 @@
# -*- coding: utf-8 -*-
# © 2016 Therp BV <http://therp.nl>
# 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

16
field_rrule/hooks.py

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# © 2016 Therp BV <http://therp.nl>
# 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

216
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"

BIN
field_rrule/static/description/icon.png

After

Width: 128  |  Height: 128  |  Size: 9.2 KiB

22
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;
}

311
field_rrule/static/src/js/field_rrule.js

@ -0,0 +1,311 @@
//-*- coding: utf-8 -*-
//© 2016 Therp BV <http://therp.nl>
//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');
};

105
field_rrule/static/src/xml/field_rrule.xml

@ -0,0 +1,105 @@
<template>
<div t-name="FieldRRule" class="oe_form_field_rrule">
<div t-if="!widget.get('effective_readonly') and !widget.options.no_add_rule">
<a class="oe_link add_rule">Add instance</a>
</div>
<form t-foreach="widget.get('value')" t-as="rule" class="rule_item">
<table t-if="rule.type == 'rrule'" class="rule_item" t-att-data-id="rule.__id">
<tr t-if="!widget.get('effective_readonly') and !widget.options.no_add_rule">
<td colspan="2" class="rule_header">
<a class="oe_link rule_remove" title="Remove this rule"><i class="fa fa-times" /></a>
</td>
</tr>
<tr>
<th>Begin</th>
<td t-if="widget.get('effective_readonly')">
<t t-esc="widget.format_field_dtstart(rule.dtstart)" />
</td>
<td t-if="!widget.get('effective_readonly')" class="oe_form_required">
<input name="dtstart" type="datetime" t-att-value="widget.format_field_dtstart(rule.dtstart)" required="required" />
</td>
</tr>
<tr>
<th>End</th>
<td t-if="widget.get('effective_readonly')">
<t t-esc="widget.format_field_dtstart(rule.until) or widget.format_field_count(rule.count)" />
</td>
<td t-if="!widget.get('effective_readonly')" class="oe_form_required">
<div>
<label>
<input type="radio" name="recurrence_type" value="until" class="rule_ignore_input" t-att-checked="rule.until and 'checked' or None" />At a date
</label>
<label>
<input type="radio" name="recurrence_type" value="count" class="rule_ignore_input" t-att-checked="!rule.until and 'checked' or None" />After an amount of occurrences
</label>
</div>
<input name="until" type="datetime" t-att-value="widget.format_field_until(rule.until)" required="required" />
<input name="count" type="number" step="1" t-att-value="rule.count" required="required" placeholder="Leave empty for no limit" />
</td>
</tr>
<tr>
<th>Recurrence</th>
<td t-if="widget.get('effective_readonly')">
<t t-esc="widget.format_field_freq(rule.freq)" />
</td>
<td t-if="!widget.get('effective_readonly')" class="oe_form_required">
<select name="freq" type="select" required="required">
<option value="2" t-att-selected="rule.freq == 2 and 'selected' or None">Weekly</option>
<option value="1" t-att-selected="rule.freq == 1 and 'selected' or None">Monthly</option>
<option value="0" t-att-selected="rule.freq == 0 and 'selected' or None">Yearly</option>
</select>
</td>
</tr>
<tr>
<th>Interval</th>
<td t-if="widget.get('effective_readonly')">
<t t-esc="rule.interval" />
</td>
<td t-if="!widget.get('effective_readonly')" class="oe_form_required">
<input type="number" step="1" name="interval" t-att-value="rule.interval" />
</td>
</tr>
<tr data-visible-freq="0" t-if="!widget.get('effective_readonly') or rule.freq == 0">
<th>Day of year</th>
<td t-if="!widget.get('effective_readonly')">
<t t-foreach="_.range(1, 366)" t-as="day_of_year">
<label><input name="byyearday" type="checkbox" t-att-value="day_of_year" t-att-checked="rule.byyearday.indexOf(day_of_year) >= 0 and 'checked' or None" /><t t-esc="day_of_year" /></label>
</t>
</td>
<td t-if="widget.get('effective_readonly')">
<t t-foreach="rule.byyearday" t-as="day_of_year">
<t t-esc="day_of_year" /><t t-if="!day_of_year_last">, </t>
</t>
</td>
</tr>
<tr data-visible-freq="1" t-if="!widget.get('effective_readonly') or rule.freq == 1">
<th>Day of month</th>
<td t-if="!widget.get('effective_readonly')">
<t t-foreach="_.range(1, 32)" t-as="day_of_month">
<label><input name="bymonthday" type="checkbox" t-att-value="day_of_month" t-att-checked="rule.bymonthday.indexOf(day_of_month) >= 0 and 'checked' or None" /><t t-esc="day_of_month" /></label>
</t>
</td>
<td t-if="widget.get('effective_readonly')">
<t t-foreach="rule.bymonthday" t-as="day_of_month">
<t t-esc="day_of_month" /><t t-if="!day_of_month_last">, </t>
</t>
</td>
</tr>
<tr data-visible-freq="2" t-if="!widget.get('effective_readonly') or rule.freq == 2">
<th>Weekday</th>
<td t-if="!widget.get('effective_readonly')">
<t t-foreach="_.range(0, 7)" t-as="weekday">
<label><input name="byweekday" type="checkbox" t-att-value="weekday" t-att-checked="rule.byweekday.indexOf(weekday) >= 0 and 'checked' or None" /><t t-esc="widget.format_field_weekday(weekday)" /></label>
</t>
</td>
<td t-if="widget.get('effective_readonly')">
<t t-foreach="rule.byweekday" t-as="weekday">
<t t-esc="widget.format_field_weekday(weekday)" /><t t-if="!weekday_last">, </t>
</t>
</td>
</tr>
</table>
<hr t-if="!rule_last" />
</form>
</div>
</template>

4
field_rrule/tests/__init__.py

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# © 2016 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from . import test_field_rrule

63
field_rrule/tests/test_field_rrule.py

@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
# © 2016 Therp BV <http://therp.nl>
# 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))

11
field_rrule/views/templates.xml

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<template id="assets_backend" name="field_rrule assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<script type="text/javascript" src="/field_rrule/static/src/js/field_rrule.js"></script>
<link rel="stylesheet" href="/field_rrule/static/src/css/field_rrule.css"/>
</xpath>
</template>
</data>
</openerp>
Loading…
Cancel
Save