Browse Source

9.0 add date range Sorrento Delivery

* [ADD] Basic structure for the new date range module

* [IMP] Add a basic description into the README

* [IMP] Basic implementation

* [IMP] First working implementation

* [IMP] Improve datamodel

* [ADD] Add basic tests for date.range

* [PEP8]

* [PYLINT]

* [DEL] Remove unused code

* [IMP] Remove unsused dependencies into the JS

* [IMP] Better operator label for date range

* [DEL] Remove unused file

* [IMP] Better user experience by showing the select input only once empty

* [FIX]Try to fix tests that fails only on travis by adding an explicit cast on the daterange methods parameters

* [FIX]Try to fix tests that fails only on travis by adding an explicit cast on the daterange methods parameters

* [FIX]Try to fix tests that fails only on travis by using postgresql 9.4

* [FIX]Try with postgresql 9.2 since the daterange method has appeared in 9.2

* [IMP] Add a limitation into the module description to warm about the minimal version of postgresql to use

* [IMP]Add multi-company rules

* [IMP]Remove unused files

* [FIX] Add missing brackets into JS

* [FIX] Overlap detection when company_id is False

* [IMP] Add default order for date.range

* [IMP] Add date range generator

* [FIX] OE compatibility

* [FIX] Travis

* [IMP] Code cleanup and improves test coverage

* [FIX] Add missing dependency on 'web'

* [PYLINT] remove unused import

* [FIX] Add missing copyright

* [FIX] Limits are included into the range

* [IMP][date_range] Security

* [IMP] Improve module description

* [IMP] Spelling
pull/411/head
Laurent Mignon (ACSONE) 8 years ago
committed by Joël Grand-Guillaume
parent
commit
d2b1272aa0
  1. 1
      .travis.yml
  2. 108
      date_range/README.rst
  3. 6
      date_range/__init__.py
  4. 27
      date_range/__openerp__.py
  5. 0
      date_range/i18n/.empty
  6. 6
      date_range/models/__init__.py
  7. 77
      date_range/models/date_range.py
  8. 28
      date_range/models/date_range_type.py
  9. 19
      date_range/security/date_range_security.xml
  10. 5
      date_range/security/ir.model.access.csv
  11. BIN
      date_range/static/description/date_range_as_filter.png
  12. BIN
      date_range/static/description/date_range_as_filter_result.png
  13. BIN
      date_range/static/description/date_range_create.png
  14. BIN
      date_range/static/description/date_range_type_as_filter.png
  15. BIN
      date_range/static/description/date_range_type_create.png
  16. BIN
      date_range/static/description/date_range_wizard.png
  17. BIN
      date_range/static/description/date_range_wizard_result.png
  18. BIN
      date_range/static/description/icon.png
  19. 79
      date_range/static/description/icon.svg
  20. 117
      date_range/static/src/js/date_range.js
  21. 12
      date_range/static/src/xml/date_range.xml
  22. 7
      date_range/tests/__init__.py
  23. 100
      date_range/tests/test_date_range.py
  24. 34
      date_range/tests/test_date_range_generator.py
  25. 20
      date_range/tests/test_date_range_type.py
  26. 8
      date_range/views/assets.xml
  27. 84
      date_range/views/date_range_view.xml
  28. 5
      date_range/wizard/__init__.py
  29. 67
      date_range/wizard/date_range_generator.py
  30. 40
      date_range/wizard/date_range_generator.xml

1
.travis.yml

@ -6,6 +6,7 @@ python:
- "2.7"
addons:
postgresql: "9.2" # minimal postgresql version for the daterange method
apt:
packages:
- expect-dev # provides unbuffer utility

108
date_range/README.rst

