mirror of
https://github.com/bringout/oca-technical.git
synced 2026-04-23 19:52:06 +02:00
Initial commit: OCA Technical packages (595 packages)
This commit is contained in:
commit
2cc02aac6e
24950 changed files with 2318079 additions and 0 deletions
45
odoo-bringout-oca-crm-crm_salesperson_planner/README.md
Normal file
45
odoo-bringout-oca-crm-crm_salesperson_planner/README.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# Crm Salesperson Planner
|
||||
|
||||
Odoo addon: crm_salesperson_planner
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install odoo-bringout-oca-crm-crm_salesperson_planner
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
This addon depends on:
|
||||
- crm
|
||||
- calendar
|
||||
|
||||
## Manifest Information
|
||||
|
||||
- **Name**: Crm Salesperson Planner
|
||||
- **Version**: 16.0.2.0.1
|
||||
- **Category**: Customer Relationship Management
|
||||
- **License**: AGPL-3
|
||||
- **Installable**: True
|
||||
|
||||
## Source
|
||||
|
||||
Based on [OCA/crm](https://github.com/OCA/crm) branch 16.0, addon `crm_salesperson_planner`.
|
||||
|
||||
## License
|
||||
|
||||
This package maintains the original AGPL-3 license from the upstream Odoo project.
|
||||
|
||||
## Documentation
|
||||
|
||||
- Overview: doc/OVERVIEW.md
|
||||
- Architecture: doc/ARCHITECTURE.md
|
||||
- Models: doc/MODELS.md
|
||||
- Controllers: doc/CONTROLLERS.md
|
||||
- Wizards: doc/WIZARDS.md
|
||||
- Install: doc/INSTALL.md
|
||||
- Usage: doc/USAGE.md
|
||||
- Configuration: doc/CONFIGURATION.md
|
||||
- Dependencies: doc/DEPENDENCIES.md
|
||||
- Troubleshooting: doc/TROUBLESHOOTING.md
|
||||
- FAQ: doc/FAQ.md
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
=======================
|
||||
Crm Salesperson Planner
|
||||
=======================
|
||||
|
||||
..
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:d1b9a09a3ad681a80bce2522a390e2f008cc0418bd007eeafc8f579b07a24627
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
|
||||
:target: https://odoo-community.org/page/development-status
|
||||
:alt: Beta
|
||||
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
|
||||
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
|
||||
:alt: License: AGPL-3
|
||||
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fcrm-lightgray.png?logo=github
|
||||
:target: https://github.com/OCA/crm/tree/16.0/crm_salesperson_planner
|
||||
:alt: OCA/crm
|
||||
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
|
||||
:target: https://translation.odoo-community.org/projects/crm-16-0/crm-16-0-crm_salesperson_planner
|
||||
:alt: Translate me on Weblate
|
||||
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
|
||||
:target: https://runboat.odoo-community.org/builds?repo=OCA/crm&target_branch=16.0
|
||||
:alt: Try me on Runboat
|
||||
|
||||
|badge1| |badge2| |badge3| |badge4| |badge5|
|
||||
|
||||
This application allows you to track and schedule salespeople visits to your customers, allowing you to determine which opportunities are going to be dealt on each visit. Visits create an all day event in calendar, and they can be easily rescheduled.
|
||||
Visits can be automatically created from a template, in which it is possible to select the frequency of visits, as well as the start and end dates. The last visit can also be calculated by selecting the total number of repetitions.
|
||||
This module creates a cron that generates visits from templates, but an option to create them manually is available from the template form view when the template is validated.
|
||||
|
||||
**Table of contents**
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
To configure this module, you need to:
|
||||
|
||||
* Go to new menu **CRM > Configuration > Salesperson Planner > Close Reasons** and create a close reason for 'Cancel' and 'Incident' types.
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
Go to new menu **CRM > Salesperson Planner > My Visits or All Visits** and create a new visit.
|
||||
or
|
||||
Go to **CRM > Salesperson Planner > Visit Templates** and create a new recurring template for create periodical visits. In this case, it is necessary to select a start date. The date of the last repetition can be calculated by selection the total number of repetitions or an end date.
|
||||
There are two options available to reschedule visits that is already validated:
|
||||
* Change the date from the visit.
|
||||
* Change the date straight from the event automatically created in the calendar.
|
||||
|
||||
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 to smash it by providing a detailed and welcomed
|
||||
`feedback <https://github.com/OCA/crm/issues/new?body=module:%20crm_salesperson_planner%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
|
||||
|
||||
Do not contact contributors directly about support or help with technical issues.
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
Authors
|
||||
~~~~~~~
|
||||
|
||||
* Sygel Technology
|
||||
|
||||
Contributors
|
||||
~~~~~~~~~~~~
|
||||
|
||||
* `Sygel <https://www.sygel.es>`__:
|
||||
|
||||
* Valentin Vinagre
|
||||
* Manuel Regidor
|
||||
|
||||
* `Pesol <https://www.pesol.es>`__:
|
||||
|
||||
* Gerardo Marin Parra <info@pesol.es>
|
||||
|
||||
* `Tecnativa <https://www.tecnativa.com>`_:
|
||||
|
||||
* Víctor Martínez
|
||||
|
||||
Maintainers
|
||||
~~~~~~~~~~~
|
||||
|
||||
This module is maintained by the OCA.
|
||||
|
||||
.. image:: https://odoo-community.org/logo.png
|
||||
:alt: Odoo Community Association
|
||||
:target: https://odoo-community.org
|
||||
|
||||
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.
|
||||
|
||||
This module is part of the `OCA/crm <https://github.com/OCA/crm/tree/16.0/crm_salesperson_planner>`_ project on GitHub.
|
||||
|
||||
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# Copyright 2021 Sygel - Valentin Vinagre
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
|
||||
|
||||
from . import models
|
||||
from . import wizards
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
# Copyright 2021 Sygel - Valentin Vinagre
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
|
||||
{
|
||||
"name": "Crm Salesperson Planner",
|
||||
"version": "16.0.2.0.1",
|
||||
"development_status": "Beta",
|
||||
"category": "Customer Relationship Management",
|
||||
"author": "Sygel Technology," "Odoo Community Association (OCA)",
|
||||
"website": "https://github.com/OCA/crm",
|
||||
"license": "AGPL-3",
|
||||
"depends": ["crm", "calendar"],
|
||||
"data": [
|
||||
"data/crm_salesperson_planner_sequence.xml",
|
||||
"wizards/crm_salesperson_planner_visit_close_wiz_view.xml",
|
||||
"wizards/crm_salesperson_planner_visit_template_create.xml",
|
||||
"views/crm_salesperson_planner_visit_views.xml",
|
||||
"views/crm_salesperson_planner_visit_close_reason_views.xml",
|
||||
"views/crm_salesperson_planner_visit_template_views.xml",
|
||||
"views/crm_salesperson_planner_menu.xml",
|
||||
"views/res_partner.xml",
|
||||
"views/crm_lead.xml",
|
||||
"data/ir_cron_data.xml",
|
||||
"security/crm_salesperson_planner_security.xml",
|
||||
"security/ir.model.access.csv",
|
||||
],
|
||||
"installable": True,
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!-- Copyright 2021 Sygel - Valentin Vinagre
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
||||
<odoo noupdate="1">
|
||||
<record id="sequence_salesperson_planner_visit" model="ir.sequence">
|
||||
<field name="name">Salesperson Planner visit Code</field>
|
||||
<field name="code">salesperson.planner.visit</field>
|
||||
<field eval="6" name="padding" />
|
||||
<field name="prefix">SPPV/</field>
|
||||
</record>
|
||||
<record id="sequence_salesperson_planner_visit_template" model="ir.sequence">
|
||||
<field name="name">Salesperson Planner visit Template Code</field>
|
||||
<field name="code">salesperson.planner.visit.template</field>
|
||||
<field eval="6" name="padding" />
|
||||
<field name="prefix">SPPVT/</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!-- Copyright 2021 Sygel - Valentin Vinagre
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
||||
<odoo noupdate="1">
|
||||
<record id="ir_cron_create_visits" model="ir.cron">
|
||||
<field name="name">CRM: Create salesperson visits</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="doall" eval="False" />
|
||||
<field name="model_id" ref="model_crm_salesperson_planner_visit_template" />
|
||||
<field name="code">model._cron_create_visits(days=7)</field>
|
||||
<field name="state">code</field>
|
||||
</record>
|
||||
</odoo>
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,9 @@
|
|||
# Copyright 2021 Sygel - Valentin Vinagre
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
|
||||
|
||||
from . import crm_salesperson_planner_visit
|
||||
from . import crm_salesperson_planner_visit_template
|
||||
from . import crm_salesperson_planner_visit_close_reason
|
||||
from . import res_partner
|
||||
from . import crm_lead
|
||||
from . import calendar_event
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
# Copyright 2021 Sygel - Manuel Regidor
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
|
||||
|
||||
from odoo import _, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class CalendarEvent(models.Model):
|
||||
_inherit = "calendar.event"
|
||||
|
||||
salesperson_planner_visit_ids = fields.One2many(
|
||||
string="Salesperson Visits",
|
||||
comodel_name="crm.salesperson.planner.visit",
|
||||
inverse_name="calendar_event_id",
|
||||
)
|
||||
|
||||
def write(self, values):
|
||||
if values.get("start") or values.get("user_id"):
|
||||
salesperson_visit_events = self.filtered(
|
||||
lambda a: a.res_model == "crm.salesperson.planner.visit"
|
||||
)
|
||||
if salesperson_visit_events:
|
||||
new_vals = {}
|
||||
if values.get("start"):
|
||||
new_vals["date"] = values.get("start")
|
||||
if values.get("user_id"):
|
||||
new_vals["user_id"] = values.get("user_id")
|
||||
user_id = self.env["res.users"].browse(values.get("user_id"))
|
||||
if user_id:
|
||||
partner_ids = self.partner_ids.filtered(
|
||||
lambda a: a != self.user_id.partner_id
|
||||
).ids
|
||||
partner_ids.append(user_id.partner_id.id)
|
||||
values["partner_ids"] = [(6, 0, partner_ids)]
|
||||
salesperson_visit_events.mapped(
|
||||
"salesperson_planner_visit_ids"
|
||||
).with_context(bypass_update_event=True).write(new_vals)
|
||||
return super().write(values)
|
||||
|
||||
def unlink(self):
|
||||
if not self.env.context.get("bypass_cancel_visit"):
|
||||
salesperson_visit_events = self.filtered(
|
||||
lambda a: a.res_model == "crm.salesperson.planner.visit"
|
||||
and a.salesperson_planner_visit_ids
|
||||
)
|
||||
if salesperson_visit_events:
|
||||
error_msg = ""
|
||||
for event in salesperson_visit_events:
|
||||
error_msg += _(
|
||||
"Event %(event_name)s is related to salesperson visit "
|
||||
"%(partner_name)s. Cancel it to delete this event.\n"
|
||||
) % {
|
||||
"event_name": event.name,
|
||||
"partner_name": fields.first(
|
||||
event.salesperson_planner_visit_ids
|
||||
).name,
|
||||
}
|
||||
raise ValidationError(error_msg)
|
||||
return super().unlink()
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# Copyright 2021 Sygel - Valentin Vinagre
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class CrmLead(models.Model):
|
||||
_inherit = "crm.lead"
|
||||
|
||||
crm_salesperson_planner_visit_ids = fields.Many2many(
|
||||
comodel_name="crm.salesperson.planner.visit",
|
||||
relation="crm_salesperson_planner_visit_crm_lead_rel",
|
||||
string="Visits",
|
||||
copy=False,
|
||||
domain="[('partner_id', 'child_of', partner_id)]",
|
||||
)
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
# Copyright 2021 Sygel - Valentin Vinagre
|
||||
# Copyright 2021 Sygel - Manuel Regidor
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class CrmSalespersonPlannerVisit(models.Model):
|
||||
_name = "crm.salesperson.planner.visit"
|
||||
_description = "Salesperson Planner Visit"
|
||||
_order = "date desc,sequence"
|
||||
_inherit = ["mail.thread", "mail.activity.mixin"]
|
||||
|
||||
name = fields.Char(
|
||||
string="Visit Number",
|
||||
required=True,
|
||||
default="/",
|
||||
readonly=True,
|
||||
copy=False,
|
||||
)
|
||||
partner_id = fields.Many2one(
|
||||
comodel_name="res.partner",
|
||||
string="Customer",
|
||||
required=True,
|
||||
)
|
||||
partner_phone = fields.Char(string="Phone", related="partner_id.phone")
|
||||
partner_mobile = fields.Char(string="Mobile", related="partner_id.mobile")
|
||||
date = fields.Date(
|
||||
default=fields.Date.context_today,
|
||||
required=True,
|
||||
)
|
||||
sequence = fields.Integer(
|
||||
help="Used to order Visits in the different views",
|
||||
default=20,
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
comodel_name="res.company",
|
||||
string="Company",
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
user_id = fields.Many2one(
|
||||
comodel_name="res.users",
|
||||
string="Salesperson",
|
||||
index=True,
|
||||
tracking=True,
|
||||
default=lambda self: self.env.user,
|
||||
domain=lambda self: [
|
||||
("groups_id", "in", self.env.ref("sales_team.group_sale_salesman").id)
|
||||
],
|
||||
)
|
||||
opportunity_ids = fields.Many2many(
|
||||
comodel_name="crm.lead",
|
||||
relation="crm_salesperson_planner_visit_crm_lead_rel",
|
||||
string="Opportunities",
|
||||
copy=False,
|
||||
domain="[('type', '=', 'opportunity'), ('partner_id', 'child_of', partner_id)]",
|
||||
)
|
||||
description = fields.Html()
|
||||
state = fields.Selection(
|
||||
string="Status",
|
||||
required=True,
|
||||
readonly=True,
|
||||
copy=False,
|
||||
tracking=True,
|
||||
selection=[
|
||||
("draft", "Draft"),
|
||||
("confirm", "Validated"),
|
||||
("done", "Visited"),
|
||||
("cancel", "Cancelled"),
|
||||
("incident", "Incident"),
|
||||
],
|
||||
default="draft",
|
||||
)
|
||||
close_reason_id = fields.Many2one(
|
||||
comodel_name="crm.salesperson.planner.visit.close.reason", string="Close Reason"
|
||||
)
|
||||
close_reason_image = fields.Image(max_width=1024, max_height=1024, attachment=True)
|
||||
close_reason_notes = fields.Text()
|
||||
visit_template_id = fields.Many2one(
|
||||
comodel_name="crm.salesperson.planner.visit.template", string="Visit Template"
|
||||
)
|
||||
calendar_event_id = fields.Many2one(
|
||||
comodel_name="calendar.event", string="Calendar Event"
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
"crm_salesperson_planner_visit_name",
|
||||
"UNIQUE (name)",
|
||||
"The visit number must be unique!",
|
||||
),
|
||||
]
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if vals.get("name", "/") == "/":
|
||||
vals["name"] = self.env["ir.sequence"].next_by_code(
|
||||
"salesperson.planner.visit"
|
||||
)
|
||||
return super().create(vals_list)
|
||||
|
||||
def action_draft(self):
|
||||
if self.state not in ["cancel", "incident", "done"]:
|
||||
raise ValidationError(
|
||||
_("The visit must be in cancelled, incident or visited state")
|
||||
)
|
||||
if self.calendar_event_id:
|
||||
self.calendar_event_id.with_context(bypass_cancel_visit=True).unlink()
|
||||
self.write({"state": "draft"})
|
||||
|
||||
def action_confirm(self):
|
||||
if self.filtered(lambda a: not a.state == "draft"):
|
||||
raise ValidationError(_("The visit must be in draft state"))
|
||||
events = self.create_calendar_event()
|
||||
if events:
|
||||
self.browse(events.mapped("res_id")).write({"state": "confirm"})
|
||||
|
||||
def action_done(self):
|
||||
if not self.state == "confirm":
|
||||
raise ValidationError(_("The visit must be in confirmed state"))
|
||||
self.write({"state": "done"})
|
||||
|
||||
def action_cancel(self, reason_id, image=None, notes=None):
|
||||
if self.state not in ["draft", "confirm"]:
|
||||
raise ValidationError(_("The visit must be in draft or validated state"))
|
||||
if self.calendar_event_id:
|
||||
self.calendar_event_id.with_context(bypass_cancel_visit=True).unlink()
|
||||
self.write(
|
||||
{
|
||||
"state": "cancel",
|
||||
"close_reason_id": reason_id.id,
|
||||
"close_reason_image": image,
|
||||
"close_reason_notes": notes,
|
||||
}
|
||||
)
|
||||
|
||||
def _prepare_calendar_event_vals(self):
|
||||
return {
|
||||
"name": self.name,
|
||||
"partner_ids": [(6, 0, [self.partner_id.id, self.user_id.partner_id.id])],
|
||||
"user_id": self.user_id.id,
|
||||
"start_date": self.date,
|
||||
"stop_date": self.date,
|
||||
"start": self.date,
|
||||
"stop": self.date,
|
||||
"allday": True,
|
||||
"res_model": self._name,
|
||||
"res_model_id": self.env.ref(
|
||||
"crm_salesperson_planner.model_crm_salesperson_planner_visit"
|
||||
).id,
|
||||
"res_id": self.id,
|
||||
}
|
||||
|
||||
def create_calendar_event(self):
|
||||
events = self.env["calendar.event"]
|
||||
for item in self:
|
||||
event = self.env["calendar.event"].create(
|
||||
item._prepare_calendar_event_vals()
|
||||
)
|
||||
if event:
|
||||
event.activity_ids.unlink()
|
||||
item.calendar_event_id = event
|
||||
events += event
|
||||
return events
|
||||
|
||||
def action_incident(self, reason_id, image=None, notes=None):
|
||||
if self.state not in ["draft", "confirm"]:
|
||||
raise ValidationError(_("The visit must be in draft or validated state"))
|
||||
self.write(
|
||||
{
|
||||
"state": "incident",
|
||||
"close_reason_id": reason_id.id,
|
||||
"close_reason_image": image,
|
||||
"close_reason_notes": notes,
|
||||
}
|
||||
)
|
||||
|
||||
def unlink(self):
|
||||
if any(sel.state not in ["draft", "cancel"] for sel in self):
|
||||
raise ValidationError(_("Visits must be in cancelled state"))
|
||||
return super().unlink()
|
||||
|
||||
def write(self, values):
|
||||
ret_val = super().write(values)
|
||||
if (values.get("date") or values.get("user_id")) and not self.env.context.get(
|
||||
"bypass_update_event"
|
||||
):
|
||||
new_vals = {}
|
||||
for item in self.filtered(lambda a: a.calendar_event_id):
|
||||
if values.get("date"):
|
||||
new_vals["start"] = values.get("date")
|
||||
new_vals["stop"] = values.get("date")
|
||||
if values.get("user_id"):
|
||||
new_vals["user_id"] = values.get("user_id")
|
||||
item.calendar_event_id.write(new_vals)
|
||||
return ret_val
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
# Copyright 2021 Sygel - Valentin Vinagre
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class CrmSalespersonPlannerVisitCloseReason(models.Model):
|
||||
_name = "crm.salesperson.planner.visit.close.reason"
|
||||
_description = "SalesPerson Planner Visit Close Reason"
|
||||
|
||||
name = fields.Char(string="Description", required=True, translate=True)
|
||||
close_type = fields.Selection(
|
||||
selection=[("cancel", "Cancel"), ("incident", "Incident")],
|
||||
string="Type",
|
||||
required=True,
|
||||
default="cancel",
|
||||
)
|
||||
require_image = fields.Boolean(default=False)
|
||||
reschedule = fields.Boolean(default=False)
|
||||
|
|
@ -0,0 +1,357 @@
|
|||
# Copyright 2021 Sygel - Valentin Vinagre
|
||||
# Copyright 2021 Sygel - Manuel Regidor
|
||||
# Copyright 2024 Tecnativa - Víctor Martínez
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
from odoo.addons.base.models.res_partner import _tz_get
|
||||
from odoo.addons.calendar.models.calendar_recurrence import (
|
||||
BYDAY_SELECTION,
|
||||
END_TYPE_SELECTION,
|
||||
MONTH_BY_SELECTION,
|
||||
RRULE_TYPE_SELECTION,
|
||||
WEEKDAY_SELECTION,
|
||||
)
|
||||
|
||||
|
||||
class CrmSalespersonPlannerVisitTemplate(models.Model):
|
||||
_name = "crm.salesperson.planner.visit.template"
|
||||
_description = "Crm Salesperson Planner Visit Template"
|
||||
_inherit = ["mail.thread"]
|
||||
|
||||
# We cannot inherit from calendar.event for several reasons:
|
||||
# 1- There are many compute recursion fields that would not allow to change them.
|
||||
# 2- Recurrence is only created correctly if the model is calendar.event
|
||||
# 3- We want to generate visits ("events") manually when we want and only the ones
|
||||
# we want.
|
||||
name = fields.Char(
|
||||
string="Visit Template Number",
|
||||
default="/",
|
||||
readonly=True,
|
||||
copy=False,
|
||||
)
|
||||
description = fields.Html()
|
||||
user_id = fields.Many2one(
|
||||
comodel_name="res.users",
|
||||
string="Salesperson",
|
||||
tracking=True,
|
||||
default=lambda self: self.env.user,
|
||||
domain=lambda self: [
|
||||
("groups_id", "in", self.env.ref("sales_team.group_sale_salesman").id)
|
||||
],
|
||||
)
|
||||
partner_id = fields.Many2one(
|
||||
comodel_name="res.partner",
|
||||
string="Scheduled by",
|
||||
related="user_id.partner_id",
|
||||
readonly=True,
|
||||
)
|
||||
partner_ids = fields.Many2many(
|
||||
comodel_name="res.partner",
|
||||
string="Customer",
|
||||
default=False,
|
||||
required=True,
|
||||
)
|
||||
sequence = fields.Integer(
|
||||
help="Used to order Visits in the different views",
|
||||
default=20,
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
comodel_name="res.company",
|
||||
string="Company",
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
categ_ids = fields.Many2many(comodel_name="calendar.event.type", string="Tags")
|
||||
alarm_ids = fields.Many2many(
|
||||
comodel_name="calendar.alarm",
|
||||
string="Reminders",
|
||||
ondelete="restrict",
|
||||
help="Notifications sent to all attendees to remind of the meeting.",
|
||||
)
|
||||
state = fields.Selection(
|
||||
string="Status",
|
||||
required=True,
|
||||
copy=False,
|
||||
tracking=True,
|
||||
selection=[
|
||||
("draft", "Draft"),
|
||||
("in-progress", "In Progress"),
|
||||
("done", "Done"),
|
||||
("cancel", "Cancelled"),
|
||||
],
|
||||
default="draft",
|
||||
)
|
||||
visit_ids = fields.One2many(
|
||||
comodel_name="crm.salesperson.planner.visit",
|
||||
inverse_name="visit_template_id",
|
||||
string="Visit Template",
|
||||
)
|
||||
visit_ids_count = fields.Integer(
|
||||
string="Number of Sales Person Visits", compute="_compute_visit_ids_count"
|
||||
)
|
||||
auto_validate = fields.Boolean(default=True)
|
||||
last_visit_date = fields.Date(compute="_compute_last_visit_date", store=True)
|
||||
final_date = fields.Date(string="Repeat Until")
|
||||
start = fields.Datetime(
|
||||
required=True,
|
||||
tracking=True,
|
||||
default=fields.Date.today,
|
||||
help="Start date of an event, without time for full days events",
|
||||
)
|
||||
stop = fields.Datetime(
|
||||
required=True,
|
||||
tracking=True,
|
||||
default=lambda self: fields.Datetime.today() + timedelta(hours=1),
|
||||
compute="_compute_stop",
|
||||
readonly=False,
|
||||
store=True,
|
||||
help="Stop date of an event, without time for full days events",
|
||||
)
|
||||
allday = fields.Boolean(string="All Day", default=True)
|
||||
start_date = fields.Date(
|
||||
store=True,
|
||||
tracking=True,
|
||||
compute="_compute_dates",
|
||||
inverse="_inverse_dates",
|
||||
)
|
||||
stop_date = fields.Date(
|
||||
string="End Date",
|
||||
store=True,
|
||||
tracking=True,
|
||||
compute="_compute_dates",
|
||||
inverse="_inverse_dates",
|
||||
)
|
||||
duration = fields.Float(compute="_compute_duration", store=True, readonly=False)
|
||||
rrule = fields.Char(string="Recurrent Rule")
|
||||
rrule_type = fields.Selection(
|
||||
RRULE_TYPE_SELECTION,
|
||||
string="Recurrence",
|
||||
help="Let the event automatically repeat at that interval",
|
||||
default="daily",
|
||||
required=True,
|
||||
)
|
||||
event_tz = fields.Selection(_tz_get, string="Timezone")
|
||||
end_type = fields.Selection(END_TYPE_SELECTION, string="Recurrence Termination")
|
||||
interval = fields.Integer(
|
||||
string="Repeat Every", help="Repeat every (Days/Week/Month/Year)"
|
||||
)
|
||||
count = fields.Integer(string="Repeat", help="Repeat x times")
|
||||
mon = fields.Boolean()
|
||||
tue = fields.Boolean()
|
||||
wed = fields.Boolean()
|
||||
thu = fields.Boolean()
|
||||
fri = fields.Boolean()
|
||||
sat = fields.Boolean()
|
||||
sun = fields.Boolean()
|
||||
month_by = fields.Selection(MONTH_BY_SELECTION, string="Option")
|
||||
day = fields.Integer(string="Date of month")
|
||||
weekday = fields.Selection(WEEKDAY_SELECTION)
|
||||
byday = fields.Selection(BYDAY_SELECTION)
|
||||
until = fields.Date()
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
"crm_salesperson_planner_visit_template_name",
|
||||
"UNIQUE (name)",
|
||||
"The visit template number must be unique!",
|
||||
),
|
||||
]
|
||||
|
||||
def _compute_visit_ids_count(self):
|
||||
visit_data = self.env["crm.salesperson.planner.visit"].read_group(
|
||||
[("visit_template_id", "in", self.ids)],
|
||||
["visit_template_id"],
|
||||
["visit_template_id"],
|
||||
)
|
||||
mapped_data = {
|
||||
m["visit_template_id"][0]: m["visit_template_id_count"] for m in visit_data
|
||||
}
|
||||
for sel in self:
|
||||
sel.visit_ids_count = mapped_data.get(sel.id, 0)
|
||||
|
||||
@api.depends("visit_ids.date")
|
||||
def _compute_last_visit_date(self):
|
||||
for sel in self.filtered(lambda x: x.visit_ids):
|
||||
sel.last_visit_date = sel.visit_ids.sorted(lambda x: x.date)[-1].date
|
||||
|
||||
@api.depends("start", "duration")
|
||||
def _compute_stop(self):
|
||||
"""Same method as in calendar.event."""
|
||||
for item in self:
|
||||
item.stop = item.start and item.start + timedelta(
|
||||
minutes=round((item.duration or 1.0) * 60)
|
||||
)
|
||||
if item.allday:
|
||||
item.stop -= timedelta(seconds=1)
|
||||
|
||||
@api.depends("allday", "start", "stop")
|
||||
def _compute_dates(self):
|
||||
"""Same method as in calendar.event."""
|
||||
for item in self:
|
||||
if item.allday and item.start and item.stop:
|
||||
item.start_date = item.start.date()
|
||||
item.stop_date = item.stop.date()
|
||||
else:
|
||||
item.start_date = False
|
||||
item.stop_date = False
|
||||
|
||||
@api.depends("stop", "start")
|
||||
def _compute_duration(self):
|
||||
"""Same method as in calendar.event."""
|
||||
for item in self:
|
||||
item.duration = self._get_duration(item.start, item.stop)
|
||||
|
||||
def _get_duration(self, start, stop):
|
||||
"""Same method as in calendar.event."""
|
||||
if not start or not stop:
|
||||
return 0
|
||||
duration = (stop - start).total_seconds() / 3600
|
||||
return round(duration, 2)
|
||||
|
||||
def _inverse_dates(self):
|
||||
"""Same method as in calendar.event."""
|
||||
for item in self:
|
||||
if item.allday:
|
||||
enddate = fields.Datetime.from_string(item.stop_date)
|
||||
enddate = enddate.replace(hour=18)
|
||||
startdate = fields.Datetime.from_string(item.start_date)
|
||||
startdate = startdate.replace(hour=8)
|
||||
item.write(
|
||||
{
|
||||
"start": startdate.replace(tzinfo=None),
|
||||
"stop": enddate.replace(tzinfo=None),
|
||||
}
|
||||
)
|
||||
|
||||
@api.constrains("partner_ids")
|
||||
def _constrains_partner_ids(self):
|
||||
for item in self:
|
||||
if len(item.partner_ids) > 1:
|
||||
raise ValidationError(_("Only one customer is allowed"))
|
||||
|
||||
@api.onchange("end_type")
|
||||
def _onchange_end_type(self):
|
||||
"""Avoid inconsistent data if you switch from one thing to another."""
|
||||
if self.end_type == "count":
|
||||
self.until = False
|
||||
elif self.end_type == "end_date":
|
||||
self.count = 0
|
||||
elif self.end_type == "forever":
|
||||
self.count = 0
|
||||
self.until = False
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if vals.get("name", "/") == "/":
|
||||
vals["name"] = self.env["ir.sequence"].next_by_code(
|
||||
"salesperson.planner.visit.template"
|
||||
)
|
||||
return super().create(vals_list)
|
||||
|
||||
def action_view_salesperson_planner_visit(self):
|
||||
action = self.env["ir.actions.act_window"]._for_xml_id(
|
||||
"crm_salesperson_planner.all_crm_salesperson_planner_visit_action"
|
||||
)
|
||||
action["domain"] = [("id", "=", self.visit_ids.ids)]
|
||||
action["context"] = {
|
||||
"default_partner_id": self.partner_id.id,
|
||||
"default_visit_template_id": self.id,
|
||||
"default_description": self.description,
|
||||
}
|
||||
return action
|
||||
|
||||
def action_validate(self):
|
||||
self.write({"state": "in-progress"})
|
||||
|
||||
def action_cancel(self):
|
||||
self.write({"state": "cancel"})
|
||||
|
||||
def action_draft(self):
|
||||
self.write({"state": "draft"})
|
||||
|
||||
def _prepare_crm_salesperson_planner_visit_vals(self, dates):
|
||||
return [
|
||||
{
|
||||
"partner_id": (
|
||||
fields.first(self.partner_ids).id if self.partner_ids else False
|
||||
),
|
||||
"date": date,
|
||||
"sequence": self.sequence,
|
||||
"user_id": self.user_id.id,
|
||||
"description": self.description,
|
||||
"company_id": self.company_id.id,
|
||||
"visit_template_id": self.id,
|
||||
}
|
||||
for date in dates
|
||||
]
|
||||
|
||||
# Get the date range from calendar.recurrence, that way the values obtained will
|
||||
# be correct (except for incompatible cases).
|
||||
def _get_start_range_dates(self):
|
||||
"""Method to get all dates (sorted) in the range."""
|
||||
duration = self.stop - self.start
|
||||
ranges = (
|
||||
self.env["calendar.recurrence"]
|
||||
.new(
|
||||
{
|
||||
"rrule_type": self.rrule_type,
|
||||
"interval": self.interval,
|
||||
"month_by": self.month_by,
|
||||
"weekday": self.weekday,
|
||||
"byday": self.byday,
|
||||
"count": self.count,
|
||||
"end_type": self.end_type,
|
||||
"until": self.until,
|
||||
"mon": self.mon,
|
||||
"tue": self.tue,
|
||||
"wed": self.wed,
|
||||
"thu": self.thu,
|
||||
"fri": self.fri,
|
||||
"sat": self.sat,
|
||||
"sun": self.sun,
|
||||
}
|
||||
)
|
||||
._range_calculation(self, duration)
|
||||
)
|
||||
start_dates = []
|
||||
for start, _stop in ranges:
|
||||
start_dates.append(start.date())
|
||||
return sorted(start_dates)
|
||||
|
||||
def _get_max_date(self):
|
||||
"""The maximum date will be the last of the range."""
|
||||
return self._get_start_range_dates()[-1]
|
||||
|
||||
def _get_recurrence_dates(self, items):
|
||||
"""For the n items, get only those that are not already generated."""
|
||||
start_dates = self._get_start_range_dates()
|
||||
dates = []
|
||||
visit_dates = self.visit_ids.mapped("date")
|
||||
for _date in start_dates[:items]:
|
||||
if _date not in visit_dates:
|
||||
dates.append(_date)
|
||||
return dates
|
||||
|
||||
def _create_visits(self, days=7):
|
||||
return self._prepare_crm_salesperson_planner_visit_vals(
|
||||
self._get_recurrence_dates(days)
|
||||
)
|
||||
|
||||
def create_visits(self, days=7):
|
||||
for item in self:
|
||||
visits = self.env["crm.salesperson.planner.visit"].create(
|
||||
item._create_visits(days)
|
||||
)
|
||||
if visits and item.auto_validate:
|
||||
visits.action_confirm()
|
||||
if item.last_visit_date >= item._get_max_date():
|
||||
item.state = "done"
|
||||
|
||||
def _cron_create_visits(self, days=7):
|
||||
templates = self.search([("state", "=", "in-progress")])
|
||||
templates.create_visits(days)
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
# Copyright 2021 Sygel - Valentin Vinagre
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = "res.partner"
|
||||
|
||||
salesperson_planner_visit_count = fields.Integer(
|
||||
string="Number of Salesperson Visits",
|
||||
compute="_compute_salesperson_planner_visit_count",
|
||||
)
|
||||
|
||||
def _compute_salesperson_planner_visit_count(self):
|
||||
partners = self | self.mapped("child_ids")
|
||||
partner_data = self.env["crm.salesperson.planner.visit"].read_group(
|
||||
[("partner_id", "in", partners.ids)], ["partner_id"], ["partner_id"]
|
||||
)
|
||||
mapped_data = {m["partner_id"][0]: m["partner_id_count"] for m in partner_data}
|
||||
for partner in self:
|
||||
visit_count = mapped_data.get(partner.id, 0)
|
||||
for child in partner.child_ids:
|
||||
visit_count += mapped_data.get(child.id, 0)
|
||||
partner.salesperson_planner_visit_count = visit_count
|
||||
|
||||
def action_view_salesperson_planner_visit(self):
|
||||
action = self.env["ir.actions.act_window"]._for_xml_id(
|
||||
"crm_salesperson_planner.all_crm_salesperson_planner_visit_action"
|
||||
)
|
||||
operator = "child_of" if self.is_company else "="
|
||||
action["domain"] = [("partner_id", operator, self.id)]
|
||||
return action
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
To configure this module, you need to:
|
||||
|
||||
* Go to new menu **CRM > Configuration > Salesperson Planner > Close Reasons** and create a close reason for 'Cancel' and 'Incident' types.
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
* `Sygel <https://www.sygel.es>`__:
|
||||
|
||||
* Valentin Vinagre
|
||||
* Manuel Regidor
|
||||
|
||||
* `Pesol <https://www.pesol.es>`__:
|
||||
|
||||
* Gerardo Marin Parra <info@pesol.es>
|
||||
|
||||
* `Tecnativa <https://www.tecnativa.com>`_:
|
||||
|
||||
* Víctor Martínez
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
This application allows you to track and schedule salespeople visits to your customers, allowing you to determine which opportunities are going to be dealt on each visit. Visits create an all day event in calendar, and they can be easily rescheduled.
|
||||
Visits can be automatically created from a template, in which it is possible to select the frequency of visits, as well as the start and end dates. The last visit can also be calculated by selecting the total number of repetitions.
|
||||
This module creates a cron that generates visits from templates, but an option to create them manually is available from the template form view when the template is validated.
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
Go to new menu **CRM > Salesperson Planner > My Visits or All Visits** and create a new visit.
|
||||
or
|
||||
Go to **CRM > Salesperson Planner > Visit Templates** and create a new recurring template for create periodical visits. In this case, it is necessary to select a start date. The date of the last repetition can be calculated by selection the total number of repetitions or an end date.
|
||||
There are two options available to reschedule visits that is already validated:
|
||||
* Change the date from the visit.
|
||||
* Change the date straight from the event automatically created in the calendar.
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!-- Copyright 2021 Sygel - Valentin Vinagre
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
||||
<odoo noupdate="1">
|
||||
<record id="crm_salesperson_planner_visit_comp_rule" model="ir.rule">
|
||||
<field name="name">CRM Salesperson planner visit multi-company</field>
|
||||
<field name="model_id" ref="model_crm_salesperson_planner_visit" />
|
||||
<field name="global" eval="True" />
|
||||
<field
|
||||
name="domain_force"
|
||||
>['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
<record id="crm_salesperson_planner_visit_template_comp_rule" model="ir.rule">
|
||||
<field name="name">CRM Salesperson planner visit template multi-company</field>
|
||||
<field name="model_id" ref="model_crm_salesperson_planner_visit" />
|
||||
<field name="global" eval="True" />
|
||||
<field
|
||||
name="domain_force"
|
||||
>['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
<record id="personal_salesperson_planner_visit" model="ir.rule">
|
||||
<field name="name">Personal Salesperson Planner Visit</field>
|
||||
<field ref="model_crm_salesperson_planner_visit" name="model_id" />
|
||||
<field
|
||||
name="domain_force"
|
||||
>['|',('user_id','=',user.id),('user_id','=',False)]</field>
|
||||
<field name="groups" eval="[(4, ref('sales_team.group_sale_salesman'))]" />
|
||||
</record>
|
||||
<record id="all_salesperson_planner_visit" model="ir.rule">
|
||||
<field name="name">All Salesperson Planner Visit</field>
|
||||
<field ref="model_crm_salesperson_planner_visit" name="model_id" />
|
||||
<field name="domain_force">[(1,'=',1)]</field>
|
||||
<field
|
||||
name="groups"
|
||||
eval="[(4, ref('sales_team.group_sale_salesman_all_leads'))]"
|
||||
/>
|
||||
</record>
|
||||
<record id="personal_salesperson_planner_visit_template" model="ir.rule">
|
||||
<field name="name">Personal Salesperson Planner Visit Template</field>
|
||||
<field ref="model_crm_salesperson_planner_visit_template" name="model_id" />
|
||||
<field
|
||||
name="domain_force"
|
||||
>['|',('user_id','=',user.id),('user_id','=',False)]</field>
|
||||
<field name="groups" eval="[(4, ref('sales_team.group_sale_salesman'))]" />
|
||||
</record>
|
||||
<record id="all_salesperson_planner_visit_template" model="ir.rule">
|
||||
<field name="name">All Salesperson Planner Visit Template</field>
|
||||
<field ref="model_crm_salesperson_planner_visit_template" name="model_id" />
|
||||
<field name="domain_force">[(1,'=',1)]</field>
|
||||
<field
|
||||
name="groups"
|
||||
eval="[(4, ref('sales_team.group_sale_salesman_all_leads'))]"
|
||||
/>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_crm_salesperson_planner_visit_user,crm.salesperson.planner.visit.user,model_crm_salesperson_planner_visit,sales_team.group_sale_salesman,1,1,1,1
|
||||
access_crm_salesperson_planner_visit_close_reason_user,crm.salesperson.planner.visit.close.reason.user,model_crm_salesperson_planner_visit_close_reason,sales_team.group_sale_salesman,1,0,0,0
|
||||
access_crm_salesperson_planner_visit_close_reason_manager,crm.salesperson.planner.visit.close.reason.manager,model_crm_salesperson_planner_visit_close_reason,sales_team.group_sale_manager,1,1,1,1
|
||||
access_crm_salesperson_planner_visit_template_user,crm.salesperson.planner.visit.template.user,model_crm_salesperson_planner_visit_template,sales_team.group_sale_salesman,1,1,1,1
|
||||
crm_salesperson_planner.access_crm_salesperson_planner_visit_close_wiz,access_crm_salesperson_planner_visit_close_wiz,crm_salesperson_planner.model_crm_salesperson_planner_visit_close_wiz,sales_team.group_sale_salesman,1,1,1,1
|
||||
crm_salesperson_planner.access_crm_salesperson_planner_visit_template_create,access_crm_salesperson_planner_visit_template_create,crm_salesperson_planner.model_crm_salesperson_planner_visit_template_create,sales_team.group_sale_salesman,1,1,1,1
|
||||
|
Binary file not shown.
|
After Width: | Height: | Size: 9.2 KiB |
|
|
@ -0,0 +1,455 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
|
||||
<title>Crm Salesperson Planner</title>
|
||||
<style type="text/css">
|
||||
|
||||
/*
|
||||
:Author: David Goodger (goodger@python.org)
|
||||
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
|
||||
:Copyright: This stylesheet has been placed in the public domain.
|
||||
|
||||
Default cascading style sheet for the HTML output of Docutils.
|
||||
Despite the name, some widely supported CSS2 features are used.
|
||||
|
||||
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
|
||||
customize this style sheet.
|
||||
*/
|
||||
|
||||
/* used to remove borders from tables and images */
|
||||
.borderless, table.borderless td, table.borderless th {
|
||||
border: 0 }
|
||||
|
||||
table.borderless td, table.borderless th {
|
||||
/* Override padding for "table.docutils td" with "! important".
|
||||
The right padding separates the table cells. */
|
||||
padding: 0 0.5em 0 0 ! important }
|
||||
|
||||
.first {
|
||||
/* Override more specific margin styles with "! important". */
|
||||
margin-top: 0 ! important }
|
||||
|
||||
.last, .with-subtitle {
|
||||
margin-bottom: 0 ! important }
|
||||
|
||||
.hidden {
|
||||
display: none }
|
||||
|
||||
.subscript {
|
||||
vertical-align: sub;
|
||||
font-size: smaller }
|
||||
|
||||
.superscript {
|
||||
vertical-align: super;
|
||||
font-size: smaller }
|
||||
|
||||
a.toc-backref {
|
||||
text-decoration: none ;
|
||||
color: black }
|
||||
|
||||
blockquote.epigraph {
|
||||
margin: 2em 5em ; }
|
||||
|
||||
dl.docutils dd {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Uncomment (and remove this text!) to get bold-faced definition list terms
|
||||
dl.docutils dt {
|
||||
font-weight: bold }
|
||||
*/
|
||||
|
||||
div.abstract {
|
||||
margin: 2em 5em }
|
||||
|
||||
div.abstract p.topic-title {
|
||||
font-weight: bold ;
|
||||
text-align: center }
|
||||
|
||||
div.admonition, div.attention, div.caution, div.danger, div.error,
|
||||
div.hint, div.important, div.note, div.tip, div.warning {
|
||||
margin: 2em ;
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.admonition p.admonition-title, div.hint p.admonition-title,
|
||||
div.important p.admonition-title, div.note p.admonition-title,
|
||||
div.tip p.admonition-title {
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
div.attention p.admonition-title, div.caution p.admonition-title,
|
||||
div.danger p.admonition-title, div.error p.admonition-title,
|
||||
div.warning p.admonition-title, .code .error {
|
||||
color: red ;
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
/* Uncomment (and remove this text!) to get reduced vertical space in
|
||||
compound paragraphs.
|
||||
div.compound .compound-first, div.compound .compound-middle {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
div.compound .compound-last, div.compound .compound-middle {
|
||||
margin-top: 0.5em }
|
||||
*/
|
||||
|
||||
div.dedication {
|
||||
margin: 2em 5em ;
|
||||
text-align: center ;
|
||||
font-style: italic }
|
||||
|
||||
div.dedication p.topic-title {
|
||||
font-weight: bold ;
|
||||
font-style: normal }
|
||||
|
||||
div.figure {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
div.footer, div.header {
|
||||
clear: both;
|
||||
font-size: smaller }
|
||||
|
||||
div.line-block {
|
||||
display: block ;
|
||||
margin-top: 1em ;
|
||||
margin-bottom: 1em }
|
||||
|
||||
div.line-block div.line-block {
|
||||
margin-top: 0 ;
|
||||
margin-bottom: 0 ;
|
||||
margin-left: 1.5em }
|
||||
|
||||
div.sidebar {
|
||||
margin: 0 0 0.5em 1em ;
|
||||
border: medium outset ;
|
||||
padding: 1em ;
|
||||
background-color: #ffffee ;
|
||||
width: 40% ;
|
||||
float: right ;
|
||||
clear: right }
|
||||
|
||||
div.sidebar p.rubric {
|
||||
font-family: sans-serif ;
|
||||
font-size: medium }
|
||||
|
||||
div.system-messages {
|
||||
margin: 5em }
|
||||
|
||||
div.system-messages h1 {
|
||||
color: red }
|
||||
|
||||
div.system-message {
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.system-message p.system-message-title {
|
||||
color: red ;
|
||||
font-weight: bold }
|
||||
|
||||
div.topic {
|
||||
margin: 2em }
|
||||
|
||||
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
|
||||
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
|
||||
margin-top: 0.4em }
|
||||
|
||||
h1.title {
|
||||
text-align: center }
|
||||
|
||||
h2.subtitle {
|
||||
text-align: center }
|
||||
|
||||
hr.docutils {
|
||||
width: 75% }
|
||||
|
||||
img.align-left, .figure.align-left, object.align-left, table.align-left {
|
||||
clear: left ;
|
||||
float: left ;
|
||||
margin-right: 1em }
|
||||
|
||||
img.align-right, .figure.align-right, object.align-right, table.align-right {
|
||||
clear: right ;
|
||||
float: right ;
|
||||
margin-left: 1em }
|
||||
|
||||
img.align-center, .figure.align-center, object.align-center {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
table.align-center {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left }
|
||||
|
||||
.align-center {
|
||||
clear: both ;
|
||||
text-align: center }
|
||||
|
||||
.align-right {
|
||||
text-align: right }
|
||||
|
||||
/* reset inner alignment in figures */
|
||||
div.align-right {
|
||||
text-align: inherit }
|
||||
|
||||
/* div.align-center * { */
|
||||
/* text-align: left } */
|
||||
|
||||
.align-top {
|
||||
vertical-align: top }
|
||||
|
||||
.align-middle {
|
||||
vertical-align: middle }
|
||||
|
||||
.align-bottom {
|
||||
vertical-align: bottom }
|
||||
|
||||
ol.simple, ul.simple {
|
||||
margin-bottom: 1em }
|
||||
|
||||
ol.arabic {
|
||||
list-style: decimal }
|
||||
|
||||
ol.loweralpha {
|
||||
list-style: lower-alpha }
|
||||
|
||||
ol.upperalpha {
|
||||
list-style: upper-alpha }
|
||||
|
||||
ol.lowerroman {
|
||||
list-style: lower-roman }
|
||||
|
||||
ol.upperroman {
|
||||
list-style: upper-roman }
|
||||
|
||||
p.attribution {
|
||||
text-align: right ;
|
||||
margin-left: 50% }
|
||||
|
||||
p.caption {
|
||||
font-style: italic }
|
||||
|
||||
p.credits {
|
||||
font-style: italic ;
|
||||
font-size: smaller }
|
||||
|
||||
p.label {
|
||||
white-space: nowrap }
|
||||
|
||||
p.rubric {
|
||||
font-weight: bold ;
|
||||
font-size: larger ;
|
||||
color: maroon ;
|
||||
text-align: center }
|
||||
|
||||
p.sidebar-title {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold ;
|
||||
font-size: larger }
|
||||
|
||||
p.sidebar-subtitle {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
p.topic-title {
|
||||
font-weight: bold }
|
||||
|
||||
pre.address {
|
||||
margin-bottom: 0 ;
|
||||
margin-top: 0 ;
|
||||
font: inherit }
|
||||
|
||||
pre.literal-block, pre.doctest-block, pre.math, pre.code {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
pre.code .ln { color: gray; } /* line numbers */
|
||||
pre.code, code { background-color: #eeeeee }
|
||||
pre.code .comment, code .comment { color: #5C6576 }
|
||||
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
|
||||
pre.code .literal.string, code .literal.string { color: #0C5404 }
|
||||
pre.code .name.builtin, code .name.builtin { color: #352B84 }
|
||||
pre.code .deleted, code .deleted { background-color: #DEB0A1}
|
||||
pre.code .inserted, code .inserted { background-color: #A3D289}
|
||||
|
||||
span.classifier {
|
||||
font-family: sans-serif ;
|
||||
font-style: oblique }
|
||||
|
||||
span.classifier-delimiter {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
span.interpreted {
|
||||
font-family: sans-serif }
|
||||
|
||||
span.option {
|
||||
white-space: nowrap }
|
||||
|
||||
span.pre {
|
||||
white-space: pre }
|
||||
|
||||
span.problematic, pre.problematic {
|
||||
color: red }
|
||||
|
||||
span.section-subtitle {
|
||||
/* font-size relative to parent (h1..h6 element) */
|
||||
font-size: 80% }
|
||||
|
||||
table.citation {
|
||||
border-left: solid 1px gray;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docinfo {
|
||||
margin: 2em 4em }
|
||||
|
||||
table.docutils {
|
||||
margin-top: 0.5em ;
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
table.footnote {
|
||||
border-left: solid 1px black;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docutils td, table.docutils th,
|
||||
table.docinfo td, table.docinfo th {
|
||||
padding-left: 0.5em ;
|
||||
padding-right: 0.5em ;
|
||||
vertical-align: top }
|
||||
|
||||
table.docutils th.field-name, table.docinfo th.docinfo-name {
|
||||
font-weight: bold ;
|
||||
text-align: left ;
|
||||
white-space: nowrap ;
|
||||
padding-left: 0 }
|
||||
|
||||
/* "booktabs" style (no vertical lines) */
|
||||
table.docutils.booktabs {
|
||||
border: 0px;
|
||||
border-top: 2px solid;
|
||||
border-bottom: 2px solid;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table.docutils.booktabs * {
|
||||
border: 0px;
|
||||
}
|
||||
table.docutils.booktabs th {
|
||||
border-bottom: thin solid;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
|
||||
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
|
||||
font-size: 100% }
|
||||
|
||||
ul.auto-toc {
|
||||
list-style-type: none }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document" id="crm-salesperson-planner">
|
||||
<h1 class="title">Crm Salesperson Planner</h1>
|
||||
|
||||
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:d1b9a09a3ad681a80bce2522a390e2f008cc0418bd007eeafc8f579b07a24627
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
|
||||
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/crm/tree/16.0/crm_salesperson_planner"><img alt="OCA/crm" src="https://img.shields.io/badge/github-OCA%2Fcrm-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/crm-16-0/crm-16-0-crm_salesperson_planner"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/crm&target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
|
||||
<p>This application allows you to track and schedule salespeople visits to your customers, allowing you to determine which opportunities are going to be dealt on each visit. Visits create an all day event in calendar, and they can be easily rescheduled.
|
||||
Visits can be automatically created from a template, in which it is possible to select the frequency of visits, as well as the start and end dates. The last visit can also be calculated by selecting the total number of repetitions.
|
||||
This module creates a cron that generates visits from templates, but an option to create them manually is available from the template form view when the template is validated.</p>
|
||||
<p><strong>Table of contents</strong></p>
|
||||
<div class="contents local topic" id="contents">
|
||||
<ul class="simple">
|
||||
<li><a class="reference internal" href="#configuration" id="toc-entry-1">Configuration</a></li>
|
||||
<li><a class="reference internal" href="#usage" id="toc-entry-2">Usage</a></li>
|
||||
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-3">Bug Tracker</a></li>
|
||||
<li><a class="reference internal" href="#credits" id="toc-entry-4">Credits</a><ul>
|
||||
<li><a class="reference internal" href="#authors" id="toc-entry-5">Authors</a></li>
|
||||
<li><a class="reference internal" href="#contributors" id="toc-entry-6">Contributors</a></li>
|
||||
<li><a class="reference internal" href="#maintainers" id="toc-entry-7">Maintainers</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="configuration">
|
||||
<h1><a class="toc-backref" href="#toc-entry-1">Configuration</a></h1>
|
||||
<p>To configure this module, you need to:</p>
|
||||
<ul class="simple">
|
||||
<li>Go to new menu <strong>CRM > Configuration > Salesperson Planner > Close Reasons</strong> and create a close reason for ‘Cancel’ and ‘Incident’ types.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="usage">
|
||||
<h1><a class="toc-backref" href="#toc-entry-2">Usage</a></h1>
|
||||
<p>Go to new menu <strong>CRM > Salesperson Planner > My Visits or All Visits</strong> and create a new visit.
|
||||
or
|
||||
Go to <strong>CRM > Salesperson Planner > Visit Templates</strong> and create a new recurring template for create periodical visits. In this case, it is necessary to select a start date. The date of the last repetition can be calculated by selection the total number of repetitions or an end date.
|
||||
There are two options available to reschedule visits that is already validated:
|
||||
* Change the date from the visit.
|
||||
* Change the date straight from the event automatically created in the calendar.</p>
|
||||
</div>
|
||||
<div class="section" id="bug-tracker">
|
||||
<h1><a class="toc-backref" href="#toc-entry-3">Bug Tracker</a></h1>
|
||||
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/crm/issues">GitHub Issues</a>.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us to smash it by providing a detailed and welcomed
|
||||
<a class="reference external" href="https://github.com/OCA/crm/issues/new?body=module:%20crm_salesperson_planner%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
|
||||
<p>Do not contact contributors directly about support or help with technical issues.</p>
|
||||
</div>
|
||||
<div class="section" id="credits">
|
||||
<h1><a class="toc-backref" href="#toc-entry-4">Credits</a></h1>
|
||||
<div class="section" id="authors">
|
||||
<h2><a class="toc-backref" href="#toc-entry-5">Authors</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Sygel Technology</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="contributors">
|
||||
<h2><a class="toc-backref" href="#toc-entry-6">Contributors</a></h2>
|
||||
<ul class="simple">
|
||||
<li><a class="reference external" href="https://www.sygel.es">Sygel</a>:<ul>
|
||||
<li>Valentin Vinagre</li>
|
||||
<li>Manuel Regidor</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a class="reference external" href="https://www.pesol.es">Pesol</a>:<ul>
|
||||
<li>Gerardo Marin Parra <<a class="reference external" href="mailto:info@pesol.es">info@pesol.es</a>></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a class="reference external" href="https://www.tecnativa.com">Tecnativa</a>:<ul>
|
||||
<li>Víctor Martínez</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="maintainers">
|
||||
<h2><a class="toc-backref" href="#toc-entry-7">Maintainers</a></h2>
|
||||
<p>This module is maintained by the OCA.</p>
|
||||
<a class="reference external image-reference" href="https://odoo-community.org">
|
||||
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
|
||||
</a>
|
||||
<p>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.</p>
|
||||
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/crm/tree/16.0/crm_salesperson_planner">OCA/crm</a> project on GitHub.</p>
|
||||
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# Copyright 2021 Sygel - Valentin Vinagre
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
|
||||
|
||||
from . import test_crm_salesperson_planner_visit
|
||||
from . import test_crm_salesperson_planner_visit_template
|
||||
|
|
@ -0,0 +1,326 @@
|
|||
# Copyright 2021 Sygel - Valentin Vinagre
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import _, fields
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tests import common
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
class TestCrmSalespersonPlannerVisitBase(common.TransactionCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.env = cls.env(
|
||||
context=dict(
|
||||
cls.env.context,
|
||||
mail_create_nolog=True,
|
||||
mail_create_nosubscribe=True,
|
||||
mail_notrack=True,
|
||||
no_reset_password=True,
|
||||
tracking_disable=True,
|
||||
)
|
||||
)
|
||||
cls.visit_model = cls.env["crm.salesperson.planner.visit"]
|
||||
cls.partner_model = cls.env["res.partner"]
|
||||
cls.close_model = cls.env["crm.salesperson.planner.visit.close.reason"]
|
||||
cls.close_wiz_model = cls.env["crm.salesperson.planner.visit.close.wiz"]
|
||||
cls.partner1 = cls.partner_model.create(
|
||||
{
|
||||
"name": "Partner Visit 1",
|
||||
"email": "partner.visit.1@example.com",
|
||||
"phone": "1234567890",
|
||||
}
|
||||
)
|
||||
cls.partner1_contact1 = cls.partner_model.create(
|
||||
{
|
||||
"name": "Partner Contact Visit 1",
|
||||
"email": "partner.visit.1@example.com",
|
||||
"phone": "1234567890",
|
||||
"parent_id": cls.partner1.id,
|
||||
}
|
||||
)
|
||||
cls.visit1 = cls.visit_model.create({"partner_id": cls.partner1.id})
|
||||
cls.visit2 = cls.visit_model.create(
|
||||
{"partner_id": cls.partner1_contact1.id, "sequence": 1}
|
||||
)
|
||||
cls.cancel = cls.close_model.create(
|
||||
{
|
||||
"name": "Cancel",
|
||||
"close_type": "cancel",
|
||||
"require_image": False,
|
||||
"reschedule": False,
|
||||
}
|
||||
)
|
||||
cls.cancel_resch = cls.close_model.create(
|
||||
{
|
||||
"name": "Cancel",
|
||||
"close_type": "cancel",
|
||||
"require_image": False,
|
||||
"reschedule": True,
|
||||
}
|
||||
)
|
||||
cls.cancel_img = cls.close_model.create(
|
||||
{
|
||||
"name": "Cancel",
|
||||
"close_type": "cancel",
|
||||
"require_image": True,
|
||||
"reschedule": False,
|
||||
}
|
||||
)
|
||||
cls.incident = cls.close_model.create(
|
||||
{
|
||||
"name": "Incident",
|
||||
"close_type": "incident",
|
||||
"require_image": False,
|
||||
"reschedule": False,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class TestCrmSalespersonPlannerVisit(TestCrmSalespersonPlannerVisitBase):
|
||||
def test_crm_salesperson_planner_visit(self):
|
||||
self.assertNotEqual(self.visit1.name, "/")
|
||||
self.assertEqual(self.visit1.state, "draft")
|
||||
self.assertEqual(self.partner1.salesperson_planner_visit_count, 2)
|
||||
self.assertEqual(self.partner1_contact1.salesperson_planner_visit_count, 1)
|
||||
self.assertEqual(self.visit1.date, fields.Date.context_today(self.visit1))
|
||||
self.assertEqual(
|
||||
self.visit_model.search(
|
||||
[("partner_id", "child_of", self.partner1.id)], limit=1
|
||||
),
|
||||
self.visit2,
|
||||
)
|
||||
|
||||
def config_close_wiz(self, att_close_type, vals):
|
||||
additionnal_context = {
|
||||
"active_model": self.visit_model._name,
|
||||
"active_ids": self.visit1.ids,
|
||||
"active_id": self.visit1.id,
|
||||
"att_close_type": att_close_type,
|
||||
}
|
||||
close_wiz = self.close_wiz_model.with_context(**additionnal_context).create(
|
||||
vals
|
||||
)
|
||||
close_wiz.action_close_reason_apply()
|
||||
|
||||
@mute_logger("odoo.models.unlink")
|
||||
def test_crm_salesperson_close_wiz_cancel(self):
|
||||
self.visit1.action_confirm()
|
||||
self.assertEqual(self.visit1.state, "confirm")
|
||||
self.config_close_wiz("close", {"reason_id": self.cancel.id, "notes": "Test"})
|
||||
self.assertEqual(self.visit1.state, "cancel")
|
||||
self.assertEqual(self.visit1.close_reason_id.id, self.cancel.id)
|
||||
self.assertEqual(self.visit1.close_reason_notes, "Test")
|
||||
self.assertEqual(
|
||||
self.visit_model.search_count(
|
||||
[("partner_id", "child_of", self.partner1.id)]
|
||||
),
|
||||
2,
|
||||
)
|
||||
|
||||
@mute_logger("odoo.models.unlink")
|
||||
def test_crm_salesperson_close_wiz_cancel_resch(self):
|
||||
self.visit1.action_confirm()
|
||||
self.assertEqual(self.visit1.state, "confirm")
|
||||
self.config_close_wiz(
|
||||
"close",
|
||||
{
|
||||
"reason_id": self.cancel_resch.id,
|
||||
"new_date": self.visit1.date + relativedelta(days=10),
|
||||
"new_sequence": 40,
|
||||
},
|
||||
)
|
||||
self.assertEqual(self.visit1.close_reason_id.id, self.cancel_resch.id)
|
||||
self.assertEqual(
|
||||
self.visit_model.search_count(
|
||||
[
|
||||
("partner_id", "=", self.partner1.id),
|
||||
("date", "=", self.visit1.date + relativedelta(days=10)),
|
||||
("sequence", "=", 40),
|
||||
("state", "=", "confirm"),
|
||||
]
|
||||
),
|
||||
1,
|
||||
)
|
||||
|
||||
@mute_logger("odoo.models.unlink")
|
||||
def test_crm_salesperson_close_wiz_cancel_img(self):
|
||||
self.visit1.action_confirm()
|
||||
self.assertEqual(self.visit1.state, "confirm")
|
||||
detail_image = b"R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
|
||||
self.config_close_wiz(
|
||||
"close", {"reason_id": self.cancel_img.id, "image": detail_image}
|
||||
)
|
||||
self.assertEqual(self.visit1.close_reason_id.id, self.cancel_img.id)
|
||||
self.assertEqual(self.visit1.close_reason_image, detail_image)
|
||||
|
||||
def test_crm_salesperson_close_wiz_incident(self):
|
||||
self.visit1.action_confirm()
|
||||
self.assertEqual(self.visit1.state, "confirm")
|
||||
self.config_close_wiz("incident", {"reason_id": self.incident.id})
|
||||
self.assertEqual(self.visit1.state, "incident")
|
||||
|
||||
def test_write_method_updates_calendar_event_user_id(self):
|
||||
partner = self.env["res.partner"].create(
|
||||
{
|
||||
"name": "Test Partner",
|
||||
}
|
||||
)
|
||||
|
||||
visit = self.env["crm.salesperson.planner.visit"].create(
|
||||
{
|
||||
"name": "Test Visit",
|
||||
"user_id": self.env.ref("base.user_demo").id,
|
||||
"partner_id": partner.id,
|
||||
}
|
||||
)
|
||||
calendar_event = self.env["calendar.event"].create(
|
||||
{
|
||||
"name": "Test Event",
|
||||
"user_id": self.env.ref("base.user_demo").id,
|
||||
"partner_id": partner.id,
|
||||
}
|
||||
)
|
||||
visit.write({"calendar_event_id": calendar_event.id})
|
||||
|
||||
new_user = self.env.ref("base.user_admin")
|
||||
visit.write({"user_id": new_user.id})
|
||||
|
||||
self.assertEqual(calendar_event.user_id, new_user)
|
||||
|
||||
def test_action_done(self):
|
||||
partner = self.env["res.partner"].create(
|
||||
{
|
||||
"name": "Ejemplo de partner",
|
||||
}
|
||||
)
|
||||
visit = self.env["crm.salesperson.planner.visit"].create(
|
||||
{
|
||||
"state": "confirm",
|
||||
"partner_id": partner.id,
|
||||
}
|
||||
)
|
||||
|
||||
visit.action_done()
|
||||
|
||||
self.assertEqual(visit.state, "done")
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
visit.action_done()
|
||||
|
||||
visit.state = "draft"
|
||||
with self.assertRaises(ValidationError):
|
||||
visit.action_done()
|
||||
|
||||
|
||||
class TestResPartner(common.TransactionCase):
|
||||
def test_action_view_salesperson_planner_visit(self):
|
||||
partner = self.env["res.partner"].create(
|
||||
{"name": "Test Partner", "is_company": True}
|
||||
)
|
||||
|
||||
action = partner.action_view_salesperson_planner_visit()
|
||||
|
||||
self.assertEqual(action["domain"], [("partner_id", "child_of", partner.id)])
|
||||
self.assertEqual(action["res_model"], "crm.salesperson.planner.visit")
|
||||
self.assertIn("tree", action["view_mode"])
|
||||
self.assertIn("form", action["view_mode"])
|
||||
self.assertIn("pivot", action["view_mode"])
|
||||
|
||||
|
||||
class TestCalendarEvent(common.TransactionCase):
|
||||
def test_write_user_id(self):
|
||||
event = self.env["calendar.event"].create({"name": "Test Event"})
|
||||
|
||||
values = {"user_id": 1}
|
||||
|
||||
event.write(values)
|
||||
|
||||
self.assertIn("user_id", values)
|
||||
self.assertEqual(values["user_id"], 1)
|
||||
|
||||
|
||||
class TestCrmSalespersonPlannerVisitTemplate(common.TransactionCase):
|
||||
def test_partner_ids_constraint(self):
|
||||
template = self.env["crm.salesperson.planner.visit.template"].create(
|
||||
{
|
||||
"name": "Test Visit Template",
|
||||
"partner_ids": [(0, 0, {"name": "Customer 1"})],
|
||||
}
|
||||
)
|
||||
|
||||
with self.assertRaises(ValidationError) as context:
|
||||
template.partner_ids = [
|
||||
(0, 0, {"name": "Customer 2"}),
|
||||
(0, 0, {"name": "Customer 3"}),
|
||||
]
|
||||
|
||||
error_msg = _("Only one customer is allowed")
|
||||
self.assertEqual(str(context.exception), error_msg)
|
||||
|
||||
def test_action_view_salesperson_planner_visit(self):
|
||||
template = self.env["crm.salesperson.planner.visit.template"].create(
|
||||
{
|
||||
"partner_id": 1,
|
||||
"description": "Ejemplo de descripción",
|
||||
}
|
||||
)
|
||||
|
||||
action = template.action_view_salesperson_planner_visit()
|
||||
|
||||
self.assertIsInstance(action, dict)
|
||||
self.assertEqual(action.get("type"), "ir.actions.act_window")
|
||||
|
||||
expected_domain = [("id", "=", template.visit_ids.ids)]
|
||||
self.assertEqual(action.get("domain"), expected_domain)
|
||||
|
||||
expected_context = {
|
||||
"default_partner_id": template.partner_id.id,
|
||||
"default_visit_template_id": template.id,
|
||||
"default_description": template.description,
|
||||
}
|
||||
self.assertEqual(action.get("context"), expected_context)
|
||||
|
||||
def test_action_cancel(self):
|
||||
template = self.env["crm.salesperson.planner.visit.template"].create(
|
||||
{
|
||||
"state": "in-progress",
|
||||
}
|
||||
)
|
||||
|
||||
template.action_cancel()
|
||||
|
||||
self.assertEqual(template.state, "cancel")
|
||||
|
||||
def test_action_draft(self):
|
||||
template = self.env["crm.salesperson.planner.visit.template"].create(
|
||||
{
|
||||
"state": "cancel",
|
||||
}
|
||||
)
|
||||
|
||||
template.action_draft()
|
||||
|
||||
self.assertEqual(template.state, "draft")
|
||||
|
||||
|
||||
class TestCrmSalespersonPlannerVisitTemplateCreate(common.TransactionCase):
|
||||
def test_default_date_to(self):
|
||||
wizard = self.env["crm.salesperson.planner.visit.template.create"].create({})
|
||||
|
||||
template = self.env["crm.salesperson.planner.visit.template"].create(
|
||||
{
|
||||
"last_visit_date": fields.Date.today(),
|
||||
}
|
||||
)
|
||||
wizard = wizard.with_context(active_id=template.id)
|
||||
|
||||
default_date_to = wizard._default_date_to()
|
||||
|
||||
expected_date = template.last_visit_date + timedelta(days=7)
|
||||
self.assertEqual(default_date_to, expected_date)
|
||||
|
|
@ -0,0 +1,339 @@
|
|||
# Copyright 2021 Sygel - Valentin Vinagre
|
||||
# Copyright 2021 Sygel - Manuel Regidor
|
||||
# Copyright 2024 Tecnativa - Víctor Martínez
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
|
||||
from datetime import timedelta
|
||||
|
||||
from freezegun import freeze_time
|
||||
|
||||
from odoo import exceptions, fields
|
||||
from odoo.tests import common
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
class TestCrmSalespersonPlannerVisitTemplate(common.TransactionCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.env = cls.env(
|
||||
context=dict(
|
||||
cls.env.context,
|
||||
mail_create_nolog=True,
|
||||
mail_create_nosubscribe=True,
|
||||
mail_notrack=True,
|
||||
no_reset_password=True,
|
||||
tracking_disable=True,
|
||||
)
|
||||
)
|
||||
cls.visit_template_model = cls.env["crm.salesperson.planner.visit.template"]
|
||||
cls.partner_model = cls.env["res.partner"]
|
||||
cls.close_reason_mode = cls.env["crm.salesperson.planner.visit.close.reason"]
|
||||
cls.partner1 = cls.partner_model.create(
|
||||
{
|
||||
"name": "Partner Visit 1",
|
||||
"email": "partner.visit.1@example.com",
|
||||
"phone": "1234567890",
|
||||
}
|
||||
)
|
||||
cls.visit_template_base = cls.visit_template_model.create(
|
||||
{
|
||||
"partner_ids": [(6, False, cls.partner1.ids)],
|
||||
"start_date": fields.Date.today(),
|
||||
"stop_date": fields.Date.today(),
|
||||
"start": fields.Date.today(),
|
||||
"stop": fields.Date.today(),
|
||||
}
|
||||
)
|
||||
cls.close_reason = cls.close_reason_mode.create(
|
||||
{"name": "close reason", "close_type": "cancel"}
|
||||
)
|
||||
|
||||
def test_01_repeat_days(self):
|
||||
self.visit_template_base.write(
|
||||
{
|
||||
"auto_validate": False,
|
||||
"interval": 1,
|
||||
"rrule_type": "daily",
|
||||
"end_type": "count",
|
||||
"count": 10,
|
||||
}
|
||||
)
|
||||
self.visit_template_base.action_validate()
|
||||
self.visit_template_base.create_visits(days=4)
|
||||
self.assertEqual(len(self.visit_template_base.visit_ids), 4)
|
||||
self.assertEqual(
|
||||
len(
|
||||
self.visit_template_base.visit_ids.filtered(
|
||||
lambda a: a.state == "draft"
|
||||
)
|
||||
),
|
||||
4,
|
||||
)
|
||||
self.assertEqual(
|
||||
len(
|
||||
self.visit_template_base.visit_ids.filtered(
|
||||
lambda a: a.calendar_event_id.id
|
||||
)
|
||||
),
|
||||
0,
|
||||
)
|
||||
self.assertEqual(self.visit_template_base.state, "in-progress")
|
||||
self.visit_template_base.create_visits(days=10)
|
||||
self.assertEqual(len(self.visit_template_base.visit_ids), 10)
|
||||
self.assertEqual(
|
||||
len(
|
||||
self.visit_template_base.visit_ids.filtered(
|
||||
lambda a: a.state == "draft"
|
||||
)
|
||||
),
|
||||
10,
|
||||
)
|
||||
self.assertEqual(
|
||||
len(
|
||||
self.visit_template_base.visit_ids.filtered(
|
||||
lambda a: a.calendar_event_id.id
|
||||
)
|
||||
),
|
||||
0,
|
||||
)
|
||||
self.assertEqual(self.visit_template_base.state, "done")
|
||||
|
||||
def test_02_repeat_days_autovalidate(self):
|
||||
self.visit_template_base.write(
|
||||
{
|
||||
"auto_validate": True,
|
||||
"interval": 1,
|
||||
"rrule_type": "daily",
|
||||
"end_type": "count",
|
||||
"count": 10,
|
||||
}
|
||||
)
|
||||
self.visit_template_base.action_validate()
|
||||
self.visit_template_base.create_visits(days=4)
|
||||
self.assertEqual(len(self.visit_template_base.visit_ids), 4)
|
||||
self.assertEqual(
|
||||
len(
|
||||
self.visit_template_base.visit_ids.filtered(
|
||||
lambda a: a.state == "draft"
|
||||
)
|
||||
),
|
||||
0,
|
||||
)
|
||||
self.assertEqual(
|
||||
len(
|
||||
self.visit_template_base.visit_ids.filtered(
|
||||
lambda a: a.calendar_event_id.id
|
||||
)
|
||||
),
|
||||
4,
|
||||
)
|
||||
self.assertEqual(self.visit_template_base.state, "in-progress")
|
||||
self.visit_template_base.create_visits(days=10)
|
||||
self.assertEqual(len(self.visit_template_base.visit_ids), 10)
|
||||
self.assertEqual(
|
||||
len(
|
||||
self.visit_template_base.visit_ids.filtered(
|
||||
lambda a: a.state == "draft"
|
||||
)
|
||||
),
|
||||
0,
|
||||
)
|
||||
self.assertEqual(
|
||||
len(
|
||||
self.visit_template_base.visit_ids.filtered(
|
||||
lambda a: a.calendar_event_id.id
|
||||
)
|
||||
),
|
||||
10,
|
||||
)
|
||||
self.assertEqual(self.visit_template_base.state, "done")
|
||||
|
||||
def test_03_change_visit_date(self):
|
||||
visit_template = self.visit_template_base.copy()
|
||||
visit_template.write(
|
||||
{
|
||||
"auto_validate": True,
|
||||
"interval": 1,
|
||||
"rrule_type": "daily",
|
||||
"end_type": "count",
|
||||
"count": 10,
|
||||
}
|
||||
)
|
||||
visit_template.create_visits(days=10)
|
||||
visit_0 = fields.first(visit_template.visit_ids)
|
||||
event_id_0 = visit_0.calendar_event_id
|
||||
self.assertEqual(visit_0.date, event_id_0.start_date)
|
||||
visit_0.write({"date": fields.Date.today() + timedelta(days=7)})
|
||||
self.assertEqual(event_id_0.start_date, fields.Date.today() + timedelta(days=7))
|
||||
event_id_0.write(
|
||||
{
|
||||
"start": fields.Datetime.today() + timedelta(days=14),
|
||||
"stop": fields.Datetime.today() + timedelta(days=14),
|
||||
}
|
||||
)
|
||||
self.assertEqual(visit_0.date, fields.Date.today() + timedelta(days=14))
|
||||
|
||||
@mute_logger("odoo.models.unlink")
|
||||
def test_04_cancel_visit(self):
|
||||
visit_template = self.visit_template_base.copy()
|
||||
visit_template.write(
|
||||
{
|
||||
"auto_validate": True,
|
||||
"interval": 1,
|
||||
"rrule_type": "daily",
|
||||
"end_type": "count",
|
||||
"count": 10,
|
||||
}
|
||||
)
|
||||
visit_template.create_visits(days=10)
|
||||
first_visit = fields.first(visit_template.visit_ids)
|
||||
self.assertTrue(first_visit.calendar_event_id)
|
||||
with self.assertRaises(exceptions.ValidationError):
|
||||
first_visit.unlink()
|
||||
self.assertEqual(len(visit_template.visit_ids), 10)
|
||||
first_visit.action_cancel(self.close_reason)
|
||||
self.assertFalse(first_visit.calendar_event_id)
|
||||
first_visit.unlink()
|
||||
self.assertEqual(len(visit_template.visit_ids), 9)
|
||||
|
||||
@freeze_time("2024-01-01 08:00")
|
||||
def test_05_repeat_weeks(self):
|
||||
self.visit_template_base.write(
|
||||
{
|
||||
"start_date": "2024-03-08",
|
||||
"interval": 1,
|
||||
"rrule_type": "weekly",
|
||||
"tue": True,
|
||||
"end_type": "end_date",
|
||||
"until": "2024-07-02",
|
||||
}
|
||||
)
|
||||
self.visit_template_base.action_validate()
|
||||
self.assertFalse(self.visit_template_base.visit_ids)
|
||||
create_model = self.env["crm.salesperson.planner.visit.template.create"]
|
||||
create_item = create_model.with_context(
|
||||
active_id=self.visit_template_base.id
|
||||
).create({"date_to": "2024-07-02"})
|
||||
create_item.create_visits()
|
||||
self.assertEqual(self.visit_template_base.state, "done")
|
||||
visit_dates = self.visit_template_base.visit_ids.mapped("date")
|
||||
self.assertIn(fields.Date.from_string("2024-03-19"), visit_dates)
|
||||
self.assertEqual(
|
||||
self.visit_template_base.last_visit_date,
|
||||
fields.Date.from_string("2024-07-02"),
|
||||
)
|
||||
|
||||
@freeze_time("2024-01-01 08:00")
|
||||
def test_06_repeat_months_count_01(self):
|
||||
self.visit_template_base.write(
|
||||
{
|
||||
"start_date": "2024-03-08",
|
||||
"interval": 1,
|
||||
"rrule_type": "monthly",
|
||||
"end_type": "count",
|
||||
"count": 2,
|
||||
"month_by": "date",
|
||||
"day": 1,
|
||||
}
|
||||
)
|
||||
self.visit_template_base.action_validate()
|
||||
self.assertFalse(self.visit_template_base.visit_ids)
|
||||
create_model = self.env["crm.salesperson.planner.visit.template.create"]
|
||||
create_item = create_model.with_context(
|
||||
active_id=self.visit_template_base.id
|
||||
).create({"date_to": "2024-12-13"})
|
||||
create_item.create_visits()
|
||||
self.assertEqual(self.visit_template_base.state, "done")
|
||||
self.assertEqual(len(self.visit_template_base.visit_ids), 2)
|
||||
visit_dates = self.visit_template_base.visit_ids.mapped("date")
|
||||
self.assertIn(fields.Date.from_string("2024-04-01"), visit_dates)
|
||||
self.assertEqual(
|
||||
self.visit_template_base.last_visit_date,
|
||||
fields.Date.from_string("2024-05-01"),
|
||||
)
|
||||
|
||||
@freeze_time("2024-01-01 08:00")
|
||||
def test_06_repeat_months_count_02(self):
|
||||
self.visit_template_base.write(
|
||||
{
|
||||
"start_date": "2024-03-08",
|
||||
"interval": 1,
|
||||
"rrule_type": "monthly",
|
||||
"end_type": "count",
|
||||
"count": 2,
|
||||
"month_by": "date",
|
||||
"day": 1,
|
||||
}
|
||||
)
|
||||
self.visit_template_base.action_validate()
|
||||
self.assertFalse(self.visit_template_base.visit_ids)
|
||||
create_model = self.env["crm.salesperson.planner.visit.template.create"]
|
||||
create_item = create_model.with_context(
|
||||
active_id=self.visit_template_base.id
|
||||
).create({"date_to": "2024-12-13"})
|
||||
create_item.create_visits()
|
||||
self.assertEqual(self.visit_template_base.state, "done")
|
||||
self.assertEqual(len(self.visit_template_base.visit_ids), 2)
|
||||
visit_dates = self.visit_template_base.visit_ids.mapped("date")
|
||||
self.assertIn(fields.Date.from_string("2024-04-01"), visit_dates)
|
||||
self.assertEqual(
|
||||
self.visit_template_base.last_visit_date,
|
||||
fields.Date.from_string("2024-05-01"),
|
||||
)
|
||||
|
||||
@freeze_time("2024-01-01 08:00")
|
||||
def test_06_repeat_months_count_03(self):
|
||||
self.visit_template_base.write(
|
||||
{
|
||||
"start_date": "2024-03-08",
|
||||
"interval": 1,
|
||||
"rrule_type": "monthly",
|
||||
"end_type": "count",
|
||||
"count": 2,
|
||||
"month_by": "day",
|
||||
"byday": "1",
|
||||
"weekday": "MON",
|
||||
}
|
||||
)
|
||||
self.visit_template_base.action_validate()
|
||||
self.assertFalse(self.visit_template_base.visit_ids)
|
||||
create_model = self.env["crm.salesperson.planner.visit.template.create"]
|
||||
create_item = create_model.with_context(
|
||||
active_id=self.visit_template_base.id
|
||||
).create({"date_to": "2024-12-13"})
|
||||
create_item.create_visits()
|
||||
self.assertEqual(self.visit_template_base.state, "done")
|
||||
self.assertEqual(len(self.visit_template_base.visit_ids), 2)
|
||||
visit_dates = self.visit_template_base.visit_ids.mapped("date")
|
||||
self.assertIn(fields.Date.from_string("2024-04-01"), visit_dates)
|
||||
self.assertEqual(
|
||||
self.visit_template_base.last_visit_date,
|
||||
fields.Date.from_string("2024-05-06"),
|
||||
)
|
||||
|
||||
@freeze_time("2024-01-01 08:00")
|
||||
def test_07_repeat_yearly_count_01(self):
|
||||
self.visit_template_base.write(
|
||||
{
|
||||
"start_date": "2024-03-08",
|
||||
"interval": 1,
|
||||
"rrule_type": "yearly",
|
||||
"end_type": "count",
|
||||
"count": 2,
|
||||
}
|
||||
)
|
||||
self.visit_template_base.action_validate()
|
||||
self.assertFalse(self.visit_template_base.visit_ids)
|
||||
create_model = self.env["crm.salesperson.planner.visit.template.create"]
|
||||
create_item = create_model.with_context(
|
||||
active_id=self.visit_template_base.id
|
||||
).create({"date_to": "2030-01-01"})
|
||||
create_item.create_visits()
|
||||
self.assertEqual(self.visit_template_base.state, "done")
|
||||
self.assertEqual(len(self.visit_template_base.visit_ids), 2)
|
||||
visit_dates = self.visit_template_base.visit_ids.mapped("date")
|
||||
self.assertIn(fields.Date.from_string("2024-03-08"), visit_dates)
|
||||
self.assertEqual(
|
||||
self.visit_template_base.last_visit_date,
|
||||
fields.Date.from_string("2025-03-08"),
|
||||
)
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!-- Copyright 2021 Sygel - Valentin Vinagre
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
||||
<odoo>
|
||||
<record id="crm_lead_crm_salesperson_planner" model="ir.ui.view">
|
||||
<field name="name">crm.lead.view.crm.salesperson.planner</field>
|
||||
<field name="model">crm.lead</field>
|
||||
<field name="inherit_id" ref="crm.crm_lead_view_form" />
|
||||
<field name="arch" type="xml">
|
||||
<notebook position="inside">
|
||||
<page string="Salesperson Visits">
|
||||
<group>
|
||||
<field name="crm_salesperson_planner_visit_ids" nolabel="1" />
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!-- Copyright 2021 Sygel - Valentin Vinagre
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
||||
<odoo>
|
||||
<menuitem
|
||||
id="menu_salesperson_planner"
|
||||
name="Salesperson Planner"
|
||||
groups="sales_team.group_sale_salesman"
|
||||
parent="crm.crm_menu_root"
|
||||
sequence="4"
|
||||
/>
|
||||
<menuitem
|
||||
name="My Visits"
|
||||
id="menu_crm_salesperson_planner_my_visits"
|
||||
parent="menu_salesperson_planner"
|
||||
action="my_crm_salesperson_planner_visit_action"
|
||||
sequence="1"
|
||||
/>
|
||||
<menuitem
|
||||
name="All Visits"
|
||||
id="menu_crm_salesperson_planner_visits"
|
||||
parent="menu_salesperson_planner"
|
||||
action="all_crm_salesperson_planner_visit_action"
|
||||
sequence="2"
|
||||
groups="sales_team.group_sale_salesman_all_leads"
|
||||
/>
|
||||
<menuitem
|
||||
name="Visit Templates"
|
||||
id="menu_crm_salesperson_planner_visit_template"
|
||||
parent="menu_salesperson_planner"
|
||||
action="crm_salesperson_planner_visit_template_action"
|
||||
sequence="3"
|
||||
/>
|
||||
<!-- CONFIGURATION -->
|
||||
<menuitem
|
||||
id="menu_crm_config_salesperson_planner"
|
||||
name="Salesperson Planner"
|
||||
parent="crm.crm_menu_config"
|
||||
sequence="30"
|
||||
groups="sales_team.group_sale_manager"
|
||||
/>
|
||||
<menuitem
|
||||
name="Close Reasons"
|
||||
id="menu_crm_config_salesperson_planner_close_reason"
|
||||
parent="menu_crm_config_salesperson_planner"
|
||||
action="crm_salesperson_planner_visit_close_reason_action"
|
||||
sequence="2"
|
||||
/>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!-- Copyright 2021 Sygel - Valentin Vinagre
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
||||
<odoo>
|
||||
<record
|
||||
model="ir.ui.view"
|
||||
id="crm_salesperson_planner_visit_close_reason_tree_view"
|
||||
>
|
||||
<field name="name">CRM - Salesperson Planner Visit Close Reason Tree</field>
|
||||
<field name="model">crm.salesperson.planner.visit.close.reason</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="name" />
|
||||
<field name="close_type" />
|
||||
<field name="require_image" />
|
||||
<field name="reschedule" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record
|
||||
model="ir.ui.view"
|
||||
id="crm_salesperson_planner_visit_close_reason_form_view"
|
||||
>
|
||||
<field name="name">CRM - Salesperson Planner Visit Close Reason Form</field>
|
||||
<field name="model">crm.salesperson.planner.visit.close.reason</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Close Reason">
|
||||
<sheet string="Close Reason">
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" />
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="close_type" />
|
||||
<field name="require_image" />
|
||||
<field name="reschedule" />
|
||||
</group>
|
||||
<group>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record
|
||||
model="ir.actions.act_window"
|
||||
id="crm_salesperson_planner_visit_close_reason_action"
|
||||
>
|
||||
<field name="name">Close Reasons</field>
|
||||
<field name="res_model">crm.salesperson.planner.visit.close.reason</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="oe_view_nocontent_create">
|
||||
Record reason for close commercial visits.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!-- Copyright 2021 Sygel - Valentin Vinagre
|
||||
Copyright 2021 Sygel - Manuel Regidor
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
||||
<odoo>
|
||||
<record model="ir.ui.view" id="crm_salesperson_planner_visit_template_tree_view">
|
||||
<field name="name">CRM - Salesperson Planner Visit Template Tree</field>
|
||||
<field name="model">crm.salesperson.planner.visit.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="name" />
|
||||
<field name="partner_id" />
|
||||
<field name="start_date" />
|
||||
<field name="sequence" />
|
||||
<field name="user_id" />
|
||||
<field name="company_id" groups="base.group_multi_company" />
|
||||
<field name="state" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="crm_salesperson_planner_visit_template_form_view">
|
||||
<field name="name">CRM - Salesperson Planner Visit Template Form</field>
|
||||
<field name="model">crm.salesperson.planner.visit.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Visit Template">
|
||||
<header>
|
||||
<button
|
||||
name="action_validate"
|
||||
string="Validate"
|
||||
type="object"
|
||||
attrs="{'invisible': [('state', '!=', 'draft')]}"
|
||||
/>
|
||||
<button
|
||||
name="%(crm_salesperson_planner_visit_template_create_action)d"
|
||||
string="Manually Create Visits"
|
||||
type="action"
|
||||
attrs="{'invisible': [('state', '!=', 'in-progress')]}"
|
||||
/>
|
||||
<button
|
||||
name="action_cancel"
|
||||
string="Cancel"
|
||||
type="object"
|
||||
attrs="{'invisible': [('state', '!=', 'in-progress')]}"
|
||||
/>
|
||||
<button
|
||||
name="action_draft"
|
||||
string="Send to Draft"
|
||||
type="object"
|
||||
attrs="{'invisible': [('state', '!=', 'cancel')]}"
|
||||
/>
|
||||
<field
|
||||
name="state"
|
||||
widget="statusbar"
|
||||
statusbar_visible="draft,in-progress,done,cancel"
|
||||
/>
|
||||
</header>
|
||||
<sheet string="Visit Template">
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button
|
||||
class="oe_stat_button"
|
||||
type="object"
|
||||
name="action_view_salesperson_planner_visit"
|
||||
icon="fa-building"
|
||||
>
|
||||
<field
|
||||
string="Sales Visits"
|
||||
name="visit_ids_count"
|
||||
widget="statinfo"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" readonly="1" />
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="partner_id" invisible="1" />
|
||||
<field name="partner_ids" widget="many2many_tags" />
|
||||
<field name="start_date" required="1" />
|
||||
<field name="start" invisible="1" />
|
||||
<field name="stop" invisible="1" />
|
||||
<field name="stop_date" invisible="1" />
|
||||
<field name="start" invisible="1" />
|
||||
<field name="sequence" />
|
||||
<field name="user_id" />
|
||||
<field
|
||||
name="company_id"
|
||||
groups="base.group_multi_company"
|
||||
/>
|
||||
<field name="last_visit_date" />
|
||||
<field name="auto_validate" />
|
||||
<field name="event_tz" invisible="1" />
|
||||
<field name="allday" force_save="1" />
|
||||
<field name="rrule" invisible="1" />
|
||||
</group>
|
||||
<!-- Display fields similar to calendar.event (Display data similar to calendar.event (for UX is better)). -->
|
||||
<group>
|
||||
<label for="interval" />
|
||||
<div class="o_col">
|
||||
<div class="o_row">
|
||||
<field
|
||||
name="interval"
|
||||
class="oe_inline"
|
||||
required="1"
|
||||
/>
|
||||
<field name="rrule_type" required="1" />
|
||||
</div>
|
||||
<widget
|
||||
name="week_days"
|
||||
attrs="{'invisible': [('rrule_type', '!=', 'weekly')]}"
|
||||
/>
|
||||
</div>
|
||||
<label string="Until" for="end_type" />
|
||||
<div class="o_row">
|
||||
<field name="end_type" required="1" />
|
||||
<field
|
||||
name="count"
|
||||
required="1"
|
||||
attrs="{'invisible': [('end_type', '!=', 'count')]}"
|
||||
/>
|
||||
<field
|
||||
name="until"
|
||||
attrs="{'invisible': [('end_type', '!=', 'end_date')], 'required': [('end_type', '=', 'end_date')]}"
|
||||
/>
|
||||
</div>
|
||||
</group>
|
||||
<group attrs="{'invisible': [('rrule_type', '!=', 'monthly')]}">
|
||||
<label string="Day of Month" for="month_by" />
|
||||
<div class="o_row">
|
||||
<field name="month_by" />
|
||||
<field
|
||||
name="day"
|
||||
attrs="{'required': [('month_by', '=', 'date'), ('rrule_type', '=', 'monthly')], 'invisible': [('month_by', '!=', 'date')]}"
|
||||
/>
|
||||
<field
|
||||
name="byday"
|
||||
string="The"
|
||||
attrs="{'required': [('month_by', '=', 'day'), ('rrule_type', '=', 'monthly')], 'invisible': [('month_by', '!=', 'day')]}"
|
||||
/>
|
||||
<field
|
||||
name="weekday"
|
||||
nolabel="1"
|
||||
attrs="{'required': [('month_by', '=', 'day'), ('rrule_type', '=', 'monthly')], 'invisible': [('month_by', '!=', 'day')]}"
|
||||
/>
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Description">
|
||||
<field name="description" />
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record
|
||||
model="ir.actions.act_window"
|
||||
id="crm_salesperson_planner_visit_template_action"
|
||||
>
|
||||
<field name="name">Visit Templates</field>
|
||||
<field name="res_model">crm.salesperson.planner.visit.template</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="oe_view_nocontent_create">
|
||||
Create and plan commercial visit templates
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,300 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!-- Copyright 2021 Sygel - Valentin Vinagre
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
||||
<odoo>
|
||||
<record model="ir.ui.view" id="crm_salesperson_planner_visit_tree_view">
|
||||
<field name="name">CRM - Salesperson Planner Visit Tree</field>
|
||||
<field name="model">crm.salesperson.planner.visit</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree
|
||||
decoration-success="state == 'done'"
|
||||
decoration-info="state == 'confirm'"
|
||||
decoration-warning="state == 'incident'"
|
||||
decoration-muted="state == 'cancel'"
|
||||
>
|
||||
<field name="name" />
|
||||
<field name="sequence" />
|
||||
<field name="date" />
|
||||
<field name="partner_id" />
|
||||
<field name="user_id" />
|
||||
<field name="company_id" groups="base.group_multi_company" />
|
||||
<field name="state" />
|
||||
<button
|
||||
name="action_draft"
|
||||
type="object"
|
||||
attrs="{'invisible': [('state', 'not in', ['cancel','incident', 'done'])]}"
|
||||
icon="fa-undo"
|
||||
/>
|
||||
<button
|
||||
name="action_confirm"
|
||||
type="object"
|
||||
icon="fa-calendar text-success"
|
||||
attrs="{'invisible': [('state', '!=', 'draft')]}"
|
||||
/>
|
||||
<button
|
||||
name="action_done"
|
||||
type="object"
|
||||
icon="fa-check"
|
||||
attrs="{'invisible': [('state', '!=', 'confirm')]}"
|
||||
/>
|
||||
<button
|
||||
name="%(crm_salesperson_planner_visit_close_wiz_action)d"
|
||||
type="action"
|
||||
icon="fa-ban text-danger"
|
||||
attrs="{'invisible': [('state', 'in', ['cancel','incident', 'done'])]}"
|
||||
context="{'att_close_type':'cancel'}"
|
||||
title="Close Reasons"
|
||||
/>
|
||||
<button
|
||||
name="%(crm_salesperson_planner_visit_close_wiz_action)d"
|
||||
type="action"
|
||||
icon="fa-exclamation-triangle"
|
||||
attrs="{'invisible': [('state', '!=', 'confirm')]}"
|
||||
context="{'att_close_type':'incident'}"
|
||||
title="Close Reasons"
|
||||
/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="crm_salesperson_planner_visit_form_view">
|
||||
<field name="name">CRM - Salesperson Planner Visit Form</field>
|
||||
<field name="model">crm.salesperson.planner.visit</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Visit">
|
||||
<header>
|
||||
<button
|
||||
name="action_draft"
|
||||
id="action_draft"
|
||||
string="Draft"
|
||||
type="object"
|
||||
attrs="{'invisible': [('state', 'not in', ['cancel','incident', 'done'])]}"
|
||||
/>
|
||||
<button
|
||||
name="action_confirm"
|
||||
id="action_confirm"
|
||||
string="Confirm"
|
||||
class="btn-primary"
|
||||
type="object"
|
||||
attrs="{'invisible': [('state', '!=', 'draft')]}"
|
||||
/>
|
||||
<button
|
||||
name="action_done"
|
||||
id="action_done"
|
||||
string="Done"
|
||||
class="btn-primary"
|
||||
type="object"
|
||||
attrs="{'invisible': [('state', '!=', 'confirm')]}"
|
||||
/>
|
||||
<button
|
||||
name="%(crm_salesperson_planner_visit_close_wiz_action)d"
|
||||
id="action_cancel"
|
||||
string="Cancel"
|
||||
type="action"
|
||||
attrs="{'invisible': [('state', 'in', ['cancel','incident', 'done'])]}"
|
||||
context="{'att_close_type':'cancel'}"
|
||||
/>
|
||||
<button
|
||||
name="%(crm_salesperson_planner_visit_close_wiz_action)d"
|
||||
id="action_incident"
|
||||
string="Incident"
|
||||
type="action"
|
||||
attrs="{'invisible': [('state', '!=', 'confirm')]}"
|
||||
context="{'att_close_type':'incident'}"
|
||||
/>
|
||||
<field
|
||||
name="state"
|
||||
widget="statusbar"
|
||||
statusbar_visible="draft,confirm,done,cancel"
|
||||
/>
|
||||
</header>
|
||||
<sheet string="Visit">
|
||||
<div class="oe_button_box" name="button_box">
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" readonly="1" />
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field
|
||||
name="partner_id"
|
||||
widget="res_partner_many2one"
|
||||
context="{'res_partner_search_mode': 'customer', 'show_address': True, 'show_vat': True}"
|
||||
options="{"always_reload": True}"
|
||||
/>
|
||||
<field
|
||||
name="partner_phone"
|
||||
widget="phone"
|
||||
attrs="{'invisible': [('partner_id', '=', False)]}"
|
||||
/>
|
||||
<field
|
||||
name="partner_mobile"
|
||||
widget="phone"
|
||||
attrs="{'invisible': [('partner_id', '=', False)]}"
|
||||
/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="date" />
|
||||
<field name="calendar_event_id" readonly="1" />
|
||||
<field name="sequence" />
|
||||
<field name="user_id" />
|
||||
<field
|
||||
name="company_id"
|
||||
groups="base.group_multi_company"
|
||||
/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page
|
||||
string="Close Info"
|
||||
attrs="{'invisible': [('state', 'not in', ['cancel', 'incident'])]}"
|
||||
>
|
||||
<group>
|
||||
<field
|
||||
name="close_reason_id"
|
||||
readonly="1"
|
||||
options="{'no_edit': True, 'no_open': True}"
|
||||
/>
|
||||
<field name="close_reason_notes" readonly="1" />
|
||||
<field
|
||||
name="close_reason_image"
|
||||
widget="image"
|
||||
readonly="1"
|
||||
attrs="{'invisible': [('close_reason_image', '=', False)]}"
|
||||
/>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Internal Notes">
|
||||
<field
|
||||
name="description"
|
||||
placeholder="Add a description..."
|
||||
/>
|
||||
</page>
|
||||
<page string="Opportunities">
|
||||
<group>
|
||||
<field
|
||||
name="opportunity_ids"
|
||||
nolabel="1"
|
||||
options="{'no_create': True}"
|
||||
/>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field
|
||||
name="message_follower_ids"
|
||||
widget="mail_followers"
|
||||
groups="base.group_user"
|
||||
/>
|
||||
<field name="activity_ids" widget="mail_activity" />
|
||||
<field name="message_ids" widget="mail_thread" />
|
||||
</div>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="crm_salesperson_planner_visit_filter" model="ir.ui.view">
|
||||
<field name="name">CRM - Salesperson Planner Visit Search</field>
|
||||
<field name="model">crm.salesperson.planner.visit</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Visits">
|
||||
<field name="name" />
|
||||
<field
|
||||
name="partner_id"
|
||||
filter_domain="[('partner_id','child_of',self)]"
|
||||
/>
|
||||
<field name="user_id" />
|
||||
<field name="date" />
|
||||
<field name="opportunity_ids" />
|
||||
<separator />
|
||||
<filter
|
||||
string="My Visits"
|
||||
name="assigned_to_me"
|
||||
domain="[('user_id', '=', uid)]"
|
||||
help="Visits that are assigned to me"
|
||||
/>
|
||||
<separator orientation="vertical" />
|
||||
<filter
|
||||
string="Late Visits"
|
||||
name="visits_overdue"
|
||||
domain="[('date', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
/>
|
||||
<filter
|
||||
string="Today Visits"
|
||||
name="visits_today"
|
||||
domain="[('date', '=', context_today().strftime('%Y-%m-%d'))]"
|
||||
/>
|
||||
<filter
|
||||
string="Future Visits"
|
||||
name="visits_upcoming_all"
|
||||
domain="[('date', '>', context_today().strftime('%Y-%m-%d'))]"
|
||||
/>
|
||||
<group expand="0" name="visits" string="Group By">
|
||||
<filter
|
||||
string="Salesperson"
|
||||
name="salesperson"
|
||||
context="{'group_by': 'user_id'}"
|
||||
/>
|
||||
<filter
|
||||
string="Partner"
|
||||
name="partner"
|
||||
context="{'group_by': 'partner_id'}"
|
||||
/>
|
||||
<filter
|
||||
string="State"
|
||||
name="state"
|
||||
context="{'group_by': 'state'}"
|
||||
/>
|
||||
<filter
|
||||
string="Company"
|
||||
name="company"
|
||||
context="{'group_by':'company_id'}"
|
||||
groups="base.group_multi_company"
|
||||
/>
|
||||
<separator orientation="vertical" />
|
||||
<filter
|
||||
string="Visit by Date"
|
||||
context="{'group_by':'date'}"
|
||||
name="date"
|
||||
/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
<record model="ir.actions.act_window" id="my_crm_salesperson_planner_visit_action">
|
||||
<field name="name">Visits</field>
|
||||
<field name="res_model">crm.salesperson.planner.visit</field>
|
||||
<field name="view_mode">tree,form,pivot</field>
|
||||
<field name="context">{
|
||||
'search_default_visits_today':1,
|
||||
}</field>
|
||||
<field name="domain">[('user_id', '=', uid)]</field>
|
||||
<field
|
||||
name="search_view_id"
|
||||
ref="crm_salesperson_planner.crm_salesperson_planner_visit_filter"
|
||||
/>
|
||||
<field name="help" type="html">
|
||||
<p class="oe_view_nocontent_create">
|
||||
Record and track my sales commercial visits.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
<record model="ir.actions.act_window" id="all_crm_salesperson_planner_visit_action">
|
||||
<field name="name">Visits</field>
|
||||
<field name="res_model">crm.salesperson.planner.visit</field>
|
||||
<field name="view_mode">tree,form,pivot</field>
|
||||
<field name="context">{
|
||||
'search_default_salesperson':1,
|
||||
}</field>
|
||||
<field
|
||||
name="search_view_id"
|
||||
ref="crm_salesperson_planner.crm_salesperson_planner_visit_filter"
|
||||
/>
|
||||
<field name="help" type="html">
|
||||
<p class="oe_view_nocontent_create">
|
||||
Record and track all sales commercial visits.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!-- Copyright 2021 Sygel - Valentin Vinagre
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
||||
<odoo>
|
||||
<record id="partner_view_crm_salesperson_planner" model="ir.ui.view">
|
||||
<field name="name">partner.view.crm.salesperson.planner</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button
|
||||
class="oe_stat_button"
|
||||
type="object"
|
||||
name="action_view_salesperson_planner_visit"
|
||||
icon="fa-building"
|
||||
groups="sales_team.group_sale_salesman"
|
||||
context="{'default_partner_id': active_id}"
|
||||
>
|
||||
<field
|
||||
string="Sales Visits"
|
||||
name="salesperson_planner_visit_count"
|
||||
widget="statinfo"
|
||||
/>
|
||||
</button>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# Copyright 2021 Sygel - Valentin Vinagre
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
|
||||
|
||||
from . import crm_salesperson_planner_visit_close_wiz
|
||||
from . import crm_salesperson_planner_visit_template_create
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
# Copyright 2021 Sygel - Valentin Vinagre
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
|
||||
|
||||
from odoo import _, fields, models
|
||||
|
||||
|
||||
class CrmSalespersonPlannerVisitCloseWiz(models.TransientModel):
|
||||
_name = "crm.salesperson.planner.visit.close.wiz"
|
||||
_description = "Get Close Reason"
|
||||
|
||||
def _default_new_date(self):
|
||||
visits = self.env["crm.salesperson.planner.visit"].browse(
|
||||
self.env.context.get("active_id")
|
||||
)
|
||||
return visits.date
|
||||
|
||||
def _default_new_sequence(self):
|
||||
visits = self.env["crm.salesperson.planner.visit"].browse(
|
||||
self.env.context.get("active_id")
|
||||
)
|
||||
return visits.sequence
|
||||
|
||||
reason_id = fields.Many2one(
|
||||
comodel_name="crm.salesperson.planner.visit.close.reason",
|
||||
string="Reason",
|
||||
required=True,
|
||||
)
|
||||
image = fields.Image(max_width=1024, max_height=1024)
|
||||
new_date = fields.Date(default=lambda self: self._default_new_date())
|
||||
new_sequence = fields.Integer(
|
||||
string="Sequence",
|
||||
help="Used to order Visits in the different views",
|
||||
default=lambda self: self._default_new_sequence(),
|
||||
)
|
||||
require_image = fields.Boolean(
|
||||
string="Require Image", related="reason_id.require_image"
|
||||
)
|
||||
reschedule = fields.Boolean(default=True)
|
||||
allow_reschedule = fields.Boolean(
|
||||
string="Allow Reschedule", related="reason_id.reschedule"
|
||||
)
|
||||
notes = fields.Text()
|
||||
|
||||
def action_close_reason_apply(self):
|
||||
visits = self.env["crm.salesperson.planner.visit"].browse(
|
||||
self.env.context.get("active_id")
|
||||
)
|
||||
visit_close_find_method_name = "action_%s" % self.reason_id.close_type
|
||||
if hasattr(visits, visit_close_find_method_name):
|
||||
getattr(visits, visit_close_find_method_name)(
|
||||
self.reason_id, self.image, self.notes
|
||||
)
|
||||
if self.allow_reschedule and self.reschedule:
|
||||
visits.copy(
|
||||
{
|
||||
"date": self.new_date,
|
||||
"sequence": self.new_sequence,
|
||||
"opportunity_ids": visits.opportunity_ids.ids,
|
||||
}
|
||||
).action_confirm()
|
||||
else:
|
||||
raise ValueError(_("The close reason type haven't a function."))
|
||||
return {"type": "ir.actions.act_window_close"}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!-- Copyright 2021 Sygel - Valentin Vinagre
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
||||
<odoo>
|
||||
<record id="crm_salesperson_planner_visit_close_wiz_view_form" model="ir.ui.view">
|
||||
<field name="name">crm.salesperson.lost.form</field>
|
||||
<field name="model">crm.salesperson.planner.visit.close.wiz</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Close Reason">
|
||||
<group>
|
||||
<field
|
||||
name="reason_id"
|
||||
options="{'no_create': True, 'no_edit': True, 'no_open': True}"
|
||||
domain="[('close_type','=', context.get('att_close_type', 'cancel'))]"
|
||||
/>
|
||||
<field name="notes" />
|
||||
<field name="allow_reschedule" invisible="1" />
|
||||
<field
|
||||
name="reschedule"
|
||||
attrs="{'invisible': [('allow_reschedule', '=', False)]}"
|
||||
/>
|
||||
<field
|
||||
name="new_date"
|
||||
attrs="{'invisible': ['|', ('allow_reschedule', '=', False), ('reschedule', '=', False)]}"
|
||||
/>
|
||||
<field
|
||||
name="new_sequence"
|
||||
attrs="{'invisible': ['|', ('allow_reschedule', '=', False), ('reschedule', '=', False)]}"
|
||||
/>
|
||||
<field name="require_image" invisible="1" />
|
||||
<field
|
||||
name="image"
|
||||
widget="image"
|
||||
attrs="{'invisible': [('require_image', '=', False)], 'required': [('require_image', '=', True)]}"
|
||||
/>
|
||||
</group>
|
||||
<footer>
|
||||
<button
|
||||
name="action_close_reason_apply"
|
||||
string="Submit"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record
|
||||
id="crm_salesperson_planner_visit_close_wiz_action"
|
||||
model="ir.actions.act_window"
|
||||
>
|
||||
<field name="name">Close Reasons</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">crm.salesperson.planner.visit.close.wiz</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="crm_salesperson_planner_visit_close_wiz_view_form" />
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
# Copyright 2021 Sygel - Valentin Vinagre
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from odoo import _, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class CrmSalespersonPlannerVisitTemplateCreate(models.TransientModel):
|
||||
_name = "crm.salesperson.planner.visit.template.create"
|
||||
_description = "crm salesperson planner visit template create"
|
||||
|
||||
def _default_date_to(self):
|
||||
template = self.env["crm.salesperson.planner.visit.template"].browse(
|
||||
self.env.context.get("active_id")
|
||||
)
|
||||
date = template.last_visit_date or fields.Date.context_today(self)
|
||||
return date + timedelta(days=7)
|
||||
|
||||
date_to = fields.Date(
|
||||
string="Date to", default=lambda self: self._default_date_to(), required=True
|
||||
)
|
||||
|
||||
def create_visits(self):
|
||||
template = self.env["crm.salesperson.planner.visit.template"].browse(
|
||||
self.env.context.get("active_id")
|
||||
)
|
||||
days = (self.date_to - fields.Date.context_today(self)).days
|
||||
if days < 0:
|
||||
raise ValidationError(_("The date can't be earlier than today"))
|
||||
# Create visits + auto-confirm + auto-done
|
||||
template.create_visits(days=days)
|
||||
return {"type": "ir.actions.act_window_close"}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!-- Copyright 2021 Sygel - Valentin Vinagre
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
||||
<odoo>
|
||||
<record
|
||||
id="crm_salesperson_planner_visit_template_create_view_form"
|
||||
model="ir.ui.view"
|
||||
>
|
||||
<field name="name">crm.salesperson.planner.visit.template.create.form</field>
|
||||
<field name="model">crm.salesperson.planner.visit.template.create</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Create Visits">
|
||||
<group class="oe_title">
|
||||
<field name="date_to" />
|
||||
</group>
|
||||
<footer>
|
||||
<button
|
||||
name="create_visits"
|
||||
string="Create"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record
|
||||
id="crm_salesperson_planner_visit_template_create_action"
|
||||
model="ir.actions.act_window"
|
||||
>
|
||||
<field name="name">Create Visits</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">crm.salesperson.planner.visit.template.create</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field
|
||||
name="view_id"
|
||||
ref="crm_salesperson_planner_visit_template_create_view_form"
|
||||
/>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
# Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
U[Users] -->|HTTP| V[Views and QWeb Templates]
|
||||
V --> C[Controllers]
|
||||
V --> W[Wizards – Transient Models]
|
||||
C --> M[Models and ORM]
|
||||
W --> M
|
||||
M --> R[Reports]
|
||||
DX[Data XML] --> M
|
||||
S[Security – ACLs and Groups] -. enforces .-> M
|
||||
|
||||
subgraph Crm_salesperson_planner Module - crm_salesperson_planner
|
||||
direction LR
|
||||
M:::layer
|
||||
W:::layer
|
||||
C:::layer
|
||||
V:::layer
|
||||
R:::layer
|
||||
S:::layer
|
||||
DX:::layer
|
||||
end
|
||||
|
||||
classDef layer fill:#eef8ff,stroke:#6ea8fe,stroke-width:1px
|
||||
```
|
||||
|
||||
Notes
|
||||
- Views include tree/form/kanban templates and report templates.
|
||||
- Controllers provide website/portal routes when present.
|
||||
- Wizards are UI flows implemented with `models.TransientModel`.
|
||||
- Data XML loads data/demo records; Security defines groups and access.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Configuration
|
||||
|
||||
Refer to Odoo settings for crm_salesperson_planner. Configure related models, access rights, and options as needed.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Controllers
|
||||
|
||||
This module does not define custom HTTP controllers.
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# Dependencies
|
||||
|
||||
This addon depends on:
|
||||
|
||||
- [crm](../../odoo-bringout-oca-ocb-crm)
|
||||
- [calendar](../../odoo-bringout-oca-ocb-calendar)
|
||||
4
odoo-bringout-oca-crm-crm_salesperson_planner/doc/FAQ.md
Normal file
4
odoo-bringout-oca-crm-crm_salesperson_planner/doc/FAQ.md
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# FAQ
|
||||
|
||||
- Q: Which Odoo version? A: 16.0 (OCA/OCB packaged).
|
||||
- Q: How to enable? A: Start server with --addon crm_salesperson_planner or install in UI.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# Install
|
||||
|
||||
```bash
|
||||
pip install odoo-bringout-oca-crm-crm_salesperson_planner"
|
||||
# or
|
||||
uv pip install odoo-bringout-oca-crm-crm_salesperson_planner"
|
||||
```
|
||||
17
odoo-bringout-oca-crm-crm_salesperson_planner/doc/MODELS.md
Normal file
17
odoo-bringout-oca-crm-crm_salesperson_planner/doc/MODELS.md
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Models
|
||||
|
||||
Detected core models and extensions in crm_salesperson_planner.
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class crm_salesperson_planner_visit
|
||||
class crm_salesperson_planner_visit_close_reason
|
||||
class crm_salesperson_planner_visit_template
|
||||
class calendar_event
|
||||
class crm_lead
|
||||
class res_partner
|
||||
```
|
||||
|
||||
Notes
|
||||
- Classes show model technical names; fields omitted for brevity.
|
||||
- Items listed under _inherit are extensions of existing models.
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# Overview
|
||||
|
||||
Packaged Odoo addon: crm_salesperson_planner. Provides features documented in upstream Odoo 16 under this addon.
|
||||
|
||||
- Source: OCA/OCB 16.0, addon crm_salesperson_planner
|
||||
- License: LGPL-3
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Reports
|
||||
|
||||
This module does not define custom reports.
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
# Security
|
||||
|
||||
Access control and security definitions in crm_salesperson_planner.
|
||||
|
||||
## Access Control Lists (ACLs)
|
||||
|
||||
Model access permissions defined in:
|
||||
- **[ir.model.access.csv](../crm_salesperson_planner/security/ir.model.access.csv)**
|
||||
- 6 model access rules
|
||||
|
||||
## Record Rules
|
||||
|
||||
Row-level security rules defined in:
|
||||
|
||||
## Security Groups & Configuration
|
||||
|
||||
Security groups and permissions defined in:
|
||||
- **[crm_salesperson_planner_security.xml](../crm_salesperson_planner/security/crm_salesperson_planner_security.xml)**
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Security Layers"
|
||||
A[Users] --> B[Groups]
|
||||
B --> C[Access Control Lists]
|
||||
C --> D[Models]
|
||||
B --> E[Record Rules]
|
||||
E --> F[Individual Records]
|
||||
end
|
||||
```
|
||||
|
||||
Security files overview:
|
||||
- **[crm_salesperson_planner_security.xml](../crm_salesperson_planner/security/crm_salesperson_planner_security.xml)**
|
||||
- Security groups, categories, and XML-based rules
|
||||
- **[ir.model.access.csv](../crm_salesperson_planner/security/ir.model.access.csv)**
|
||||
- Model access permissions (CRUD rights)
|
||||
|
||||
Notes
|
||||
- Access Control Lists define which groups can access which models
|
||||
- Record Rules provide row-level security (filter records by user/group)
|
||||
- Security groups organize users and define permission sets
|
||||
- All security is enforced at the ORM level by Odoo
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# Troubleshooting
|
||||
|
||||
- Ensure Python and Odoo environment matches repo guidance.
|
||||
- Check database connectivity and logs if startup fails.
|
||||
- Validate that dependent addons listed in DEPENDENCIES.md are installed.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# Usage
|
||||
|
||||
Start Odoo including this addon (from repo root):
|
||||
|
||||
```bash
|
||||
python3 scripts/nix_odoo_web_server.py --db-name mydb --addon crm_salesperson_planner
|
||||
```
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Wizards
|
||||
|
||||
This module does not include UI wizards.
|
||||
43
odoo-bringout-oca-crm-crm_salesperson_planner/pyproject.toml
Normal file
43
odoo-bringout-oca-crm-crm_salesperson_planner/pyproject.toml
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[project]
|
||||
name = "odoo-bringout-oca-crm-crm_salesperson_planner"
|
||||
version = "16.0.0"
|
||||
description = "Crm Salesperson Planner - Odoo addon"
|
||||
authors = [
|
||||
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
|
||||
]
|
||||
dependencies = [
|
||||
"odoo-bringout-oca-ocb-crm>=16.0.0",
|
||||
"odoo-bringout-oca-ocb-calendar>=16.0.0",
|
||||
"requests>=2.25.1"
|
||||
]
|
||||
readme = "README.md"
|
||||
requires-python = ">= 3.11"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Topic :: Office/Business",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
homepage = "https://github.com/bringout/0"
|
||||
repository = "https://github.com/bringout/0"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.metadata]
|
||||
allow-direct-references = true
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["crm_salesperson_planner"]
|
||||
|
||||
[tool.rye]
|
||||
managed = true
|
||||
dev-dependencies = [
|
||||
"pytest>=8.4.1",
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue