Holger Brunn
9 years ago
No known key found for this signature in database
GPG Key ID: 1C9760FECA3AE18
13 changed files with 647 additions and 0 deletions
-
67field_rrule/README.rst
-
5field_rrule/__init__.py
-
20field_rrule/__openerp__.py
-
26field_rrule/demo/res_partner.py
-
15field_rrule/demo/res_partner.xml
-
102field_rrule/field_rrule.py
-
BINfield_rrule/static/description/icon.png
-
22field_rrule/static/src/css/field_rrule.css
-
261field_rrule/static/src/js/field_rrule.js
-
105field_rrule/static/src/xml/field_rrule.xml
-
4field_rrule/tests/__init__.py
-
9field_rrule/tests/test_field_rrule.py
-
11field_rrule/views/templates.xml
@ -0,0 +1,67 @@ |
|||||
|
.. 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.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``. |
||||
|
|
||||
|
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 . import res_partner |
@ -0,0 +1,20 @@ |
|||||
|
# -*- 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', |
||||
|
], |
||||
|
"qweb": [ |
||||
|
'static/src/xml/field_rrule.xml', |
||||
|
], |
||||
|
} |
@ -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="d" 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,102 @@ |
|||||
|
# -*- 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, YEARLY |
||||
|
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.Model): |
||||
|
return self |
||||
|
result = rruleset() |
||||
|
result._rrule = self._rrule |
||||
|
result._rdate = self._rdate |
||||
|
result._exrule = self._exrule |
||||
|
result._exdate = self._exdate |
||||
|
return result |
||||
|
|
||||
|
def __exit__(self): |
||||
|
pass |
||||
|
|
||||
|
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)) |
||||
|
|
||||
|
|
||||
|
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 |
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,261 @@ |
|||||
|
//-*- 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(val) |
||||
|
{ |
||||
|
var result = this._super(jQuery.extend([], val)); |
||||
|
_.each(this.get('value'), function(rule) |
||||
|
{ |
||||
|
rule.__id = _.uniqueId(); |
||||
|
}); |
||||
|
this.reinitialize(); |
||||
|
return result; |
||||
|
}, |
||||
|
get_value() |
||||
|
{ |
||||
|
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({ |
||||
|
onSelect: self.proxy('on_timepicker_select'), |
||||
|
onClose: self.proxy('on_timepicker_select'), |
||||
|
closeOnDateSelect: true, |
||||
|
value: current_value ? instance.web.str_to_datetime( |
||||
|
instance.web.parse_value( |
||||
|
input.val(), {type: 'datetime'})) : new Date(), |
||||
|
}); |
||||
|
}); |
||||
|
}, |
||||
|
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) |
||||
|
{ |
||||
|
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'); |
||||
|
} |
||||
|
}); |
||||
|
}, |
||||
|
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')"> |
||||
|
<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')"> |
||||
|
<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 None or 'checked'" />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,9 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2016 Therp BV <http://therp.nl> |
||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). |
||||
|
from openerp.tests.common import TransactionCase |
||||
|
|
||||
|
|
||||
|
class TestFieldRrule(TransactionCase): |
||||
|
def test_field_rrule(self): |
||||
|
pass |
@ -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