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] Spellingpull/3/head
-
108date_range/README.rst
-
6date_range/__init__.py
-
27date_range/__openerp__.py
-
0date_range/i18n/.empty
-
6date_range/models/__init__.py
-
77date_range/models/date_range.py
-
28date_range/models/date_range_type.py
-
19date_range/security/date_range_security.xml
-
5date_range/security/ir.model.access.csv
-
BINdate_range/static/description/date_range_as_filter.png
-
BINdate_range/static/description/date_range_as_filter_result.png
-
BINdate_range/static/description/date_range_create.png
-
BINdate_range/static/description/date_range_type_as_filter.png
-
BINdate_range/static/description/date_range_type_create.png
-
BINdate_range/static/description/date_range_wizard.png
-
BINdate_range/static/description/date_range_wizard_result.png
-
BINdate_range/static/description/icon.png
-
79date_range/static/description/icon.svg
-
117date_range/static/src/js/date_range.js
-
12date_range/static/src/xml/date_range.xml
-
7date_range/tests/__init__.py
-
100date_range/tests/test_date_range.py
-
34date_range/tests/test_date_range_generator.py
-
20date_range/tests/test_date_range_type.py
-
8date_range/views/assets.xml
-
84date_range/views/date_range_view.xml
-
5date_range/wizard/__init__.py
-
67date_range/wizard/date_range_generator.py
-
40date_range/wizard/date_range_generator.xml
@ -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. |
@ -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 |
@ -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,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 |
@ -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)] |
@ -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 !')] |
@ -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> |
@ -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 |
After Width: 1412 | Height: 429 | Size: 140 KiB |
After Width: 1896 | Height: 640 | Size: 242 KiB |
After Width: 1529 | Height: 541 | Size: 99 KiB |
After Width: 1394 | Height: 544 | Size: 182 KiB |
After Width: 1901 | Height: 699 | Size: 78 KiB |
After Width: 1026 | Height: 346 | Size: 28 KiB |
After Width: 1545 | Height: 294 | Size: 56 KiB |
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
@ -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; |
|||
}, |
|||
|
|||
}); |
|||
|
|||
}); |
@ -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> |
@ -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 |
@ -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')]) |
@ -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) |
@ -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) |
@ -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> |
@ -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> |
@ -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 |
@ -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') |
@ -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> |