@ -0,0 +1,108 @@
.. 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
==========
Date Range
==========
This module lets you define global date ranges that can be used to filter
your values in tree views.
Usage
=====
To configure this module, you need to:
* Go to Settings > Technical > Date ranges > Date Range Types where
you can create types of date ranges.
.. figure:: static/description/date_range_type_create.png
:scale: 80 %
:alt: Create a type of date range
* Go to Settings > Technical > Date ranges > Date Ranges where
you can create date ranges.
.. figure:: static/description/date_range_create.png
:scale: 80 %
:alt: Date range creation
It's also possible to launch a wizard from the 'Generate Date Ranges' menu.
.. figure:: static/description/date_range_wizard.png
:scale: 80 %
:alt: Date range wizard
The wizard is useful to generate recurring periods.
.. figure:: static/description/date_range_wizard_result.png
:scale: 80 %
:alt: Date range wizard result
* Your date ranges are now available in the search filter for any date or datetime fields
Date range types are proposed as a filter operator
.. figure:: static/description/date_range_type_as_filter.png
:scale: 80 %
:alt: Date range type available as filter operator
Once a type is selected, date ranges of this type are porposed as a filter value
.. figure:: static/description/date_range_as_filter.png
:scale: 80 %
:alt: Date range as filter value
And the dates specified into the date range are used to filter your result.
.. figure:: static/description/date_range_as_filter_result.png
:scale: 80 %
:alt: Date range as filter result
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
:alt: Try me on Runbot
:target: https://runbot.odoo-community.org/runbot/149/9.0
Known issues / Roadmap
======================
* The addon use the daterange method from postgres. This method is supported as of postgresql 9.2
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
------------
* Laurent Mignon <laurent.mignon@acsone.eu>
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.

6
date_range/__init__.py

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# © 2016 ACSONE SA/NV (<http://acsone.eu>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import models
from . import wizard

27
date_range/__openerp__.py

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# © 2016 ACSONE SA/NV (<http://acsone.eu>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
"name": "Date Range",
"summary": "Manage all kind of date range",
"version": "9.0.1.0.0",
"category": "Uncategorized",
"website": "https://odoo-community.org/",
"author": "ACSONE SA/NV, Odoo Community Association (OCA)",
"license": "AGPL-3",
"application": False,
"installable": True,
"depends": [
"web",
],
"data": [
"security/ir.model.access.csv",
"security/date_range_security.xml",
"views/assets.xml",
"views/date_range_view.xml",
"wizard/date_range_generator.xml",
],
"qweb": [
"static/src/xml/date_range.xml",
]
}

0
date_range/i18n/.empty

6
date_range/models/__init__.py

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# © 2016 ACSONE SA/NV (<http://acsone.eu>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import date_range_type
from . import date_range

77
date_range/models/date_range.py

@ -0,0 +1,77 @@
# -*- coding: utf-8 -*-
# © 2016 ACSONE SA/NV (<http://acsone.eu>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from openerp import api, fields, models
from openerp.tools.translate import _
from openerp.exceptions import ValidationError
class DateRange(models.Model):
_name = "date.range"
_order = "type_name,date_start"
@api.model
def _default_company(self):
return self.env['res.company']._company_default_get('date.range')
name = fields.Char(required=True, translate=True)
date_start = fields.Date(string='Start date', required=True)
date_end = fields.Date(string='End date', required=True)
type_id = fields.Many2one(
comodel_name='date.range.type', string='Type', select=1, required=True)
type_name = fields.Char(
string='Type', related='type_id.name', readonly=True, store=True)
company_id = fields.Many2one(
comodel_name='res.company', string='Company', select=1,
default=_default_company)
active = fields.Boolean(
help="The active field allows you to hide the date range without "
"removing it.", default=True)
_sql_constraints = [
('date_range_uniq', 'unique (name,type_id, company_id)',
'A date range must be unique per company !')]
@api.constrains('type_id', 'date_start', 'date_end', 'company_id')
def _validate_range(self):
for this in self:
start = fields.Date.from_string(this.date_start)
end = fields.Date.from_string(this.date_end)
if start >= end:
raise ValidationError(
_("%s is not a valid range (%s >= %s)") % (
this.name, this.date_start, this.date_end))
if this.type_id.allow_overlap:
continue
# here we use a plain SQL query to benefit of the daterange
# function available in PostgresSQL
# (http://www.postgresql.org/docs/current/static/rangetypes.html)
SQL = """
SELECT
id
FROM
date_range dt
WHERE
DATERANGE(dt.date_start, dt.date_end, '[]') &&
DATERANGE(%s::date, %s::date, '[]')
AND dt.id != %s
AND dt.active
AND dt.company_id = %s
AND dt.type_id=%s;"""
self.env.cr.execute(SQL, (this.date_start,
this.date_end,
this.id,
this.company_id.id or None,
this.type_id.id))
res = self.env.cr.fetchall()
if res:
dt = self.browse(res[0][0])
raise ValidationError(
_("%s overlaps %s") % (this.name, dt.name))
@api.multi
def get_domain(self, field_name):
self.ensure_one()
return [(field_name, '>=', self.date_start),
(field_name, '<=', self.date_end)]

28
date_range/models/date_range_type.py

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# © 2016 ACSONE SA/NV (<http://acsone.eu>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from openerp import api, fields, models
class DateRangeType(models.Model):
_name = "date.range.type"
@api.model
def _default_company(self):
return self.env['res.company']._company_default_get('date.range')
name = fields.Char(required=True, translate=True)
allow_overlap = fields.Boolean(
help="If sets date range of same type must not overlap.",
default=False)
active = fields.Boolean(
help="The active field allows you to hide the date range without "
"removing it.", default=True)
company_id = fields.Many2one(
comodel_name='res.company', string='Company', select=1,
default=_default_company)
_sql_constraints = [
('date_range_type_uniq', 'unique (name,company_id)',
'A date range type must be unique per company !')]

19
date_range/security/date_range_security.xml

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<data noupdate="1">
<record id="date_range_type_comp_rule" model="ir.rule">
<field name="name">Date Range Type multi-company</field>
<field name="model_id" ref="model_date_range_type"/>
<field eval="True" name="global"/>
<field name="domain_force"> ['|',('company_id','=',user.company_id.id),('company_id','=',False)]</field>
<field eval="False" name="active"/>
</record>
<record id="date_range_comp_rule" model="ir.rule">
<field name="name">Date Range multi-company</field>
<field name="model_id" ref="model_date_range"/>
<field eval="True" name="global"/>
<field name="domain_force"> ['|',('company_id','=',user.company_id.id),('company_id','=',False)]</field>
<field eval="False" name="active"/>
</record>
</data>
</odoo>

5
date_range/security/ir.model.access.csv

@ -0,0 +1,5 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_date_range_date_range,date_range.date_range,model_date_range,base.group_user,1,0,0,0
access_date_range_date_range_type,date_range.date_range_type,model_date_range_type,base.group_user,1,0,0,0
access_date_range_date_range_config,date_range.date_range.config,model_date_range,base.group_configuration,1,1,1,1
access_date_range_date_range_type_config,date_range.date_range_type.config,model_date_range_type,base.group_configuration,1,1,1,1

BIN
date_range/static/description/date_range_as_filter.png

After

Width: 1412  |  Height: 429  |  Size: 140 KiB

BIN
date_range/static/description/date_range_as_filter_result.png

After

Width: 1896  |  Height: 640  |  Size: 242 KiB

BIN
date_range/static/description/date_range_create.png

After

Width: 1529  |  Height: 541  |  Size: 99 KiB

BIN
date_range/static/description/date_range_type_as_filter.png

After

Width: 1394  |  Height: 544  |  Size: 182 KiB

BIN
date_range/static/description/date_range_type_create.png

After

Width: 1901  |  Height: 699  |  Size: 78 KiB

BIN
date_range/static/description/date_range_wizard.png

After

Width: 1026  |  Height: 346  |  Size: 28 KiB

BIN
date_range/static/description/date_range_wizard_result.png

After

Width: 1545  |  Height: 294  |  Size: 56 KiB

BIN
date_range/static/description/icon.png

After

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

79
date_range/static/description/icon.svg
File diff suppressed because it is too large
View File

117
date_range/static/src/js/date_range.js

