[ADD]: Add/Remove dynamic digest field, group and misc changes
This commit is contained in:
parent
44e47f76a1
commit
84ed2b2571
@ -19,9 +19,11 @@ Send KPI Digests periodically
|
|||||||
'data/ir_cron_data.xml',
|
'data/ir_cron_data.xml',
|
||||||
'data/res_config_settings_data.xml',
|
'data/res_config_settings_data.xml',
|
||||||
'views/digest_views.xml',
|
'views/digest_views.xml',
|
||||||
|
'wizard/digest_custom_fields_view.xml',
|
||||||
|
'wizard/digest_custom_remove_view.xml',
|
||||||
|
'views/digest_views_inherit.xml',
|
||||||
'views/digest_templates.xml',
|
'views/digest_templates.xml',
|
||||||
'views/res_config_settings_views.xml',
|
'views/res_config_settings_views.xml',
|
||||||
'wizard/digest_custom_fields_view.xml',
|
|
||||||
],
|
],
|
||||||
'installable': True,
|
'installable': True,
|
||||||
}
|
}
|
||||||
|
@ -62,43 +62,6 @@
|
|||||||
<group name="kpi_sales"/>
|
<group name="kpi_sales"/>
|
||||||
</group>
|
</group>
|
||||||
</page>
|
</page>
|
||||||
<page name="how_to" string="How to customize your digest?" groups="base.group_no_one">
|
|
||||||
<div>
|
|
||||||
<button type="action" name="%(digest.digest_custom_fields_action)d" string="Customized Digest"/>
|
|
||||||
</div>
|
|
||||||
<div class="alert alert-info" role="alert">
|
|
||||||
In order to build your customized digest, follow these steps:
|
|
||||||
<ol>
|
|
||||||
<li>
|
|
||||||
You may want to add new computed fields:
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
you must create 2 fields on the
|
|
||||||
<code>digest</code>
|
|
||||||
object:
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
first create a boolean field called
|
|
||||||
<code>x_kpi_field_name</code>
|
|
||||||
and display it in the KPI's tab;
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
then create a computed field called
|
|
||||||
<code>x_kpi_field_name_value</code>
|
|
||||||
that will compute your customized KPI.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li>Select your KPIs in the KPI's tab.</li>
|
|
||||||
<li>
|
|
||||||
Create or edit the mail template: you may get computed KPI's value using these fields:
|
|
||||||
<code>
|
|
||||||
<field name="available_fields" class="oe_inline" />
|
|
||||||
</code>
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</page>
|
|
||||||
</notebook>
|
</notebook>
|
||||||
</sheet>
|
</sheet>
|
||||||
</form>
|
</form>
|
||||||
|
50
addons/digest/views/digest_views_inherit.xml
Normal file
50
addons/digest/views/digest_views_inherit.xml
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<flectra>
|
||||||
|
<record id="digest_digest_view_form_inherit" model="ir.ui.view">
|
||||||
|
<field name="name">digest.digest.view.form.inherit</field>
|
||||||
|
<field name="model">digest.digest</field>
|
||||||
|
<field name="inherit_id" ref="digest.digest_digest_view_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//page[@name='kpis']" position="after">
|
||||||
|
<page name="how_to" string="How to customize your digest?" groups="base.group_no_one">
|
||||||
|
<div>
|
||||||
|
<button type="action" class="oe_highlight" name="%(digest.digest_custom_fields_action)d" string="Add Customized Digest"/>
|
||||||
|
<button type="action" name="%(digest.digest_custom_remove_action)d" string="Remove Customized Digest"/>
|
||||||
|
</div>
|
||||||
|
<div class="alert alert-info" role="alert">
|
||||||
|
In order to build your customized digest, follow these steps:
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
You may want to add new computed fields:
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
you must create 2 fields on the
|
||||||
|
<code>digest</code>
|
||||||
|
object:
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
first create a boolean field called
|
||||||
|
<code>x_kpi_field_name</code>
|
||||||
|
and display it in the KPI's tab;
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
then create a computed field called
|
||||||
|
<code>x_kpi_field_name_value</code>
|
||||||
|
that will compute your customized KPI.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>Select your KPIs in the KPI's tab.</li>
|
||||||
|
<li>
|
||||||
|
Create or edit the mail template: you may get computed KPI's value using these fields:
|
||||||
|
<code>
|
||||||
|
<field name="available_fields" class="oe_inline" />
|
||||||
|
</code>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</page>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</flectra>
|
@ -2,3 +2,4 @@
|
|||||||
# Part of Flectra. See LICENSE file for full copyright and licensing details.
|
# Part of Flectra. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
from . import digest_custom_fields
|
from . import digest_custom_fields
|
||||||
|
from . import digest_custom_remove
|
@ -5,6 +5,7 @@ from flectra import api, fields, models, _
|
|||||||
from flectra.exceptions import ValidationError, UserError
|
from flectra.exceptions import ValidationError, UserError
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
from flectra.tools.safe_eval import test_python_expr
|
from flectra.tools.safe_eval import test_python_expr
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
|
||||||
class DigestCustomFields(models.TransientModel):
|
class DigestCustomFields(models.TransientModel):
|
||||||
@ -19,29 +20,37 @@ class DigestCustomFields(models.TransientModel):
|
|||||||
# - time, datetime, dateutil, timezone: useful Python libraries
|
# - time, datetime, dateutil, timezone: useful Python libraries
|
||||||
# - log: log(message, level='info'): logging function to record debug information in ir.logging table
|
# - log: log(message, level='info'): logging function to record debug information in ir.logging table
|
||||||
# - Warning: Warning Exception to use with raise
|
# - Warning: Warning Exception to use with raise
|
||||||
# To return an action, assign: action = {...}\n\n\n\n"""
|
# To return an action, assign: action = {...}
|
||||||
|
for rec in self:
|
||||||
|
rec[''] = self.env[''].search([])\n\n\n\n"""
|
||||||
|
|
||||||
|
|
||||||
# field = fields.Many2one('ir.model.fields', domain="[('model_id', '=', 'digest.digest'), ('name', 'ilike', 'x_kpi'), ('depends', '=', False)]")
|
|
||||||
# compute_field = fields.Many2one('ir.model.fields', domain="[('model_id', '=', 'digest.digest'), ('name', 'ilike', 'x_kpi'), ('depends', '!=', False)]")
|
|
||||||
|
|
||||||
field_name = fields.Char('Field Name', default='x_kpi_', required=True)
|
field_name = fields.Char('Field Name', default='x_kpi_', required=True)
|
||||||
label_name = fields.Char('Label Name', required=True)
|
label_name = fields.Char('Label Name', required=True)
|
||||||
group_name = fields.Char('Group Name', required=True)
|
# group_type = fields.Selection([('new', 'New Group'), ('existing', 'Existing Group')], string='Group Type', required=True)
|
||||||
ttype = fields.Selection([('integer', 'Integer'), ('monetary', 'Monetary')], string='Field Type', required=True)
|
new_group_name = fields.Char('Group Name')
|
||||||
# compute = fields.Text(help="Code to compute the value of the field.\n"
|
ttype = fields.Selection([('integer', 'Integer'), ('monetary', 'Monetary')], string='Field Type', required=True, default='integer')
|
||||||
# "Iterate on the recordset 'self' and assign the field's value:\n\n"
|
|
||||||
# " for record in self:\n"
|
|
||||||
# " record['size'] = len(record.name)\n\n"
|
|
||||||
# "Modules time, datetime, dateutil are available.")
|
|
||||||
|
|
||||||
|
|
||||||
compute = fields.Text(string='Python Code', groups='base.group_system',
|
compute = fields.Text(string='Python Code', groups='base.group_system',
|
||||||
default=DEFAULT_PYTHON_CODE,
|
default=DEFAULT_PYTHON_CODE,
|
||||||
help="Write Python code that the action will execute. Some variables are "
|
help="Write Python code that the action will execute. Some variables are "
|
||||||
"available for use; help about pyhon expression is given in the help tab.")
|
"available for use; help about pyhon expression is given in the help tab.")
|
||||||
|
|
||||||
compute_field_name = fields.Char(compute='_compute_get_field_name', string='Compute Field Name')
|
compute_field_name = fields.Char(compute='_compute_get_field_name', string='Compute Field Name')
|
||||||
|
available_group_name = fields.Selection('_get_group_name', string='Available Group')
|
||||||
|
position = fields.Selection([('before', 'Before'), ('after', 'After'), ('inside', 'Inside')], string='Position')
|
||||||
|
|
||||||
|
def _get_group_name(self):
|
||||||
|
print("=====self=========", self.env.context)
|
||||||
|
digest_view_id = self.env.ref('digest.digest_digest_view_form').id
|
||||||
|
view_ids = self.env['ir.ui.view'].search([('inherit_id', 'child_of', digest_view_id)])
|
||||||
|
group_value = {}
|
||||||
|
for view_id in view_ids:
|
||||||
|
root = ET.fromstring(view_id.arch_base)
|
||||||
|
for group_name in root.iter('group'):
|
||||||
|
if group_name.attrib.get('name', False) and group_name.attrib.get('string', False):
|
||||||
|
group_key = str(view_id.id) + '_' + str(group_name.attrib['name'])
|
||||||
|
group_value.update({group_key : group_name.attrib['string']})
|
||||||
|
return [(x) for x in group_value.items()]
|
||||||
|
|
||||||
@api.constrains('compute')
|
@api.constrains('compute')
|
||||||
def _check_python_code(self):
|
def _check_python_code(self):
|
||||||
@ -61,6 +70,8 @@ class DigestCustomFields(models.TransientModel):
|
|||||||
for field in self:
|
for field in self:
|
||||||
if not field.field_name.startswith('x_kpi_'):
|
if not field.field_name.startswith('x_kpi_'):
|
||||||
raise ValidationError(_("Custom fields must have a name that starts with 'x_kpi_'!"))
|
raise ValidationError(_("Custom fields must have a name that starts with 'x_kpi_'!"))
|
||||||
|
# if self.position != 'inside' and not field.new_group_name.startswith('x_kpi_'):
|
||||||
|
# raise ValidationError(_("Group Name must have a name that starts with 'x_kpi_'!"))
|
||||||
try:
|
try:
|
||||||
models.check_pg_name(field.field_name)
|
models.check_pg_name(field.field_name)
|
||||||
except ValidationError:
|
except ValidationError:
|
||||||
@ -90,41 +101,44 @@ class DigestCustomFields(models.TransientModel):
|
|||||||
'depends': first_field_name,
|
'depends': first_field_name,
|
||||||
'compute': self.compute
|
'compute': self.compute
|
||||||
}
|
}
|
||||||
print("====values=======", values)
|
|
||||||
ir_model_fields_obj.create(values)
|
ir_model_fields_obj.create(values)
|
||||||
|
|
||||||
def field_arch(self):
|
def field_arch(self):
|
||||||
xpath = etree.Element('xpath')
|
xpath = etree.Element('xpath')
|
||||||
xpath_type = "group"
|
name = self.available_group_name and self.available_group_name.split('_', 1)[1] or "kpis"
|
||||||
name = "kpi_general"
|
expr = '//' + 'group' + '[@name="' + name + '"]'
|
||||||
position = "after"
|
|
||||||
xpath_field = self.field_name
|
|
||||||
expr = '//' + xpath_type + '[@name="' + name + '"][not(ancestor::field)]'
|
|
||||||
xpath.set('expr', expr)
|
xpath.set('expr', expr)
|
||||||
xpath.set('position', position)
|
xpath.set('position', self.position)
|
||||||
if position == 'after' or position == 'before' or position == 'inside':
|
|
||||||
expr = '//' + xpath_type + '[@name="' + name + '"][not(ancestor::field)]'
|
if self.position == 'inside':
|
||||||
group = etree.Element('group')
|
|
||||||
group.set('string', self.group_name)
|
|
||||||
field = etree.Element('field')
|
field = etree.Element('field')
|
||||||
field.set('name', xpath_field)
|
field.set('name', self.field_name)
|
||||||
|
xpath.set('expr', expr)
|
||||||
|
xpath.append(field)
|
||||||
|
else:
|
||||||
|
group = etree.Element('group')
|
||||||
|
group.set('name', 'x_kpi_' + self.new_group_name.replace(" ", "_"))
|
||||||
|
group.set('string', self.new_group_name)
|
||||||
|
field = etree.Element('field')
|
||||||
|
field.set('name', self.field_name)
|
||||||
xpath.set('expr', expr)
|
xpath.set('expr', expr)
|
||||||
group.append(field)
|
group.append(field)
|
||||||
xpath.append(group)
|
xpath.append(group)
|
||||||
|
|
||||||
return etree.tostring(xpath).decode("utf-8")
|
return etree.tostring(xpath).decode("utf-8")
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def action_customize_digest(self):
|
def action_add_customize_digest(self):
|
||||||
self.add_new_fields()
|
self.add_new_fields()
|
||||||
arch = '<data>' + str(self.field_arch()) + '</data>'
|
arch = '<?xml version="1.0"?>' + str(self.field_arch())
|
||||||
print("====arch=======", arch)
|
view_id = self.available_group_name and self.available_group_name.split('_', 1)[0] or False
|
||||||
vals = {
|
vals = {
|
||||||
'type': 'form',
|
'type': 'form',
|
||||||
'model': 'digest.digest',
|
'model': 'digest.digest',
|
||||||
'inherit_id': self.env.ref('digest.digest_digest_view_form').id,
|
'inherit_id': view_id or self.env.ref('digest.digest_digest_view_form').id,
|
||||||
'mode': 'extension',
|
'mode': 'extension',
|
||||||
'arch_base': arch,
|
'arch_base': arch,
|
||||||
'name': 'x_kpi_' + self.field_name + "_Customization",
|
'name': 'x_kpi_' + self.field_name + "_customization",
|
||||||
}
|
}
|
||||||
ir_model = self.env['ir.model'].search([('model', '=', 'digest.digest')])
|
ir_model = self.env['ir.model'].search([('model', '=', 'digest.digest')])
|
||||||
if hasattr(ir_model, 'module_id'):
|
if hasattr(ir_model, 'module_id'):
|
||||||
|
@ -5,10 +5,12 @@
|
|||||||
<field name="model">digest.custom.fields</field>
|
<field name="model">digest.custom.fields</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<form string="Customized Digest">
|
<form string="Customized Digest">
|
||||||
<group class="oe_title" col="4">
|
<group col="4">
|
||||||
<field name="field_name"/>
|
<field name="field_name"/>
|
||||||
<field name="label_name"/>
|
<field name="label_name"/>
|
||||||
<field name="group_name"/>
|
<field name="available_group_name" required="1"/>
|
||||||
|
<field name="position" required="1"/>
|
||||||
|
<field name="new_group_name" attrs="{'required': [('position', '!=', 'inside')], 'invisible': [('position', 'not in', ['before', 'after'])]}"/>
|
||||||
</group>
|
</group>
|
||||||
<group string="Compute Details">
|
<group string="Compute Details">
|
||||||
<field name="compute_field_name"/>
|
<field name="compute_field_name"/>
|
||||||
@ -16,7 +18,7 @@
|
|||||||
<field name="compute" widget="ace" options="{'mode': 'python'}"/>
|
<field name="compute" widget="ace" options="{'mode': 'python'}"/>
|
||||||
</group>
|
</group>
|
||||||
<footer>
|
<footer>
|
||||||
<button name="action_customize_digest" string="Save" type="object" class="btn btn-sm btn-primary"/>
|
<button name="action_add_customize_digest" string="Save" type="object" class="btn btn-sm btn-primary"/>
|
||||||
<button string="Cancel" class="btn btn-sm btn-default" special="cancel"/>
|
<button string="Cancel" class="btn btn-sm btn-default" special="cancel"/>
|
||||||
</footer>
|
</footer>
|
||||||
</form>
|
</form>
|
||||||
|
76
addons/digest/wizard/digest_custom_remove.py
Normal file
76
addons/digest/wizard/digest_custom_remove.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Flectra. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from flectra import api, fields, models, _
|
||||||
|
from flectra.exceptions import ValidationError, UserError
|
||||||
|
from lxml import etree
|
||||||
|
from flectra.tools.safe_eval import test_python_expr
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from flectra.osv import expression
|
||||||
|
|
||||||
|
|
||||||
|
class DigestCustomRemove(models.TransientModel):
|
||||||
|
_name = 'digest.custom.remove'
|
||||||
|
|
||||||
|
remove_type = fields.Selection([('group', 'Group'), ('field', 'Field')], string='Remove Type')
|
||||||
|
field_id = fields.Many2one('ir.model.fields', 'Field', domain=[('model', '=', 'digest.digest'), ('required', '=', False), ('ttype', '=', 'boolean'), ('name', 'ilike', 'x_kpi_')])
|
||||||
|
available_group_name = fields.Selection('_get_group_name', string='Available Group')
|
||||||
|
|
||||||
|
def _get_group_name(self):
|
||||||
|
digest_view_id = self.env.ref('digest.digest_digest_view_form').id
|
||||||
|
view_ids = self.env['ir.ui.view'].search([('inherit_id', 'child_of', digest_view_id)])
|
||||||
|
group_value = {}
|
||||||
|
for view_id in view_ids:
|
||||||
|
root = ET.fromstring(view_id.arch_base)
|
||||||
|
for group_name in root.iter('group'):
|
||||||
|
if group_name.attrib.get('name', False) and group_name.attrib.get('string', False) and group_name.attrib['name'].startswith('x_kpi_'):
|
||||||
|
group_key = str(view_id.id) + '_' + str(group_name.attrib['name'])
|
||||||
|
group_value.update({group_key : group_name.attrib['string']})
|
||||||
|
return [(x) for x in group_value.items()]
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def action_customize_digest_remove(self):
|
||||||
|
ir_model_fields_obj = self.env['ir.model.fields']
|
||||||
|
ir_ui_view_obj = self.env['ir.ui.view']
|
||||||
|
if self.remove_type == 'group':
|
||||||
|
find_view_id = self.available_group_name and self.available_group_name.split('_', 1)[0] or False
|
||||||
|
print("===find_view_id==", find_view_id)
|
||||||
|
view_ids = ir_ui_view_obj.search([('inherit_id', 'child_of', int(find_view_id))], order="id desc")
|
||||||
|
field_list = []
|
||||||
|
for view_id in view_ids:
|
||||||
|
print("===view_id========", view_id)
|
||||||
|
root = ET.fromstring(view_id.arch_base)
|
||||||
|
print("==========root=====", root)
|
||||||
|
for child in root.iter('group'):
|
||||||
|
name = child.find('field')
|
||||||
|
if name.attrib and name.attrib.get('name', False):
|
||||||
|
field_list.append(name.attrib.get('name', False))
|
||||||
|
field_ids = ir_model_fields_obj.search([('name', 'in', field_list)])
|
||||||
|
print("====field_ids====", field_ids, view_ids.ids)
|
||||||
|
view_ids.unlink()
|
||||||
|
for field_id in field_ids:
|
||||||
|
ir_model_fields_obj.search([('depends', '=', field_id.name)]).unlink()
|
||||||
|
field_ids.unlink()
|
||||||
|
else:
|
||||||
|
domain = expression.OR([('arch_db', 'like', record.name)] for record in self.field_id)
|
||||||
|
print("===domain======", domain)
|
||||||
|
view_ids = ir_ui_view_obj.search(domain)
|
||||||
|
print("==========>>>>>>>>.", view_ids)
|
||||||
|
for view_id in view_ids:
|
||||||
|
# print("=====view_id.arch_base======before========", view_id.arch_base)
|
||||||
|
root = ET.fromstring(view_id.arch_base)
|
||||||
|
# result = len(root.getchildren())
|
||||||
|
# count = sum(1 for root in root.iter("field"))
|
||||||
|
# print("===============result==============>", result, count)
|
||||||
|
for child in root.iter('field'):
|
||||||
|
# print("===========>>>>",child.text, child.attrib, child.tag)
|
||||||
|
if child.attrib and child.attrib.get('name', False) == self.field_id.name:
|
||||||
|
view_id.unlink()
|
||||||
|
# root.remove(child)
|
||||||
|
# view_id.write({'arch_base': ET.tostring(root)})
|
||||||
|
ir_model_fields_obj.search([('depends', '=', self.field_id.name)]).unlink()
|
||||||
|
self.field_id.unlink()
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.client',
|
||||||
|
'tag': 'reload',
|
||||||
|
}
|
31
addons/digest/wizard/digest_custom_remove_view.xml
Normal file
31
addons/digest/wizard/digest_custom_remove_view.xml
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<flectra>
|
||||||
|
<record id="digest_custom_remove_view_form" model="ir.ui.view">
|
||||||
|
<field name="name">digest.custom.remove.form</field>
|
||||||
|
<field name="model">digest.custom.remove</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Customized Digest">
|
||||||
|
<group>
|
||||||
|
<field name="remove_type"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="field_id" attrs="{'invisible': [('remove_type', '!=', 'field')]}" options="{'no_create': True}"/>
|
||||||
|
<field name="available_group_name" attrs="{'invisible': [('remove_type', '!=', 'group')]}"/>
|
||||||
|
</group>
|
||||||
|
<footer>
|
||||||
|
<button name="action_customize_digest_remove" string="Remove" type="object" class="btn btn-sm btn-primary"/>
|
||||||
|
<button string="Cancel" class="btn btn-sm btn-default" special="cancel"/>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="digest_custom_remove_action" model="ir.actions.act_window">
|
||||||
|
<field name="name">Customized Digest Remove</field>
|
||||||
|
<field name="type">ir.actions.act_window</field>
|
||||||
|
<field name="res_model">digest.custom.remove</field>
|
||||||
|
<field name="view_mode">form</field>
|
||||||
|
<field name="view_id" ref="digest_custom_remove_view_form"/>
|
||||||
|
<field name="target">new</field>
|
||||||
|
</record>
|
||||||
|
</flectra>
|
@ -202,7 +202,9 @@ class EventEvent(models.Model):
|
|||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _tz_get(self):
|
def _tz_get(self):
|
||||||
return [(x, x) for x in pytz.all_timezones]
|
a = [(x, x) for x in pytz.all_timezones]
|
||||||
|
print("\n\n\n=================>>>>>>.", a)
|
||||||
|
return a
|
||||||
|
|
||||||
@api.one
|
@api.one
|
||||||
@api.depends('date_tz', 'date_begin')
|
@api.depends('date_tz', 'date_begin')
|
||||||
|
Loading…
Reference in New Issue
Block a user