Browse Source

Now uses the phonenumber lib to reformat numbers in "clean" format and store them in E.164 format. Start to modify the code to take advantage of this great lib.

Thanks to Ludovic Gasc for pointing this lib to me during PyconFR 2012 in Paris !

Fix a bug with Open Calling Partner when CallerID has non-ASCII chars.

Move all code of wizard to wizard directory.

IMPORTANT : when you upgrade to this revision, run the wizard "Reformat all phone numbers" from Settings > Configuration > Asterisk.
pull/26/head
Alexis de Lattre 12 years ago
parent
commit
94269758c5
  1. 2
      asterisk_click2dial/__init__.py
  2. 2
      asterisk_click2dial/__openerp__.py
  3. 380
      asterisk_click2dial/asterisk_click2dial.py
  4. 3
      asterisk_click2dial/asterisk_server_view.xml
  5. 56
      asterisk_click2dial/res_partner_view.xml
  6. 23
      asterisk_click2dial/wizard/__init__.py
  7. 218
      asterisk_click2dial/wizard/open_calling_partner.py
  8. 68
      asterisk_click2dial/wizard/open_calling_partner_view.xml
  9. 63
      asterisk_click2dial/wizard/reformat_all_phonenumbers.py
  10. 39
      asterisk_click2dial/wizard/reformat_all_phonenumbers_view.xml

2
asterisk_click2dial/__init__.py

@ -20,4 +20,4 @@
##############################################################################
import asterisk_click2dial
import wizard

2
asterisk_click2dial/__openerp__.py

@ -55,6 +55,8 @@ A detailed documentation for this module is available on the Akretion Web site :
'asterisk_server_view.xml',
'res_users_view.xml',
'res_partner_view.xml',
'wizard/open_calling_partner_view.xml',
'wizard/reformat_all_phonenumbers_view.xml',
'security/asterisk_server_security.xml',
],
'demo_xml': ['asterisk_click2dial_demo.xml'],

380
asterisk_click2dial/asterisk_click2dial.py

@ -26,8 +26,8 @@ import socket
import logging
# Lib to translate error messages
from tools.translate import _
# Lib for regexp
import re
# Lib for phone number reformating -> pip install phonenumbers
import phonenumbers
_logger = logging.getLogger(__name__)
@ -44,7 +44,6 @@ class asterisk_server(osv.osv):
'national_prefix': fields.char('National prefix', size=4, help="Prefix for national phone calls (don't include the 'out prefix'). For e.g., in France, the phone numbers look like '01 41 98 12 42' : the National prefix is '0'."),
'international_prefix': fields.char('International prefix', required=True, size=4, help="Prefix to add to make international phone calls (don't include the 'out prefix'). For e.g., in France, the International prefix is '00'."),
'country_prefix': fields.char('My country prefix', required=True, size=4, help="Phone prefix of the country where the Asterisk server is located. For e.g. the phone prefix for France is '33'. If the phone number to dial starts with the 'My country prefix', OpenERP will remove the country prefix from the phone number and add the 'out prefix' followed by the 'national prefix'. If the phone number to dial doesn't start with the 'My country prefix', OpenERP will add the 'out prefix' followed by the 'international prefix'."),
'national_format_allowed': fields.boolean('National format allowed ?', help="Do we allow to use click2dial on phone numbers written in national format, e.g. 01 41 98 12 42, or only in the international format, e.g. +33 1 41 98 12 42 ?"),
'login': fields.char('AMI login', size=30, required=True, help="Login that OpenERP will use to communicate with the Asterisk Manager Interface. Refer to /etc/asterisk/manager.conf on your Asterisk server."),
'password': fields.char('AMI password', size=30, required=True, help="Password that Asterisk will use to communicate with the Asterisk Manager Interface. Refer to /etc/asterisk/manager.conf on your Asterisk server."),
'context': fields.char('Dialplan context', size=50, required=True, help="Asterisk dialplan context from which the calls will be made. Refer to /etc/asterisk/extensions.conf on your Asterisk server."),
@ -120,53 +119,40 @@ class asterisk_server(osv.osv):
if not tmp_number:
raise osv.except_osv(error_title_msg, invalid_format_msg)
# treat (0) as a special condition as we dont want an extra 0 to be inserted in the number
# FIXME all 0s after the country prefix should be stripped off
tmp_number = tmp_number.replace('(0)','')
# First, we remove all stupid caracters and spaces
for char_to_remove in [' ', '.', '(', ')', '[', ']', '-', '/']:
tmp_number = tmp_number.replace(char_to_remove, '')
# Before starting to use prefix, we convert empty prefix whose value
# is False to an empty string
country_prefix = (ast_server.country_prefix or '')
national_prefix = (ast_server.national_prefix or '')
international_prefix = (ast_server.international_prefix or '')
out_prefix = (ast_server.out_prefix or '')
country_prefix = ast_server.country_prefix or ''
national_prefix = ast_server.national_prefix or ''
international_prefix = ast_server.international_prefix or ''
out_prefix = ast_server.out_prefix or ''
# Maybe one day we will use
# phonenumbers.format_out_of_country_calling_number(phonenumbers.parse('<phone_number_e164', None), 'FR')
# The country code seems to be OK with the ones of OpenERP
# But it returns sometimes numbers with '-'... we have to investigate this first
# International format
if tmp_number[0] == '+':
if tmp_number[0] != '+':
raise # This should never happen
# Remove the starting '+' of the number
tmp_number = tmp_number.replace('+','')
_logger.debug('Number after removal of special char = %s' % tmp_number)
# At this stage, 'tmp_number' should only contain digits
if not tmp_number.isdigit():
raise osv.except_osv(error_title_msg, invalid_format_msg)
tmp_number = tmp_number.replace('+','')
_logger.debug('Number after removal of special char = %s' % tmp_number)
_logger.debug('Country prefix = %s' % country_prefix)
if country_prefix == tmp_number[0:len(country_prefix)]:
# If the number is a national number,
# remove 'my country prefix' and add 'national prefix'
tmp_number = (national_prefix) + tmp_number[len(country_prefix):len(tmp_number)]
_logger.debug('National prefix = %s - Number with national prefix = %s' % (national_prefix, tmp_number))
else:
# If the number is an international number,
# add 'international prefix'
tmp_number = international_prefix + tmp_number
_logger.debug('International prefix = %s - Number with international prefix = %s' % (international_prefix, tmp_number))
# At this stage, 'tmp_number' should only contain digits
if not tmp_number.isdigit():
raise osv.except_osv(error_title_msg, invalid_format_msg)
# National format, allowed
elif ast_server.national_format_allowed:
# No treatment required
if not tmp_number.isdigit():
raise osv.except_osv(error_title_msg, invalid_national_format_msg)
_logger.debug('Country prefix = %s' % country_prefix)
if country_prefix == tmp_number[0:len(country_prefix)]:
# If the number is a national number,
# remove 'my country prefix' and add 'national prefix'
tmp_number = (national_prefix) + tmp_number[len(country_prefix):len(tmp_number)]
_logger.debug('National prefix = %s - Number with national prefix = %s' % (national_prefix, tmp_number))
# National format, disallowed
elif not ast_server.national_format_allowed:
raise osv.except_osv(error_title_msg, invalid_international_format_msg)
else:
# If the number is an international number,
# add 'international prefix'
tmp_number = international_prefix + tmp_number
_logger.debug('International prefix = %s - Number with international prefix = %s' % (international_prefix, tmp_number))
# Add 'out prefix' to all numbers
tmp_number = out_prefix + tmp_number
@ -206,6 +192,10 @@ class asterisk_server(osv.osv):
'''Parse the answer of the Asterisk Manager Interface'''
answer = ''
data = ''
# TODO : if there is an error, we will stay in the while loop
# ex :
# Response: Error
# Message: Permission denied
while end_string not in data:
data = sock.recv(1024)
if data:
@ -306,7 +296,7 @@ class asterisk_server(osv.osv):
elif method == "get_calling_number":
status_act = 'Action: Status\r\n\r\n' # TODO : add ActionID
sock.send(status_act.encode('ascii'))
status_answer = self._parse_asterisk_answer(cr, uid, sock, end_string='Event: StatusComplete', context=context)
status_answer = self._parse_asterisk_answer(cr, uid, sock, end_string='Event: StatusComplete', context=context).decode('utf-8')
if 'Response: Success' in status_answer:
_logger.debug('Successfull Status command :\n%s' % status_answer)
@ -414,26 +404,98 @@ class res_partner_address(osv.osv):
_inherit = "res.partner.address"
def dial(self, cr, uid, ids, phone_field='phone', context=None):
def _format_phonenumber_to_e164(self, cr, uid, ids, name, arg, context=None):
result = {}
for addr in self.read(cr, uid, ids, ['phone', 'mobile', 'fax'], context=context):
result[addr['id']] = {}
for fromfield, tofield in [('phone', 'phone_e164'), ('mobile', 'mobile_e164'), ('fax', 'fax_e164')]:
if not addr.get(fromfield):
res = False
else:
try:
res = phonenumbers.format_number(phonenumbers.parse(addr.get(fromfield), None), phonenumbers.PhoneNumberFormat.E164)
except Exception, e:
_logger.error("Cannot reformat the phone number '%s' to E.164 format. Error message: %s" % (addr.get(fromfield), e))
_logger.error("You should fix this number and run the wizard 'Reformat all phone numbers' from the menu Settings > Configuration > Asterisk")
# If I raise an exception here, it won't be possible to install
# the module on a DB with bad phone numbers
#raise osv.except_osv(_('Error :'), _("Cannot reformat the phone number '%s' to E.164 format. Error message: %s" % (addr.get(fromfield), e)))
res = False
result[addr['id']][tofield] = res
#print "RESULT _format_phonenumber_to_e164", result
return result
_columns = {
'phone_e164': fields.function(_format_phonenumber_to_e164, type='char', size=64, string='Phone in E.164 format', readonly=True, multi="e164", store={
'res.partner.address': (lambda self, cr, uid, ids, c={}: ids, ['phone'], 10),
}),
'mobile_e164': fields.function(_format_phonenumber_to_e164, type='char', size=64, string='Mobile in E.164 format', readonly=True, multi="e164", store={
'res.partner.address': (lambda self, cr, uid, ids, c={}: ids, ['mobile'], 10),
}),
'fax_e164': fields.function(_format_phonenumber_to_e164, type='char', size=64, string='Fax in E.164 format', readonly=True, multi="e164", store={
'res.partner.address': (lambda self, cr, uid, ids, c={}: ids, ['fax'], 10),
}),
}
def _reformat_phonenumbers(self, cr, uid, vals, context=None):
"""Reformat phone numbers in international format i.e. +33141981242"""
phonefields = ['phone', 'fax', 'mobile']
if any([vals.get(field) for field in phonefields]):
user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
# country_id on res.company is a fields.function that looks at
# company_id.partner_id.addres(default).country_id
if user.company_id.country_id:
user_countrycode = user.company_id.country_id.code
else:
# We need to raise an exception here because, if we pass None as second arg of phonenumbers.parse(), it will raise an exception when you try to enter a phone number in national format... so it's better to raise the exception here
raise osv.except_osv(_('Error :'), _("You should set a country on the company '%s'" % user.company_id.name))
#print "user_countrycode=", user_countrycode
for field in phonefields:
if vals.get(field):
try:
res_parse = phonenumbers.parse(vals.get(field), user_countrycode)
except Exception, e:
raise osv.except_osv(_('Error :'), _("Cannot reformat the phone number '%s' to international format. Error message: %s" % (vals.get(field), e)))
#print "res_parse=", res_parse
vals[field] = phonenumbers.format_number(res_parse, phonenumbers.PhoneNumberFormat.INTERNATIONAL)
return vals
def create(self, cr, uid, vals, context=None):
vals_reformated = self._reformat_phonenumbers(cr, uid, vals, context=context)
return super(res_partner_address, self).create(cr, uid, vals_reformated, context=context)
def write(self, cr, uid, ids, vals, context=None):
vals_reformated = self._reformat_phonenumbers(cr, uid, vals, context=context)
return super(res_partner_address, self).write(cr, uid, ids, vals_reformated, context=context)
def dial(self, cr, uid, ids, phone_field=['phone', 'phone_e164'], context=None):
'''Read the number to dial and call _connect_to_asterisk the right way'''
erp_number = self.read(cr, uid, ids, [phone_field], context=context)[0][phone_field]
erp_number_read = self.read(cr, uid, ids[0], phone_field, context=context)
erp_number_e164 = erp_number_read[phone_field[1]]
erp_number_display = erp_number_read[phone_field[0]]
# Check if the number to dial is not empty
if not erp_number:
if not erp_number_display:
raise osv.except_osv(_('Error :'), _('There is no phone number !'))
options = {'erp_number': erp_number}
elif erp_number_display and not erp_number_e164:
raise osv.except_osv(_('Error :'), _("The phone number isn't stored in the standard E.164 format. Try to run the wizard 'Reformat all phone numbers' from the menu Settings > Configuration > Asterisk."))
options = {'erp_number': erp_number_e164}
return self.pool.get('asterisk.server')._connect_to_asterisk(cr, uid, method='dial', options=options, context=context)
def action_dial_phone(self, cr, uid, ids, context=None):
'''Function called by the button 'Dial' next to the 'phone' field
in the partner address view'''
return self.dial(cr, uid, ids, phone_field='phone', context=context)
return self.dial(cr, uid, ids, phone_field=['phone', 'phone_e164'], context=context)
def action_dial_mobile(self, cr, uid, ids, context=None):
'''Function called by the button 'Dial' next to the 'mobile' field
in the partner address view'''
return self.dial(cr, uid, ids, phone_field='mobile', context=context)
return self.dial(cr, uid, ids, phone_field=['mobile', 'mobile_e164'], context=context)
def get_name_from_phone_number(self, cr, uid, number, context=None):
@ -452,7 +514,6 @@ class res_partner_address(osv.osv):
def get_partner_from_phone_number(self, cr, uid, number, context=None):
res = {}
# We check that "number" is really a number
if not isinstance(number, str):
return False
@ -460,214 +521,21 @@ class res_partner_address(osv.osv):
return False
_logger.debug(u"Call get_name_from_phone_number with number = %s" % number)
# Get all the partner addresses :
all_ids = self.search(cr, uid, [], context=context)
# For each partner address, we check if the number matches on the "phone" or "mobile" fields
for entry in self.browse(cr, uid, all_ids, context=context):
if entry.phone:
# We use a regexp on the phone field to remove non-digit caracters
if re.sub(r'\D', '', entry.phone).endswith(number):
_logger.debug(u"Answer get_name_from_phone_number with name = %s" % entry.name)
return (entry.id, entry.partner_id.id, entry.name)
if entry.mobile:
if re.sub(r'\D', '', entry.mobile).endswith(number):
_logger.debug(u"Answer get_name_from_phone_number with name = %s" % entry.name)
return (entry.id, entry.partner_id.id, entry.name)
_logger.debug(u"No match for phone number %s" % number)
return False
res_partner_address()
class wizard_open_calling_partner(osv.osv_memory):
_name = "wizard.open.calling.partner"
_description = "Open calling partner"
_columns = {
# I can't set any field to readonly, because otherwize it would call
# default_get (and thus connect to Asterisk) a second time when the user
# clicks on one of the buttons
'calling_number': fields.char('Calling number', size=30, help="Phone number of calling party that has been obtained from Asterisk."),
'partner_address_id': fields.many2one('res.partner.address', 'Contact name', help="Partner contact related to the calling number. If there is none and you want to update an existing partner"),
'partner_id': fields.many2one('res.partner', 'Partner', help="Partner related to the calling number."),
'to_update_partner_address_id': fields.many2one('res.partner.address', 'Contact to update', help="Partner contact on which the phone or mobile number will be written"),
'current_phone': fields.related('to_update_partner_address_id', 'phone', type='char', relation='res.partner.address', string='Current phone'),
'current_mobile': fields.related('to_update_partner_address_id', 'mobile', type='char', relation='res.partner.address', string='Current mobile'),
}
def default_get(self, cr, uid, fields, context=None):
'''Thanks to the default_get method, we are able to query Asterisk and
get the corresponding partner when we launch the wizard'''
res = {}
calling_number = self.pool.get('asterisk.server')._connect_to_asterisk(cr, uid, method='get_calling_number', context=context)
#To test the code without Asterisk server
#calling_number = "0141981242"
if calling_number:
res['calling_number'] = calling_number
# We match only on the end of the phone number
if len(calling_number) >= 9:
number_to_search = calling_number[-9:len(calling_number)]
else:
number_to_search = calling_number
partner = self.pool.get('res.partner.address').get_partner_from_phone_number(cr, uid, number_to_search, context=context)
if partner:
res['partner_address_id'] = partner[0]
res['partner_id'] = partner[1]
else:
res['partner_id'] = False
res['partner_address_id'] = False
res['to_update_partner_address_id'] = False
else:
_logger.debug("Could not get the calling number from Asterisk.")
raise osv.except_osv(_('Error :'), _("Could not get the calling number from Asterisk. Are you currently on the phone ? If yes, check your setup and look at the OpenERP debug logs."))
return res
def open_filtered_object(self, cr, uid, ids, oerp_object, context=None):
'''Returns the action that opens the list view of the 'oerp_object'
given as argument filtered on the partner'''
# This module only depends on "base"
# and I don't want to add a dependancy on "sale" or "account"
# So I just check here that the model exists, to avoid a crash
if not self.pool.get('ir.model').search(cr, uid, [('model', '=', oerp_object._name)], context=context):
raise osv.except_osv(_('Error :'), _("The object '%s' is not found in your OpenERP database, probably because the related module is not installed." % oerp_object._description))
partner = self.read(cr, uid, ids[0], ['partner_id'], context=context)['partner_id']
if partner:
action = {
'name': oerp_object._description,
'view_type': 'form',
'view_mode': 'tree,form',
'res_model': oerp_object._name,
'type': 'ir.actions.act_window',
'nodestroy': False, # close the pop-up wizard after action
'target': 'current',
'domain': [('partner_id', '=', partner[0])],
}
return action
# We try to match a phone or mobile number with the same end
pg_seach_number = '%' + number
res_ids = self.search(cr, uid, ['|', ('phone_e164', 'ilike', pg_seach_number), ('mobile_e164', 'ilike', pg_seach_number)], context=context)
# TODO : use is_number_match() of the phonenumber lib ?
if len(res_ids) > 1:
_logger.warning(u"There are several partners addresses (IDS = %s) with the same phone number %s" % (str(res_ids), number))
if res_ids:
entry = self.read(cr, uid, res_ids[0], ['name', 'partner_id'], context=context)
_logger.debug(u"Answer get_partner_from_phone_number with name = %s" % entry['name'])
return (entry['id'], entry['partner_id'] and entry['partner_id'][0] or False, entry['name'])
else:
_logger.debug(u"No match for phone number %s" % number)
return False
def open_sale_orders(self, cr, uid, ids, context=None):
'''Function called by the related button of the wizard'''
return self.open_filtered_object(cr, uid, ids, self.pool.get('sale.order'), context=context)
def open_invoices(self, cr, uid, ids, context=None):
'''Function called by the related button of the wizard'''
return self.open_filtered_object(cr, uid, ids, self.pool.get('account.invoice'), context=context)
def simple_open(self, cr, uid, ids, object_name='res.partner', context=None):
if object_name == 'res.partner':
field = 'partner_id'
label = 'Partner'
elif object_name == 'res.partner.address':
field = 'partner_address_id'
label = 'Contact'
else:
raise osv.except_osv(_('Error :'), "This object '%s' is not supported" % object_name)
record_to_open = self.read(cr, uid, ids[0], [field], context=context)[field]
if record_to_open:
return {
'name': label,
'view_type': 'form',
'view_mode': 'form,tree',
'res_model': object_name,
'type': 'ir.actions.act_window',
'nodestroy': False, # close the pop-up wizard after action
'target': 'current',
'res_id': record_to_open[0],
}
else:
return False
def open_partner(self, cr, uid, ids, context=None):
'''Function called by the related button of the wizard'''
return self.simple_open(cr, uid, ids, object_name='res.partner', context=context)
def open_partner_address(self, cr, uid, ids, context=None):
'''Function called by the related button of the wizard'''
return self.simple_open(cr, uid, ids, object_name='res.partner.address', context=context)
def create_partner_address(self, cr, uid, ids, phone_type='phone', context=None):
'''Function called by the related button of the wizard'''
calling_number = self.read(cr, uid, ids[0], ['calling_number'], context=context)['calling_number']
user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
ast_server = self.pool.get('asterisk.server')._get_asterisk_server_from_user(cr, uid, user, context=context)
# Convert the number to the international format
number_to_write = self.pool.get('asterisk.server')._convert_number_to_international_format(cr, uid, calling_number, ast_server, context=context)
new_partner_address_id = self.pool.get('res.partner.address').create(cr, uid, {phone_type: number_to_write}, context=context)
action = {
'name': 'Create new contact',
'view_type': 'form',
'view_mode': 'form,tree',
'res_model': 'res.partner.address',
'type': 'ir.actions.act_window',
'nodestroy': False,
'target': 'current',
'res_id': new_partner_address_id,
}
return action
def create_partner_address_phone(self, cr, uid, ids, context=None):
return self.create_partner_address(cr, uid, ids, phone_type='phone', context=context)
def create_partner_address_mobile(self, cr, uid, ids, context=None):
return self.create_partner_address(cr, uid, ids, phone_type='mobile', context=context)
def update_partner_address(self, cr, uid, ids, phone_type='mobile', context=None):
cur_wizard = self.browse(cr, uid, ids[0], context=context)
if not cur_wizard.to_update_partner_address_id:
raise osv.except_osv(_('Error :'), _("Select the contact to update."))
user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
ast_server = self.pool.get('asterisk.server')._get_asterisk_server_from_user(cr, uid, user, context=context)
number_to_write = self.pool.get('asterisk.server')._convert_number_to_international_format(cr, uid, cur_wizard.calling_number, ast_server, context=context)
self.pool.get('res.partner.address').write(cr, uid, cur_wizard.to_update_partner_address_id.id, {phone_type: number_to_write}, context=context)
action = {
'name': 'Contact: ' + cur_wizard.to_update_partner_address_id.name,
'view_type': 'form',
'view_mode': 'form,tree',
'res_model': 'res.partner.address',
'type': 'ir.actions.act_window',
'nodestroy': False,
'target': 'current',
'res_id': cur_wizard.to_update_partner_address_id.id
}
return action
def update_partner_address_phone(self, cr, uid, ids, context=None):
return self.update_partner_address(cr, uid, ids, phone_type='phone', context=context)
def update_partner_address_mobile(self, cr, uid, ids, context=None):
return self.update_partner_address(cr, uid, ids, phone_type='mobile', context=context)
def onchange_to_update_partner_address(self, cr, uid, ids, to_update_partner_address_id, context=None):
res = {}
res['value'] = {}
if to_update_partner_address_id:
to_update_partner_address = self.pool.get('res.partner.address').browse(cr, uid, to_update_partner_address_id, context=context)
res['value'].update({'current_phone': to_update_partner_address.phone,
'current_mobile': to_update_partner_address.mobile})
else:
res['value'].update({'current_phone': False, 'current_mobile': False})
return res
wizard_open_calling_partner()
res_partner_address()
# This module supports multi-company

3
asterisk_click2dial/asterisk_server_view.xml

@ -43,7 +43,6 @@
<field name="national_prefix" />
<field name="international_prefix" />
<field name="country_prefix" select="1" />
<field name="national_format_allowed" />
<field name="alert_info" />
<field name="wait_time" />
</form>
@ -76,7 +75,7 @@
</record>
<!-- Menu entry under Settings > Configuration -->
<menuitem id="menu_asterisk_servers" name="Asterisk servers" parent="base.menu_config" groups="base.group_extended"/>
<menuitem id="menu_asterisk_servers" name="Asterisk" parent="base.menu_config" groups="base.group_extended"/>
<menuitem action="action_asterisk_server" id="act_menu_ast_server" parent="menu_asterisk_servers" sequence="50"/>

56
asterisk_click2dial/res_partner_view.xml

@ -80,62 +80,6 @@
</field>
</record>
<!-- Get partner from incoming phone call -->
<record id="view_open_calling_partner" model="ir.ui.view">
<field name="name">view_open_calling_partner</field>
<field name="model">wizard.open.calling.partner</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Open calling partner">
<field name="calling_number" colspan="4"/>
<field name="partner_address_id" attrs="{'invisible':[('partner_address_id','=',False)]}"/>
<field name="partner_id" attrs="{'invisible':[('partner_address_id','=',False)]}" /> <!-- I want it visible when partner_address_id != False and partner_id = False, so that the user can see that this partner_address doesn't have a partner -->
<newline />
<group colspan="4" attrs="{'invisible':[('partner_id','=',False)]}">
<button name="open_partner" icon="gtk-go-forward" string="Partner form" type="object" />
<button name="open_sale_orders" icon="gtk-go-forward" string="Related sale orders" type="object" />
<button name="open_invoices" icon="gtk-go-forward" string="Related invoices" type="object" />
<button special="cancel" icon="gtk-cancel" string="Cancel" />
</group>
<!-- I display the button "Contact form" when the partner address exists but not the partner -->
<group colspan="4" attrs="{'invisible':['|', '&amp;', ('partner_address_id','!=',False), ('partner_id','!=',False), ('partner_address_id','=',False)]}">
<button name="open_partner_address" icon="gtk-go-forward" string="Contact form" type="object" colspan="2"/>
<button special="cancel" icon="gtk-cancel" string="Cancel" colspan="2"/>
</group>
<group attrs="{'invisible':[('partner_address_id','!=',False)]}" colspan="4" col="8">
<label string="No partner contact found in OpenERP with this number" colspan="8" />
<separator string="Create a new contact" colspan="8" />
<button name="create_partner_address_phone" icon="gtk-new" string="with calling number as phone" type="object" colspan="4"/>
<button name="create_partner_address_mobile" icon="gtk-new" string="with calling number as mobile" type="object" colspan="4"/>
<newline />
<separator string="Update an existing contact" colspan="8" />
<field name="to_update_partner_address_id" colspan="8" on_change="onchange_to_update_partner_address(to_update_partner_address_id)"/>
<field name="current_phone" colspan="6"/>
<button name="update_partner_address_phone" icon="gtk-convert" string="Update phone" type="object" colspan="2"/>
<field name="current_mobile" colspan="6"/>
<button name="update_partner_address_mobile" icon="gtk-convert" string="Update mobile" type="object" colspan="2"/>
<newline />
<button special="cancel" icon="gtk-cancel" string="Cancel" colspan="8" />
</group>
<!-- I repeat the cancel button for layout reasons -->
</form>
</field>
</record>
<record id="action_open_calling_partner" model="ir.actions.act_window">
<field name="name">Open calling partner</field>
<field name="res_model">wizard.open.calling.partner</field>
<field name="view_type">form</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<menuitem id="menu_open_calling_partner_sales" parent="base.menu_address_book" action="action_open_calling_partner" sequence="50" />
</data>
</openerp>

23
asterisk_click2dial/wizard/__init__.py

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Asterisk Click2Dial module for OpenERP
# Copyright (C) 2012 Alexis de Lattre <alexis@via.ecp.fr>
#
# 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 reformat_all_phonenumbers
import open_calling_partner

218
asterisk_click2dial/wizard/open_calling_partner.py

@ -0,0 +1,218 @@
# -*- encoding: utf-8 -*-
##############################################################################
#
# Asterisk Click2dial module for OpenERP
# Copyright (C) 2010-2012 Alexis de Lattre <alexis@via.ecp.fr>
#
# 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/>.
#
##############################################################################
from osv import osv, fields
# Lib required to open a socket (needed to communicate with Asterisk server)
import logging
# Lib to translate error messages
from tools.translate import _
_logger = logging.getLogger(__name__)
class wizard_open_calling_partner(osv.osv_memory):
_name = "wizard.open.calling.partner"
_description = "Open calling partner"
_columns = {
# I can't set any field to readonly, because otherwize it would call
# default_get (and thus connect to Asterisk) a second time when the user
# clicks on one of the buttons
'calling_number': fields.char('Calling number', size=30, help="Phone number of calling party that has been obtained from Asterisk."),
'partner_address_id': fields.many2one('res.partner.address', 'Contact name', help="Partner contact related to the calling number. If there is none and you want to update an existing partner"),
'partner_id': fields.many2one('res.partner', 'Partner', help="Partner related to the calling number."),
'to_update_partner_address_id': fields.many2one('res.partner.address', 'Contact to update', help="Partner contact on which the phone or mobile number will be written"),
'current_phone': fields.related('to_update_partner_address_id', 'phone', type='char', relation='res.partner.address', string='Current phone'),
'current_mobile': fields.related('to_update_partner_address_id', 'mobile', type='char', relation='res.partner.address', string='Current mobile'),
}
def default_get(self, cr, uid, fields, context=None):
'''Thanks to the default_get method, we are able to query Asterisk and
get the corresponding partner when we launch the wizard'''
res = {}
calling_number = self.pool.get('asterisk.server')._connect_to_asterisk(cr, uid, method='get_calling_number', context=context)
#To test the code without Asterisk server
#calling_number = "0141981242"
if calling_number:
res['calling_number'] = calling_number
# We match only on the end of the phone number
if len(calling_number) >= 9:
number_to_search = calling_number[-9:len(calling_number)]
else:
number_to_search = calling_number
partner = self.pool.get('res.partner.address').get_partner_from_phone_number(cr, uid, number_to_search, context=context)
if partner:
res['partner_address_id'] = partner[0]
res['partner_id'] = partner[1]
else:
res['partner_id'] = False
res['partner_address_id'] = False
res['to_update_partner_address_id'] = False
else:
_logger.debug("Could not get the calling number from Asterisk.")
raise osv.except_osv(_('Error :'), _("Could not get the calling number from Asterisk. Are you currently on the phone ? If yes, check your setup and look at the OpenERP debug logs."))
return res
def open_filtered_object(self, cr, uid, ids, oerp_object, context=None):
'''Returns the action that opens the list view of the 'oerp_object'
given as argument filtered on the partner'''
# This module only depends on "base"
# and I don't want to add a dependancy on "sale" or "account"
# So I just check here that the model exists, to avoid a crash
if not self.pool.get('ir.model').search(cr, uid, [('model', '=', oerp_object._name)], context=context):
raise osv.except_osv(_('Error :'), _("The object '%s' is not found in your OpenERP database, probably because the related module is not installed." % oerp_object._description))
partner = self.read(cr, uid, ids[0], ['partner_id'], context=context)['partner_id']
if partner:
action = {
'name': oerp_object._description,
'view_type': 'form',
'view_mode': 'tree,form',
'res_model': oerp_object._name,
'type': 'ir.actions.act_window',
'nodestroy': False, # close the pop-up wizard after action
'target': 'current',
'domain': [('partner_id', '=', partner[0])],
}
return action
else:
return False
def open_sale_orders(self, cr, uid, ids, context=None):
'''Function called by the related button of the wizard'''
return self.open_filtered_object(cr, uid, ids, self.pool.get('sale.order'), context=context)
def open_invoices(self, cr, uid, ids, context=None):
'''Function called by the related button of the wizard'''
return self.open_filtered_object(cr, uid, ids, self.pool.get('account.invoice'), context=context)
def simple_open(self, cr, uid, ids, object_name='res.partner', context=None):
if object_name == 'res.partner':
field = 'partner_id'
label = 'Partner'
elif object_name == 'res.partner.address':
field = 'partner_address_id'
label = 'Contact'
else:
raise osv.except_osv(_('Error :'), "This object '%s' is not supported" % object_name)
record_to_open = self.read(cr, uid, ids[0], [field], context=context)[field]
if record_to_open:
return {
'name': label,
'view_type': 'form',
'view_mode': 'form,tree',
'res_model': object_name,
'type': 'ir.actions.act_window',
'nodestroy': False, # close the pop-up wizard after action
'target': 'current',
'res_id': record_to_open[0],
}
else:
return False
def open_partner(self, cr, uid, ids, context=None):
'''Function called by the related button of the wizard'''
return self.simple_open(cr, uid, ids, object_name='res.partner', context=context)
def open_partner_address(self, cr, uid, ids, context=None):
'''Function called by the related button of the wizard'''
return self.simple_open(cr, uid, ids, object_name='res.partner.address', context=context)
def create_partner_address(self, cr, uid, ids, phone_type='phone', context=None):
'''Function called by the related button of the wizard'''
calling_number = self.read(cr, uid, ids[0], ['calling_number'], context=context)['calling_number']
user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
ast_server = self.pool.get('asterisk.server')._get_asterisk_server_from_user(cr, uid, user, context=context)
# Convert the number to the international format
number_to_write = self.pool.get('asterisk.server')._convert_number_to_international_format(cr, uid, calling_number, ast_server, context=context)
new_partner_address_id = self.pool.get('res.partner.address').create(cr, uid, {phone_type: number_to_write}, context=context)
action = {
'name': 'Create new contact',
'view_type': 'form',
'view_mode': 'form,tree',
'res_model': 'res.partner.address',
'type': 'ir.actions.act_window',
'nodestroy': False,
'target': 'current',
'res_id': new_partner_address_id,
}
return action
def create_partner_address_phone(self, cr, uid, ids, context=None):
return self.create_partner_address(cr, uid, ids, phone_type='phone', context=context)
def create_partner_address_mobile(self, cr, uid, ids, context=None):
return self.create_partner_address(cr, uid, ids, phone_type='mobile', context=context)
def update_partner_address(self, cr, uid, ids, phone_type='mobile', context=None):
cur_wizard = self.browse(cr, uid, ids[0], context=context)
if not cur_wizard.to_update_partner_address_id:
raise osv.except_osv(_('Error :'), _("Select the contact to update."))
user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
ast_server = self.pool.get('asterisk.server')._get_asterisk_server_from_user(cr, uid, user, context=context)
number_to_write = self.pool.get('asterisk.server')._convert_number_to_international_format(cr, uid, cur_wizard.calling_number, ast_server, context=context)
self.pool.get('res.partner.address').write(cr, uid, cur_wizard.to_update_partner_address_id.id, {phone_type: number_to_write}, context=context)
action = {
'name': 'Contact: ' + cur_wizard.to_update_partner_address_id.name,
'view_type': 'form',
'view_mode': 'form,tree',
'res_model': 'res.partner.address',
'type': 'ir.actions.act_window',
'nodestroy': False,
'target': 'current',
'res_id': cur_wizard.to_update_partner_address_id.id
}
return action
def update_partner_address_phone(self, cr, uid, ids, context=None):
return self.update_partner_address(cr, uid, ids, phone_type='phone', context=context)
def update_partner_address_mobile(self, cr, uid, ids, context=None):
return self.update_partner_address(cr, uid, ids, phone_type='mobile', context=context)
def onchange_to_update_partner_address(self, cr, uid, ids, to_update_partner_address_id, context=None):
res = {}
res['value'] = {}
if to_update_partner_address_id:
to_update_partner_address = self.pool.get('res.partner.address').browse(cr, uid, to_update_partner_address_id, context=context)
res['value'].update({'current_phone': to_update_partner_address.phone,
'current_mobile': to_update_partner_address.mobile})
else:
res['value'].update({'current_phone': False, 'current_mobile': False})
return res
wizard_open_calling_partner()

