Daniel Reis
8 years ago
committed by
GitHub
16 changed files with 1014 additions and 0 deletions
-
73field_rrule/README.rst
-
5field_rrule/__init__.py
-
26field_rrule/__openerp__.py
-
4field_rrule/demo/__init__.py
-
26field_rrule/demo/res_partner.py
-
15field_rrule/demo/res_partner.xml
-
117field_rrule/field_rrule.py
-
16field_rrule/hooks.py
-
216field_rrule/i18n/nl.po
-
BINfield_rrule/static/description/icon.png
-
22field_rrule/static/src/css/field_rrule.css
-
311field_rrule/static/src/js/field_rrule.js
-
105field_rrule/static/src/xml/field_rrule.xml
-
4field_rrule/tests/__init__.py
-
63field_rrule/tests/test_field_rrule.py
-
11field_rrule/views/templates.xml
@ -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. |
@ -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 |
@ -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', |
|||
], |
|||
} |
@ -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 |
@ -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) |
@ -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> |
@ -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 |
@ -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 |
@ -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" |
|||
|
After Width: 128 | Height: 128 | Size: 9.2 KiB |
@ -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; |
|||
} |
@ -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'); |
|||
}; |
@ -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> |
@ -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 |
@ -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)) |
@ -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> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue