Initial commit: OCA Technical packages (595 packages)

This commit is contained in:
Ernad Husremovic 2025-08-29 15:43:03 +02:00
commit 2cc02aac6e
24950 changed files with 2318079 additions and 0 deletions

View file

@ -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

View file

@ -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()

View file

@ -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)]",
)

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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