@ -0,0 +1,117 @@
/* © 2016 ACSONE SA/NV (<http://acsone.eu>)
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */
odoo.define('date_range.search_filters', function (require) {
"use strict";
var core = require('web.core');
var data = require('web.data');
var filters = require('web.search_filters');
var Model = require('web.Model');
var framework = require('web.framework');
var _t = core._t;
var _lt = core._lt;
filters.ExtendedSearchProposition.include({
select_field: function(field) {
this._super.apply(this, arguments);
this.is_date_range_selected = false;
this.is_date = field.type == 'date' || field.type == 'datetime';
this.$value = this.$el.find('.searchview_extended_prop_value, .o_searchview_extended_prop_value');
if (this.is_date){
var ds = new data.DataSetSearch(this, 'date.range.type', this.context, [[1, '=', 1]]);
ds.read_slice(['name'], {}).done(this.proxy('add_date_range_types_operator'));
}
},
add_date_range_types_operator: function(date_range_types){
var self = this;
_.each(date_range_types, function(drt) {
$('<option>', {value: 'drt_' + drt.id})
.text(_('in ') + drt.name)
.appendTo(self.$el.find('.searchview_extended_prop_op, .o_searchview_extended_prop_op'));
});
},
operator_changed: function (e) {
var val = $(e.target).val();
this.is_date_range_selected = val.startsWith('drt_');
if (this.is_date_range_selected){
var type_id = val.replace('drt_', '');
this.date_range_type_operator_selected(type_id);
return;
}
this._super.apply(this, arguments);
},
date_range_type_operator_selected: function(type_id){
this.$value.empty().show();
var ds = new data.DataSetSearch(this, 'date.range', this.context, [['type_id', '=', parseInt(type_id)]]);
ds.read_slice(['name','date_start', 'date_end'], {}).done(this.proxy('on_range_type_selected'));
},
on_range_type_selected: function(date_range_values){
this.value = new filters.ExtendedSearchProposition.DateRange(this, this.value.field, date_range_values);
this.value.appendTo(this.$value);
if (!this.$el.hasClass('o_filter_condition')){
this.$value.find('.date-range-select').addClass('form-control');
}
this.value.on_range_selected();
},
get_filter: function () {
var res = this._super.apply(this, arguments);
if (this.is_date_range_selected){
// in case of date.range, the domain is provided by the server and we don't
// want to put nest the returned value into an array.
res.attrs.domain = this.value.domain;
}
return res;
},
});
filters.ExtendedSearchProposition.DateRange = filters.ExtendedSearchProposition.Field.extend({
template: 'SearchView.extended_search.dateRange.selection',
events: {
'change': 'on_range_selected',
},
init: function (parent, field, date_range_values) {
this._super(parent, field);
this.date_range_values = date_range_values;
},
toString: function () {
var select = this.$el[0];
var option = select.options[select.selectedIndex];
return option.label || option.text;
},
get_value: function() {
return parseInt(this.$el.val());
},
on_range_selected: function(e){
var self = this;
self.domain = '';
framework.blockUI();
new Model("date.range")
.call("get_domain", [
[this.get_value()],
this.field.name,
{}
])
.then(function (domain) {
framework.unblockUI();
self.domain = domain;
});
},
get_domain: function (field, operator) {
return this.domain;
},
});
});

12
date_range/static/src/xml/date_range.xml

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<template>
<t t-name="SearchView.extended_search.dateRange.selection">
<select class="date-range-select">
<t t-as="element" t-foreach="widget.date_range_values">
<option t-att-value="element.id">
<t t-esc="element.name"/>
</option>
</t>
</select>
</t>
</template>

7
date_range/tests/__init__.py

@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
# © 2016 ACSONE SA/NV (<http://acsone.eu>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import test_date_range_type
from . import test_date_range
from . import test_date_range_generator

100
date_range/tests/test_date_range.py

@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
# © 2016 ACSONE SA/NV (<http://acsone.eu>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
from openerp.tests.common import TransactionCase
from openerp.exceptions import ValidationError
class DateRangeTest(TransactionCase):
def setUp(self):
super(DateRangeTest, self).setUp()
self.type = self.env['date.range.type'].create(
{'name': 'Fiscal year',
'company_id': False,
'allow_overlap': False})
def test_default_company(self):
date_range = self.env['date.range']
dt = date_range.create({
'name': 'FS2016',
'date_start': '2015-01-01',
'date_end': '2016-12-31',
'type_id': self.type.id,
})
self.assertTrue(dt.company_id)
# you can specify company_id to False
dt = date_range.create({
'name': 'FS2016_NO_COMPANY',
'date_start': '2015-01-01',
'date_end': '2016-12-31',
'type_id': self.type.id,
'company_id': False
})
self.assertFalse(dt.company_id)
def test_empty_company(self):
date_range = self.env['date.range']
dt = date_range.create({
'name': 'FS2016',
'date_start': '2015-01-01',
'date_end': '2016-12-31',
'type_id': self.type.id,
'company_id': None,
})
self.assertEqual(dt.name, 'FS2016')
def test_invalid(self):
date_range = self.env['date.range']
with self.assertRaises(ValidationError) as cm:
date_range.create({
'name': 'FS2016',
'date_end': '2015-01-01',
'date_start': '2016-12-31',
'type_id': self.type.id,
})
self.assertEqual(
cm.exception.name,
'FS2016 is not a valid range (2016-12-31 >= 2015-01-01)')
def test_overlap(self):
date_range = self.env['date.range']
date_range.create({
'name': 'FS2015',
'date_start': '2015-01-01',
'date_end': '2015-12-31',
'type_id': self.type.id,
})
with self.assertRaises(ValidationError) as cm, self.env.cr.savepoint():
date_range.create({
'name': 'FS2016',
'date_start': '2015-01-01',
'date_end': '2016-12-31',
'type_id': self.type.id,
})
self.assertEqual(cm.exception.name, 'FS2016 overlaps FS2015')
# check it's possible to overlap if it's allowed by the date range type
self.type.allow_overlap = True
dr = date_range.create({
'name': 'FS2016',
'date_start': '2015-01-01',
'date_end': '2016-12-31',
'type_id': self.type.id,
})
self.assertEquals(dr.name, 'FS2016')
def test_domain(self):
date_range = self.env['date.range']
dr = date_range.create({
'name': 'FS2015',
'date_start': '2015-01-01',
'date_end': '2015-12-31',
'type_id': self.type.id,
})
domain = dr.get_domain('my_field')
# By default the domain include limits
self.assertEquals(
domain,
[('my_field', '>=', '2015-01-01'),
('my_field', '<=', '2015-12-31')])

