Browse Source

[ADD] partner_phonecall_schedule: Know partner's best phonecall time (#475)

pull/480/head
Jairo Llopis 7 years ago
committed by Pedro M. Baeza
parent
commit
8c3d54a51d
  1. 75
      partner_phonecall_schedule/README.rst
  2. 4
      partner_phonecall_schedule/__init__.py
  3. 20
      partner_phonecall_schedule/__openerp__.py
  4. 58
      partner_phonecall_schedule/i18n/es.po
  5. 4
      partner_phonecall_schedule/models/__init__.py
  6. 78
      partner_phonecall_schedule/models/res_partner.py
  7. BIN
      partner_phonecall_schedule/static/description/icon.png
  8. 4
      partner_phonecall_schedule/tests/__init__.py
  9. 173
      partner_phonecall_schedule/tests/test_schedule.py
  10. 44
      partner_phonecall_schedule/views/res_partner_view.xml

75
partner_phonecall_schedule/README.rst

@ -0,0 +1,75 @@
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:target: http://www.gnu.org/licenses/agpl
:alt: License: AGPL-3
===========================
Partner phonecalls schedule
===========================
If you know the best moment to call your partners, use this addon to keep
track of it and be able to filter partners that you can comfortably call now.
Usage
=====
To use the phonecall schedules, you need to:
#. Go to any partner form.
#. Go to the *Phone calls* tab.
#. Select any of the available schedules.
#. A readonly checkbox will tell you if it is a good time to call him/her.
#. A readonly aggregated schedule is visible too.
To filter partners available to call right now:
#. Go to the partners view.
#. Use the built-in filter *Available for phone calls now*
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
:alt: Try me on Runbot
:target: https://runbot.odoo-community.org/runbot/111/9.0
Bug Tracker
===========
Bugs are tracked on `GitHub Issues
<https://github.com/OCA/crm/issues>`_. In case of trouble, please
check there if your issue has already been reported. If you spotted it first,
help us smash it by providing detailed and welcomed feedback.
Credits
=======
Images
------
* Odoo Community Association: `Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_.
Contributors
------------
* Jairo Llopis <jairo.llopis@tecnativa.com>
Do not contact contributors directly about support or help with technical issues.
Funders
-------
The development of this module has been financially supported by:
* `Tecnativa <https://www.tecnativa.com>`_
Maintainer
----------
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
This module is maintained by the OCA.
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
To contribute to this module, please visit https://odoo-community.org.

4
partner_phonecall_schedule/__init__.py

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import models

20
partner_phonecall_schedule/__openerp__.py

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Jairo Llopis <jairo.llopis@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
"name": "Partner phonecalls schedule",
"summary": "Track the time and days your partners expect phone calls",
"version": "9.0.1.0.0",
"category": "Customer Relationship Management",
"website": "https://github.com/OCA/crm",
"author": "Tecnativa, Odoo Community Association (OCA)",
"license": "AGPL-3",
"application": False,
"installable": True,
"depends": [
"resource",
],
"data": [
"views/res_partner_view.xml",
],
}

58
partner_phonecall_schedule/i18n/es.po

@ -0,0 +1,58 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * partner_phonecall_schedule
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 9.0c\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-09-15 12:30+0000\n"
"PO-Revision-Date: 2017-09-15 14:32+0200\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 2.0.3\n"
"Last-Translator: Jairo Llopis <yajo.sk8@gmail.com>\n"
"Language: es_ES\n"
#. module: partner_phonecall_schedule
#: model:ir.model.fields,field_description:partner_phonecall_schedule.field_res_partner_phonecall_calendar_attendance_ids
msgid "Aggregated phonecall schedule"
msgstr "Horario agregado de llamadas telefónicas"
#. module: partner_phonecall_schedule
#: model:ir.ui.view,arch_db:partner_phonecall_schedule.view_res_partner_filter
msgid "Available for phone calls now"
msgstr "Disponible para llamar por teléfono ahora"
#. module: partner_phonecall_schedule
#: model:ir.model.fields,field_description:partner_phonecall_schedule.field_res_partner_phonecall_available
msgid "Available to call"
msgstr "Disponible para llamar"
#. module: partner_phonecall_schedule
#: model:ir.model.fields,help:partner_phonecall_schedule.field_res_partner_phonecall_calendar_ids
msgid "Best schedule when the contact expects to be called."
msgstr "El mejor horario para llamar por teléfono al contacto."
#. module: partner_phonecall_schedule
#: model:ir.model.fields,help:partner_phonecall_schedule.field_res_partner_phonecall_available
msgid "Is it now a good time to call this partner?"
msgstr "¿Es un buen momento para llamar a este contacto?"
#. module: partner_phonecall_schedule
#: model:ir.model,name:partner_phonecall_schedule.model_res_partner
msgid "Partner"
msgstr "Contacto"
#. module: partner_phonecall_schedule
#: model:ir.ui.view,arch_db:partner_phonecall_schedule.view_partner_form
msgid "Phone calls"
msgstr "Llamadas telefónicas"
#. module: partner_phonecall_schedule
#: model:ir.model.fields,field_description:partner_phonecall_schedule.field_res_partner_phonecall_calendar_ids
msgid "Phonecall schedule"
msgstr "Horario de llamadas telefónicas"

4
partner_phonecall_schedule/models/__init__.py

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import res_partner

78
partner_phonecall_schedule/models/res_partner.py

@ -0,0 +1,78 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Jairo Llopis <jairo.llopis@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from __future__ import division
from datetime import datetime
from openerp import api, fields, models
class ResPartner(models.Model):
_inherit = "res.partner"
phonecall_available = fields.Boolean(
"Available to call",
compute="_compute_phonecall_available",
search="_search_phonecall_available",
help="Is it now a good time to call this partner?",
)
phonecall_calendar_ids = fields.Many2many(
comodel_name="resource.calendar",
string="Phonecall schedule",
help="Best schedule when the contact expects to be called.",
)
phonecall_calendar_attendance_ids = fields.One2many(
comodel_name="resource.calendar.attendance",
string="Aggregated phonecall schedule",
compute="_compute_phonecall_calendar_ids",
help="Aggregation of all available phonecall schedules.",
)
@api.depends("phonecall_calendar_ids", "phonecall_calendar_attendance_ids")
def _compute_phonecall_available(self):
"""Know if a partner is available to call right now."""
Attendance = self.env["resource.calendar.attendance"]
for one in self:
domain = [
("calendar_id", "in", one.phonecall_calendar_ids.ids)
] + one._phonecall_available_domain()
found = Attendance.search(domain, limit=1)
one.phonecall_available = bool(found)
@api.depends("phonecall_calendar_ids")
def _compute_phonecall_calendar_ids(self):
"""Fill attendance aggregation."""
for one in self:
one.phonecall_calendar_attendance_ids = one.mapped(
"phonecall_calendar_ids.attendance_ids")
def _search_phonecall_available(self, operator, value):
"""Search quickly if partner is available to call right now."""
Attendance = self.env["resource.calendar.attendance"]
available = Attendance.search(
self._phonecall_available_domain(),
)
if operator == "!=" or "not" in operator:
value = not value
operator = "in" if value else "not in"
return [("phonecall_calendar_ids.attendance_ids",
operator, available.ids)]
def _phonecall_available_domain(self):
"""Get a domain to know if we are available to call a partner."""
now = self.env.context.get("now", datetime.now())
try:
now = fields.Datetime.from_string(now)
except TypeError:
# `now` is already a datetime object
pass
date = fields.Date.to_string(now)
now_tz = fields.Datetime.context_timestamp(self, now)
float_time = now_tz.hour + ((now_tz.minute / 60) + now_tz.second) / 60
return [
("dayofweek", "=", str(now.weekday())),
"|", ("date_from", "=", False), ("date_from", "<=", date),
"|", ("date_to", "=", False), ("date_to", ">=", date),
("hour_from", "<=", float_time),
("hour_to", ">=", float_time),
]

BIN
partner_phonecall_schedule/static/description/icon.png

After

Width: 128  |  Height: 128  |  Size: 9.2 KiB

4
partner_phonecall_schedule/tests/__init__.py

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import test_schedule

173
partner_phonecall_schedule/tests/test_schedule.py

@ -0,0 +1,173 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Jairo Llopis <jairo.llopis@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from datetime import datetime, timedelta
from mock import patch
from openerp import fields
from openerp.tests.common import SavepointCase
PATH = ("openerp.addons.partner_phonecall_schedule.models"
".res_partner.datetime")
class CanICallCase(SavepointCase):
@classmethod
def setUpClass(cls):
super(CanICallCase, cls).setUpClass()
cls.Calendar = cls.env["resource.calendar"].with_context(tz="UTC")
cls.Partner = cls.env["res.partner"].with_context(tz="UTC")
cls.some_mornings = cls.Calendar.create({
"name": "Some mornings",
"attendance_ids": [
(0, 0, {
"name": "Friday morning",
"dayofweek": "4",
"hour_from": 8,
"hour_to": 12,
}),
(0, 0, {
"name": "Next monday morning",
"dayofweek": "0",
"hour_from": 8,
"hour_to": 12,
"date_from": "2017-09-18",
"date_to": "2017-09-18",
}),
],
})
cls.some_evenings = cls.Calendar.create({
"name": "Some evenings",
"attendance_ids": [
(0, 0, {
"name": "Friday evening",
"dayofweek": "4",
"hour_from": 15,
"hour_to": 19,
}),
(0, 0, {
"name": "Next monday evening",
"dayofweek": "0",
"hour_from": 15,
"hour_to": 19,
"date_from": "2017-09-18",
"date_to": "2017-09-18",
}),
],
})
cls.dude = cls.Partner.create({
"name": "Dude",
})
cls.dude.phonecall_calendar_ids = cls.some_mornings
def setUp(self):
super(CanICallCase, self).setUp()
# Now it is a friday morning
self.datetime = datetime(2017, 9, 15, 10, 53, 30)
def _allowed(self, now=None):
dude, Partner = self.dude, self.Partner
if now:
dude = dude.with_context(now=now)
Partner = Partner.with_context(now=now)
self.assertTrue(dude.phonecall_available)
self.assertTrue(Partner.search([
("id", "=", dude.id),
("phonecall_available", "=", True),
]))
self.assertTrue(Partner.search([
("id", "=", dude.id),
("phonecall_available", "!=", False),
]))
self.assertFalse(Partner.search([
("id", "=", dude.id),
("phonecall_available", "=", False),
]))
self.assertFalse(Partner.search([
("id", "=", dude.id),
("phonecall_available", "!=", True),
]))
def allowed(self):
# Test mocking datetime.now()
with patch(PATH) as mocked_dt:
mocked_dt.now.return_value = self.datetime
mocked_dt.date.return_value = self.datetime.date()
self._allowed()
# Test sending a datetime object in the context
self._allowed(self.datetime)
# Test sending a string in the context
self._allowed(
fields.Datetime.to_string(self.datetime))
def _disallowed(self, now=None):
dude, Partner = self.dude, self.Partner
if now:
dude = dude.with_context(now=now)
Partner = Partner.with_context(now=now)
self.assertFalse(dude.phonecall_available)
self.assertFalse(Partner.search([
("id", "=", dude.id),
("phonecall_available", "=", True),
]))
self.assertFalse(Partner.search([
("id", "=", dude.id),
("phonecall_available", "!=", False),
]))
self.assertTrue(Partner.search([
("id", "=", dude.id),
("phonecall_available", "=", False),
]))
self.assertTrue(Partner.search([
("id", "=", dude.id),
("phonecall_available", "!=", True),
]))
def disallowed(self):
# Test mocking datetime.now()
with patch(PATH) as mocked_dt:
mocked_dt.now.return_value = self.datetime
mocked_dt.date.return_value = self.datetime.date()
self._disallowed()
# Test sending a datetime object in the context
self._disallowed(self.datetime)
# Test sending a string in the context
self._disallowed(
fields.Datetime.to_string(self.datetime))
def test_friday_morning(self):
"""I can call dude this morning"""
self.allowed()
def test_friday_evening(self):
"""I cannot call dude this evening"""
self.datetime += timedelta(hours=4)
self.disallowed()
def test_saturday_morning(self):
"""I cannot call dude tomorrow morning"""
self.datetime += timedelta(days=1)
self.disallowed()
def test_saturday_evening(self):
"""I cannot call dude tomorrow evening"""
self.datetime += timedelta(days=1, hours=4)
self.disallowed()
def test_next_monday_morning(self):
"""I can call dude next monday morning"""
self.datetime += timedelta(days=3)
self.allowed()
def test_second_next_monday_morning(self):
"""I cannot call dude second next monday morning"""
self.datetime += timedelta(days=10, hours=4)
self.disallowed()
def test_aggregated_attendances(self):
"""I get aggregated schedules correctly."""
self.dude.phonecall_calendar_ids |= self.some_evenings
all_attendances = (self.some_mornings | self.some_evenings).mapped(
"attendance_ids")
self.assertEqual(
self.dude.phonecall_calendar_attendance_ids, all_attendances)

44
partner_phonecall_schedule/views/res_partner_view.xml

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2017 Jairo Llopis <jairo.llopis@tecnativa.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="view_partner_form" model="ir.ui.view">
<field name="name">Partner phonecall schedule</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<xpath expr="//notebook">
<!-- `many2many_tags` widget would not allow the user to click
and edit a calendar record as the list widget does;
management menus are under global settings, so only
admins could do it; we need plenty of space to display
the aggregated schedule. All that said, we have to use a
list widget inside a dedicated page. -->
<page string="Phone calls" name="phonecalls">
<group>
<field name="phonecall_available"/>
<field name="phonecall_calendar_ids"/>
<field name="phonecall_calendar_attendance_ids"/>
</group>
</page>
</xpath>
</field>
</record>
<record id="view_res_partner_filter" model="ir.ui.view">
<field name="name">Partner phonecall availability</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_res_partner_filter"/>
<field name="arch" type="xml">
<xpath expr="//separator[last()]" position="before">
<filter
string="Available for phone calls now"
name="phonecall_available"
domain="[('phonecall_available', '=', True)]"/>
</xpath>
</field>
</record>
</odoo>
Loading…
Cancel
Save