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