[ADD] base_action_rule: new module to define diffrent actions for any objects
bzr revid: hmo@tinyerp.com-20100215144224-o4sztjze8yksfsdz
This commit is contained in:
commit
ee87ba0a2b
|
@ -0,0 +1,24 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
import base_action_rule
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
@ -0,0 +1,39 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
{
|
||||
'name': 'Action Rule',
|
||||
'version': '1.0',
|
||||
'category': 'Generic Modules/Others',
|
||||
'description': "This module allows to implement action rules for any object.",
|
||||
'author': 'Tiny',
|
||||
'website': 'http://www.openerp.com',
|
||||
'depends': ['base'],
|
||||
'init_xml': [],
|
||||
'update_xml': [
|
||||
'base_action_rule_view.xml',
|
||||
'security/ir.model.access.csv',
|
||||
],
|
||||
'demo_xml': [],
|
||||
'installable': True,
|
||||
'active': False,
|
||||
}
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
@ -0,0 +1,305 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
import time
|
||||
import mx.DateTime
|
||||
import re
|
||||
|
||||
import tools
|
||||
from osv import fields, osv, orm
|
||||
from osv.orm import except_orm
|
||||
from tools.translate import _
|
||||
|
||||
class base_action_rule(osv.osv):
|
||||
_name = 'base.action.rule'
|
||||
_description = 'Action Rules'
|
||||
|
||||
|
||||
_columns = {
|
||||
'name': fields.many2one('ir.model', 'Model', required=True),
|
||||
'max_level': fields.integer('Max Level', help='Specifies maximum level.'),
|
||||
'rule_lines': fields.one2many('base.action.rule.line','rule_id','Rule Lines'),
|
||||
'create_date': fields.datetime('Create Date', readonly=1),
|
||||
'active': fields.boolean('Active')
|
||||
}
|
||||
|
||||
_defaults = {
|
||||
'active': lambda *a: True,
|
||||
'max_level': lambda *a: 15,
|
||||
}
|
||||
|
||||
def format_body(self, body):
|
||||
return body and tools.ustr(body) or ''
|
||||
|
||||
def format_mail(self, obj, body):
|
||||
data = {
|
||||
'object_id': obj.id,
|
||||
'object_subject': hasattr(obj, 'name') and obj.name or False,
|
||||
'object_date': hasattr(obj, 'date') and obj.date or False,
|
||||
'object_description': hasattr(obj, 'description') and obj.description or False,
|
||||
'object_user': hasattr(obj, 'user_id') and (obj.user_id and obj.user_id.name) or '/',
|
||||
'object_user_email': hasattr(obj, 'user_id') and (obj.user_id and obj.user_id.address_id and obj.user_id.address_id.email) or '/',
|
||||
'object_user_phone': hasattr(obj, 'user_id') and (obj.user_id and obj.user_id.address_id and obj.user_id.address_id.phone) or '/',
|
||||
'partner': hasattr(obj, 'partner_id') and (obj.partner_id and obj.partner_id.name) or '/',
|
||||
'partner_email': hasattr(obj, 'partner_address_id') and (obj.partner_address_id and obj.partner_address_id.email) or '/',
|
||||
}
|
||||
return format_body(body % data)
|
||||
|
||||
def email_send(self, cr, uid, obj, emails, body, emailfrom=tools.config.get('email_from',False), context={}):
|
||||
body = self.format_mail(obj, body)
|
||||
if not emailfrom:
|
||||
if hasattr(obj, 'user_id') and obj.user_id and obj.user_id.address_id and obj.user_id.address_id.email:
|
||||
emailfrom = obj.user_id.address_id.email
|
||||
|
||||
name = '[%d] %s' % (obj.id, tools.ustr(obj.name))
|
||||
emailfrom = tools.ustr(emailfrom)
|
||||
reply_to = emailfrom
|
||||
if not emailfrom:
|
||||
raise osv.except_osv(_('Error!'),
|
||||
_("No E-Mail ID Found for your Company address!"))
|
||||
return tools.email_send(emailfrom, emails, name, body, reply_to=reply_to, openobject_id=str(obj.id))
|
||||
|
||||
|
||||
def do_check(self, cr, uid, action, obj, context={}):
|
||||
ok = True
|
||||
if hasattr(obj, 'user_id'):
|
||||
ok = ok and (not action.trg_user_id.id or action.trg_user_id.id==obj.user_id.id)
|
||||
if hasattr(obj, 'partner_id'):
|
||||
ok = ok and (not action.trg_partner_id.id or action.trg_partner_id.id==obj.partner_id.id)
|
||||
ok = ok and (
|
||||
not action.trg_partner_categ_id.id or
|
||||
(
|
||||
obj.partner_id.id and
|
||||
(action.trg_partner_categ_id.id in map(lambda x: x.id, obj.partner_id.category_id or []))
|
||||
)
|
||||
)
|
||||
state_to = context.get('state_to', False)
|
||||
if hasattr(obj, 'state'):
|
||||
ok = ok and (not action.trg_state_from or action.trg_state_from==obj.state)
|
||||
if state_to:
|
||||
ok = ok and (not action.trg_state_to or action.trg_state_to==state_to)
|
||||
|
||||
if hasattr(obj, 'priority'):
|
||||
ok = ok and (not action.trg_priority_from or action.trg_priority_from>=obj.priority)
|
||||
ok = ok and (not action.trg_priority_to or action.trg_priority_to<=obj.priority)
|
||||
|
||||
reg_name = action.regex_name
|
||||
result_name = True
|
||||
if reg_name:
|
||||
ptrn = re.compile(str(reg_name))
|
||||
_result = ptrn.search(str(obj.name))
|
||||
if not _result:
|
||||
result_name = False
|
||||
regex_n = not reg_name or result_name
|
||||
ok = ok and regex_n
|
||||
return ok
|
||||
|
||||
def do_action(self, cr, uid, action, model_obj, obj, context={}):
|
||||
if action.server_action_id:
|
||||
context.update({'active_id':obj.id,'active_ids':[obj.id]})
|
||||
self.pool.get('ir.actions.server').run(cr, uid, [action.server_action_id.id], context)
|
||||
write = {}
|
||||
if hasattr(obj, 'user_id') and action.act_user_id:
|
||||
obj.user_id = action.act_user_id
|
||||
write['user_id'] = action.act_user_id.id
|
||||
if hasattr(obj, 'date_action_last'):
|
||||
write['date_action_last'] = time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
if hasattr(obj, 'state') and action.act_state:
|
||||
obj.state = action.act_state
|
||||
write['state'] = action.act_state
|
||||
|
||||
if hasattr(obj, 'priority') and action.act_priority:
|
||||
obj.priority = action.act_priority
|
||||
write['priority'] = action.act_priority
|
||||
|
||||
model_obj.write(cr, uid, [obj.id], write, context)
|
||||
|
||||
if hasattr(model_obj, 'remind_user') and action.act_remind_user:
|
||||
model_obj.remind_user(cr, uid, [obj.id], context, attach=action.act_remind_attach)
|
||||
if hasattr(model_obj, 'remind_partner') and action.act_remind_partner:
|
||||
model_obj.remind_partner(cr, uid, [obj.id], context, attach=action.act_remind_attach)
|
||||
if action.act_method:
|
||||
getattr(model_obj, 'act_method')(cr, uid, [obj.id], action, context)
|
||||
emails = []
|
||||
if hasattr(obj, 'user_id') and action.act_mail_to_user:
|
||||
if obj.user_id and obj.user_id.address_id:
|
||||
emails.append(obj.user_id.address_id.email)
|
||||
|
||||
if action.act_mail_to_watchers:
|
||||
emails += (action.act_email_cc or '').split(',')
|
||||
if action.act_mail_to_email:
|
||||
emails += (action.act_mail_to_email or '').split(',')
|
||||
emails = filter(None, emails)
|
||||
if len(emails) and action.act_mail_body:
|
||||
emails = list(set(emails))
|
||||
self.email_send(cr, uid, obj, emails, action.act_mail_body)
|
||||
return True
|
||||
|
||||
def _action(self, cr, uid, ids, objects, scrit=None, context={}):
|
||||
if not scrit:
|
||||
scrit = []
|
||||
rule_line_obj = self.pool.get('base.action.rule.line')
|
||||
for rule in self.browse(cr, uid, ids):
|
||||
level = rule.max_level
|
||||
if not level:
|
||||
break
|
||||
newactions = []
|
||||
scrit += [('rule_id','=',rule.id)]
|
||||
line_ids = rule_line_obj.search(cr, uid, scrit)
|
||||
actions = rule_line_obj.browse(cr, uid, line_ids, context=context)
|
||||
model_obj = self.pool.get(rule.name.model)
|
||||
for obj in objects:
|
||||
for action in actions:
|
||||
ok = self.do_check(cr, uid, action, obj, context=context)
|
||||
if not ok:
|
||||
continue
|
||||
|
||||
base = False
|
||||
if hasattr(obj, 'create_date') and action.trg_date_type=='create':
|
||||
base = mx.DateTime.strptime(obj.create_date[:19], '%Y-%m-%d %H:%M:%S')
|
||||
elif hasattr(obj, 'create_date') and action.trg_date_type=='action_last':
|
||||
if hasattr(obj, 'date_action_last') and obj.date_action_last:
|
||||
base = mx.DateTime.strptime(obj.date_action_last, '%Y-%m-%d %H:%M:%S')
|
||||
else:
|
||||
base = mx.DateTime.strptime(obj.create_date[:19], '%Y-%m-%d %H:%M:%S')
|
||||
elif hasattr(obj, 'date_deadline') and action.trg_date_type=='deadline' and obj.date_deadline:
|
||||
base = mx.DateTime.strptime(obj.date_deadline, '%Y-%m-%d %H:%M:%S')
|
||||
elif hasattr(obj, 'date') and action.trg_date_type=='date' and obj.date:
|
||||
base = mx.DateTime.strptime(obj.date, '%Y-%m-%d %H:%M:%S')
|
||||
if base:
|
||||
fnct = {
|
||||
'minutes': lambda interval: mx.DateTime.RelativeDateTime(minutes=interval),
|
||||
'day': lambda interval: mx.DateTime.RelativeDateTime(days=interval),
|
||||
'hour': lambda interval: mx.DateTime.RelativeDateTime(hours=interval),
|
||||
'month': lambda interval: mx.DateTime.RelativeDateTime(months=interval),
|
||||
}
|
||||
d = base + fnct[action.trg_date_range_type](action.trg_date_range)
|
||||
dt = d.strftime('%Y-%m-%d %H:%M:%S')
|
||||
ok = False
|
||||
if hasattr(obj, 'date_action_last') and hasattr(obj, 'date_action_next'):
|
||||
ok = (dt <= time.strftime('%Y-%m-%d %H:%M:%S')) and \
|
||||
((not obj.date_action_next) or \
|
||||
(dt >= obj.date_action_next and \
|
||||
obj.date_action_last < obj.date_action_next))
|
||||
if not ok:
|
||||
if not obj.date_action_next or dt < obj.date_action_next:
|
||||
obj.date_action_next = dt
|
||||
model_obj.write(cr, uid, [obj.id], {'date_action_next': dt}, context)
|
||||
else:
|
||||
ok = action.trg_date_type=='none'
|
||||
|
||||
if ok:
|
||||
self.do_action(cr, uid, action, model_obj, obj, context)
|
||||
break
|
||||
level -= 1
|
||||
return True
|
||||
base_action_rule()
|
||||
|
||||
class base_action_rule_line(osv.osv):
|
||||
_name = 'base.action.rule.line'
|
||||
_description = 'Action Rule Lines'
|
||||
|
||||
def _state_get(self, cr, uid, context={}):
|
||||
return self.state_get(cr, uid, context=context)
|
||||
def _priority_get(self, cr, uid, context={}):
|
||||
return self.priority_get(cr, uid, context=context)
|
||||
|
||||
def state_get(self, cr, uid, context={}):
|
||||
return [('','')]
|
||||
def priority_get(self, cr, uid, context={}):
|
||||
return [('','')]
|
||||
|
||||
_columns = {
|
||||
'name': fields.char('Rule Name',size=64, required=True),
|
||||
'rule_id': fields.many2one('base.action.rule','Rule'),
|
||||
'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the rule without removing it."),
|
||||
'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of rules."),
|
||||
|
||||
'trg_date_type': fields.selection([
|
||||
('none','None'),
|
||||
('create','Creation Date'),
|
||||
('action_last','Last Action Date'),
|
||||
('date','Date'),
|
||||
], 'Trigger Date', size=16),
|
||||
'trg_date_range': fields.integer('Delay after trigger date',help="Delay After Trigger Date, specifies you can put a negative number " \
|
||||
"if you need a delay before the trigger date, like sending a reminder 15 minutes before a meeting."),
|
||||
'trg_date_range_type': fields.selection([('minutes', 'Minutes'),('hour','Hours'),('day','Days'),('month','Months')], 'Delay type'),
|
||||
|
||||
|
||||
'trg_user_id': fields.many2one('res.users', 'Responsible'),
|
||||
|
||||
'trg_partner_id': fields.many2one('res.partner', 'Partner'),
|
||||
'trg_partner_categ_id': fields.many2one('res.partner.category', 'Partner Category'),
|
||||
'trg_state_from': fields.selection(_state_get, 'State', size=16),
|
||||
'trg_state_to': fields.selection(_state_get, 'Button Pressed', size=16),
|
||||
'trg_priority_from': fields.selection(_priority_get, 'Minimum Priority'),
|
||||
'trg_priority_to': fields.selection(_priority_get, 'Maximum Priority'),
|
||||
|
||||
'act_method': fields.char('Call Object Method', size=64),
|
||||
'act_user_id': fields.many2one('res.users', 'Set responsible to'),
|
||||
'act_state': fields.selection(_state_get, 'Set state to', size=16),
|
||||
'act_priority': fields.selection(_priority_get, 'Set priority to'),
|
||||
'act_email_cc': fields.char('Add watchers (Cc)', size=250, help="These people will receive a copy of the future communication between partner and users by email"),
|
||||
|
||||
'act_remind_partner': fields.boolean('Remind Partner', help="Check this if you want the rule to send a reminder by email to the partner."),
|
||||
'act_remind_user': fields.boolean('Remind responsible', help="Check this if you want the rule to send a reminder by email to the user."),
|
||||
'act_reply_to': fields.char('Reply-To', size=64),
|
||||
'act_remind_attach': fields.boolean('Remind with attachment', help="Check this if you want that all documents attached to the object be attached to the reminder email sent."),
|
||||
|
||||
'act_mail_to_user': fields.boolean('Mail to responsible',help="Check this if you want the rule to send an email to the responsible person."),
|
||||
'act_mail_to_watchers': fields.boolean('Mail to watchers (CC)',help="Check this if you want the rule to mark CC(mail to any other person defined in actions)."),
|
||||
'act_mail_to_email': fields.char('Mail to these emails', size=128,help="Email-id of the persons whom mail is to be sent"),
|
||||
'act_mail_body': fields.text('Mail body',help="Content of mail"),
|
||||
'regex_name': fields.char('Regular Expression on Model Name', size=128),
|
||||
'server_action_id': fields.many2one('ir.actions.server','Server Action',help="Describes the action name." \
|
||||
"eg:on which object which action to be taken on basis of which condition"),
|
||||
}
|
||||
|
||||
_defaults = {
|
||||
'active': lambda *a: 1,
|
||||
'trg_date_type': lambda *a: 'none',
|
||||
'trg_date_range_type': lambda *a: 'day',
|
||||
'act_mail_to_user': lambda *a: 0,
|
||||
'act_remind_partner': lambda *a: 0,
|
||||
'act_remind_user': lambda *a: 0,
|
||||
'act_mail_to_watchers': lambda *a: 0,
|
||||
}
|
||||
|
||||
_order = 'sequence'
|
||||
|
||||
|
||||
def _check_mail(self, cr, uid, ids, context=None):
|
||||
empty = orm.browse_null()
|
||||
rule_obj = self.pool.get('base.action.rule')
|
||||
for rule in self.browse(cr, uid, ids):
|
||||
if rule.act_mail_body:
|
||||
try:
|
||||
rule_obj.format_mail(empty, rule.act_mail_body)
|
||||
except (ValueError, KeyError, TypeError):
|
||||
return False
|
||||
return True
|
||||
|
||||
_constraints = [
|
||||
(_check_mail, 'Error: The mail is not well formated', ['act_mail_body']),
|
||||
]
|
||||
|
||||
base_action_rule_line()
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
@ -0,0 +1,130 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<menuitem id="menu_base_action_rule" name="Action Rule" parent="base.menu_base_config" sequence="0"/>
|
||||
|
||||
<!--
|
||||
Action Rule
|
||||
-->
|
||||
<record id="view_base_action_rule_form" model="ir.ui.view">
|
||||
<field name="name">base.action.rule.form</field>
|
||||
<field name="model">base.action.rule</field>
|
||||
<field name="type">form</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Action Rule">
|
||||
<field name="name"/>
|
||||
<field name="active"/>
|
||||
<field name="max_level" />
|
||||
<separator colspan="4" string="Rule lines"/>
|
||||
<field name="rule_lines" colspan="4" nolabel="1"/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_base_action_rule_tree" model="ir.ui.view">
|
||||
<field name="name">base.action.rule.tree</field>
|
||||
<field name="model">base.action.rule</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Action Rule">
|
||||
<field name="name"/>
|
||||
<field name="max_level"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="base_action_rule_act" model="ir.actions.act_window">
|
||||
<field name="name">Action Rules</field>
|
||||
<field name="res_model">base.action.rule</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_id" ref="view_base_action_rule_tree"/>
|
||||
</record>
|
||||
<menuitem id="menu_base_action_rule_form" parent="menu_base_action_rule" action="base_action_rule_act"/>
|
||||
|
||||
<!--
|
||||
Action Rule Lines
|
||||
-->
|
||||
<record id="view_base_action_rule_line_form" model="ir.ui.view">
|
||||
<field name="name">base.action.rule.line.form</field>
|
||||
<field name="model">base.action.rule.line</field>
|
||||
<field name="type">form</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Action Rule Line">
|
||||
<field name="name" select="1"/>
|
||||
<field name="active" select="2"/>
|
||||
<notebook colspan="4">
|
||||
<page string="Conditions">
|
||||
<separator colspan="4" string="Conditions on Model Fields"/>
|
||||
<field name="regex_name" string="Regex on Model Name" colspan="2"/>
|
||||
<field name="trg_user_id" select="2"/>
|
||||
<separator colspan="4" string="Conditions on Model Partner"/>
|
||||
<field name="trg_partner_id"/>
|
||||
<field name="trg_partner_categ_id"/>
|
||||
<separator colspan="4" string="Conditions on States"/>
|
||||
<field name="trg_state_from" select="2"/>
|
||||
<field name="trg_state_to" select="2"/>
|
||||
<separator colspan="4" string="Conditions on Priority Range"/>
|
||||
<field name="trg_priority_from"/>
|
||||
<field name="trg_priority_to"/>
|
||||
<separator colspan="4" string="Conditions on Timing"/>
|
||||
<field name="trg_date_type"/>
|
||||
<label align="1.0" string="Delay After Trigger Date:"/>
|
||||
<group col="2" colspan="1">
|
||||
<field name="trg_date_range" nolabel="1"/>
|
||||
<field name="trg_date_range_type" nolabel="1"/>
|
||||
</group>
|
||||
<separator colspan="4" string="Note"/>
|
||||
<label align="0.0" string="The rule use a AND operator. The model must match all non empty fields so that the rule execute the action described in the 'Actions' tab." colspan="4"/>
|
||||
</page>
|
||||
<page string="Actions">
|
||||
<separator colspan="4" string="Fields to Change"/>
|
||||
<field name="act_user_id"/>
|
||||
<field name="act_state"/>
|
||||
<field name="act_priority"/>
|
||||
<separator colspan="4" string="E-Mail Reminders (includes the content of the object)"/>
|
||||
<field name="act_remind_partner"/>
|
||||
<field name="act_remind_attach"/>
|
||||
<field name="act_remind_user"/>
|
||||
<group col="2" colspan="2" attrs="{'invisible': [('act_remind_user','=',False)]}">
|
||||
<field name="act_reply_to" attrs="{'required':[('act_remind_user','=',True)]}"/>
|
||||
</group>
|
||||
<field colspan="4" name="act_email_cc"/>
|
||||
<separator colspan="4" string="Server Action to be Triggered"/>
|
||||
<field name="server_action_id"/>
|
||||
</page>
|
||||
<page string="E-Mail Actions">
|
||||
<separator colspan="4" string="Template of Email to Send"/>
|
||||
<field name="act_mail_to_user"/>
|
||||
<field name="act_mail_to_watchers"/>
|
||||
<field colspan="4" name="act_mail_to_email"/>
|
||||
<field colspan="4" name="act_mail_body" attrs="{'required':[('act_remind_user','=',True)]}"/>
|
||||
<separator colspan="4" string="Special Keywords to Be Used in The Body"/>
|
||||
<label align="0.0" string="%%(object_id)s = Object ID" colspan="2"/>
|
||||
<label align="0.0" string="%%(object_subject)s = Object subject" colspan="2"/>
|
||||
<label align="0.0" string="%%(object_description)s = Object description" colspan="2"/>
|
||||
<label align="0.0" string="%%(object_date)s = Creation date" colspan="2"/>
|
||||
<label align="0.0" string="%%(partner)s = Partner name" colspan="2"/>
|
||||
<label align="0.0" string="%%(partner_email)s = Partner email" colspan="2"/>
|
||||
<label align="0.0" string="%%(object_user)s = Responsible name" colspan="2"/>
|
||||
<label align="0.0" string="%%(object_user_email)s = Responsible email" colspan="2"/>
|
||||
<label align="0.0" string="%%(object_user_phone)s = Responsible phone" colspan="2"/>
|
||||
<label align="0.0" string="%% = The '%%' Character" colspan="2"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_base_action_rule_line_tree" model="ir.ui.view">
|
||||
<field name="name">base.action.rule.line.tree</field>
|
||||
<field name="model">base.action.rule.line</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Action Rule Lines">
|
||||
<field name="name"/>
|
||||
<field name="active"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
|
@ -0,0 +1,3 @@
|
|||
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
|
||||
"access_base_action_rule","base.action.rule","model_base_action_rule",,1,1,1,1
|
||||
"access_base_action_rule_line","base.action.rule.line","model_base_action_rule_line",,1,1,1,1
|
|
|
@ -240,12 +240,12 @@
|
|||
|
||||
|
||||
<!-- Available alarms-->
|
||||
<menuitem id="menu_calendar_configuration" name="Calendar"
|
||||
<menuitem id="base.menu_calendar_configuration" name="Calendar"
|
||||
parent="base.menu_base_config" sequence="10" />
|
||||
|
||||
<menuitem name="Available Alarms" id="menu_crm_meeting_avail_alarm"
|
||||
action="base_calendar.action_res_alarm_view"
|
||||
parent="menu_calendar_configuration" />
|
||||
parent="base.menu_calendar_configuration" />
|
||||
|
||||
|
||||
</data>
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
"name" : "Share Calendar using Caldav",
|
||||
"version" : "1.0",
|
||||
"depends" : [
|
||||
"base_calendar",
|
||||
"base",
|
||||
],
|
||||
'description': """
|
||||
Contains basic functionality for caldav system like:
|
||||
|
|
|
@ -50,10 +50,11 @@
|
|||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
|
||||
<menuitem id="base.menu_calendar_configuration" name="Calendar"
|
||||
parent="base.menu_base_config" sequence="10" />
|
||||
|
||||
<menuitem id="menu_calendar"
|
||||
name="Calendar" parent="base_calendar.menu_calendar_configuration"
|
||||
name="Calendar" parent="base.menu_calendar_configuration"
|
||||
sequence="5" action="action_view_calendar" />
|
||||
|
||||
</data>
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
|
||||
import crm
|
||||
import crm_mailgate
|
||||
import crm_action_rule
|
||||
import crm_segmentation
|
||||
import crm_meeting
|
||||
import crm_lead
|
||||
|
|
|
@ -44,7 +44,7 @@ The CRM module has a email gateway for the synchronisation interface
|
|||
between mails and Open ERP.""",
|
||||
'author': 'Tiny',
|
||||
'website': 'http://www.openerp.com',
|
||||
'depends': ['base',
|
||||
'depends': ['base', 'base_action_rule',
|
||||
'process',
|
||||
'mail_gateway',
|
||||
'base_calendar',
|
||||
|
@ -63,6 +63,7 @@ between mails and Open ERP.""",
|
|||
'update_xml': [
|
||||
'crm_wizard.xml',
|
||||
'crm_view.xml',
|
||||
'crm_action_rule_view.xml',
|
||||
'crm_lead_wizard.xml',
|
||||
'crm_lead_view.xml',
|
||||
'crm_lead_menu.xml',
|
||||
|
|
|
@ -163,108 +163,6 @@ class crm_case_stage(osv.osv):
|
|||
|
||||
crm_case_stage()
|
||||
|
||||
|
||||
class crm_case_rule(osv.osv):
|
||||
_name = "crm.case.rule"
|
||||
_description = "Case Rule"
|
||||
_columns = {
|
||||
'name': fields.char('Rule Name',size=64, required=True),
|
||||
'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the case rule without removing it."),
|
||||
'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of case rules."),
|
||||
|
||||
'trg_state_from': fields.selection([('',''),('escalate','Escalate')]+AVAILABLE_STATES, 'Case State', size=16),
|
||||
'trg_state_to': fields.selection([('',''),('escalate','Escalate')]+AVAILABLE_STATES, 'Button Pressed', size=16),
|
||||
|
||||
'trg_date_type': fields.selection([
|
||||
('none','None'),
|
||||
('create','Creation Date'),
|
||||
('action_last','Last Action Date'),
|
||||
('deadline','Deadline'),
|
||||
('date','Date'),
|
||||
], 'Trigger Date', size=16),
|
||||
'trg_date_range': fields.integer('Delay after trigger date',help="Delay After Trigger Date, specifies you can put a negative number " \
|
||||
"if you need a delay before the trigger date, like sending a reminder 15 minutes before a meeting."),
|
||||
'trg_date_range_type': fields.selection([('minutes', 'Minutes'),('hour','Hours'),('day','Days'),('month','Months')], 'Delay type'),
|
||||
|
||||
'trg_section_id': fields.many2one('crm.case.section', 'Section'),
|
||||
|
||||
'trg_categ_id': fields.many2one('crm.case.categ', 'Category', domain="[('section_id','=',trg_section_id)]"),
|
||||
'trg_user_id': fields.many2one('res.users', 'Responsible'),
|
||||
|
||||
'trg_partner_id': fields.many2one('res.partner', 'Partner'),
|
||||
'trg_partner_categ_id': fields.many2one('res.partner.category', 'Partner Category'),
|
||||
|
||||
'trg_priority_from': fields.selection([('','')] + AVAILABLE_PRIORITIES, 'Minimum Priority'),
|
||||
'trg_priority_to': fields.selection([('','')] + AVAILABLE_PRIORITIES, 'Maximim Priority'),
|
||||
'trg_max_history': fields.integer('Maximum Communication History'),
|
||||
|
||||
'act_method': fields.char('Call Object Method', size=64),
|
||||
'act_state': fields.selection([('','')]+AVAILABLE_STATES, 'Set state to', size=16),
|
||||
'act_section_id': fields.many2one('crm.case.section', 'Set section to'),
|
||||
'act_user_id': fields.many2one('res.users', 'Set responsible to'),
|
||||
'act_priority': fields.selection([('','')] + AVAILABLE_PRIORITIES, 'Set priority to'),
|
||||
'act_email_cc': fields.char('Add watchers (Cc)', size=250, help="These people will receive a copy of the future communication between partner and users by email"),
|
||||
|
||||
'act_remind_partner': fields.boolean('Remind Partner', help="Check this if you want the rule to send a reminder by email to the partner."),
|
||||
'act_remind_user': fields.boolean('Remind responsible', help="Check this if you want the rule to send a reminder by email to the user."),
|
||||
'act_remind_attach': fields.boolean('Remind with attachment', help="Check this if you want that all documents attached to the case be attached to the reminder email sent."),
|
||||
|
||||
'act_mail_to_user': fields.boolean('Mail to responsible',help="Check this if you want the rule to send an email to the responsible person."),
|
||||
'act_mail_to_partner': fields.boolean('Mail to partner',help="Check this if you want the rule to send an email to the partner."),
|
||||
'act_mail_to_watchers': fields.boolean('Mail to watchers (CC)',help="Check this if you want the rule to mark CC(mail to any other person defined in actions)."),
|
||||
'act_mail_to_email': fields.char('Mail to these emails', size=128,help="Email-id of the persons whom mail is to be sent"),
|
||||
'act_mail_body': fields.text('Mail body',help="Content of mail"),
|
||||
'regex_name' : fields.char('Regular Expression on Case Name', size=128),
|
||||
'regex_history' : fields.char('Regular Expression on Case History', size=128),
|
||||
'server_action_id' : fields.many2one('ir.actions.server','Server Action',help="Describes the action name." \
|
||||
"eg:on which object which ation to be taken on basis of which condition"),
|
||||
}
|
||||
_defaults = {
|
||||
'active': lambda *a: 1,
|
||||
'trg_date_type': lambda *a: 'none',
|
||||
'trg_date_range_type': lambda *a: 'day',
|
||||
'act_mail_to_user': lambda *a: 0,
|
||||
'act_remind_partner': lambda *a: 0,
|
||||
'act_remind_user': lambda *a: 0,
|
||||
'act_mail_to_partner': lambda *a: 0,
|
||||
'act_mail_to_watchers': lambda *a: 0,
|
||||
}
|
||||
_order = 'sequence'
|
||||
|
||||
def _check(self, cr, uid, ids=False, context={}):
|
||||
'''
|
||||
Function called by the scheduler to process cases for date actions
|
||||
Only works on not done and cancelled cases
|
||||
'''
|
||||
cr.execute('select * from crm_case \
|
||||
where (date_action_last<%s or date_action_last is null) \
|
||||
and (date_action_next<=%s or date_action_next is null) \
|
||||
and state not in (\'cancel\',\'done\')',
|
||||
(time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
time.strftime('%Y-%m-%d %H:%M:%S')))
|
||||
ids2 = map(lambda x: x[0], cr.fetchall() or [])
|
||||
case_obj = self.pool.get('crm.case')
|
||||
cases = case_obj.browse(cr, uid, ids2, context)
|
||||
return case_obj._action(cr, uid, cases, False, context=context)
|
||||
|
||||
|
||||
def _check_mail(self, cr, uid, ids, context=None):
|
||||
caseobj = self.pool.get('crm.case')
|
||||
emptycase = orm.browse_null()
|
||||
for rule in self.browse(cr, uid, ids):
|
||||
if rule.act_mail_body:
|
||||
try:
|
||||
caseobj.format_mail(emptycase, rule.act_mail_body)
|
||||
except (ValueError, KeyError, TypeError):
|
||||
return False
|
||||
return True
|
||||
|
||||
_constraints = [
|
||||
(_check_mail, 'Error: The mail is not well formated', ['act_mail_body']),
|
||||
]
|
||||
|
||||
crm_case_rule()
|
||||
|
||||
def _links_get(self, cr, uid, context={}):
|
||||
obj = self.pool.get('res.request.link')
|
||||
ids = obj.search(cr, uid, [])
|
||||
|
@ -335,9 +233,6 @@ class crm_case(osv.osv):
|
|||
\nIf the case is in progress the state is set to \'Open\'.\
|
||||
\nWhen the case is over, the state is set to \'Done\'.\
|
||||
\nIf the case needs to be reviewed then the state is set to \'Pending\'.'),
|
||||
|
||||
'date_action_last': fields.datetime('Last Action', readonly=1),
|
||||
'date_action_next': fields.datetime('Next Action', readonly=1),
|
||||
'company_id': fields.many2one('res.company','Company'),
|
||||
}
|
||||
def _get_default_partner_address(self, cr, uid, context):
|
||||
|
@ -427,164 +322,6 @@ class crm_case(osv.osv):
|
|||
value['email_from'] = case.email_from
|
||||
return {'value': value}
|
||||
|
||||
def _action(self, cr, uid, cases, state_to, scrit=None, context={}):
|
||||
if not scrit:
|
||||
scrit = []
|
||||
action_ids = self.pool.get('crm.case.rule').search(cr, uid, scrit)
|
||||
level = MAX_LEVEL
|
||||
while len(action_ids) and level:
|
||||
newactions = []
|
||||
actions = self.pool.get('crm.case.rule').browse(cr, uid, action_ids, context)
|
||||
for case in cases:
|
||||
for action in actions:
|
||||
ok = True
|
||||
ok = ok and (not action.trg_state_from or action.trg_state_from==case.state)
|
||||
ok = ok and (not action.trg_state_to or action.trg_state_to==state_to)
|
||||
ok = ok and (not action.trg_section_id or action.trg_section_id.id==case.section_id.id)
|
||||
ok = ok and (not action.trg_categ_id or action.trg_categ_id.id==case.categ_id.id)
|
||||
ok = ok and (not action.trg_user_id.id or action.trg_user_id.id==case.user_id.id)
|
||||
ok = ok and (not action.trg_partner_id.id or action.trg_partner_id.id==case.partner_id.id)
|
||||
ok = ok and (not action.trg_max_history or action.trg_max_history<=(len(case.history_line)+1))
|
||||
ok = ok and (
|
||||
not action.trg_partner_categ_id.id or
|
||||
(
|
||||
case.partner_id.id and
|
||||
(action.trg_partner_categ_id.id in map(lambda x: x.id, case.partner_id.category_id or []))
|
||||
)
|
||||
)
|
||||
ok = ok and (not action.trg_priority_from or action.trg_priority_from>=case.priority)
|
||||
ok = ok and (not action.trg_priority_to or action.trg_priority_to<=case.priority)
|
||||
|
||||
reg_name = action.regex_name
|
||||
result_name = True
|
||||
if reg_name:
|
||||
ptrn = re.compile(str(reg_name))
|
||||
_result = ptrn.search(str(case.name))
|
||||
if not _result:
|
||||
result_name = False
|
||||
regex_n = not reg_name or result_name
|
||||
ok = ok and regex_n
|
||||
|
||||
reg_history = action.regex_history
|
||||
result_history = True
|
||||
if reg_history:
|
||||
ptrn = re.compile(str(reg_history))
|
||||
if case.history_line:
|
||||
_result = ptrn.search(str(case.history_line[0].description))
|
||||
if not _result:
|
||||
result_history = False
|
||||
regex_h = not reg_history or result_history
|
||||
ok = ok and regex_h
|
||||
|
||||
if not ok:
|
||||
continue
|
||||
|
||||
base = False
|
||||
if action.trg_date_type=='create':
|
||||
base = mx.DateTime.strptime(case.create_date[:19], '%Y-%m-%d %H:%M:%S')
|
||||
elif action.trg_date_type=='action_last':
|
||||
if case.date_action_last:
|
||||
base = mx.DateTime.strptime(case.date_action_last, '%Y-%m-%d %H:%M:%S')
|
||||
else:
|
||||
base = mx.DateTime.strptime(case.create_date[:19], '%Y-%m-%d %H:%M:%S')
|
||||
elif action.trg_date_type=='deadline' and case.date_deadline:
|
||||
base = mx.DateTime.strptime(case.date_deadline, '%Y-%m-%d %H:%M:%S')
|
||||
elif action.trg_date_type=='date' and case.date:
|
||||
base = mx.DateTime.strptime(case.date, '%Y-%m-%d %H:%M:%S')
|
||||
if base:
|
||||
fnct = {
|
||||
'minutes': lambda interval: mx.DateTime.RelativeDateTime(minutes=interval),
|
||||
'day': lambda interval: mx.DateTime.RelativeDateTime(days=interval),
|
||||
'hour': lambda interval: mx.DateTime.RelativeDateTime(hours=interval),
|
||||
'month': lambda interval: mx.DateTime.RelativeDateTime(months=interval),
|
||||
}
|
||||
d = base + fnct[action.trg_date_range_type](action.trg_date_range)
|
||||
dt = d.strftime('%Y-%m-%d %H:%M:%S')
|
||||
ok = (dt <= time.strftime('%Y-%m-%d %H:%M:%S')) and \
|
||||
((not case.date_action_next) or \
|
||||
(dt >= case.date_action_next and \
|
||||
case.date_action_last < case.date_action_next))
|
||||
if not ok:
|
||||
if not case.date_action_next or dt < case.date_action_next:
|
||||
case.date_action_next = dt
|
||||
self.write(cr, uid, [case.id], {'date_action_next': dt}, context)
|
||||
|
||||
else:
|
||||
ok = action.trg_date_type=='none'
|
||||
|
||||
if ok:
|
||||
if action.server_action_id:
|
||||
context.update({'active_id':case.id,'active_ids':[case.id]})
|
||||
self.pool.get('ir.actions.server').run(cr, uid, [action.server_action_id.id], context)
|
||||
write = {}
|
||||
if action.act_state:
|
||||
case.state = action.act_state
|
||||
write['state'] = action.act_state
|
||||
if action.act_section_id:
|
||||
case.section_id = action.act_section_id
|
||||
write['section_id'] = action.act_section_id.id
|
||||
if action.act_user_id:
|
||||
case.user_id = action.act_user_id
|
||||
write['user_id'] = action.act_user_id.id
|
||||
if action.act_priority:
|
||||
case.priority = action.act_priority
|
||||
write['priority'] = action.act_priority
|
||||
if action.act_email_cc:
|
||||
if '@' in (case.email_cc or ''):
|
||||
emails = case.email_cc.split(",")
|
||||
if action.act_email_cc not in emails:# and '<'+str(action.act_email_cc)+">" not in emails:
|
||||
write['email_cc'] = case.email_cc+','+action.act_email_cc
|
||||
else:
|
||||
write['email_cc'] = action.act_email_cc
|
||||
write['date_action_last'] = time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
self.write(cr, uid, [case.id], write, context)
|
||||
caseobj = self.pool.get('crm.case')
|
||||
if action.act_remind_user:
|
||||
caseobj.remind_user(cr, uid, [case.id], context, attach=action.act_remind_attach)
|
||||
if action.act_remind_partner:
|
||||
caseobj.remind_partner(cr, uid, [case.id], context, attach=action.act_remind_attach)
|
||||
if action.act_method:
|
||||
getattr(caseobj, 'act_method')(cr, uid, [case.id], action, context)
|
||||
emails = []
|
||||
if action.act_mail_to_user:
|
||||
if case.user_id and case.user_id.address_id:
|
||||
emails.append(case.user_id.address_id.email)
|
||||
if action.act_mail_to_partner:
|
||||
emails.append(case.email_from)
|
||||
if action.act_mail_to_watchers:
|
||||
emails += (action.act_email_cc or '').split(',')
|
||||
if action.act_mail_to_email:
|
||||
emails += (action.act_mail_to_email or '').split(',')
|
||||
emails = filter(None, emails)
|
||||
if len(emails) and action.act_mail_body:
|
||||
emails = list(set(emails))
|
||||
self.email_send(cr, uid, case, emails, action.act_mail_body)
|
||||
break
|
||||
action_ids = newactions
|
||||
level -= 1
|
||||
return True
|
||||
|
||||
def format_body(self, body):
|
||||
return body and tools.ustr(body) or ''
|
||||
|
||||
def format_mail(self, case, body):
|
||||
data = {
|
||||
'case_id': case.id,
|
||||
'case_subject': case.name,
|
||||
'case_date': case.date,
|
||||
'case_description': case.description,
|
||||
|
||||
'case_user': (case.user_id and case.user_id.name) or '/',
|
||||
'case_user_email': (case.user_id and case.user_id.address_id and case.user_id.address_id.email) or '/',
|
||||
'case_user_phone': (case.user_id and case.user_id.address_id and case.user_id.address_id.phone) or '/',
|
||||
|
||||
'email_from': case.email_from,
|
||||
'partner': (case.partner_id and case.partner_id.name) or '/',
|
||||
'partner_email': (case.partner_address_id and case.partner_address_id.email) or '/',
|
||||
}
|
||||
return self.format_body(body % data)
|
||||
|
||||
|
||||
def __history(self, cr, uid, cases, keyword, history=False, email=False, details=None, context={}):
|
||||
model_obj = self.pool.get('ir.model')
|
||||
for case in cases:
|
||||
|
@ -615,50 +352,6 @@ class crm_case(osv.osv):
|
|||
self._action(cr,uid, cases, 'draft')
|
||||
return res
|
||||
|
||||
def remind_partner(self, cr, uid, ids, context={}, attach=False):
|
||||
return self.remind_user(cr, uid, ids, context, attach,
|
||||
destination=False)
|
||||
|
||||
def remind_user(self, cr, uid, ids, context={}, attach=False,
|
||||
destination=True):
|
||||
for case in self.browse(cr, uid, ids):
|
||||
if not case.section_id.reply_to:
|
||||
raise osv.except_osv(_('Error!'), ("Reply To is not specified in Section"))
|
||||
if not case.email_from:
|
||||
raise osv.except_osv(_('Error!'), ("Partner Email is not specified in Case"))
|
||||
if case.section_id.reply_to and case.email_from:
|
||||
src = case.email_from
|
||||
dest = case.section_id.reply_to
|
||||
body = case.email_last or case.description
|
||||
if not destination:
|
||||
src, dest = dest, src
|
||||
if case.user_id.signature:
|
||||
body += '\n\n%s' % (case.user_id.signature or '')
|
||||
dest = [dest]
|
||||
|
||||
attach_to_send = None
|
||||
|
||||
if attach:
|
||||
attach_ids = self.pool.get('ir.attachment').search(cr, uid, [('res_model', '=', 'crm.case'), ('res_id', '=', case.id)])
|
||||
attach_to_send = self.pool.get('ir.attachment').read(cr, uid, attach_ids, ['datas_fname','datas'])
|
||||
attach_to_send = map(lambda x: (x['datas_fname'], base64.decodestring(x['datas'])), attach_to_send)
|
||||
|
||||
# Send an email
|
||||
flag = tools.email_send(
|
||||
src,
|
||||
dest,
|
||||
"Reminder: [%s] %s" % (str(case.id), case.name, ),
|
||||
self.format_body(body),
|
||||
reply_to=case.section_id.reply_to,
|
||||
openobject_id=str(case.id),
|
||||
attach=attach_to_send
|
||||
)
|
||||
if flag:
|
||||
raise osv.except_osv(_('Email!'),("Email Successfully Sent"))
|
||||
else:
|
||||
raise osv.except_osv(_('Email Fail!'),("Email is not sent successfully"))
|
||||
return True
|
||||
|
||||
def add_reply(self, cursor, user, ids, context=None):
|
||||
for case in self.browse(cursor, user, ids, context=context):
|
||||
if case.email_last:
|
||||
|
@ -757,7 +450,7 @@ class crm_case(osv.osv):
|
|||
self.write(cr, uid, ids, data)
|
||||
cases = self.browse(cr, uid, ids)
|
||||
self.__history(cr, uid, cases, _('Escalate'))
|
||||
self._action(cr, uid, cases, 'escalate')
|
||||
self._action(cr,uid, cases, 'escalate')
|
||||
return True
|
||||
|
||||
|
||||
|
@ -794,8 +487,9 @@ class crm_case(osv.osv):
|
|||
cases[0].state # to fill the browse record cache
|
||||
self.__history(cr, uid, cases, _('Draft'))
|
||||
self.write(cr, uid, ids, {'state':'draft', 'active':True})
|
||||
self._action(cr, uid, cases, 'draft')
|
||||
return True
|
||||
self._action(cr,uid, cases, 'draft')
|
||||
return True
|
||||
|
||||
crm_case()
|
||||
|
||||
|
||||
|
@ -893,13 +587,6 @@ class crm_email_add_cc_wizard(osv.osv_memory):
|
|||
|
||||
crm_email_add_cc_wizard()
|
||||
|
||||
def _section_get(self, cr, uid, context={}):
|
||||
obj = self.pool.get('crm.case.section')
|
||||
ids = obj.search(cr, uid, [])
|
||||
res = obj.read(cr, uid, ids, ['id','name'], context)
|
||||
res = [(str(r['id']),r['name']) for r in res]
|
||||
return res
|
||||
|
||||
class users(osv.osv):
|
||||
_inherit = 'res.users'
|
||||
_description = "Users"
|
||||
|
@ -907,6 +594,3 @@ class users(osv.osv):
|
|||
'context_section_id': fields.many2one('crm.case.section', 'Sales Section'),
|
||||
}
|
||||
users()
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,193 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
import time
|
||||
import re
|
||||
import os
|
||||
import base64
|
||||
import tools
|
||||
import mx.DateTime
|
||||
|
||||
from tools.translate import _
|
||||
from osv import fields
|
||||
from osv import osv
|
||||
from osv import orm
|
||||
from osv.orm import except_orm
|
||||
|
||||
import crm
|
||||
|
||||
class case(osv.osv):
|
||||
_inherit = 'crm.case'
|
||||
_columns = {
|
||||
'date_action_last': fields.datetime('Last Action', readonly=1),
|
||||
'date_action_next': fields.datetime('Next Action', readonly=1),
|
||||
}
|
||||
|
||||
def remind_partner(self, cr, uid, ids, context={}, attach=False):
|
||||
return self.remind_user(cr, uid, ids, context, attach,
|
||||
destination=False)
|
||||
|
||||
def remind_user(self, cr, uid, ids, context={}, attach=False,
|
||||
destination=True):
|
||||
for case in self.browse(cr, uid, ids):
|
||||
if not case.section_id.reply_to:
|
||||
raise osv.except_osv(_('Error!'), ("Reply To is not specified in Section"))
|
||||
if not case.email_from:
|
||||
raise osv.except_osv(_('Error!'), ("Partner Email is not specified in Case"))
|
||||
if case.section_id.reply_to and case.email_from:
|
||||
src = case.email_from
|
||||
dest = case.section_id.reply_to
|
||||
body = case.email_last or case.description
|
||||
if not destination:
|
||||
src, dest = dest, src
|
||||
if case.user_id.signature:
|
||||
body += '\n\n%s' % (case.user_id.signature or '')
|
||||
dest = [dest]
|
||||
|
||||
attach_to_send = None
|
||||
|
||||
if attach:
|
||||
attach_ids = self.pool.get('ir.attachment').search(cr, uid, [('res_model', '=', 'crm.case'), ('res_id', '=', case.id)])
|
||||
attach_to_send = self.pool.get('ir.attachment').read(cr, uid, attach_ids, ['datas_fname','datas'])
|
||||
attach_to_send = map(lambda x: (x['datas_fname'], base64.decodestring(x['datas'])), attach_to_send)
|
||||
|
||||
# Send an email
|
||||
flag = tools.email_send(
|
||||
src,
|
||||
dest,
|
||||
"Reminder: [%s] %s" % (str(case.id), case.name, ),
|
||||
self.format_body(body),
|
||||
reply_to=case.section_id.reply_to,
|
||||
openobject_id=str(case.id),
|
||||
attach=attach_to_send
|
||||
)
|
||||
if flag:
|
||||
raise osv.except_osv(_('Email!'),("Email Successfully Sent"))
|
||||
else:
|
||||
raise osv.except_osv(_('Email Fail!'),("Email is not sent successfully"))
|
||||
return True
|
||||
|
||||
def _check(self, cr, uid, ids=False, context={}):
|
||||
'''
|
||||
Function called by the scheduler to process cases for date actions
|
||||
Only works on not done and cancelled cases
|
||||
'''
|
||||
cr.execute('select * from crm_case \
|
||||
where (date_action_last<%s or date_action_last is null) \
|
||||
and (date_action_next<=%s or date_action_next is null) \
|
||||
and state not in (\'cancel\',\'done\')',
|
||||
(time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
time.strftime('%Y-%m-%d %H:%M:%S')))
|
||||
ids2 = map(lambda x: x[0], cr.fetchall() or [])
|
||||
cases = self.browse(cr, uid, ids2, context)
|
||||
return self._action(cr, uid, cases, False, context=context)
|
||||
|
||||
def _action(self, cr, uid, cases, state_to, scrit=None, context={}):
|
||||
if not context:
|
||||
context = {}
|
||||
context['state_to'] = state_to
|
||||
rule_obj = self.pool.get('base.action.rule')
|
||||
model_obj = self.pool.get('ir.model')
|
||||
model_ids = model_obj.search(cr, uid, [('model','=',self._name)])
|
||||
rule_ids = rule_obj.search(cr, uid, [('name','=',model_ids[0])])
|
||||
return rule_obj._action(cr, uid, rule_ids, cases, scrit=scrit, context=context)
|
||||
|
||||
def format_body(self, body):
|
||||
return self.pool.get('base.action.rule').format_body(body)
|
||||
|
||||
def format_mail(self, obj, body):
|
||||
return self.pool.get('base.action.rule').format_mail(obj, body)
|
||||
case()
|
||||
|
||||
class base_action_rule(osv.osv):
|
||||
_inherit = 'base.action.rule'
|
||||
_description = 'Action Rules'
|
||||
|
||||
def do_check(self, cr, uid, action, obj, context={}):
|
||||
ok = super(base_action_rule, self).do_check(cr, uid, action, obj, context=context)
|
||||
|
||||
if hasattr(obj, 'section_id'):
|
||||
ok = ok and (not action.trg_section_id or action.trg_section_id.id==obj.section_id.id)
|
||||
if hasattr(obj, 'categ_id'):
|
||||
ok = ok and (not action.trg_categ_id or action.trg_categ_id.id==obj.categ_id.id)
|
||||
if hasattr(obj, 'history_line'):
|
||||
ok = ok and (not action.trg_max_history or action.trg_max_history<=(len(obj.history_line)+1))
|
||||
reg_history = action.regex_history
|
||||
result_history = True
|
||||
if reg_history:
|
||||
ptrn = re.compile(str(reg_history))
|
||||
if obj.history_line:
|
||||
_result = ptrn.search(str(obj.history_line[0].description))
|
||||
if not _result:
|
||||
result_history = False
|
||||
regex_h = not reg_history or result_history
|
||||
ok = ok and regex_h
|
||||
return ok
|
||||
|
||||
def do_action(self, cr, uid, action, model_obj, obj, context={}):
|
||||
res = super(base_action_rule, self).do_action(cr, uid, action, model_obj, obj, context=context)
|
||||
write = {}
|
||||
|
||||
if action.act_section_id:
|
||||
obj.section_id = action.act_section_id
|
||||
write['section_id'] = action.act_section_id.id
|
||||
|
||||
if hasattr(obj, 'email_cc') and action.act_email_cc:
|
||||
if '@' in (obj.email_cc or ''):
|
||||
emails = obj.email_cc.split(",")
|
||||
if obj.act_email_cc not in emails:# and '<'+str(action.act_email_cc)+">" not in emails:
|
||||
write['email_cc'] = obj.email_cc+','+obj.act_email_cc
|
||||
else:
|
||||
write['email_cc'] = obj.act_email_cc
|
||||
|
||||
model_obj.write(cr, uid, [obj.id], write, context)
|
||||
if hasattr(obj, 'email_from') and action.act_mail_to_partner:
|
||||
emails.append(obj.email_from)
|
||||
emails = filter(None, emails)
|
||||
if len(emails) and action.act_mail_body:
|
||||
emails = list(set(emails))
|
||||
self.email_send(cr, uid, obj, emails, action.act_mail_body)
|
||||
return True
|
||||
|
||||
|
||||
base_action_rule()
|
||||
|
||||
class base_action_rule_line(osv.osv):
|
||||
_inherit = 'base.action.rule.line'
|
||||
|
||||
def state_get(self, cr, uid, context={}):
|
||||
res = super(base_action_rule_line, self).state_get(cr, uid, context=context)
|
||||
return res + [('escalate','Escalate')] + crm.AVAILABLE_STATES
|
||||
|
||||
def priority_get(self, cr, uid, context={}):
|
||||
res = super(base_action_rule_line, self).priority_get(cr, uid, context=context)
|
||||
return res + crm.AVAILABLE_PRIORITIES
|
||||
|
||||
_columns = {
|
||||
'trg_section_id': fields.many2one('crm.case.section', 'Section'),
|
||||
'trg_max_history': fields.integer('Maximum Communication History'),
|
||||
'trg_categ_id': fields.many2one('crm.case.categ', 'Category'),
|
||||
'regex_history' : fields.char('Regular Expression on Case History', size=128),
|
||||
'act_section_id': fields.many2one('crm.case.section', 'Set section to'),
|
||||
'act_mail_to_partner': fields.boolean('Mail to partner',help="Check this if you want the rule to send an email to the partner."),
|
||||
}
|
||||
|
||||
base_action_rule_line()
|
|
@ -0,0 +1,45 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<!-- Case rules -->
|
||||
<record id="view_base_action_rule_line_form1" model="ir.ui.view">
|
||||
<field name="name">base.action.rule.line.form.inherit</field>
|
||||
<field name="model">base.action.rule.line</field>
|
||||
<field name="inherit_id" ref="base_action_rule.view_base_action_rule_line_form"/>
|
||||
<field name="type">form</field>
|
||||
<field name="arch" type="xml">
|
||||
<field name="trg_partner_categ_id" position="after">
|
||||
<separator colspan="4" string="Condition on Communication History"/>
|
||||
<field name="regex_history"/>
|
||||
<field name="trg_max_history"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_base_action_rule_line_form2" model="ir.ui.view">
|
||||
<field name="name">base.action.rule.line.form2.inherit</field>
|
||||
<field name="model">base.action.rule.line</field>
|
||||
<field name="inherit_id" ref="base_action_rule.view_base_action_rule_line_form"/>
|
||||
<field name="type">form</field>
|
||||
<field name="arch" type="xml">
|
||||
<field name="regex_name" position="after">
|
||||
<field name="trg_section_id" select="1" widget="selection"/>
|
||||
<field name="trg_categ_id"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_base_action_rule_line_form3" model="ir.ui.view">
|
||||
<field name="name">base.action.rule.line.form3.inherit</field>
|
||||
<field name="model">base.action.rule.line</field>
|
||||
<field name="inherit_id" ref="base_action_rule.view_base_action_rule_line_form"/>
|
||||
<field name="type">form</field>
|
||||
<field name="arch" type="xml">
|
||||
<field name="act_user_id" position="after">
|
||||
<field name="act_section_id"/>
|
||||
<field name="act_mail_to_partner"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
|
@ -110,8 +110,6 @@
|
|||
<separator colspan="4" string="Dates"/>
|
||||
<field name="create_date"/>
|
||||
<field name="date_closed"/>
|
||||
<field name="date_action_last"/>
|
||||
<field name="date_action_next"/>
|
||||
<field name="log_ids" nolabel="1" colspan="4">
|
||||
<form string="Actions">
|
||||
<separator string="Action Information" colspan="4"/>
|
||||
|
|
|
@ -66,7 +66,7 @@
|
|||
<field name="interval_type">hours</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field eval="False" name="doall"/>
|
||||
<field eval="'crm.case.rule'" name="model"/>
|
||||
<field eval="'crm.case'" name="model"/>
|
||||
<field eval="'_check'" name="function"/>
|
||||
<field eval="'()'" name="args"/>
|
||||
</record>
|
||||
|
|
|
@ -109,8 +109,6 @@
|
|||
<separator colspan="4" string="Dates"/>
|
||||
<field name="create_date"/>
|
||||
<field name="date_closed"/>
|
||||
<field name="date_action_last"/>
|
||||
<field name="date_action_next"/>
|
||||
<separator colspan="4" string="References"/>
|
||||
<field name="ref" colspan="4"/>
|
||||
<field name="ref2" colspan="4"/>
|
||||
|
|
|
@ -70,8 +70,6 @@
|
|||
<separator colspan="4" string="Dates"/>
|
||||
<field name="create_date"/>
|
||||
<field name="date_closed"/>
|
||||
<field name="date_action_last"/>
|
||||
<field name="date_action_next"/>
|
||||
<separator colspan="4" string="Estimates"/>
|
||||
<field name="planned_revenue"/>
|
||||
<field name="planned_cost"/>
|
||||
|
|
|
@ -99,8 +99,6 @@
|
|||
<separator colspan="4" string="Dates"/>
|
||||
<field name="create_date"/>
|
||||
<field name="date_closed"/>
|
||||
<field name="date_action_last"/>
|
||||
<field name="date_action_next"/>
|
||||
<separator colspan="4" string="References"/>
|
||||
<field name="ref" colspan="4"/>
|
||||
<field name="ref2" colspan="4"/>
|
||||
|
|
|
@ -129,109 +129,8 @@
|
|||
<field name="view_id" ref="crm_case_section_view_tree"/>
|
||||
</record>
|
||||
<!-- <menuitem action="crm_case_section_act_tree" id="menu_crm_case_section_act_tree" parent="next_id_52"/>-->
|
||||
|
||||
|
||||
|
||||
<!-- Case rules -->
|
||||
<record id="crm_case_rule-view" model="ir.ui.view">
|
||||
<field name="name">crm.case.rule.form</field>
|
||||
<field name="model">crm.case.rule</field>
|
||||
<field name="type">form</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Case Rule">
|
||||
<field name="name" select="1"/>
|
||||
<field name="active" select="2"/>
|
||||
<notebook colspan="4">
|
||||
<page string="Conditions">
|
||||
<separator colspan="4" string="Conditions on States"/>
|
||||
<field name="trg_state_from" select="2"/>
|
||||
<field name="trg_state_to" select="2"/>
|
||||
|
||||
<separator colspan="4" string="Conditions on Case Fields"/>
|
||||
<field name="regex_name" string="Regex on Case Name" colspan="2"/>
|
||||
<field name="trg_section_id" select="1" widget="selection"/>
|
||||
<!--<field name="trg_categ_id"/>-->
|
||||
<field name="trg_user_id" select="2"/>
|
||||
<separator colspan="4" string="Conditions on Case Partner"/>
|
||||
<field name="trg_partner_id"/>
|
||||
<field name="trg_partner_categ_id"/>
|
||||
<separator colspan="4" string="Conditions on Priority Range"/>
|
||||
<field name="trg_priority_from"/>
|
||||
<field name="trg_priority_to"/>
|
||||
<separator colspan="4" string="Conditions on Timing"/>
|
||||
<field name="trg_date_type"/>
|
||||
<label align="1.0" string="Delay After Trigger Date:"/>
|
||||
<group col="2" colspan="1">
|
||||
<field name="trg_date_range" nolabel="1"/>
|
||||
<field name="trg_date_range_type" nolabel="1"/>
|
||||
</group>
|
||||
<separator colspan="4" string="Condition on Communication History"/>
|
||||
<field name="regex_history" string="Regex on Communication History"/>
|
||||
<field name="trg_max_history"/>
|
||||
<separator colspan="4" string="Note"/>
|
||||
<label align="0.0" string="The rule use a AND operator. The case must match all non empty fields so that the rule execute the action described in the 'Actions' tab." colspan="4"/>
|
||||
</page>
|
||||
<page string="Actions">
|
||||
<separator colspan="4" string="Fields to Change"/>
|
||||
<field name="act_state"/>
|
||||
<field name="act_section_id"/>
|
||||
<field name="act_user_id"/>
|
||||
<field name="act_priority"/>
|
||||
<separator colspan="4" string="E-Mail Reminders (includes the content of the case)"/>
|
||||
<field name="act_remind_user"/>
|
||||
<field name="act_remind_partner"/>
|
||||
<field name="act_remind_attach"/>
|
||||
<field colspan="4" name="act_email_cc"/>
|
||||
<separator colspan="4" string="Server Action to be Triggered"/>
|
||||
<field name="server_action_id"/>
|
||||
<!--
|
||||
<field name="act_method" colspan="4" readonly="1"/>
|
||||
-->
|
||||
</page>
|
||||
<page string="E-Mail Actions">
|
||||
<separator colspan="4" string="Template of Email to Send"/>
|
||||
<field name="act_mail_to_user"/>
|
||||
<field name="act_mail_to_partner"/>
|
||||
<field name="act_mail_to_watchers"/>
|
||||
<field colspan="4" name="act_mail_to_email"/>
|
||||
<field colspan="4" name="act_mail_body"/>
|
||||
|
||||
<separator colspan="4" string="Special Keywords to Be Used in The Body"/>
|
||||
<label align="0.0" string="%%(case_id)s = Case ID" colspan="2"/>
|
||||
<label align="0.0" string="%%(case_subject)s = Case subject" colspan="2"/>
|
||||
<label align="0.0" string="%%(case_description)s = Case description" colspan="2"/>
|
||||
<label align="0.0" string="%%(case_date)s = Creation date" colspan="2"/>
|
||||
<label align="0.0" string="%%(email_from)s = Partner email" colspan="2"/>
|
||||
<label align="0.0" string="%%(partner)s = Partner name" colspan="2"/>
|
||||
<label align="0.0" string="%%(partner_email)s = Partner email" colspan="2"/>
|
||||
<label align="0.0" string="%%(case_user)s = Responsible name" colspan="2"/>
|
||||
<label align="0.0" string="%%(case_user_email)s = Responsible email" colspan="2"/>
|
||||
<label align="0.0" string="%%(case_user_phone)s = Responsible phone" colspan="2"/>
|
||||
<label align="0.0" string="%% = The '%%' Character" colspan="2"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="crm_case_rule_tree-view" model="ir.ui.view">
|
||||
<field name="name">crm.case.rule.tree</field>
|
||||
<field name="model">crm.case.rule</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Case Rule">
|
||||
<field name="name"/>
|
||||
<field name="active"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="crm_case_rule-act" model="ir.actions.act_window">
|
||||
<field name="name">Rules</field>
|
||||
<field name="res_model">crm.case.rule</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_id" ref="crm_case_rule_tree-view"/>
|
||||
</record>
|
||||
<menuitem action="crm_case_rule-act" id="menu_crm_case_rule-act" parent="crm.menu_crm_configuration"/>
|
||||
|
||||
|
||||
|
||||
<record id="view_crm_email_add_cc_wizard" model="ir.ui.view">
|
||||
<field name="name">Add CC</field>
|
||||
<field name="model">crm.email.add.cc</field>
|
||||
|
@ -377,8 +276,6 @@
|
|||
<field name="active" select="2"/>
|
||||
<separator colspan="4" string="Dates"/>
|
||||
<field name="create_date"/>
|
||||
<field name="date_action_last"/>
|
||||
<field name="date_action_next"/>
|
||||
<field colspan="4" name="log_ids" nolabel="1">
|
||||
<form string="Actions">
|
||||
<separator colspan="4" string="Action Information"/>
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
"access_crm_segmentation_line","crm.segmentation.line","model_crm_segmentation_line","crm.group_crm_manager",1,1,1,1
|
||||
"access_crm_case_section","crm.case.section","model_crm_case_section","crm.group_crm_user",1,0,0,0
|
||||
"access_crm_case_categ","crm.case.categ","model_crm_case_categ","crm.group_crm_user",1,0,0,0
|
||||
"access_crm_case_rule","crm.case.rule","model_crm_case_rule","crm.group_crm_user",1,0,0,0
|
||||
"access_crm_case_manger","crm.case manager","model_crm_case","crm.group_crm_user",1,1,1,1
|
||||
"access_crm_case","crm.case","model_crm_case","crm.group_crm_manager",1,1,1,1
|
||||
"access_crm_claim","crm.claim","model_crm_claim","crm.group_crm_manager",1,1,1,1
|
||||
|
@ -17,7 +16,6 @@
|
|||
"access_crm_case_history","crm.case.history","model_crm_case_history","crm.group_crm_user",1,1,1,1
|
||||
"access_crm_case_section_manager","crm.case.section.manager","model_crm_case_section","crm.group_crm_manager",1,1,1,1
|
||||
"access_crm_case_categ_manager","crm.case.categ.manager","model_crm_case_categ","crm.group_crm_manager",1,1,1,1
|
||||
"access_crm_case_rule_manager","crm.case.rule.manager","model_crm_case_rule","crm.group_crm_manager",1,1,1,1
|
||||
"access_crm_case_log_manager","crm.case.log manager","model_crm_case_log","crm.group_crm_manager",1,1,1,1
|
||||
"access_crm_case_history_manager","crm.case.history manager","model_crm_case_history","crm.group_crm_manager",1,1,1,1
|
||||
"access_crm_email_add_cc_manager","crm.email.add.cc","model_crm_email_add_cc","crm.group_crm_manager",1,1,1,1
|
||||
|
|
|
|
@ -19,22 +19,9 @@
|
|||
#
|
||||
##############################################################################
|
||||
|
||||
import time
|
||||
import re
|
||||
import os
|
||||
|
||||
import mx.DateTime
|
||||
import base64
|
||||
|
||||
from tools.translate import _
|
||||
|
||||
import tools
|
||||
from osv import fields,osv,orm
|
||||
from osv.orm import except_orm
|
||||
|
||||
from crm import crm
|
||||
|
||||
|
||||
class crm_job(osv.osv):
|
||||
_name = "crm.job"
|
||||
_description = "Job Cases"
|
||||
|
@ -63,13 +50,6 @@ class crm_job(osv.osv):
|
|||
"the partner mentality in relation to our services.The scale has" \
|
||||
"to be created with a factor for each level from 0 (Very dissatisfied) to 10 (Extremely satisfied)."),
|
||||
'phonecall_id':fields.many2one ('crm.phonecall', 'Phonecall'),
|
||||
|
||||
|
||||
}
|
||||
def onchange_categ_id(self, cr, uid, ids, categ, context={}):
|
||||
if not categ:
|
||||
return {'value':{}}
|
||||
cat = self.pool.get('crm.case.categ').browse(cr, uid, categ, context).probability
|
||||
return {'value':{'probability':cat}}
|
||||
|
||||
crm_job()
|
||||
|
|
|
@ -127,8 +127,6 @@
|
|||
<separator colspan="4" string="Dates"/>
|
||||
<field name="create_date"/>
|
||||
<field name="date_closed"/>
|
||||
<field name="date_action_last"/>
|
||||
<field name="date_action_next"/>
|
||||
<separator colspan="4" string="References"/>
|
||||
<field name="ref" colspan="4"/>
|
||||
<field name="ref2" colspan="4"/>
|
||||
|
|
|
@ -80,8 +80,6 @@
|
|||
<separator colspan="4" string="Dates"/>
|
||||
<field name="create_date"/>
|
||||
<field name="date_closed"/>
|
||||
<field name="date_action_last"/>
|
||||
<field name="date_action_next"/>
|
||||
<separator colspan="4" string="Estimates"/>
|
||||
<field name="planned_revenue"/>
|
||||
<field name="planned_cost"/>
|
||||
|
|
|
@ -216,8 +216,6 @@
|
|||
<separator colspan="4" string="Dates"/>
|
||||
<field name="create_date"/>
|
||||
<field name="date_closed"/>
|
||||
<field name="date_action_last"/>
|
||||
<field name="date_action_next"/>
|
||||
<separator colspan="4" string="References"/>
|
||||
<field name="ref" colspan="4"/>
|
||||
<field name="ref2" colspan="4"/>
|
||||
|
|
Loading…
Reference in New Issue