68
asterisk_click2dial/wizard/open_calling_partner_view.xml

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Asterisk Click2dial module for OpenERP
Copyright (C) 2012 Alexis de Lattre <alexis@via.ecp.fr>
The licence is in the file __openerp__.py
-->
<openerp>
<data>
<!-- Get partner from incoming phone call -->
<record id="view_open_calling_partner" model="ir.ui.view">
<field name="name">view_open_calling_partner</field>
<field name="model">wizard.open.calling.partner</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Open calling partner">
<field name="calling_number" colspan="4"/>
<field name="partner_address_id" attrs="{'invisible':[('partner_address_id','=',False)]}"/>
<field name="partner_id" attrs="{'invisible':[('partner_address_id','=',False)]}" /> <!-- I want it visible when partner_address_id != False and partner_id = False, so that the user can see that this partner_address doesn't have a partner -->
<newline />
<group colspan="4" attrs="{'invisible':[('partner_id','=',False)]}">
<button name="open_partner" icon="gtk-go-forward" string="Partner form" type="object" />
<button name="open_sale_orders" icon="gtk-go-forward" string="Related sale orders" type="object" />
<button name="open_invoices" icon="gtk-go-forward" string="Related invoices" type="object" />
<button special="cancel" icon="gtk-cancel" string="Cancel" />
</group>
<!-- I display the button "Contact form" when the partner address exists but not the partner -->
<group colspan="4" attrs="{'invisible':['|', '&amp;', ('partner_address_id','!=',False), ('partner_id','!=',False), ('partner_address_id','=',False)]}">
<button name="open_partner_address" icon="gtk-go-forward" string="Contact form" type="object" colspan="2"/>
<button special="cancel" icon="gtk-cancel" string="Cancel" colspan="2"/>
</group>
<group attrs="{'invisible':[('partner_address_id','!=',False)]}" colspan="4" col="8">
<label string="No partner contact found in OpenERP with this number" colspan="8" />
<separator string="Create a new contact" colspan="8" />
<button name="create_partner_address_phone" icon="gtk-new" string="with calling number as phone" type="object" colspan="4"/>
<button name="create_partner_address_mobile" icon="gtk-new" string="with calling number as mobile" type="object" colspan="4"/>
<newline />
<separator string="Update an existing contact" colspan="8" />
<field name="to_update_partner_address_id" colspan="8" on_change="onchange_to_update_partner_address(to_update_partner_address_id)"/>
<field name="current_phone" colspan="6"/>
<button name="update_partner_address_phone" icon="gtk-convert" string="Update phone" type="object" colspan="2"/>
<field name="current_mobile" colspan="6"/>
<button name="update_partner_address_mobile" icon="gtk-convert" string="Update mobile" type="object" colspan="2"/>
<newline />
<button special="cancel" icon="gtk-cancel" string="Cancel" colspan="8" />
</group>
<!-- I repeat the cancel button for layout reasons -->
</form>
</field>
</record>
<record id="action_open_calling_partner" model="ir.actions.act_window">
<field name="name">Open calling partner</field>
<field name="res_model">wizard.open.calling.partner</field>
<field name="view_type">form</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<menuitem id="menu_open_calling_partner_sales" parent="base.menu_address_book" action="action_open_calling_partner" sequence="50" />
</data>
</openerp>

63
asterisk_click2dial/wizard/reformat_all_phonenumbers.py

@ -0,0 +1,63 @@
# -*- encoding: utf-8 -*-
##############################################################################
#
# Asterisk Click2dial module for OpenERP
# Copyright (C) 2012 Alexis de Lattre <alexis@via.ecp.fr>
#
# 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/>.
#
##############################################################################
from osv import osv, fields
import logging
from tools.translate import _
_logger = logging.getLogger(__name__)
class reformat_all_phonenumbers(osv.osv_memory):
_name = "reformat.all.phonenumbers"
_description = "Reformat all phone numbers"
_columns = {
'phonenumbers_not_reformatted': fields.text("Phone numbers that couldn't be reformatted"),
}
def run_reformat_all_phonenumbers(self, cr, uid, ids, context=None):
addr_obj = self.pool.get('res.partner.address')
phonefields = ['phone', 'fax', 'mobile']
_logger.info('Starting to reformat all the phone numbers')
all_addr_ids = addr_obj.search(cr, uid, ['|', ('active', '=', True), ('active', '=', False)], context=context)
phonenumbers_not_reformatted = ''
for addr in addr_obj.read(cr, uid, all_addr_ids, ['name'] + phonefields, context=context):
init_addr = addr.copy()
# addr is _updated_ by the fonction _reformat_phonenumbers()
try:
addr_obj._reformat_phonenumbers(cr, uid, addr, context=context)
except Exception, e:
#raise osv.except_osv(_('Error :'), _("Problem on partner contact '%s'. Error message: %s" % (init_addr.get('name'), e[1])))
phonenumbers_not_reformatted += "Problem on partner contact '%s'. Error message: %s" % (init_addr.get('name'), e[1]) + "\n"
_logger.warning("Problem on partner contact '%s'. Error message: %s" % (init_addr.get('name'), e[1]))
continue
# Test if the phone numbers have been changed
if any([init_addr.get(field) != addr.get(field) for field in phonefields]):
addr.pop('id')
addr.pop('name')
_logger.info('Reformating phone number: FROM %s TO %s' % (unicode(init_addr), unicode(addr)))
addr_obj.write(cr, uid, init_addr['id'], addr, context=context)
if not phonenumbers_not_reformatted:
phonenumbers_not_reformatted = 'All phone numbers have been reformatted successfully.'
self.write(cr, uid, ids[0], {'phonenumbers_not_reformatted': phonenumbers_not_reformatted}, context=context)
_logger.info('End of the phone number reformatting wizard')
return True

39
asterisk_click2dial/wizard/reformat_all_phonenumbers_view.xml

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Asterisk Click2dial module for OpenERP
Copyright (C) 2012 Alexis de Lattre <alexis@via.ecp.fr>
The licence is in the file __openerp__.py
-->
<openerp>
<data>
<record id="reformat_all_phonenumbers_form" model="ir.ui.view">
<field name="name">reformat_all_phonenumbers.form</field>
<field name="model">reformat.all.phonenumbers</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Reformat all phone numbers">
<label string="This wizard reformats the phone, mobile and fax numbers of all partner contacts in standard international format e.g. +33141981242" colspan="4"/>
<button special="cancel" icon="gtk-cancel" string="Close" />
<button name="run_reformat_all_phonenumbers" icon="gtk-ok" string="Reformat all phone numbers" type="object" />
<label colspan="4" string="Phone numbers that couldn't be reformatted:"/>
<field name="phonenumbers_not_reformatted" colspan="4" nolabel="1"/>
</form>
</field>
</record>
<record id="reformat_all_phonenumbers_action" model="ir.actions.act_window">
<field name="name">Reformat all phone numbers</field>
<field name="res_model">reformat.all.phonenumbers</field>
<field name="view_type">form</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<!-- Menu entry under Settings > Configuration -->
<menuitem id="reformat_all_phonenumbers_menu" action="reformat_all_phonenumbers_action" parent="menu_asterisk_servers" sequence="100"/>
</data>
</openerp>
Loading…
Cancel
Save