34
date_range/tests/test_date_range_generator.py

@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
# © 2016 ACSONE SA/NV (<http://acsone.eu>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)nses/agpl).
from openerp.tests.common import TransactionCase
from dateutil.rrule import MONTHLY
class DateRangeGeneratorTest(TransactionCase):
def setUp(self):
super(DateRangeGeneratorTest, self).setUp()
self.type = self.env['date.range.type'].create(
{'name': 'Fiscal year',
'company_id': False,
'allow_overlap': False})
def test_generate(self):
generator = self.env['date.range.generator']
generator = generator.create({
'date_start': '1943-01-01',
'name_prefix': '1943-',
'type_id': self.type.id,
'duration_count': 3,
'unit_of_time': MONTHLY,
'count': 4})
generator.action_apply()
ranges = self.env['date.range'].search(
[('type_id', '=', self.type.id)])
self.assertEquals(len(ranges), 4)
range4 = ranges[3]
self.assertEqual(range4.date_start, '1943-10-01')
self.assertEqual(range4.date_end, '1943-12-31')
self.assertEqual(range4.type_id, self.type)

20
date_range/tests/test_date_range_type.py

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# © 2016 ACSONE SA/NV (<http://acsone.eu>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
from openerp.tests.common import TransactionCase
class DateRangeTypeTest(TransactionCase):
def test_default_company(self):
drt = self.env['date.range.type'].create(
{'name': 'Fiscal year',
'allow_overlap': False})
self.assertTrue(drt.company_id)
# you can specify company_id to False
drt = self.env['date.range.type'].create(
{'name': 'Fiscal year',
'company_id': False,
'allow_overlap': False})
self.assertFalse(drt.company_id)

8
date_range/views/assets.xml

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<template id="assets_backend" inherit_id="web.assets_backend" name="Module name backend assets">
<xpath expr="." position="inside">
<script src="/date_range/static/src/js/date_range.js" type="text/javascript"></script>
</xpath>
</template>
</odoo>

84
date_range/views/date_range_view.xml

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="view_date_range_tree" model="ir.ui.view">
<field name="name">date.range.tree</field>
<field name="model">date.range</field>
<field name="arch" type="xml">
<tree editable="bottom" string="Date range">
<field name="name"/>
<field name="type_id"/>
<field name="date_start"/>
<field name="date_end"/>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
<field name="active"/>
</tree>
</field>
</record>
<record id="view_date_range_form_view" model="ir.ui.view">
<field name="name">date.range.form</field>
<field name="model">date.range</field>
<field name="arch" type="xml">
<form string="Date Range">
<group col="4">
<field name="name"/>
<field name="type_id"/>
<field name="date_start"/>
<field name="date_end"/>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
<field name="active"/>
</group>
</form>
</field>
</record>
<record id="view_date_range_type_tree" model="ir.ui.view">
<field name="name">date.range.type.tree</field>
<field name="model">date.range.type</field>
<field name="arch" type="xml">
<tree editable="bottom" string="Date range type">
<field name="name"/>
<field name="allow_overlap"/>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
<field name="active"/>
</tree>
</field>
</record>
<record id="view_date_range_type_form_view" model="ir.ui.view">
<field name="name">date.range.type.form</field>
<field name="model">date.range.type</field>
<field name="arch" type="xml">
<form string="Date Range Type">
<group col="4">
<field name="name"/>
<field name="allow_overlap"/>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
<field name="active"/>
</group>
</form>
</field>
</record>
<record id="date_range_action" model="ir.actions.act_window">
<field name="name">Date Ranges</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">date.range</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="view_date_range_tree"/>
<field name="domain">[]</field>
<field name="context">{}</field>
</record>
<record id="date_range_type_action" model="ir.actions.act_window">
<field name="name">Date Range Types</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">date.range.type</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="view_date_range_type_tree"/>
<field name="domain">[]</field>
<field name="context">{}</field>
</record>
<menuitem id="menu_date_range" name="Date ranges"
parent="base.menu_custom" sequence="1"/>
<menuitem action="date_range_action" id="menu_date_range_action" parent="menu_date_range"/>
<menuitem action="date_range_type_action"
id="menu_date_range_type_action" parent="menu_date_range"/>
</odoo>

5
date_range/wizard/__init__.py

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# © 2016 ACSONE SA/NV (<http://acsone.eu>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import date_range_generator

67
date_range/wizard/date_range_generator.py

@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
# © 2016 ACSONE SA/NV (<http://acsone.eu>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from openerp import api, fields, models
from dateutil.rrule import (rrule,
YEARLY,
MONTHLY,
WEEKLY,
DAILY)
from dateutil.relativedelta import relativedelta
class DateRangeGenerator(models.TransientModel):
_name = 'date.range.generator'
@api.model
def _default_company(self):
return self.env['res.company']._company_default_get('date.range')
name_prefix = fields.Char('Range name prefix', required=True)
date_start = fields.Date(strint='Start date', required=True)
type_id = fields.Many2one(
comodel_name='date.range.type', string='Type', required=True,
ondelete='cascade')
company_id = fields.Many2one(
comodel_name='res.company', string='Company',
default=_default_company)
unit_of_time = fields.Selection([
(YEARLY, 'years'),
(MONTHLY, 'months'),
(WEEKLY, 'weeks'),
(DAILY, 'days')], required=True)
duration_count = fields.Integer('Duration', required=True)
count = fields.Integer(
string="Number of ranges to generate", required=True)
@api.multi
def _compute_date_ranges(self):
self.ensure_one()
vals = rrule(freq=self.unit_of_time, interval=self.duration_count,
dtstart=fields.Date.from_string(self.date_start),
count=self.count+1)
vals = list(vals)
date_ranges = []
for idx, dt_start in enumerate(vals[:-1]):
date_start = fields.Date.to_string(dt_start.date())
# always remove 1 day for the date_end since range limits are
# inclusive
dt_end = vals[idx+1].date() - relativedelta(days=1)
date_end = fields.Date.to_string(dt_end)
date_ranges.append({
'name': '%s-%d' % (self.name_prefix, idx + 1),
'date_start': date_start,
'date_end': date_end,
'type_id': self.type_id.id,
'company_id': self.company_id.id})
return date_ranges
@api.multi
def action_apply(self):
date_ranges = self._compute_date_ranges()
if date_ranges:
for dr in date_ranges:
self.env['date.range'].create(dr)
return self.env['ir.actions.act_window'].for_xml_id(
module='date_range', xml_id='date_range_action')

40
date_range/wizard/date_range_generator.xml

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="date_range_generator_view_form" model="ir.ui.view">
<field name="name">date.range.generator.form</field>
<field name="model">date.range.generator</field>
<field name="arch" type="xml">
<form string="Genrate Date Ranges">
<group col="4">
<field name="name_prefix"/>
<field name="type_id"/>
<label for="duration_count"/>
<div>
<field class="oe_inline" name="duration_count"/>
<field class="oe_inline" name="unit_of_time"/>
</div>
<field name="date_start"/>
<field name="count"/>
<field groups="base.group_multi_company"
name="company_id" options="{'no_create': True}"/>
</group>
<footer>
<button class="btn btn-sm btn-primary"
name="action_apply" string="Submit" type="object"/>
<button class="btn btn-sm btn-default"
special="cancel" string="Cancel"/>
</footer>
</form>
</field>
</record>
<record id="date_range_generator_action" model="ir.actions.act_window">
<field name="name">Generate Date Ranges</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">date.range.generator</field>
<field name="view_mode">form</field>
<field name="view_id" ref="date_range_generator_view_form"/>
<field name="target">new</field>
</record>
<menuitem action="date_range_generator_action"
id="menu_date_range_generator_action" parent="menu_date_range"/>
</odoo>
Loading…
Cancel
Save