mirror of
https://github.com/bringout/oca-workflow-process.git
synced 2026-04-23 20:52:01 +02:00
Initial commit: OCA Workflow Process packages (456 packages)
This commit is contained in:
commit
d366e42934
18799 changed files with 1284507 additions and 0 deletions
|
|
@ -0,0 +1,48 @@
|
|||
# Sale planner calendar
|
||||
|
||||
Odoo addon: sale_planner_calendar
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install odoo-bringout-oca-sale-workflow-sale_planner_calendar
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
This addon depends on:
|
||||
- calendar
|
||||
- sale
|
||||
- sale_payment_sheet
|
||||
|
||||
## Manifest Information
|
||||
|
||||
- **Name**: Sale planner calendar
|
||||
- **Version**: 16.0.3.0.0
|
||||
- **Category**: Sale
|
||||
- **License**: AGPL-3
|
||||
- **Installable**: True
|
||||
|
||||
## Source
|
||||
|
||||
Based on [OCA/sale-workflow](https://github.com/OCA/sale-workflow) branch 16.0, addon `sale_planner_calendar`.
|
||||
|
||||
## 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
|
||||
- Reports: doc/REPORTS.md
|
||||
- Security: doc/SECURITY.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,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 Sale_planner_calendar Module - sale_planner_calendar
|
||||
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 sale_planner_calendar. 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,7 @@
|
|||
# Dependencies
|
||||
|
||||
This addon depends on:
|
||||
|
||||
- [calendar](../../odoo-bringout-oca-ocb-calendar)
|
||||
- [sale](../../odoo-bringout-oca-ocb-sale)
|
||||
- [sale_payment_sheet](../../odoo-bringout-oca-sale-workflow-sale_payment_sheet)
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# FAQ
|
||||
|
||||
- Q: Which Odoo version? A: 16.0 (OCA/OCB packaged).
|
||||
- Q: How to enable? A: Start server with --addon sale_planner_calendar or install in UI.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# Install
|
||||
|
||||
```bash
|
||||
pip install odoo-bringout-oca-sale-workflow-sale_planner_calendar"
|
||||
# or
|
||||
uv pip install odoo-bringout-oca-sale-workflow-sale_planner_calendar"
|
||||
```
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
# Models
|
||||
|
||||
Detected core models and extensions in sale_planner_calendar.
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class sale_planner_calendar_issue_type
|
||||
class sale_planner_calendar_summary
|
||||
class calendar_attendee
|
||||
class calendar_event
|
||||
class calendar_event_type
|
||||
class mail_thread
|
||||
class res_company
|
||||
class res_config_settings
|
||||
class res_partner
|
||||
class res_users
|
||||
class sale_order
|
||||
class sale_payment_sheet_line
|
||||
```
|
||||
|
||||
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: sale_planner_calendar. Provides features documented in upstream Odoo 16 under this addon.
|
||||
|
||||
- Source: OCA/OCB 16.0, addon sale_planner_calendar
|
||||
- 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 sale_planner_calendar.
|
||||
|
||||
## Access Control Lists (ACLs)
|
||||
|
||||
Model access permissions defined in:
|
||||
- **[ir.model.access.csv](../sale_planner_calendar/security/ir.model.access.csv)**
|
||||
- 5 model access rules
|
||||
|
||||
## Record Rules
|
||||
|
||||
Row-level security rules defined in:
|
||||
|
||||
## Security Groups & Configuration
|
||||
|
||||
Security groups and permissions defined in:
|
||||
- **[sale_planner_calendar_security.xml](../sale_planner_calendar/security/sale_planner_calendar_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:
|
||||
- **[ir.model.access.csv](../sale_planner_calendar/security/ir.model.access.csv)**
|
||||
- Model access permissions (CRUD rights)
|
||||
- **[sale_planner_calendar_security.xml](../sale_planner_calendar/security/sale_planner_calendar_security.xml)**
|
||||
- Security groups, categories, and XML-based rules
|
||||
|
||||
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 sale_planner_calendar
|
||||
```
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
# Wizards
|
||||
|
||||
Transient models exposed as UI wizards in sale_planner_calendar.
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class SaleInvoicePaymentWiz
|
||||
class SalePlannerCalendarReassignLineWiz
|
||||
class SalePlannerCalendarReassignWiz
|
||||
class SalePlannerCalendarWizard
|
||||
```
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
[project]
|
||||
name = "odoo-bringout-oca-sale-workflow-sale_planner_calendar"
|
||||
version = "16.0.0"
|
||||
description = "Sale planner calendar - Sale planner calendar"
|
||||
authors = [
|
||||
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
|
||||
]
|
||||
dependencies = [
|
||||
"odoo-bringout-oca-ocb-calendar>=16.0.0",
|
||||
"odoo-bringout-oca-ocb-sale>=16.0.0",
|
||||
"odoo-bringout-oca-sale-workflow-sale_payment_sheet>=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 = ["sale_planner_calendar"]
|
||||
|
||||
[tool.rye]
|
||||
managed = true
|
||||
dev-dependencies = [
|
||||
"pytest>=8.4.1",
|
||||
]
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
=====================
|
||||
Sale planner calendar
|
||||
=====================
|
||||
|
||||
..
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:9536f0a762b2909e0b503ba68e80f5fbd7d358124a934194fbc5d67b657e4510
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
.. |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%2Fsale--workflow-lightgray.png?logo=github
|
||||
:target: https://github.com/OCA/sale-workflow/tree/16.0/sale_planner_calendar
|
||||
:alt: OCA/sale-workflow
|
||||
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
|
||||
:target: https://translation.odoo-community.org/projects/sale-workflow-16-0/sale-workflow-16-0-sale_planner_calendar
|
||||
: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/sale-workflow&target_branch=16.0
|
||||
:alt: Try me on Runboat
|
||||
|
||||
|badge1| |badge2| |badge3| |badge4| |badge5|
|
||||
|
||||
This module allows to manage commercial visits to partners by using recurrence events.
|
||||
|
||||
**Table of contents**
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
You can find the following configurations in the settings:
|
||||
|
||||
#. **Subscriptions Backward Days**: Backward days to search documents to update
|
||||
subscriptions.
|
||||
|
||||
#. **Sale Planner Forward Months**: Forward months to create calendar events.
|
||||
|
||||
#. **Send invitation to attendees**: Send invitations to attendees when a planner event
|
||||
is created.
|
||||
|
||||
#. **Sale planner order cut hour**: Time of the next day until which orders of the
|
||||
current day are assigned.
|
||||
|
||||
#. **Calendar event max duration**: Show a warning message when duration is more than
|
||||
this time. Set 00:00 to disable warning.
|
||||
|
||||
Other setting are available with system parameters
|
||||
|
||||
#. **Sale order partner** when a so is created from a event planned. You can create or
|
||||
update the system parameter **sale_planner_calendar.create_so_to_commercial_partner**
|
||||
with True value to create the sale order to commercial partner instead of partner
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
You can create now the recurrent events directly from the partners by clicking next
|
||||
smart button:
|
||||
|
||||
.. image:: https://raw.githubusercontent.com/OCA/sale-workflow/16.0/sale_planner_calendar/static/img/smart_button.png
|
||||
|
||||
By default the end of the recurrence is set by the settings field
|
||||
**Sale Planner Forward Months**.
|
||||
|
||||
You can manage this new recurrent events from *Calendar planner* menu entry.
|
||||
|
||||
.. image:: https://raw.githubusercontent.com/OCA/sale-workflow/16.0/sale_planner_calendar/static/img/menu_entry.png
|
||||
|
||||
On the first window, you will find a summary of the events that the user has to do
|
||||
today.
|
||||
|
||||
By going to *Calendar planner > Calendar events* you will have two options:
|
||||
|
||||
#. View the calendar events related to your user (*My Calendar*).
|
||||
|
||||
#. View your base events related to the recurrences (*Recurrent calendar events*).
|
||||
|
||||
Finally on *Calendar planner > Wizards* you will have again two options:
|
||||
|
||||
#. You can change the hour of the start of all the events related to a recurrency by using
|
||||
*Sale planner calendar wizard*.
|
||||
|
||||
#. You can change the salesperson assigned to events related to a period of time by using
|
||||
the wizard *Reassignment of salesperson*.
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `GitHub Issues <https://github.com/OCA/sale-workflow/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/sale-workflow/issues/new?body=module:%20sale_planner_calendar%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
|
||||
~~~~~~~
|
||||
|
||||
* Tecnativa
|
||||
|
||||
Contributors
|
||||
~~~~~~~~~~~~
|
||||
|
||||
* `Tecnativa <https://www.tecnativa.com>`__:
|
||||
|
||||
* Sergio Teruel
|
||||
* Carlos Dauden
|
||||
* Carlos Roca
|
||||
* Pilar Vargas
|
||||
|
||||
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/sale-workflow <https://github.com/OCA/sale-workflow/tree/16.0/sale_planner_calendar>`_ project on GitHub.
|
||||
|
||||
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
from . import models
|
||||
from . import wizard
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
# Copyright 2020 Tecnativa - Sergio Teruel
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
{
|
||||
"name": "Sale planner calendar",
|
||||
"summary": "Sale planner calendar",
|
||||
"version": "16.0.3.0.0",
|
||||
"development_status": "Beta",
|
||||
"category": "Sale",
|
||||
"website": "https://github.com/OCA/sale-workflow",
|
||||
"author": "Tecnativa, Odoo Community Association (OCA)",
|
||||
"license": "AGPL-3",
|
||||
"application": False,
|
||||
"installable": True,
|
||||
"depends": ["calendar", "sale", "sale_payment_sheet"],
|
||||
"data": [
|
||||
"security/ir.model.access.csv",
|
||||
"security/sale_planner_calendar_security.xml",
|
||||
"data/sale_planner_calendar_action_server.xml",
|
||||
"data/sale_planner_calendar_cron.xml",
|
||||
"data/sale_planner_calendar_data.xml",
|
||||
"views/calendar_event_type_view.xml",
|
||||
"views/calendar_view.xml",
|
||||
"views/res_config_settings_views.xml",
|
||||
"views/res_partner_view.xml",
|
||||
"views/sale_planner_calendar_event_view.xml",
|
||||
"views/sale_planner_calendar_issue_type_view.xml",
|
||||
"views/sale_planner_calendar_summary_view.xml",
|
||||
"wizard/sale_planner_calendar_reassign.xml",
|
||||
"wizard/sale_planner_calendar_wizard.xml",
|
||||
# Menu position fixed
|
||||
"views/sale_planner_calendar_menu.xml",
|
||||
],
|
||||
"assets": {
|
||||
"web.assets_backend": [
|
||||
"sale_planner_calendar/static/src/xml/categ_icons_widget_template.xml",
|
||||
"sale_planner_calendar/static/src/xml/sale_planner_calendar_event_sales.xml",
|
||||
"sale_planner_calendar/static/src/xml/activity_menu_view.xml",
|
||||
"sale_planner_calendar/static/src/scss/sale_planner_calendar.scss",
|
||||
"sale_planner_calendar/static/src/js/*.js",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo noupdate="1">
|
||||
<!--
|
||||
If wizard record is NewId the changes in calendar_event_ids are not saved.
|
||||
To fix this, we create the wizard first and open later.
|
||||
-->
|
||||
<record id="sale_planner_calendar_wizard_action" model="ir.actions.server">
|
||||
<field name="name">Sale planner calendar wizard</field>
|
||||
<field
|
||||
name="model_id"
|
||||
ref="sale_planner_calendar.model_sale_planner_calendar_wizard"
|
||||
/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
wiz = model.create({})
|
||||
action = wiz.get_formview_action()
|
||||
action['context']['form_view_initial_mode'] = 'edit'
|
||||
action['context']['control_panel_hidden'] = True
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_set_planner_calendar_event" model="ir.actions.server">
|
||||
<field name="name">Set planner calendar event</field>
|
||||
<field name="model_id" ref="sale.model_sale_order" />
|
||||
<field name="binding_model_id" ref="sale.model_sale_order" />
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
records.action_set_planner_calendar_event()
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo noupdate="1">
|
||||
<record
|
||||
forcecreate="True"
|
||||
id="sale_planner_calendar_cron_unsubscribe"
|
||||
model="ir.cron"
|
||||
>
|
||||
<field name="name">Sale planner calendar: Unsubscribe documents</field>
|
||||
<field name="model_id" ref="model_sale_planner_calendar_reassign_wiz" />
|
||||
<field name="state">code</field>
|
||||
<field name="code">model.cron_unsubscribe()</field>
|
||||
<field name="user_id" ref="base.user_root" />
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="active" eval="True" />
|
||||
<field name="doall" eval="False" />
|
||||
</record>
|
||||
<record forcecreate="True" id="cron_update_dynamic_final_date" model="ir.cron">
|
||||
<field
|
||||
name="name"
|
||||
>Sale planner calendar: Update dynamic event final date</field>
|
||||
<field name="model_id" ref="model_calendar_event" />
|
||||
<field name="state">code</field>
|
||||
<field name="code">model.cron_update_dynamic_final_date()</field>
|
||||
<field name="user_id" ref="base.user_root" />
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">months</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="active" eval="True" />
|
||||
<field name="doall" eval="False" />
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo noupdate="1">
|
||||
<record id="event_type_commercial_visit" model="calendar.event.type">
|
||||
<field name="name">Commercial visit</field>
|
||||
<field name="duration">0.25</field>
|
||||
<field name="icon">fa-car</field>
|
||||
</record>
|
||||
<record id="event_type_remote_selling" model="calendar.event.type">
|
||||
<field name="name">Remote selling</field>
|
||||
<field name="duration">0.25</field>
|
||||
<field name="icon">fa-phone</field>
|
||||
</record>
|
||||
<record id="event_type_delivery" model="calendar.event.type">
|
||||
<field name="name">Delivery</field>
|
||||
<field name="duration">0.25</field>
|
||||
<field name="icon">fa-truck</field>
|
||||
</record>
|
||||
<record id="event_type_message" model="calendar.event.type">
|
||||
<field name="name">Message</field>
|
||||
<field name="duration">0.25</field>
|
||||
<field name="icon">fa-envelope</field>
|
||||
</record>
|
||||
|
||||
<!-- Event planner issue types -->
|
||||
<record
|
||||
id="event_planner_issue_type_missing"
|
||||
model="sale.planner.calendar.issue.type"
|
||||
>
|
||||
<field name="name">Missing</field>
|
||||
</record>
|
||||
<record
|
||||
id="event_planner_issue_type_close"
|
||||
model="sale.planner.calendar.issue.type"
|
||||
>
|
||||
<field name="name">Closed</field>
|
||||
</record>
|
||||
|
||||
<record id="config_parameter_partner_name_field" model="ir.config_parameter">
|
||||
<field name="key">sale_planner_calendar.partner_name_field</field>
|
||||
<field name="value">name</field>
|
||||
</record>
|
||||
<record id="config_parameter_max_duration" model="ir.config_parameter">
|
||||
<field name="key">sale_planner_calendar.max_duration</field>
|
||||
<field name="value">4.0</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
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,110 @@
|
|||
# Copyright 2024 Tecnativa - Sergio Teruel
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
from openupgradelib import openupgrade
|
||||
from psycopg2.extensions import AsIs
|
||||
|
||||
|
||||
def _sale_planner_calendar_event_to_calendar_event(env):
|
||||
"""Move all data from sale_planner_calendar_event to code model calendar_event"""
|
||||
sql = """
|
||||
UPDATE calendar_event SET
|
||||
sale_planner_state = spce.state,
|
||||
calendar_issue_type_id = spce.calendar_issue_type_id,
|
||||
calendar_event_profile_id = spce.calendar_event_profile_id,
|
||||
comment = spce.comment,
|
||||
calendar_summary_id = spce.calendar_summary_id,
|
||||
off_planning = spce.off_planning
|
||||
FROM sale_planner_calendar_event spce
|
||||
WHERE spce.calendar_event_id = calendar_event.id
|
||||
"""
|
||||
openupgrade.logged_query(env.cr, sql)
|
||||
|
||||
|
||||
def _sale_order_to_calendar_event(env):
|
||||
"""Link sale order to calendar events instead of sale planner calendar event model"""
|
||||
# drop_constraint(env.cr, 'sale_order', 'sale_order_sale_planner_calendar_event_id_fkey')
|
||||
openupgrade.logged_query(
|
||||
env.cr,
|
||||
"""
|
||||
UPDATE sale_order SET
|
||||
sale_planner_calendar_event_id = spce.calendar_event_id
|
||||
FROM sale_planner_calendar_event spce
|
||||
WHERE sale_order.%(old_column)s = spce.id
|
||||
""",
|
||||
{
|
||||
"old_column": AsIs(
|
||||
openupgrade.get_legacy_name("sale_planner_calendar_event_id")
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _payment_sheet_to_calendar_event(env):
|
||||
"""Link sale payment sheet to calendar events instead of sale planner calendar
|
||||
event model
|
||||
"""
|
||||
# drop_constraint(env.cr, 'sale_payment_sheet_line',
|
||||
# 'sale_payment_sheet_line_sale_planner_calendar_event_id_fkey')
|
||||
openupgrade.logged_query(
|
||||
env.cr,
|
||||
"""
|
||||
UPDATE sale_payment_sheet_line SET
|
||||
sale_planner_calendar_event_id = spce.calendar_event_id
|
||||
FROM sale_planner_calendar_event spce
|
||||
WHERE sale_payment_sheet_line.%(old_column)s = spce.id
|
||||
""",
|
||||
{
|
||||
"old_column": AsIs(
|
||||
openupgrade.get_legacy_name("sale_planner_calendar_event_id")
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _profiles_to_calendar_event_type(env):
|
||||
openupgrade.logged_query(
|
||||
env.cr,
|
||||
"""
|
||||
INSERT INTO calendar_event_type (name, icon, old_sale_planner_profile_id)
|
||||
SELECT name, icon, id FROM sale_planner_calendar_event_profile
|
||||
""",
|
||||
)
|
||||
|
||||
# Update event linked to profiles
|
||||
openupgrade.logged_query(
|
||||
env.cr,
|
||||
"""
|
||||
INSERT INTO meeting_category_rel (event_id, type_id)
|
||||
SELECT ce.id, cet.id
|
||||
FROM calendar_event ce
|
||||
JOIN calendar_event_type cet
|
||||
ON cet.old_sale_planner_profile_id = ce.calendar_event_profile_id
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
def _remove_renamed_selection_values(env):
|
||||
"""After rename value from upper to lower the values are duplicated.
|
||||
This method removes old upper values"""
|
||||
week_list_field = env.ref(
|
||||
"sale_planner_calendar.field_sale_planner_calendar_wizard__week_list"
|
||||
)
|
||||
value_ids = week_list_field.selection_ids.filtered(lambda x: x.value.isupper()).ids
|
||||
if value_ids:
|
||||
openupgrade.logged_query(
|
||||
env.cr,
|
||||
"""
|
||||
DELETE FROM ir_model_fields_selection
|
||||
WHERE id in %(selection_value_ids)s
|
||||
""",
|
||||
{"selection_value_ids": tuple(value_ids)},
|
||||
)
|
||||
|
||||
|
||||
@openupgrade.migrate()
|
||||
def migrate(env, version):
|
||||
_sale_planner_calendar_event_to_calendar_event(env)
|
||||
_sale_order_to_calendar_event(env)
|
||||
_payment_sheet_to_calendar_event(env)
|
||||
_profiles_to_calendar_event_type(env)
|
||||
_remove_renamed_selection_values(env)
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
# Copyright 2024 Tecnativa - Sergio Teruel
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
from openupgradelib import openupgrade
|
||||
|
||||
_column_renames = {
|
||||
"sale_order": [
|
||||
("sale_planner_calendar_event_id", None),
|
||||
],
|
||||
"sale_payment_sheet_line": [("sale_planner_calendar_event_id", None)],
|
||||
}
|
||||
|
||||
|
||||
def _remove_selection_field_values(env):
|
||||
sql = """
|
||||
DELETE FROM ir_model_fields_selection
|
||||
WHERE field_id IN
|
||||
(SELECT id
|
||||
FROM ir_model_fields
|
||||
WHERE ttype='selection' AND model='sale.planner.calendar.event')
|
||||
"""
|
||||
openupgrade.logged_query(env.cr, sql)
|
||||
|
||||
|
||||
def _add_event_profile_helper_column(env):
|
||||
openupgrade.logged_query(
|
||||
env.cr,
|
||||
"""
|
||||
ALTER TABLE calendar_event_type
|
||||
ADD COLUMN old_sale_planner_profile_id integer""",
|
||||
)
|
||||
|
||||
|
||||
@openupgrade.migrate()
|
||||
def migrate(env, version):
|
||||
openupgrade.rename_columns(env.cr, _column_renames)
|
||||
_remove_selection_field_values(env)
|
||||
_add_event_profile_helper_column(env)
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
from . import calendar_event
|
||||
from . import calendar_event_type
|
||||
from . import calendar_attendee
|
||||
from . import res_config_settings
|
||||
from . import res_partner
|
||||
from . import res_users
|
||||
from . import sale_order
|
||||
from . import sale_payment_sheet
|
||||
from . import sale_planner_calendar_issue_type
|
||||
from . import sale_planner_calendar_summary
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
# Copyright 2024 Tecnativa - Sergio Teruel
|
||||
# Copyright 2024 Tecnativa - Carlos Dauden
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
|
||||
class CalendarAttendee(models.Model):
|
||||
_inherit = "calendar.attendee"
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
if not self.env.company.sale_planner_mail_to_attendees:
|
||||
new_vals_list = []
|
||||
for vals in vals_list:
|
||||
event = self.env["calendar.event"].browse(vals["event_id"])
|
||||
if not event.target_partner_id:
|
||||
new_vals_list.append(vals)
|
||||
vals_list = new_vals_list
|
||||
return super().create(vals_list)
|
||||
|
|
@ -0,0 +1,435 @@
|
|||
# Copyright 2021-2024 Tecnativa - Sergio Teruel
|
||||
# Copyright 2021-2024 Tecnativa - Carlos Dauden
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
import re
|
||||
from datetime import timedelta
|
||||
|
||||
import pytz
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
|
||||
|
||||
class CalendarEvent(models.Model):
|
||||
_inherit = "calendar.event"
|
||||
|
||||
target_partner_id = fields.Many2one(
|
||||
comodel_name="res.partner",
|
||||
string="Sale planner partner",
|
||||
help="Is the partner used in planner",
|
||||
)
|
||||
hour = fields.Float(compute="_compute_hour", inverse="_inverse_hour")
|
||||
target_partner_mobile = fields.Char(related="target_partner_id.mobile")
|
||||
# When arrive this date we will unsubscribe user from partner documents
|
||||
unsubscribe_date = fields.Date()
|
||||
is_dynamic_end_date = fields.Boolean(copy=False)
|
||||
# dynamic_end_date = fields.Date(compute="_compute_dynamic_end_date")
|
||||
advanced_cycle = fields.Boolean()
|
||||
cycle_number = fields.Integer()
|
||||
cycle_skip = fields.Integer()
|
||||
# Field to know which event creates the recurrence
|
||||
is_base_recurrent_event = fields.Boolean(
|
||||
compute="_compute_is_base_recurrent_event", store=True
|
||||
)
|
||||
sale_planner_currency_id = fields.Many2one(
|
||||
comodel_name="res.currency",
|
||||
related="target_partner_id.currency_id",
|
||||
)
|
||||
sale_ids = fields.One2many(
|
||||
comodel_name="sale.order",
|
||||
inverse_name="sale_planner_calendar_event_id",
|
||||
)
|
||||
sale_planner_state = fields.Selection(
|
||||
[
|
||||
("pending", "Pending"),
|
||||
("done", "Done"),
|
||||
("cancel", "Cancelled"),
|
||||
],
|
||||
default="pending",
|
||||
readonly=True,
|
||||
tracking=True,
|
||||
)
|
||||
calendar_issue_type_id = fields.Many2one(
|
||||
comodel_name="sale.planner.calendar.issue.type", ondelete="restrict"
|
||||
)
|
||||
comment = fields.Text()
|
||||
sale_order_subtotal = fields.Monetary(
|
||||
compute="_compute_sale_order_subtotal",
|
||||
currency_field="sale_planner_currency_id",
|
||||
)
|
||||
calendar_summary_id = fields.Many2one(
|
||||
comodel_name="sale.planner.calendar.summary",
|
||||
copy=False,
|
||||
)
|
||||
invoice_amount_residual = fields.Monetary(
|
||||
string="Invoice amount due",
|
||||
compute="_compute_invoice_amount_residual",
|
||||
compute_sudo=True,
|
||||
currency_field="sale_planner_currency_id",
|
||||
)
|
||||
off_planning = fields.Boolean(copy=False)
|
||||
payment_sheet_line_ids = fields.One2many(
|
||||
comodel_name="sale.payment.sheet.line",
|
||||
inverse_name="sale_planner_calendar_event_id",
|
||||
)
|
||||
# Helper fields for kanban views
|
||||
partner_ref = fields.Char(related="target_partner_id.ref")
|
||||
partner_name = fields.Char(compute="_compute_partner_name")
|
||||
partner_commercial_name = fields.Char(
|
||||
string="Commercial partner name",
|
||||
related="target_partner_id.commercial_partner_id.name",
|
||||
)
|
||||
partner_street = fields.Char(related="target_partner_id.street")
|
||||
partner_mobile = fields.Char(compute="_compute_contact")
|
||||
partner_contact_name = fields.Char(compute="_compute_contact")
|
||||
partner_city = fields.Char(related="target_partner_id.city")
|
||||
partner_user_id = fields.Many2one(
|
||||
related="target_partner_id.user_id", string="Partner salesperson"
|
||||
)
|
||||
sanitized_partner_mobile = fields.Char(compute="_compute_sanitized_partner_mobile")
|
||||
location_url = fields.Char(compute="_compute_location_url")
|
||||
categ_icons = fields.Char(compute="_compute_categ_icons")
|
||||
# Adding code \uFE0E to force monochrome emoji. Not supported with 🙁 and 🙂 emojis
|
||||
sale_planner_rating = fields.Selection(
|
||||
[
|
||||
("1", "😞\uFE0E"),
|
||||
("3", "😐\uFE0E"),
|
||||
("5", "😊\uFE0E"),
|
||||
],
|
||||
)
|
||||
|
||||
@api.depends("recurrence_id", "recurrence_id.calendar_event_ids")
|
||||
def _compute_is_base_recurrent_event(self):
|
||||
for record in self:
|
||||
record.is_base_recurrent_event = (
|
||||
record == record.recurrence_id.calendar_event_ids.sorted("start")[:1]
|
||||
)
|
||||
|
||||
@api.depends("start")
|
||||
def _compute_hour(self):
|
||||
for rec in self:
|
||||
date = rec._get_hour_tz_offset()
|
||||
rec.hour = date.hour + date.minute / 60
|
||||
|
||||
@api.depends("sale_ids.amount_untaxed")
|
||||
def _compute_sale_order_subtotal(self):
|
||||
for rec in self:
|
||||
rec.sale_order_subtotal = sum(rec.mapped("sale_ids.amount_untaxed"))
|
||||
|
||||
@api.depends("target_partner_id")
|
||||
def _compute_invoice_amount_residual(self):
|
||||
partner_ids = self.mapped("target_partner_id.commercial_partner_id").ids
|
||||
groups = self.env["account.move"]._read_group(
|
||||
domain=[
|
||||
("state", "=", "posted"),
|
||||
("payment_state", "!=", "paid"),
|
||||
("partner_id", "in", partner_ids),
|
||||
],
|
||||
fields=["amount_residual_signed"],
|
||||
groupby=["partner_id"],
|
||||
)
|
||||
invoice_dic = {g["partner_id"][0]: g["amount_residual_signed"] for g in groups}
|
||||
for rec in self:
|
||||
amount_residual = invoice_dic.get(
|
||||
rec.target_partner_id.commercial_partner_id.id, 0.0
|
||||
)
|
||||
payment_amount = sum(
|
||||
rec.payment_sheet_line_ids.filtered(
|
||||
lambda p: p.sheet_id.state == "open"
|
||||
).mapped("amount")
|
||||
)
|
||||
rec.invoice_amount_residual = amount_residual - payment_amount
|
||||
|
||||
@api.depends("target_partner_id")
|
||||
def _compute_partner_name(self):
|
||||
field_name = (
|
||||
self.env["ir.config_parameter"]
|
||||
.sudo()
|
||||
.get_param(
|
||||
"sale_planner_calendar.partner_name_field",
|
||||
default="name",
|
||||
)
|
||||
)
|
||||
for event in self:
|
||||
# If more flexibility is needed use event.mapped(field_name)[0]
|
||||
event.partner_name = (
|
||||
event.target_partner_id[field_name]
|
||||
or event.target_partner_id.name
|
||||
or event.target_partner_id.commercial_partner_id.name
|
||||
)
|
||||
|
||||
@api.depends("target_partner_id")
|
||||
def _compute_contact(self):
|
||||
for rec in self:
|
||||
contact = rec.target_partner_id.child_ids.filtered(
|
||||
"is_sale_planner_contact"
|
||||
)[:1]
|
||||
rec.partner_mobile = (contact.mobile or contact.phone) or (
|
||||
rec.target_partner_id.mobile or rec.target_partner_id.phone
|
||||
)
|
||||
rec.partner_contact_name = contact.name
|
||||
|
||||
@api.depends("partner_mobile")
|
||||
def _compute_sanitized_partner_mobile(self):
|
||||
self.sanitized_partner_mobile = False
|
||||
for rec in self.filtered("partner_mobile"):
|
||||
rec.sanitized_partner_mobile = re.sub(r"\W+", "", rec.partner_mobile)
|
||||
|
||||
@api.depends("target_partner_id")
|
||||
def _compute_location_url(self):
|
||||
# The url is built to access the location from a google link. This will be done
|
||||
# taking into account the location of the calendar event associated with the calendar
|
||||
# planner event. If this location is not defined, the client's coordinates will
|
||||
# be taken into account if they are defined, otherwise the client's address
|
||||
# will be taken into account.
|
||||
self.location_url = False
|
||||
for event in self:
|
||||
event_location = event.location
|
||||
partner_latitude = str(event.target_partner_id.partner_latitude).replace(
|
||||
",", "."
|
||||
)
|
||||
partner_longitude = str(event.target_partner_id.partner_longitude).replace(
|
||||
",", "."
|
||||
)
|
||||
partner_location = f"{event.partner_city}+{event.partner_street}"
|
||||
if event_location:
|
||||
event.location_url = event_location.replace(" ", "+")
|
||||
elif partner_latitude != "0.0" or partner_longitude != "0.0":
|
||||
event.location_url = f"{partner_latitude}%2C{partner_longitude}"
|
||||
elif partner_location:
|
||||
event.location_url = partner_location.replace(" ", "+")
|
||||
|
||||
@api.depends("categ_ids")
|
||||
def _compute_categ_icons(self):
|
||||
for event in self:
|
||||
categ_icons_list = []
|
||||
for categ in event.categ_ids.filtered("icon"):
|
||||
# Avoid repeat same icon
|
||||
if categ.icon not in categ_icons_list:
|
||||
categ_icons_list.append(categ.icon)
|
||||
event.categ_icons = ",".join(categ_icons_list)
|
||||
|
||||
def _inverse_hour(self):
|
||||
for rec in self:
|
||||
duration = rec.duration
|
||||
date = self._get_hour_tz_offset()
|
||||
new_time = date.replace(
|
||||
hour=int(rec.hour), minute=int(round((rec.hour % 1) * 60))
|
||||
)
|
||||
# Force to onchange get correct value
|
||||
new_time = new_time.astimezone(pytz.utc).replace(tzinfo=None)
|
||||
rec.write(
|
||||
{
|
||||
"recurrence_update": "all_events",
|
||||
"start": new_time,
|
||||
"stop": new_time + timedelta(minutes=round((duration or 0.5) * 60)),
|
||||
}
|
||||
)
|
||||
|
||||
@api.onchange("start", "stop", "duration")
|
||||
def _onchange_duration(self):
|
||||
"""Show warning if duration more than max duration set in config."""
|
||||
if not self.target_partner_id:
|
||||
return
|
||||
max_duration = float(
|
||||
self.env["ir.config_parameter"]
|
||||
.sudo()
|
||||
.get_param(
|
||||
"sale_planner_calendar.max_duration",
|
||||
default="0.0",
|
||||
)
|
||||
)
|
||||
if max_duration and self.duration > max_duration:
|
||||
return {
|
||||
"warning": {
|
||||
"title": "Max duration exceeded",
|
||||
"message": "Max duration set in config parameters is {} hours".format(
|
||||
max_duration
|
||||
),
|
||||
"type": "notification",
|
||||
}
|
||||
}
|
||||
|
||||
@api.onchange("categ_ids")
|
||||
def _onchange_categ_ids(self):
|
||||
if not self.name and self.categ_ids:
|
||||
self.name = self.categ_ids[:1].name
|
||||
# Clean name if is equal to stored categ name and this categ is removed
|
||||
elif not self.categ_ids and self.name == self._origin.categ_ids[:1].name:
|
||||
self.name = False
|
||||
|
||||
def action_open_sale_order(self, new_order=False):
|
||||
"""
|
||||
Search or Create an event planner linked to sale order
|
||||
"""
|
||||
action_xml_id = (
|
||||
self.env["ir.config_parameter"]
|
||||
.sudo()
|
||||
.get_param(
|
||||
"sale_planner_calendar.action_open_sale_order",
|
||||
"sale.action_quotations_with_onboarding",
|
||||
)
|
||||
)
|
||||
action = self.env["ir.actions.act_window"]._for_xml_id(action_xml_id)
|
||||
if new_order:
|
||||
action["name"] = "New Quotation"
|
||||
action["context"] = self.env.context
|
||||
return action
|
||||
# Create sale order to planner partner or commercial partner depending of the
|
||||
# system parameter
|
||||
create_so_to_commercial_partner = (
|
||||
self.env["ir.config_parameter"]
|
||||
.sudo()
|
||||
.get_param("sale_planner_calendar.create_so_to_commercial_partner", "False")
|
||||
)
|
||||
partner = (
|
||||
self.target_partner_id
|
||||
if create_so_to_commercial_partner == "False"
|
||||
else self.target_partner_id.commercial_partner_id
|
||||
)
|
||||
action["context"] = {
|
||||
"default_sale_planner_calendar_event_id": self.id,
|
||||
"default_partner_id": partner.id,
|
||||
"default_partner_shipping_id": self.target_partner_id.id,
|
||||
"default_user_id": self.user_id.id,
|
||||
}
|
||||
if len(self.sale_ids) > 1:
|
||||
action["domain"] = [("sale_planner_calendar_event_id", "=", self.id)]
|
||||
else:
|
||||
action["views"] = list(filter(lambda v: v[1] == "form", action["views"]))
|
||||
action["res_id"] = self.sale_ids.id
|
||||
return action
|
||||
|
||||
def action_open_invoices(self):
|
||||
action = self.env["ir.actions.act_window"]._for_xml_id(
|
||||
"sale_payment_sheet.action_invoice_sale_payment_sheet"
|
||||
)
|
||||
ctx = safe_eval(action["context"])
|
||||
ctx.update(
|
||||
{
|
||||
"default_partner_id": self.target_partner_id.id,
|
||||
}
|
||||
)
|
||||
action["context"] = ctx
|
||||
domain = safe_eval(action["domain"])
|
||||
domain.append(
|
||||
("partner_id", "=", self.target_partner_id.commercial_partner_id.id),
|
||||
)
|
||||
action["domain"] = domain
|
||||
return action
|
||||
|
||||
def action_open_unpaid_invoice(self):
|
||||
domain = [
|
||||
("state", "=", "posted"),
|
||||
("move_type", "in", ["out_invoice", "out_refund"]),
|
||||
("partner_id", "=", self.target_partner_id.commercial_partner_id.id),
|
||||
("payment_state", "!=", "paid"),
|
||||
]
|
||||
unpaid_invoices = self.env["account.move"].search(domain)
|
||||
action = self.env["ir.actions.act_window"]._for_xml_id(
|
||||
"sale_payment_sheet.action_sale_invoice_payment_wiz"
|
||||
)
|
||||
ctx = safe_eval(action["context"])
|
||||
ctx.update(
|
||||
{
|
||||
"invoice_ids": unpaid_invoices.ids,
|
||||
"default_sale_planner_calendar_event_id": self.id,
|
||||
"default_partner_id": self.target_partner_id.id,
|
||||
}
|
||||
)
|
||||
action["context"] = ctx
|
||||
return action
|
||||
|
||||
def action_done(self):
|
||||
self.write(
|
||||
{
|
||||
"sale_planner_state": "done",
|
||||
}
|
||||
)
|
||||
|
||||
def action_cancel(self):
|
||||
self.write(
|
||||
{
|
||||
"sale_planner_state": "cancel",
|
||||
"comment": "Not done",
|
||||
}
|
||||
)
|
||||
|
||||
def action_pending(self):
|
||||
self.write(
|
||||
{
|
||||
"sale_planner_state": "pending",
|
||||
"comment": False,
|
||||
"sale_planner_rating": False,
|
||||
}
|
||||
)
|
||||
|
||||
def action_open_issue(self):
|
||||
action = self.env["ir.actions.act_window"]._for_xml_id(
|
||||
"sale_planner_calendar.action_sale_planner_calendar_issue"
|
||||
)
|
||||
action["res_id"] = self.id
|
||||
return action
|
||||
|
||||
def action_apply_issue(self):
|
||||
pass
|
||||
|
||||
def action_open_rating(self):
|
||||
action = self.env["ir.actions.act_window"]._for_xml_id(
|
||||
"sale_planner_calendar.action_sale_planner_calendar_rating"
|
||||
)
|
||||
action["res_id"] = self.id
|
||||
return action
|
||||
|
||||
def action_set_sale_planner_rating(self):
|
||||
self.action_done()
|
||||
|
||||
def _get_hour_tz_offset(self):
|
||||
timezone = self._context.get("tz") or self.env.user.partner_id.tz or "UTC"
|
||||
self_tz = self.with_context(tz=timezone)
|
||||
date = fields.Datetime.context_timestamp(self_tz, self.start)
|
||||
return date
|
||||
|
||||
def get_week_days_count(self):
|
||||
days = ["mo", "tu", "we", "th", "fr", "sa"]
|
||||
days_count = 0
|
||||
for day in days:
|
||||
if self[day]:
|
||||
days_count += 1
|
||||
return days_count
|
||||
|
||||
def _get_recurrent_dates_by_event(self):
|
||||
dates_list = super()._get_recurrent_dates_by_event()
|
||||
if not self.advanced_cycle:
|
||||
return dates_list
|
||||
new_dates = []
|
||||
index = 0
|
||||
skip_count = 0
|
||||
|
||||
cycle_number = self.cycle_number
|
||||
cycle_skip = self.cycle_skip
|
||||
if self.rrule_type == "weekly":
|
||||
days_count = self.get_week_days_count()
|
||||
cycle_number *= days_count
|
||||
cycle_skip *= days_count
|
||||
for dates in dates_list:
|
||||
if index < cycle_number:
|
||||
new_dates.append(dates)
|
||||
index += 1
|
||||
else:
|
||||
skip_count += 1
|
||||
if skip_count >= cycle_skip:
|
||||
index = 0
|
||||
skip_count = 0
|
||||
return new_dates
|
||||
|
||||
@api.model
|
||||
def cron_update_dynamic_final_date(self):
|
||||
events_to_update = self.search(
|
||||
[("is_dynamic_end_date", "=", True), ("is_base_recurrent_event", "=", True)]
|
||||
)
|
||||
new_date = fields.Date.today() + relativedelta(
|
||||
months=self.env.company.sale_planner_forward_months, day=31
|
||||
)
|
||||
events_to_update.until = new_date
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
# Copyright 2021-2024 Tecnativa - Sergio Teruel
|
||||
# Copyright 2021-2024 Tecnativa - Carlos Dauden
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class CalendarEventType(models.Model):
|
||||
_inherit = "calendar.event.type"
|
||||
|
||||
duration = fields.Float()
|
||||
icon = fields.Char(help="Font awesome icon e.g. fa-tasks")
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
# Copyright 2020 Sergio Teruel - Tecnativa
|
||||
# Copyright 2021 Tecnativa - Carlos Dauden
|
||||
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = "res.config.settings"
|
||||
|
||||
susbscriptions_backward_days = fields.Integer(
|
||||
related="company_id.susbscriptions_backward_days",
|
||||
readonly=False,
|
||||
)
|
||||
sale_planner_forward_months = fields.Integer(
|
||||
related="company_id.sale_planner_forward_months",
|
||||
readonly=False,
|
||||
)
|
||||
sale_planner_mail_to_attendees = fields.Boolean(
|
||||
related="company_id.sale_planner_mail_to_attendees",
|
||||
readonly=False,
|
||||
)
|
||||
sale_planner_order_cut_hour = fields.Float(
|
||||
related="company_id.sale_planner_order_cut_hour",
|
||||
readonly=False,
|
||||
)
|
||||
sale_planner_calendar_max_duration = fields.Float(
|
||||
string="Calendar event max duration",
|
||||
config_parameter="sale_planner_calendar.max_duration",
|
||||
)
|
||||
|
||||
|
||||
class ResCompany(models.Model):
|
||||
_inherit = "res.company"
|
||||
|
||||
susbscriptions_backward_days = fields.Integer(
|
||||
default=180,
|
||||
)
|
||||
sale_planner_forward_months = fields.Integer(
|
||||
default=12,
|
||||
)
|
||||
sale_planner_mail_to_attendees = fields.Boolean(
|
||||
string="Send invitation to attendees", default=True
|
||||
)
|
||||
sale_planner_order_cut_hour = fields.Float()
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
# Copyright 2021 Tecnativa - Sergio Teruel
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
from datetime import timedelta
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import _, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = "res.partner"
|
||||
|
||||
is_sale_planner_contact = fields.Boolean()
|
||||
|
||||
def action_calendar_planner(self):
|
||||
categ = self.env.ref("sale_planner_calendar.event_type_commercial_visit")
|
||||
action = self.env["ir.actions.act_window"]._for_xml_id(
|
||||
"calendar.action_calendar_event"
|
||||
)
|
||||
sale_planner_forward_months = self.env.company.sale_planner_forward_months
|
||||
# TODO: Get default values from res.config.settings
|
||||
action["context"] = {
|
||||
"default_target_partner_id": self.id,
|
||||
"default_categ_ids": [(4, categ.id)],
|
||||
# Passing True omits the partner name, ensuring precise calculation of GPS location.
|
||||
"default_location": self._display_address(True).replace("\n", " "),
|
||||
"default_duration": categ.duration,
|
||||
"default_name": categ.name,
|
||||
"default_start": fields.Datetime.now(),
|
||||
"default_stop": fields.Datetime.now()
|
||||
+ timedelta(minutes=round((categ.duration or 1.0) * 60)),
|
||||
"default_recurrency": True,
|
||||
"default_rrule_type": "weekly",
|
||||
"default_end_type": "end_date",
|
||||
"default_until": fields.Date.today()
|
||||
+ relativedelta(months=sale_planner_forward_months),
|
||||
"default_is_dynamic_end_date": True,
|
||||
"default_user_id": self.user_id.id or self.env.user.id,
|
||||
"default_partner_ids": [
|
||||
(
|
||||
6,
|
||||
0,
|
||||
[
|
||||
self.id,
|
||||
self.user_id.partner_id.id or self.env.user.partner_id.id,
|
||||
],
|
||||
)
|
||||
],
|
||||
"choose_unlink_method": True,
|
||||
}
|
||||
if not self.env.company.sale_planner_mail_to_attendees:
|
||||
action["context"].update(
|
||||
{
|
||||
"no_mail_to_attendees": True,
|
||||
"dont_notify": True,
|
||||
}
|
||||
)
|
||||
action["view_mode"] = "tree,form"
|
||||
action["view_id"] = False
|
||||
action["views"] = []
|
||||
action["domain"] = [
|
||||
("target_partner_id", "=", self.id),
|
||||
("recurrency", "=", True),
|
||||
("recurrence_id.until", ">", fields.Date.today()),
|
||||
("is_base_recurrent_event", "=", True),
|
||||
]
|
||||
return action
|
||||
|
||||
def write(self, vals):
|
||||
if (
|
||||
"user_id" in vals
|
||||
and vals.get("user_id")
|
||||
and not self.env.context.get("skip_sale_planner_check", False)
|
||||
):
|
||||
calendar_events = self.env["calendar.event"].search(
|
||||
[
|
||||
("target_partner_id", "in", self.ids),
|
||||
("recurrency", "!=", False),
|
||||
("user_id", "!=", vals["user_id"]),
|
||||
("is_base_recurrent_event", "=", True),
|
||||
]
|
||||
)
|
||||
if calendar_events:
|
||||
msg = _(
|
||||
"This partner has sale planned events\n"
|
||||
"You must change salesperson from the planner wizard"
|
||||
)
|
||||
raise ValidationError(msg)
|
||||
return super().write(vals)
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
# Copyright 2024 Tecnativa - Carlos Roca
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
import pytz
|
||||
|
||||
from odoo import _, api, fields, models, modules
|
||||
|
||||
|
||||
class ResUsers(models.Model):
|
||||
_inherit = "res.users"
|
||||
|
||||
def _get_sale_planner_calendar_events_domain(self):
|
||||
return [
|
||||
("user_id", "=", self.env.user.id),
|
||||
("target_partner_id", "!=", False),
|
||||
("sale_planner_state", "=", "pending"),
|
||||
]
|
||||
|
||||
@api.model
|
||||
def get_action_sale_planner_calendar_event(self):
|
||||
return self.env["ir.actions.act_window"]._for_xml_id(
|
||||
"sale_planner_calendar.action_sale_planner_calendar_event"
|
||||
)
|
||||
|
||||
@api.model
|
||||
def systray_get_activities(self):
|
||||
res = super().systray_get_activities()
|
||||
# Get user timezone or UTC if not set
|
||||
user_tz = pytz.timezone(self.env.user.tz or "UTC")
|
||||
# Get current time in user's timezone
|
||||
datetime_now = datetime.now(user_tz)
|
||||
domain = self._get_sale_planner_calendar_events_domain()
|
||||
# Start of day in user's timezone
|
||||
start_date_today_tz = datetime_now.replace(hour=0, minute=0, second=0)
|
||||
# Convert to UTC for domain
|
||||
start_date_today_utc = start_date_today_tz.astimezone(pytz.UTC)
|
||||
# End of day in user's timezone
|
||||
end_date_today_tz = datetime_now.replace(hour=23, minute=59, second=59)
|
||||
# Convert to UTC for domain
|
||||
end_date_today_utc = end_date_today_tz.astimezone(pytz.UTC)
|
||||
domain_today = domain + [
|
||||
("start", ">=", fields.Datetime.to_string(start_date_today_utc)),
|
||||
("start", "<=", fields.Datetime.to_string(end_date_today_utc)),
|
||||
]
|
||||
events_today_count = self.env["calendar.event"].search_count(domain_today)
|
||||
events_overdue_count = self.env["calendar.event"].search_count(
|
||||
domain + [("start", "<", start_date_today_utc)]
|
||||
)
|
||||
events_planned_count = self.env["calendar.event"].search_count(
|
||||
domain + [("start", ">", end_date_today_utc)]
|
||||
)
|
||||
total_count = events_today_count + events_overdue_count
|
||||
if total_count:
|
||||
res.append(
|
||||
{
|
||||
"id": self.env["ir.model"]._get("calendar.event").id,
|
||||
"type": "activity",
|
||||
"name": _("Sale planner calendar events"),
|
||||
"model": "calendar.event",
|
||||
"icon": modules.module.get_module_icon(
|
||||
self.env["calendar.event"]._original_module
|
||||
),
|
||||
"total_count": total_count,
|
||||
"today_count": events_today_count,
|
||||
"overdue_count": events_overdue_count,
|
||||
"planned_count": events_planned_count,
|
||||
"is_planner": True,
|
||||
"domain": json.dumps(domain),
|
||||
}
|
||||
)
|
||||
return res
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
# Copyright 2021 Tecnativa - Sergio Teruel
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
from datetime import timedelta
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = "sale.order"
|
||||
|
||||
sale_planner_calendar_event_id = fields.Many2one(comodel_name="calendar.event")
|
||||
|
||||
def _action_confirm(self):
|
||||
event_obj = self.env["calendar.event"]
|
||||
event_type_delivery = self.env.ref("sale_planner_calendar.event_type_delivery")
|
||||
for order in self:
|
||||
if not order.expected_date or order.commitment_date:
|
||||
continue
|
||||
delivery_event = event_obj.search(
|
||||
[
|
||||
("target_partner_id", "=", order.partner_id.id),
|
||||
("start", ">", fields.Datetime.to_string(order.expected_date)),
|
||||
("categ_ids", "in", event_type_delivery.ids),
|
||||
],
|
||||
order="start",
|
||||
limit=1,
|
||||
)
|
||||
if delivery_event:
|
||||
order.commitment_date = delivery_event.start
|
||||
return super()._action_confirm()
|
||||
|
||||
def _prepare_calendar_event_planner(self):
|
||||
categ = self.env.ref("sale_planner_calendar.event_type_commercial_visit")
|
||||
return {
|
||||
"name": _("Sale off planning"),
|
||||
"target_partner_id": self.partner_id.id,
|
||||
"user_id": self.user_id.id,
|
||||
"start": self.date_order,
|
||||
"stop": self.date_order
|
||||
+ timedelta(minutes=round((categ.duration or 1.0) * 60)),
|
||||
"off_planning": True,
|
||||
"sale_planner_state": "done",
|
||||
}
|
||||
|
||||
def action_set_planner_calendar_event(self, planner_summary=False):
|
||||
orders = self.filtered(lambda so: not so.sale_planner_calendar_event_id)
|
||||
if not orders:
|
||||
return
|
||||
order_dates = orders.mapped("date_order")
|
||||
if not planner_summary:
|
||||
planner_summary = self.env["sale.planner.calendar.summary"]
|
||||
date_from = planner_summary._get_datetime_from_date_tz_hour(
|
||||
min(order_dates), self.env.company.sale_planner_order_cut_hour
|
||||
)
|
||||
date_to = planner_summary._get_datetime_from_date_tz_hour(
|
||||
max(order_dates), self.env.company.sale_planner_order_cut_hour
|
||||
) + relativedelta(days=1)
|
||||
calendar_event_domain = [
|
||||
("target_partner_id", "in", orders.partner_id.ids),
|
||||
("user_id", "in", orders.user_id.ids),
|
||||
("start", ">=", date_from),
|
||||
("start", "<", date_to),
|
||||
]
|
||||
if planner_summary.event_type_id:
|
||||
calendar_event_domain.append(
|
||||
("categ_ids", "in", planner_summary.event_type_id.ids)
|
||||
)
|
||||
calendar_events = self.env["calendar.event"].search(calendar_event_domain)
|
||||
planner_summary_domain = [
|
||||
("user_id", "in", orders.user_id.ids),
|
||||
("date", ">=", date_from.date()),
|
||||
("date", "<", date_to.date()),
|
||||
]
|
||||
if planner_summary.event_type_id:
|
||||
planner_summary_domain.append(
|
||||
("event_type_id", "=", planner_summary.event_type_id.id)
|
||||
)
|
||||
event_summaries = planner_summary or self.env[
|
||||
"sale.planner.calendar.summary"
|
||||
].search(planner_summary_domain)
|
||||
if not event_summaries:
|
||||
return
|
||||
cut_time = date_from.time()
|
||||
for order in orders:
|
||||
event = calendar_events.filtered(
|
||||
lambda ev: ev.target_partner_id == order.partner_id
|
||||
and ev.user_id == order.user_id
|
||||
and (ev.start.combine(ev.start.date(), cut_time) <= order.date_order)
|
||||
and (
|
||||
ev.start.combine(ev.start.date(), cut_time) + relativedelta(days=1)
|
||||
> order.date_order
|
||||
)
|
||||
)[:1]
|
||||
if not event:
|
||||
event_summary = event_summaries.filtered(
|
||||
lambda sm: sm.user_id == order.user_id
|
||||
and (
|
||||
(
|
||||
sm.date == order.date_order.date()
|
||||
and order.date_order.time() >= cut_time
|
||||
)
|
||||
or (
|
||||
sm.date == order.date_order.date() + relativedelta(days=1)
|
||||
and order.date_order.time() < cut_time
|
||||
)
|
||||
)
|
||||
)
|
||||
if len(event_summary) > 1:
|
||||
# Sorted to select first summary without event_type
|
||||
event_summary = event_summary.sorted("event_type_id")[:1]
|
||||
if not event_summary:
|
||||
continue
|
||||
event_vals = order._prepare_calendar_event_planner()
|
||||
event_vals["calendar_summary_id"] = event_summary.id
|
||||
event = self.env["calendar.event"].create(event_vals)
|
||||
order.sale_planner_calendar_event_id = event
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
orders = super().create(vals_list)
|
||||
if self.env.context.get("calendar_summary_id"):
|
||||
planner_summary = self.env["sale.planner.calendar.summary"].browse(
|
||||
self.env.context["calendar_summary_id"]
|
||||
)
|
||||
orders.action_set_planner_calendar_event(planner_summary=planner_summary)
|
||||
return orders
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# Copyright 2021 Tecnativa - Sergio Teruel
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class SalePaymentSheetLine(models.Model):
|
||||
_inherit = "sale.payment.sheet.line"
|
||||
|
||||
sale_planner_calendar_event_id = fields.Many2one(comodel_name="calendar.event")
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
# Copyright 2021 Tecnativa - Sergio Teruel
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class SaleEventPlannerIssueType(models.Model):
|
||||
_name = "sale.planner.calendar.issue.type"
|
||||
_description = "Sale planner calendar issue type"
|
||||
|
||||
name = fields.Char(translate=True)
|
||||
|
|
@ -0,0 +1,366 @@
|
|||
# Copyright 2021 Tecnativa - Sergio Teruel
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
from datetime import timedelta
|
||||
|
||||
import pytz
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class SalePlannerCalendarSummary(models.Model):
|
||||
_name = "sale.planner.calendar.summary"
|
||||
_description = "Sale planner calendar summary"
|
||||
_inherit = "mail.thread"
|
||||
|
||||
company_id = fields.Many2one(
|
||||
comodel_name="res.company", default=lambda self: self.env.company.id
|
||||
)
|
||||
currency_id = fields.Many2one(
|
||||
comodel_name="res.currency",
|
||||
related="company_id.currency_id",
|
||||
)
|
||||
date = fields.Date(default=fields.Date.context_today)
|
||||
sale_planner_calendar_event_ids = fields.One2many(
|
||||
comodel_name="calendar.event",
|
||||
inverse_name="calendar_summary_id",
|
||||
)
|
||||
user_id = fields.Many2one(
|
||||
comodel_name="res.users",
|
||||
default=lambda self: self.env.user.id,
|
||||
index=True,
|
||||
domain="[('share','=',False)]",
|
||||
)
|
||||
sale_ids = fields.One2many(
|
||||
comodel_name="sale.order",
|
||||
compute="_compute_sale_ids",
|
||||
)
|
||||
state = fields.Selection(
|
||||
[
|
||||
("pending", "Pending"),
|
||||
("done", "Done"),
|
||||
("cancel", "Cancelled"),
|
||||
],
|
||||
default="pending",
|
||||
)
|
||||
comment = fields.Text()
|
||||
sale_order_subtotal = fields.Monetary(
|
||||
compute="_compute_sale_ids", currency_field="currency_id"
|
||||
)
|
||||
event_type_id = fields.Many2one(
|
||||
comodel_name="calendar.event.type",
|
||||
string="Event type",
|
||||
)
|
||||
# Summary data results
|
||||
event_total_count = fields.Integer(compute="_compute_event_planner_count")
|
||||
event_done_count = fields.Integer(compute="_compute_event_planner_count")
|
||||
event_effective_count = fields.Integer(compute="_compute_event_planner_count")
|
||||
event_off_planning_count = fields.Integer(compute="_compute_event_planner_count")
|
||||
sale_order_count = fields.Integer(compute="_compute_sale_ids")
|
||||
payment_count = fields.Integer(compute="_compute_event_planner_count")
|
||||
payment_amount = fields.Monetary(compute="_compute_event_planner_count")
|
||||
|
||||
@api.depends("sale_planner_calendar_event_ids")
|
||||
def _compute_sale_ids(self):
|
||||
for rec in self:
|
||||
sales = rec.sale_planner_calendar_event_ids.mapped("sale_ids")
|
||||
rec.sale_ids = sales
|
||||
rec.sale_order_subtotal = sum(sales.mapped("amount_untaxed"))
|
||||
rec.sale_order_count = len(sales)
|
||||
|
||||
@api.depends(
|
||||
"sale_planner_calendar_event_ids",
|
||||
"sale_planner_calendar_event_ids.sale_planner_state",
|
||||
"sale_planner_calendar_event_ids.sale_ids",
|
||||
"sale_planner_calendar_event_ids.off_planning",
|
||||
)
|
||||
def _compute_event_planner_count(self):
|
||||
for summary in self:
|
||||
event_total_count = 0
|
||||
event_done_count = 0
|
||||
event_effective_count = 0
|
||||
event_off_planning_count = 0
|
||||
payment_count = 0
|
||||
payment_amount = 0.0
|
||||
for event in summary.sale_planner_calendar_event_ids:
|
||||
event_total_count += 1
|
||||
if event.sale_planner_state == "done":
|
||||
event_done_count += 1
|
||||
if event.sale_ids:
|
||||
event_effective_count += 1
|
||||
if event.off_planning:
|
||||
event_off_planning_count += 1
|
||||
for payment in event.payment_sheet_line_ids:
|
||||
payment_count += 1
|
||||
payment_amount += payment.amount
|
||||
summary.event_total_count = event_total_count
|
||||
summary.event_done_count = event_done_count
|
||||
summary.event_effective_count = event_effective_count
|
||||
summary.event_off_planning_count = event_off_planning_count
|
||||
summary.payment_count = payment_count
|
||||
summary.payment_amount = payment_amount
|
||||
|
||||
@api.constrains("user_id", "date", "event_type_id")
|
||||
def _check_existing_summaries(self):
|
||||
for rec in self:
|
||||
summaries = self.search(
|
||||
[
|
||||
("user_id", "=", rec.user_id.id),
|
||||
("date", "=", rec.date),
|
||||
("event_type_id", "=", rec.event_type_id.id),
|
||||
("id", "!=", rec.id),
|
||||
]
|
||||
)
|
||||
if summaries:
|
||||
raise ValidationError(
|
||||
_(
|
||||
"Already exists a summary with same user, date and event type)\n"
|
||||
"Access with 'Sale planner calendar summary' menu option"
|
||||
)
|
||||
)
|
||||
|
||||
def name_get(self):
|
||||
res = []
|
||||
DateField = self.env["ir.qweb.field.date"]
|
||||
for line in self:
|
||||
name = "{} {}".format(
|
||||
DateField.value_to_html(line.date, {}), line.user_id.name
|
||||
)
|
||||
res.append((line.id, name))
|
||||
return res
|
||||
|
||||
def action_open_sale_order(self):
|
||||
"""
|
||||
Search or Create an event planner linked to sale order
|
||||
"""
|
||||
action = self.env["ir.actions.act_window"]._for_xml_id(
|
||||
"sale.action_quotations_with_onboarding"
|
||||
)
|
||||
action["context"] = {
|
||||
"default_user_id": self.user_id.id,
|
||||
}
|
||||
if len(self.sale_ids) > 1:
|
||||
action["domain"] = [
|
||||
(
|
||||
"sale_planner_calendar_event_id",
|
||||
"in",
|
||||
self.sale_planner_calendar_event_ids.ids,
|
||||
)
|
||||
]
|
||||
else:
|
||||
action["views"] = [(self.env.ref("sale.view_order_form").id, "form")]
|
||||
action["res_id"] = self.sale_ids.id
|
||||
return action
|
||||
|
||||
@api.model
|
||||
def action_get_today_summary(self):
|
||||
domain = [
|
||||
("user_id", "=", self.env.user.id),
|
||||
("date", "=", fields.Date.today()),
|
||||
]
|
||||
summary = self.search(domain)
|
||||
action = self.env["ir.actions.act_window"]._for_xml_id(
|
||||
"sale_planner_calendar.action_sale_planner_calendar_summary"
|
||||
)
|
||||
|
||||
if len(summary) > 1:
|
||||
action["domain"] = [("id", "in", summary.ids)]
|
||||
else:
|
||||
action["views"] = [
|
||||
(
|
||||
self.env.ref(
|
||||
"sale_planner_calendar.view_sale_planner_calendar_summary_form"
|
||||
).id,
|
||||
"form",
|
||||
)
|
||||
]
|
||||
action["res_id"] = summary.id
|
||||
return action
|
||||
|
||||
def action_done(self):
|
||||
self.write(
|
||||
{
|
||||
"state": "done",
|
||||
# "comment": 'Done',
|
||||
}
|
||||
)
|
||||
|
||||
def action_cancel(self):
|
||||
self.write(
|
||||
{
|
||||
"state": "cancel",
|
||||
"comment": "Not done",
|
||||
}
|
||||
)
|
||||
|
||||
def action_pending(self):
|
||||
self.write(
|
||||
{
|
||||
"state": "pending",
|
||||
"comment": False,
|
||||
}
|
||||
)
|
||||
|
||||
def action_process(self):
|
||||
calendar_event_domain = [
|
||||
(
|
||||
"start",
|
||||
">=",
|
||||
self._get_datetime_from_date_tz_hour(self.date, "00:00:00"),
|
||||
),
|
||||
(
|
||||
"start",
|
||||
"<=",
|
||||
self._get_datetime_from_date_tz_hour(self.date, "23:59:59"),
|
||||
),
|
||||
("user_id", "=", self.user_id.id),
|
||||
("target_partner_id", "!=", False),
|
||||
("calendar_summary_id", "=", False),
|
||||
]
|
||||
if self.event_type_id:
|
||||
calendar_event_domain.append(("categ_ids", "in", self.event_type_id.ids))
|
||||
calendar_events = self.env["calendar.event"].search(calendar_event_domain)
|
||||
calendar_events.calendar_summary_id = self
|
||||
#
|
||||
# event_planner_domain = [
|
||||
# (
|
||||
# "start",
|
||||
# ">=",
|
||||
# self._get_datetime_from_date_tz_hour(self.date, "00:00:00"),
|
||||
# ),
|
||||
# (
|
||||
# "start",
|
||||
# "<=",
|
||||
# self._get_datetime_from_date_tz_hour(self.date, "23:59:59"),
|
||||
# ),
|
||||
# ("user_id", "=", self.user_id.id),
|
||||
# "|",
|
||||
# ("calendar_summary_id", "=", False),
|
||||
# ("calendar_summary_id", "=", self.id),
|
||||
# ]
|
||||
# events_planner = self.env["calendar.event"].search(
|
||||
# event_planner_domain
|
||||
# )
|
||||
# # We can not do a typical search due to returned virtual ids like this
|
||||
# # ("calendar_event_id.categ_ids", "in", self.event_type_id.ids)
|
||||
# if self.event_type_id:
|
||||
# events_planner = events_planner.filtered(
|
||||
# lambda p: self.event_type_id.id in p.calendar_event_id.categ_ids.ids
|
||||
# )
|
||||
#
|
||||
# for calendar_event in calendar_events:
|
||||
# event_planner = events_planner.filtered(
|
||||
# lambda r: r.start
|
||||
# == fields.Datetime.to_datetime(calendar_event.start)
|
||||
# and r.partner_id == calendar_event.target_partner_id
|
||||
# and r.user_id == calendar_event.user_id
|
||||
# )
|
||||
# if event_planner:
|
||||
# if event_planner.calendar_summary_id != self:
|
||||
# event_planner.calendar_summary_id = self
|
||||
# event_planner.off_planning = True
|
||||
# else:
|
||||
# calendar_event.with_context(
|
||||
# default_calendar_summary_id=self.id,
|
||||
# default_date=calendar_event.start,
|
||||
# )._create_event_planner()
|
||||
|
||||
# Search sale orders off planning
|
||||
date_from = self._get_datetime_from_date_tz_hour(
|
||||
self.date, self.env.company.sale_planner_order_cut_hour
|
||||
)
|
||||
date_to = date_from + timedelta(days=1)
|
||||
sales = self.env["sale.order"].search(
|
||||
[
|
||||
("user_id", "=", self.user_id.id),
|
||||
("date_order", ">=", date_from),
|
||||
("date_order", "<", date_to),
|
||||
("sale_planner_calendar_event_id", "=", False),
|
||||
]
|
||||
)
|
||||
sales.action_set_planner_calendar_event(self)
|
||||
|
||||
@api.model
|
||||
def _get_datetime_from_date_tz_hour(self, date, hour_float):
|
||||
"""
|
||||
Compute date in UTC format
|
||||
:return: Datetime in UTC format
|
||||
"""
|
||||
if isinstance(hour_float, str):
|
||||
hour_str = hour_float
|
||||
else:
|
||||
hour_str = str(timedelta(hours=hour_float)).zfill(8)
|
||||
date_str = "{} {}".format(fields.Date.to_string(date), hour_str)
|
||||
date_time = fields.Datetime.to_datetime(date_str)
|
||||
user_tz = pytz.timezone(self.env.user.tz)
|
||||
utc_tz = pytz.timezone("UTC")
|
||||
time_utc = user_tz.localize(date_time).astimezone(utc_tz).replace(tzinfo=None)
|
||||
return time_utc
|
||||
|
||||
def action_event_planner(self):
|
||||
self.action_process()
|
||||
action = self.env["ir.actions.act_window"]._for_xml_id(
|
||||
"sale_planner_calendar.action_sale_planner_calendar_event"
|
||||
)
|
||||
action["domain"] = [("id", "in", self.sale_planner_calendar_event_ids.ids)]
|
||||
action["context"] = {
|
||||
"default_off_planning": True,
|
||||
"default_calendar_summary_id": self.id,
|
||||
"search_default_state_pending": 1,
|
||||
}
|
||||
action["views"] = [
|
||||
(
|
||||
self.env.ref(
|
||||
"sale_planner_calendar.view_sale_planner_calendar_kanban"
|
||||
).id,
|
||||
"kanban",
|
||||
),
|
||||
(
|
||||
self.env.ref(
|
||||
"sale_planner_calendar.view_sale_planner_calendar_form"
|
||||
).id,
|
||||
"form",
|
||||
),
|
||||
(
|
||||
self.env.ref(
|
||||
"sale_planner_calendar.view_sale_planner_calendar_tree"
|
||||
).id,
|
||||
"tree",
|
||||
),
|
||||
]
|
||||
action["view_mode"] = "kanban,form,tree"
|
||||
return action
|
||||
|
||||
def action_open_payment_sheet(self):
|
||||
"""
|
||||
Open payments sheets related to any event
|
||||
"""
|
||||
payment_sheets = self.sale_planner_calendar_event_ids.mapped(
|
||||
"payment_sheet_line_ids.sheet_id"
|
||||
)
|
||||
action = self.env["ir.actions.act_window"]._for_xml_id(
|
||||
"sale_payment_sheet.action_sale_payment_sheet"
|
||||
)
|
||||
if len(payment_sheets) > 1:
|
||||
action["domain"] = [("id", "in", payment_sheets.ids)]
|
||||
else:
|
||||
action["views"] = [
|
||||
(
|
||||
self.env.ref("sale_payment_sheet.view_sale_payment_sheet_form").id,
|
||||
"form",
|
||||
)
|
||||
]
|
||||
action["res_id"] = payment_sheets.id
|
||||
return action
|
||||
|
||||
def action_open_issue(self):
|
||||
"""
|
||||
Open issues related to any event
|
||||
"""
|
||||
issues = self.sale_planner_calendar_event_ids.filtered("calendar_issue_type_id")
|
||||
action = self.env["ir.actions.act_window"]._for_xml_id(
|
||||
"sale_planner_calendar.action_sale_planner_calendar_issue_tree"
|
||||
)
|
||||
|
||||
action["domain"] = [("id", "in", issues.ids)]
|
||||
return action
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
You can find the following configurations in the settings:
|
||||
|
||||
#. **Subscriptions Backward Days**: Backward days to search documents to update
|
||||
subscriptions.
|
||||
|
||||
#. **Sale Planner Forward Months**: Forward months to create calendar events.
|
||||
|
||||
#. **Send invitation to attendees**: Send invitations to attendees when a planner event
|
||||
is created.
|
||||
|
||||
#. **Sale planner order cut hour**: Time of the next day until which orders of the
|
||||
current day are assigned.
|
||||
|
||||
#. **Calendar event max duration**: Show a warning message when duration is more than
|
||||
this time. Set 00:00 to disable warning.
|
||||
|
||||
Other setting are available with system parameters
|
||||
|
||||
#. **Sale order partner** when a so is created from a event planned. You can create or
|
||||
update the system parameter **sale_planner_calendar.create_so_to_commercial_partner**
|
||||
with True value to create the sale order to commercial partner instead of partner
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
* `Tecnativa <https://www.tecnativa.com>`__:
|
||||
|
||||
* Sergio Teruel
|
||||
* Carlos Dauden
|
||||
* Carlos Roca
|
||||
* Pilar Vargas
|
||||
|
|
@ -0,0 +1 @@
|
|||
This module allows to manage commercial visits to partners by using recurrence events.
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
You can create now the recurrent events directly from the partners by clicking next
|
||||
smart button:
|
||||
|
||||
.. image:: ../static/img/smart_button.png
|
||||
|
||||
By default the end of the recurrence is set by the settings field
|
||||
**Sale Planner Forward Months**.
|
||||
|
||||
You can manage this new recurrent events from *Calendar planner* menu entry.
|
||||
|
||||
.. image:: ../static/img/menu_entry.png
|
||||
|
||||
On the first window, you will find a summary of the events that the user has to do
|
||||
today.
|
||||
|
||||
By going to *Calendar planner > Calendar events* you will have two options:
|
||||
|
||||
#. View the calendar events related to your user (*My Calendar*).
|
||||
|
||||
#. View your base events related to the recurrences (*Recurrent calendar events*).
|
||||
|
||||
Finally on *Calendar planner > Wizards* you will have again two options:
|
||||
|
||||
#. You can change the hour of the start of all the events related to a recurrency by using
|
||||
*Sale planner calendar wizard*.
|
||||
|
||||
#. You can change the salesperson assigned to events related to a period of time by using
|
||||
the wizard *Reassignment of salesperson*.
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_sale_planner_calendar_summary,access_sale_planner_calendar_summary,model_sale_planner_calendar_summary,sales_team.group_sale_salesman,1,1,1,1
|
||||
access_sale_planner_calendar_issue_type,access_sale_planner_calendar_issue_type,model_sale_planner_calendar_issue_type,sales_team.group_sale_salesman,1,1,1,1
|
||||
access_sale_planner_calendar_reassign_wiz,access_sale_planner_calendar_reassign_wiz,model_sale_planner_calendar_reassign_wiz,sales_team.group_sale_salesman,1,1,1,1
|
||||
access_sale_planner_calendar_reassign_line_wiz,access_sale_planner_calendar_reassign_line_wiz,model_sale_planner_calendar_reassign_line_wiz,sales_team.group_sale_salesman,1,1,1,1
|
||||
access_sale_planner_calendar_wizard,access_sale_planner_calendar_wizard,model_sale_planner_calendar_wizard,sales_team.group_sale_salesman,1,1,1,1
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo noupdate="1">
|
||||
<record id="partner_all_documents_follower_rule" model="ir.rule">
|
||||
<field name="name">Partner all documents follower</field>
|
||||
<field ref="base.model_res_partner" 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="partner_follower_rule" model="ir.rule">
|
||||
<field name="name">Partner Follower</field>
|
||||
<field ref="base.model_res_partner" name="model_id" />
|
||||
<field
|
||||
name="domain_force"
|
||||
>[('message_partner_ids', 'in', user.partner_id.ids)]</field>
|
||||
<field name="groups" eval="[(4, ref('sales_team.group_sale_salesman'))]" />
|
||||
</record>
|
||||
|
||||
<record id="sale_order_follower_rule" model="ir.rule">
|
||||
<field name="name">Sales Follower Orders</field>
|
||||
<field ref="sale.model_sale_order" name="model_id" />
|
||||
<field
|
||||
name="domain_force"
|
||||
>['|', ('message_partner_ids', 'in', user.partner_id.ids), ('partner_id.message_partner_ids', 'in', user.partner_id.ids)]</field>
|
||||
<field name="groups" eval="[(4, ref('sales_team.group_sale_salesman'))]" />
|
||||
</record>
|
||||
<record id="sale_order_line_follower_rule" model="ir.rule">
|
||||
<field name="name">Sales Follower Order Lines</field>
|
||||
<field ref="sale.model_sale_order_line" name="model_id" />
|
||||
<field
|
||||
name="domain_force"
|
||||
>['|', ('order_id.message_partner_ids', 'in', user.partner_id.ids), ('order_partner_id.message_partner_ids', 'in', user.partner_id.ids)]</field>
|
||||
<field name="groups" eval="[(4, ref('sales_team.group_sale_salesman'))]" />
|
||||
</record>
|
||||
|
||||
<record id="account_move_follower_rule" model="ir.rule">
|
||||
<field name="name">Sales Follower Invoices</field>
|
||||
<field ref="account.model_account_move" name="model_id" />
|
||||
<field
|
||||
name="domain_force"
|
||||
>[('message_partner_ids', 'in', user.partner_id.ids)]</field>
|
||||
<field name="groups" eval="[(4, ref('sales_team.group_sale_salesman'))]" />
|
||||
</record>
|
||||
<record id="account_move_line_follower_rule" model="ir.rule">
|
||||
<field name="name">Sales Follower Invoice Lines</field>
|
||||
<field ref="account.model_account_move_line" name="model_id" />
|
||||
<field
|
||||
name="domain_force"
|
||||
>[('move_id.message_partner_ids', 'in', user.partner_id.ids)]</field>
|
||||
<field name="groups" eval="[(4, ref('sales_team.group_sale_salesman'))]" />
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
|
|
@ -0,0 +1,476 @@
|
|||
<!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>Sale planner calendar</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="sale-planner-calendar">
|
||||
<h1 class="title">Sale planner calendar</h1>
|
||||
|
||||
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:9536f0a762b2909e0b503ba68e80f5fbd7d358124a934194fbc5d67b657e4510
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
|
||||
<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/sale-workflow/tree/16.0/sale_planner_calendar"><img alt="OCA/sale-workflow" src="https://img.shields.io/badge/github-OCA%2Fsale--workflow-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/sale-workflow-16-0/sale-workflow-16-0-sale_planner_calendar"><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/sale-workflow&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 module allows to manage commercial visits to partners by using recurrence events.</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>You can find the following configurations in the settings:</p>
|
||||
<ol class="arabic simple">
|
||||
<li><strong>Subscriptions Backward Days</strong>: Backward days to search documents to update
|
||||
subscriptions.</li>
|
||||
<li><strong>Sale Planner Forward Months</strong>: Forward months to create calendar events.</li>
|
||||
<li><strong>Send invitation to attendees</strong>: Send invitations to attendees when a planner event
|
||||
is created.</li>
|
||||
<li><strong>Sale planner order cut hour</strong>: Time of the next day until which orders of the
|
||||
current day are assigned.</li>
|
||||
<li><strong>Calendar event max duration</strong>: Show a warning message when duration is more than
|
||||
this time. Set 00:00 to disable warning.</li>
|
||||
</ol>
|
||||
<p>Other setting are available with system parameters</p>
|
||||
<ol class="arabic simple">
|
||||
<li><strong>Sale order partner</strong> when a so is created from a event planned. You can create or
|
||||
update the system parameter <strong>sale_planner_calendar.create_so_to_commercial_partner</strong>
|
||||
with True value to create the sale order to commercial partner instead of partner</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="section" id="usage">
|
||||
<h1><a class="toc-backref" href="#toc-entry-2">Usage</a></h1>
|
||||
<p>You can create now the recurrent events directly from the partners by clicking next
|
||||
smart button:</p>
|
||||
<img alt="https://raw.githubusercontent.com/OCA/sale-workflow/16.0/sale_planner_calendar/static/img/smart_button.png" src="https://raw.githubusercontent.com/OCA/sale-workflow/16.0/sale_planner_calendar/static/img/smart_button.png" />
|
||||
<p>By default the end of the recurrence is set by the settings field
|
||||
<strong>Sale Planner Forward Months</strong>.</p>
|
||||
<p>You can manage this new recurrent events from <em>Calendar planner</em> menu entry.</p>
|
||||
<img alt="https://raw.githubusercontent.com/OCA/sale-workflow/16.0/sale_planner_calendar/static/img/menu_entry.png" src="https://raw.githubusercontent.com/OCA/sale-workflow/16.0/sale_planner_calendar/static/img/menu_entry.png" />
|
||||
<p>On the first window, you will find a summary of the events that the user has to do
|
||||
today.</p>
|
||||
<p>By going to <em>Calendar planner > Calendar events</em> you will have two options:</p>
|
||||
<ol class="arabic simple">
|
||||
<li>View the calendar events related to your user (<em>My Calendar</em>).</li>
|
||||
<li>View your base events related to the recurrences (<em>Recurrent calendar events</em>).</li>
|
||||
</ol>
|
||||
<p>Finally on <em>Calendar planner > Wizards</em> you will have again two options:</p>
|
||||
<ol class="arabic simple">
|
||||
<li>You can change the hour of the start of all the events related to a recurrency by using
|
||||
<em>Sale planner calendar wizard</em>.</li>
|
||||
<li>You can change the salesperson assigned to events related to a period of time by using
|
||||
the wizard <em>Reassignment of salesperson</em>.</li>
|
||||
</ol>
|
||||
</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/sale-workflow/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/sale-workflow/issues/new?body=module:%20sale_planner_calendar%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>Tecnativa</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.tecnativa.com">Tecnativa</a>:<ul>
|
||||
<li>Sergio Teruel</li>
|
||||
<li>Carlos Dauden</li>
|
||||
<li>Carlos Roca</li>
|
||||
<li>Pilar Vargas</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/sale-workflow/tree/16.0/sale_planner_calendar">OCA/sale-workflow</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>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 128 KiB |
|
|
@ -0,0 +1,50 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import {registerPatch} from "@mail/model/model_core";
|
||||
import {attr} from "@mail/model/model_field";
|
||||
|
||||
registerPatch({
|
||||
name: "ActivityGroup",
|
||||
modelMethods: {
|
||||
convertData(data) {
|
||||
const data2 = this._super(data);
|
||||
data2.is_planner = data.is_planner;
|
||||
return data2;
|
||||
},
|
||||
},
|
||||
fields: {
|
||||
is_planner: attr({default: false}),
|
||||
},
|
||||
});
|
||||
|
||||
registerPatch({
|
||||
name: "ActivityGroupView",
|
||||
recordMethods: {
|
||||
onClickFilterButton(ev) {
|
||||
this._super(...arguments);
|
||||
const data = _.extend({}, $(ev.currentTarget).data(), $(ev.target).data());
|
||||
if (data.is_planner === 1) {
|
||||
const context = {};
|
||||
if (data.filter === "my") {
|
||||
context.search_default_planner_overdue = 1;
|
||||
context.search_default_planner_today = 1;
|
||||
} else {
|
||||
context["search_default_planner_" + data.filter] = 1;
|
||||
}
|
||||
this.env.services.orm
|
||||
.call("res.users", "get_action_sale_planner_calendar_event")
|
||||
.then((action) => {
|
||||
action.domain = data.domain;
|
||||
const action_ctx = JSON.parse(
|
||||
action.context.replace(/'/g, '"')
|
||||
);
|
||||
action.context = {...action_ctx, ...context};
|
||||
delete action.context.search_default_my_event_planner;
|
||||
this.env.services.action.doAction(action, {
|
||||
clearBreadcrumbs: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
/** @odoo-module **/
|
||||
import {registry} from "@web/core/registry";
|
||||
import {Component} from "@odoo/owl";
|
||||
|
||||
class CategIconsWidget extends Component {
|
||||
setup() {
|
||||
const categIcons = this.props.record.data.categ_icons;
|
||||
this.iconList = categIcons ? categIcons.split(",") : [];
|
||||
}
|
||||
}
|
||||
|
||||
CategIconsWidget.template = "sale_planner_calendar.CategIconsWidget";
|
||||
|
||||
registry.category("fields").add("categ_icons_widget", CategIconsWidget);
|
||||
export default CategIconsWidget;
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import {FormController} from "@web/views/form/form_controller";
|
||||
import {patch} from "@web/core/utils/patch";
|
||||
import {useAskRecurrenceUpdatePolicy} from "@calendar/views/ask_recurrence_update_policy_hook";
|
||||
|
||||
patch(FormController.prototype, "sale_planner_calendar.FormController", {
|
||||
setup() {
|
||||
this._super(...arguments);
|
||||
this.askRecurrenceUpdatePolicy = useAskRecurrenceUpdatePolicy();
|
||||
},
|
||||
async deleteRecord() {
|
||||
const _super = this._super.bind(this);
|
||||
var record = this.model.root.data;
|
||||
if (
|
||||
this.props.context &&
|
||||
this.props.context.choose_unlink_method &&
|
||||
record.recurrency
|
||||
) {
|
||||
const recurrenceUpdate = await this.askRecurrenceUpdatePolicy();
|
||||
if (recurrenceUpdate && recurrenceUpdate !== "self_only") {
|
||||
await this._launchMassDeletion(record, recurrenceUpdate);
|
||||
return this.env.config.historyBack();
|
||||
} else if (recurrenceUpdate) {
|
||||
await this.model.root.delete();
|
||||
return this.env.config.historyBack();
|
||||
}
|
||||
return;
|
||||
}
|
||||
return _super(...arguments);
|
||||
},
|
||||
_launchMassDeletion: async function (record, recurrenceUpdate) {
|
||||
const resId = record.id;
|
||||
await this.env.services.orm.call(this.props.resModel, "action_mass_deletion", [
|
||||
[resId],
|
||||
recurrenceUpdate,
|
||||
]);
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import {ListController} from "@web/views/list/list_controller";
|
||||
import {patch} from "@web/core/utils/patch";
|
||||
import {useAskRecurrenceUpdatePolicy} from "@calendar/views/ask_recurrence_update_policy_hook";
|
||||
|
||||
patch(ListController.prototype, "sale_planner_calendar.ListController", {
|
||||
setup() {
|
||||
this._super(...arguments);
|
||||
this.askRecurrenceUpdatePolicy = useAskRecurrenceUpdatePolicy();
|
||||
},
|
||||
async onDeleteSelectedRecords() {
|
||||
const _super = this._super.bind(this);
|
||||
const resIds = await this.model.root.getResIds(true);
|
||||
var records = this.model.root.records.filter((record) =>
|
||||
resIds.includes(record.resId)
|
||||
);
|
||||
if (
|
||||
this.props.context &&
|
||||
this.props.context.choose_unlink_method &&
|
||||
!records.some((record) => !record.data.recurrency)
|
||||
) {
|
||||
const recurrenceUpdate = await this.askRecurrenceUpdatePolicy();
|
||||
if (recurrenceUpdate && recurrenceUpdate !== "self_only") {
|
||||
await this._launchMassDeletion(records, recurrenceUpdate);
|
||||
await this.model.load();
|
||||
return this.model.notify();
|
||||
} else if (recurrenceUpdate) {
|
||||
await this.model.root.deleteRecords();
|
||||
return this.model.notify();
|
||||
}
|
||||
return;
|
||||
}
|
||||
return _super(...arguments);
|
||||
},
|
||||
_launchMassDeletion: async function (records, recurrenceUpdate) {
|
||||
let recs = [...records];
|
||||
while (recs.length) {
|
||||
const record = recs[0];
|
||||
const recordsSameRecurrence = recs.filter(
|
||||
(rec) => rec.resId === record.resId
|
||||
);
|
||||
const event = recordsSameRecurrence.reduce((prev, curr) => {
|
||||
return prev.resId < curr.resId ? prev : curr;
|
||||
});
|
||||
await this.env.services.orm.call(
|
||||
this.props.resModel,
|
||||
"action_mass_deletion",
|
||||
[[event.resId], recurrenceUpdate]
|
||||
);
|
||||
recs = recs.filter((el) => !recordsSameRecurrence.includes(el));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import {KanbanController} from "@web/views/kanban/kanban_controller";
|
||||
import {kanbanView} from "@web/views/kanban/kanban_view";
|
||||
import {registry} from "@web/core/registry";
|
||||
import {useService} from "@web/core/utils/hooks";
|
||||
|
||||
export class SalePlannerCalendarEventKanbanController extends KanbanController {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.orm = useService("orm");
|
||||
this.action = useService("action");
|
||||
}
|
||||
async onClickNewSaleOrder() {
|
||||
console.log(this);
|
||||
const calendar_summary_id = this.props.context.default_calendar_summary_id;
|
||||
const action = await this.orm.call(
|
||||
"calendar.event",
|
||||
"action_open_sale_order",
|
||||
[false, {new_order: true}],
|
||||
{context: {calendar_summary_id: calendar_summary_id || false}}
|
||||
);
|
||||
this.action.doAction(action);
|
||||
}
|
||||
}
|
||||
|
||||
export const SalePlannerCalendarEventKanbanView = {
|
||||
...kanbanView,
|
||||
Controller: SalePlannerCalendarEventKanbanController,
|
||||
buttonTemplate: "SalePlannerCalendarEventKanbanView.buttons",
|
||||
};
|
||||
|
||||
registry
|
||||
.category("views")
|
||||
.add("sale_planner_calendar_event_kanban", SalePlannerCalendarEventKanbanView);
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import {ListController} from "@web/views/list/list_controller";
|
||||
import {listView} from "@web/views/list/list_view";
|
||||
import {registry} from "@web/core/registry";
|
||||
import {useService} from "@web/core/utils/hooks";
|
||||
|
||||
export class SalePlannerCalendarEventListController extends ListController {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.orm = useService("orm");
|
||||
this.action = useService("action");
|
||||
}
|
||||
async onClickNewSaleOrder() {
|
||||
console.log(this);
|
||||
const calendar_summary_id = this.props.context.default_calendar_summary_id;
|
||||
const action = await this.orm.call(
|
||||
"calendar.event",
|
||||
"action_open_sale_order",
|
||||
[false, {new_order: true}],
|
||||
{context: {calendar_summary_id: calendar_summary_id || false}}
|
||||
);
|
||||
this.action.doAction(action);
|
||||
}
|
||||
}
|
||||
|
||||
export const SalePlannerCalendarEventListView = {
|
||||
...listView,
|
||||
Controller: SalePlannerCalendarEventListController,
|
||||
buttonTemplate: "SalePlannerCalendarEventListView.buttons",
|
||||
};
|
||||
|
||||
registry
|
||||
.category("views")
|
||||
.add("sale_planner_calendar_event_tree", SalePlannerCalendarEventListView);
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
.oe_kanban_card_full_width {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.o_kanban_renderer.o_sale_planner_calendar_kanban {
|
||||
--KanbanRecord-width: 100%;
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-inherit="mail.ActivityMenuView" t-inherit-mode="extension">
|
||||
<xpath
|
||||
expr="//*[hasclass('o_ActivityMenuView_activityGroup')]"
|
||||
position="attributes"
|
||||
>
|
||||
<attribute
|
||||
name="t-att-data-is_planner"
|
||||
>activityGroupView.activityGroup.is_planner ? 1 : 0</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates>
|
||||
<t t-name="sale_planner_calendar.CategIconsWidget" owl="1">
|
||||
<div class="o_categ_icons_widget">
|
||||
<t t-foreach="iconList" t-as="categ_icon" t-key="categ_icon">
|
||||
<i t-att-class="'me-1 fa ' + categ_icon" />
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates>
|
||||
<t t-name="sale_planner_calendar.newSaleOrderButton" owl="1">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary o_button_new_sale_order"
|
||||
t-on-click="onClickNewSaleOrder"
|
||||
>
|
||||
New Quotation
|
||||
</button>
|
||||
</t>
|
||||
|
||||
<t
|
||||
t-name="SalePlannerCalendarEventListView.buttons"
|
||||
t-inherit="web.ListView.Buttons"
|
||||
t-inherit-mode="primary"
|
||||
owl="1"
|
||||
>
|
||||
<xpath expr="//button[hasclass('o_list_button_add')]" position="after">
|
||||
<t t-call="sale_planner_calendar.newSaleOrderButton" />
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t
|
||||
t-name="SalePlannerCalendarEventKanbanView.buttons"
|
||||
t-inherit="web.KanbanView.Buttons"
|
||||
t-inherit-mode="primary"
|
||||
owl="1"
|
||||
>
|
||||
<xpath expr="//button[hasclass('o-kanban-button-new')]" position="after">
|
||||
<t t-call="sale_planner_calendar.newSaleOrderButton" />
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import test_sale_planner_calendar
|
||||
|
|
@ -0,0 +1,376 @@
|
|||
# Copyright 2021 Tecnativa - Sergio Teruel
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from datetime import date, timedelta
|
||||
|
||||
from freezegun import freeze_time
|
||||
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.tests.common import Form, TransactionCase, tagged
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
@freeze_time("2022-02-04 09:00:00")
|
||||
class TestSalePlannerCalendar(TransactionCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
# Remove this variable in v16 and put instead:
|
||||
# from odoo.addons.base.tests.common import DISABLED_MAIL_CONTEXT
|
||||
DISABLED_MAIL_CONTEXT = {
|
||||
"tracking_disable": True,
|
||||
"mail_create_nolog": True,
|
||||
"mail_create_nosubscribe": True,
|
||||
"mail_notrack": True,
|
||||
"no_reset_password": True,
|
||||
}
|
||||
cls.env = cls.env(context=dict(cls.env.context, **DISABLED_MAIL_CONTEXT))
|
||||
cls.CalendarEvent = cls.env["calendar.event"]
|
||||
cls.ResUsers = cls.env["res.users"]
|
||||
cls.Partner = cls.env["res.partner"]
|
||||
cls.ProductTemplate = cls.env["product.template"]
|
||||
cls.Product = cls.env["product.product"]
|
||||
cls.AccountInvoice = cls.env["account.move"]
|
||||
cls.AccountInvoiceLine = cls.env["account.move.line"]
|
||||
cls.AccountJournal = cls.env["account.journal"]
|
||||
cls.SaleOrder = cls.env["sale.order"]
|
||||
cls.SalePlannerCalendarEvent = cls.env["calendar.event"]
|
||||
|
||||
account_group = cls.env.ref("account.group_account_user")
|
||||
cls.env.user.write({"groups_id": [(4, account_group.id)]})
|
||||
|
||||
cls.event_type_commercial_visit = cls.env.ref(
|
||||
"sale_planner_calendar.event_type_commercial_visit"
|
||||
)
|
||||
cls.event_type_delivery = cls.env.ref(
|
||||
"sale_planner_calendar.event_type_delivery"
|
||||
)
|
||||
cls.pricelist = cls.env["product.pricelist"].create(
|
||||
{"name": "Test pricelist", "currency_id": cls.env.company.currency_id.id}
|
||||
)
|
||||
|
||||
# Create some products
|
||||
cls._create_products()
|
||||
|
||||
# Create some commercial users
|
||||
cls._create_commercial_users()
|
||||
|
||||
# Create some partners
|
||||
cls._create_partners()
|
||||
|
||||
# Create some calendar planner events
|
||||
cls.create_calendar_planner_event()
|
||||
|
||||
# Some account data
|
||||
cls.account = cls.env["account.account"].create(
|
||||
{
|
||||
"code": "test",
|
||||
"name": "Test account",
|
||||
"account_type": "income",
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _create_commercial_users(cls):
|
||||
# Create commercial_user_1 and commercial_user_2 with Own Documents
|
||||
# Only security group
|
||||
cls.commercial_users = cls.ResUsers.browse()
|
||||
for i in range(2):
|
||||
index = i + 1
|
||||
user = cls.ResUsers.create(
|
||||
{
|
||||
"name": "Commercial user %s" % index,
|
||||
"login": "Commercial user %s" % index,
|
||||
"groups_id": [
|
||||
(4, cls.env.ref("sales_team.group_sale_salesman").id)
|
||||
],
|
||||
}
|
||||
)
|
||||
setattr(cls, "commercial_user_%s" % index, user)
|
||||
cls.commercial_users |= user
|
||||
|
||||
@classmethod
|
||||
def _create_partners(cls):
|
||||
cls.partners = cls.Partner.browse()
|
||||
cls.partner_1 = cls.Partner.create(
|
||||
{
|
||||
"name": "Partner 1",
|
||||
"user_id": cls.commercial_user_1.id,
|
||||
"property_product_pricelist": cls.pricelist.id,
|
||||
}
|
||||
)
|
||||
cls.partner_2 = cls.Partner.create(
|
||||
{
|
||||
"name": "Partner 2",
|
||||
"user_id": cls.commercial_user_1.id,
|
||||
"property_product_pricelist": cls.pricelist.id,
|
||||
}
|
||||
)
|
||||
cls.commercial_partner_3 = cls.Partner.create(
|
||||
{
|
||||
"name": "Company partner 3",
|
||||
"user_id": cls.commercial_user_2.id,
|
||||
"property_product_pricelist": cls.pricelist.id,
|
||||
}
|
||||
)
|
||||
cls.partner_3 = cls.Partner.create(
|
||||
{
|
||||
"name": "Partner 3",
|
||||
"user_id": cls.commercial_user_2.id,
|
||||
"property_product_pricelist": cls.pricelist.id,
|
||||
"parent_id": cls.commercial_partner_3.id,
|
||||
}
|
||||
)
|
||||
cls.partners = cls.partner_1 + cls.partner_2 + cls.partner_3
|
||||
|
||||
@classmethod
|
||||
def _create_products(cls):
|
||||
cls.product = cls.Product.create(
|
||||
{
|
||||
"name": "Product test 1",
|
||||
"list_price": 100.00,
|
||||
}
|
||||
)
|
||||
|
||||
def _create_sale_order(self):
|
||||
so_form = Form(self.SaleOrder)
|
||||
so_form.partner_id = self.partner_1
|
||||
with so_form.order_line.new() as line_form:
|
||||
line_form.product_id = self.product
|
||||
line_form.tax_id.remove(index=0)
|
||||
return so_form.save()
|
||||
|
||||
@classmethod
|
||||
def create_calendar_planner_event(cls):
|
||||
# Create one planned recurrent event for every partner.
|
||||
cls.planned_events = cls.CalendarEvent.browse()
|
||||
for i, partner in enumerate(cls.partners):
|
||||
action = partner.action_calendar_planner()
|
||||
context = dict(action["context"], default_wed=True, default_fri=True)
|
||||
# We use Form for auto-computing the recurrence model, that is triggered
|
||||
# directly from the initialization of it
|
||||
event_form = Form(cls.CalendarEvent.with_context(**context))
|
||||
cls.planned_events |= event_form.save()
|
||||
if i == 0:
|
||||
# Create a delivery event for partner 1. We can delivery goods
|
||||
# all mondays at 09:00
|
||||
context = dict(
|
||||
action["context"],
|
||||
default_name="Delivery",
|
||||
default_start="2022-02-07 09:00:00",
|
||||
default_stop="2022-02-07 10:00:00",
|
||||
default_mon=True,
|
||||
default_categ_ids=[(4, cls.event_type_delivery.id)],
|
||||
)
|
||||
event_form = Form(cls.CalendarEvent.with_context(**context))
|
||||
cls.planned_events |= event_form.save()
|
||||
|
||||
def _create_sale_order_from_planner(self, event_planner_id):
|
||||
so_form = Form(
|
||||
self.SaleOrder.with_context(
|
||||
default_user_id=event_planner_id.user_id.id,
|
||||
default_sale_planner_calendar_event_id=event_planner_id.id,
|
||||
default_partner_id=event_planner_id.target_partner_id.id,
|
||||
)
|
||||
)
|
||||
with so_form.order_line.new() as line_form:
|
||||
line_form.product_id = self.product
|
||||
line_form.product_uom_qty = 1
|
||||
line_form.tax_id.remove(index=0)
|
||||
return so_form.save()
|
||||
|
||||
def _create_invoice(self, partner):
|
||||
with Form(
|
||||
self.env["account.move"].with_context(default_move_type="out_invoice")
|
||||
) as invoice_form:
|
||||
invoice_form.partner_id = partner
|
||||
with invoice_form.invoice_line_ids.new() as line_form:
|
||||
line_form.name = "invoice test"
|
||||
line_form.account_id = self.account
|
||||
line_form.quantity = 1.0
|
||||
line_form.price_unit = 100.00
|
||||
line_form.tax_ids.remove(index=0)
|
||||
return invoice_form.save()
|
||||
|
||||
def test_create_calendar_planner_event(self):
|
||||
# Test the values for one planned recurrent event created
|
||||
event = self.planned_events[0]
|
||||
self.assertTrue(event.user_id in self.commercial_users)
|
||||
self.assertEqual(event.rrule_type, "weekly")
|
||||
self.assertEqual(
|
||||
event.location,
|
||||
event.target_partner_id._display_address(True).replace("\n", " "),
|
||||
)
|
||||
|
||||
def test_planner_calendar_wizard(self):
|
||||
wiz_form = Form(self.env["sale.planner.calendar.wizard"])
|
||||
# This user has three planned events
|
||||
wiz_form.user_id = self.commercial_user_1
|
||||
self.assertEqual(len(wiz_form.calendar_event_ids), 3)
|
||||
wiz_form.event_type_id = self.event_type_delivery
|
||||
self.assertEqual(len(wiz_form.calendar_event_ids), 1)
|
||||
wiz_form.event_type_id = self.event_type_commercial_visit
|
||||
self.assertEqual(len(wiz_form.calendar_event_ids), 2)
|
||||
|
||||
def test_summary_and_event_today(self):
|
||||
summary_obj = self.env["sale.planner.calendar.summary"]
|
||||
summary_form = Form(summary_obj)
|
||||
summary_form.user_id = self.commercial_user_1
|
||||
summary = summary_form.save()
|
||||
summary.action_process()
|
||||
self.assertEqual(summary.event_total_count, 2)
|
||||
|
||||
event_planner_id = summary.sale_planner_calendar_event_ids[0]
|
||||
# Create a new sale order from planner event
|
||||
self._create_sale_order_from_planner(event_planner_id)
|
||||
self.assertEqual(summary.sale_order_count, 1)
|
||||
self.assertEqual(summary.sale_order_subtotal, 100)
|
||||
self._create_sale_order_from_planner(event_planner_id)
|
||||
self.assertEqual(summary.sale_order_count, 2)
|
||||
self.assertEqual(summary.sale_order_subtotal, 200)
|
||||
# Create a new invoice from planner event
|
||||
self.invoice1 = self._create_invoice(event_planner_id.target_partner_id)
|
||||
self.invoice1.action_post()
|
||||
self.assertEqual(event_planner_id.invoice_amount_residual, 100)
|
||||
# Set event to done state
|
||||
event_planner_id.action_done()
|
||||
self.assertEqual(summary.event_total_count, 2)
|
||||
self.assertEqual(summary.event_done_count, 1)
|
||||
self.assertEqual(summary.event_effective_count, 1)
|
||||
|
||||
def test_reassign_wizard(self):
|
||||
wiz_form = Form(self.env["sale.planner.calendar.reassign.wiz"])
|
||||
wiz_form.user_id = self.commercial_user_1
|
||||
wiz_form.new_start = date.today()
|
||||
record = wiz_form.save()
|
||||
# Recover all planned event lines for commercial user 1
|
||||
record.action_get_lines()
|
||||
self.assertEqual(len(record.line_ids), 3)
|
||||
# Select line behaviour for update new commercial user
|
||||
wiz_form.new_user_id = self.commercial_user_2
|
||||
record = wiz_form.save()
|
||||
record.select_all_lines()
|
||||
record.action_assign_new_values()
|
||||
self.assertEqual(len(record.line_ids.mapped("new_user_id")), 1)
|
||||
wiz_form.new_user_id = self.commercial_user_2
|
||||
record = wiz_form.save()
|
||||
record.line_ids = False
|
||||
record.action_get_lines()
|
||||
record.line_ids[0].selected = True
|
||||
record.action_assign_new_values()
|
||||
self.assertEqual(len(record.line_ids.filtered(lambda ln: ln.new_user_id)), 1)
|
||||
|
||||
def test_reassign_wizard_apply(self):
|
||||
# When creating new recurring events for reallocated changes,
|
||||
# each event must have a new recurrence. This test is
|
||||
# incorporated to control that no event is left without recurrence.
|
||||
wiz_form = Form(self.env["sale.planner.calendar.reassign.wiz"])
|
||||
wiz_form.user_id = self.commercial_user_1
|
||||
wiz_form.assign_new_salesperson_to_partner = True
|
||||
wiz_form.new_start = date.today() + timedelta(days=8)
|
||||
wiz_form.new_end = wiz_form.new_start + timedelta(days=20)
|
||||
record = wiz_form.save()
|
||||
record.action_get_lines()
|
||||
record.line_ids[0].new_user_id = self.commercial_user_2
|
||||
old_event = record.line_ids[0].calendar_event_id
|
||||
recurrence_events = old_event.recurrence_id.calendar_event_ids
|
||||
new_base_event_start = recurrence_events.filtered(
|
||||
lambda ce: ce.start.date() >= record.new_start
|
||||
).sorted("start")[:1]
|
||||
self.assertTrue(new_base_event_start.recurrence_id)
|
||||
self.assertEqual(new_base_event_start.recurrence_id, old_event.recurrence_id)
|
||||
new_base_event_end = recurrence_events.filtered(
|
||||
lambda ce: ce.start.date() >= record.new_end
|
||||
).sorted("start")[:1]
|
||||
self.assertTrue(new_base_event_end.recurrence_id)
|
||||
self.assertEqual(new_base_event_end.recurrence_id, old_event.recurrence_id)
|
||||
record.apply()
|
||||
# Events created for changes must have a new recurrence created from the old event
|
||||
self.assertTrue(
|
||||
self.env["calendar.event"].browse(new_base_event_start.id).recurrence_id
|
||||
)
|
||||
self.assertNotEqual(
|
||||
self.env["calendar.event"].browse(new_base_event_start.id).recurrence_id,
|
||||
old_event.recurrence_id,
|
||||
)
|
||||
self.assertTrue(
|
||||
self.env["calendar.event"].browse(new_base_event_end.id).recurrence_id
|
||||
)
|
||||
self.assertNotEqual(
|
||||
self.env["calendar.event"].browse(new_base_event_end.id).recurrence_id,
|
||||
old_event.recurrence_id,
|
||||
)
|
||||
# The original event must maintain its recurrence
|
||||
self.assertEqual(
|
||||
self.env["calendar.event"].browse(old_event.id).recurrence_id,
|
||||
old_event.recurrence_id,
|
||||
)
|
||||
|
||||
def test_reassign_wizard_subscriptions(self):
|
||||
# Create a SO for partner 1 and user commercial 1
|
||||
sale_order = self._create_sale_order()
|
||||
invoice = self._create_invoice(self.partner_1)
|
||||
|
||||
# Check document permissions based on followers
|
||||
with self.assertRaises(AccessError):
|
||||
self.assertIsNotNone(
|
||||
sale_order.with_user(self.commercial_user_2).check_access_rule("read")
|
||||
)
|
||||
self.assertIsNotNone(
|
||||
sale_order.with_user(self.commercial_user_2).check_access_rule("write")
|
||||
)
|
||||
self.assertIsNotNone(
|
||||
invoice.with_user(self.commercial_user_2).check_access_rule("read")
|
||||
)
|
||||
self.assertIsNotNone(
|
||||
invoice.with_user(self.commercial_user_2).check_access_rule("write")
|
||||
)
|
||||
|
||||
wiz_form = Form(self.env["sale.planner.calendar.reassign.wiz"])
|
||||
wiz_form.user_id = self.commercial_user_1
|
||||
wiz_form.new_start = date.today()
|
||||
record = wiz_form.save()
|
||||
# Recover all planned event lines for commercial user 1
|
||||
record.action_get_lines()
|
||||
wiz_form.new_user_id = self.commercial_user_2
|
||||
record = wiz_form.save()
|
||||
event_planner_partner_1 = record.line_ids.filtered(
|
||||
lambda ln: ln.partner_id == self.partner_1
|
||||
)
|
||||
event_planner_partner_1.selected = True
|
||||
record.action_assign_new_values()
|
||||
record.apply()
|
||||
# Check document permissions based on followers
|
||||
# Sale order
|
||||
self.assertIsNone(
|
||||
sale_order.with_user(self.commercial_user_2).check_access_rule("read")
|
||||
)
|
||||
self.assertIsNone(
|
||||
sale_order.with_user(self.commercial_user_2).check_access_rule("write")
|
||||
)
|
||||
# Account move (Invoice)
|
||||
self.assertIsNone(
|
||||
invoice.with_user(self.commercial_user_2).check_access_rule("read")
|
||||
)
|
||||
self.assertIsNone(
|
||||
invoice.with_user(self.commercial_user_2).check_access_rule("write")
|
||||
)
|
||||
|
||||
def test_parter_sale_order(self):
|
||||
"""User can setup a system parameter to create sale order from a event planner
|
||||
for a event planner partner or commercial partner
|
||||
"""
|
||||
sale_planned_event = self.planned_events.filtered(
|
||||
lambda p: p.target_partner_id == self.partner_3
|
||||
)[:1]
|
||||
so_action = sale_planned_event.action_open_sale_order()
|
||||
self.assertEqual(so_action["context"]["default_partner_id"], self.partner_3.id)
|
||||
# Set parameter to create sale order to commercial partner
|
||||
self.env["ir.config_parameter"].sudo().set_param(
|
||||
"sale_planner_calendar.create_so_to_commercial_partner", "True"
|
||||
)
|
||||
so_action = sale_planned_event.action_open_sale_order()
|
||||
self.assertEqual(
|
||||
so_action["context"]["default_partner_id"],
|
||||
self.partner_3.commercial_partner_id.id,
|
||||
)
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_calendar_event_type_tree" model="ir.ui.view">
|
||||
<field name="name">calendar.event.type</field>
|
||||
<field name="model">calendar.event.type</field>
|
||||
<field name="inherit_id" ref="calendar.view_calendar_event_type_tree" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="name" position="after">
|
||||
<field name="duration" />
|
||||
<field name="icon" />
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="view_calendar_event_tree" model="ir.ui.view">
|
||||
<field name="model">calendar.event</field>
|
||||
<field name="inherit_id" ref="calendar.view_calendar_event_tree" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="duration" position="after">
|
||||
<field name="until" optional="hide" />
|
||||
<field name="recurrence_id" invisible="1" />
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_calendar_event_tree_primary_only" model="ir.ui.view">
|
||||
<field name="name">calendar.event.primary.only</field>
|
||||
<field name="model">calendar.event</field>
|
||||
<field name="priority" eval="9999" />
|
||||
<field name="arch" type="xml">
|
||||
<tree default_order="start">
|
||||
<field name="user_id" optional="show" />
|
||||
<field name="name" string="Subject" optional="show" />
|
||||
<field name="target_partner_id" optional="hide" />
|
||||
<field name="start" string="Start Date" optional="show" />
|
||||
<field name="until" optional="hide" />
|
||||
<field
|
||||
name="categ_ids"
|
||||
widget="many2many_tags"
|
||||
optional="show"
|
||||
options="{'color_field': 'color', 'no_create_edit': True}"
|
||||
/>
|
||||
<field name="location" optional="hide" />
|
||||
<field name="duration" widget="float_time" optional="hide" />
|
||||
<field name="recurrency" optional="hide" />
|
||||
<field name="recurrence_id" invisible="1" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="calendar_event_tree_primary_only_action" model="ir.actions.act_window">
|
||||
<field name="name">Recurrent calendar events</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">calendar.event</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_id" ref="view_calendar_event_tree_primary_only" />
|
||||
<field name="context">{
|
||||
'default_recurrency':True,
|
||||
'search_default_user_id':uid,
|
||||
'choose_unlink_method': True,
|
||||
}</field>
|
||||
<field name="domain">[('recurrency', '=', True),
|
||||
("is_base_recurrent_event", "=", True), ('target_partner_id', '!=', False), '|', ('recurrence_id.until', '>=', datetime.date.today().strftime("%Y-%m-%d")), ('recurrence_id.until', '=', False)]</field>
|
||||
</record>
|
||||
|
||||
<record id="view_calendar_event_search" model="ir.ui.view">
|
||||
<field name="model">calendar.event</field>
|
||||
<field name="inherit_id" ref="calendar.view_calendar_event_search" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="partner_ids" position="before">
|
||||
<field name="target_partner_id" />
|
||||
</field>
|
||||
<filter name="inactive" position="before">
|
||||
<separator />
|
||||
<filter
|
||||
string="Today"
|
||||
name="start_today"
|
||||
domain="[('start','>=', time.strftime('%%Y-%%m-%%d 00:00:00')),('start','<',(datetime.date.today() + datetime.timedelta(days=1)).strftime('%%Y-%%m-%%d 00:00:00'))]"
|
||||
/>
|
||||
<filter
|
||||
string="Tomorrow"
|
||||
name="start_tomorrow"
|
||||
domain="[('start','>', time.strftime('%%Y-%%m-%%d 23:59:59')),('start','<',(datetime.date.today() + datetime.timedelta(days=2)).strftime('%%Y-%%m-%%d 00:00:00'))]"
|
||||
/>
|
||||
<separator />
|
||||
</filter>
|
||||
<filter name="responsible" position="after">
|
||||
<filter
|
||||
string="Sale planner partner"
|
||||
name="group_by_target_partner"
|
||||
domain="[]"
|
||||
context="{'group_by': 'target_partner_id'}"
|
||||
/>
|
||||
<filter
|
||||
string="Tags"
|
||||
name="group_by_tags"
|
||||
domain="[]"
|
||||
context="{'group_by': 'categ_ids'}"
|
||||
/>
|
||||
</filter>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_calendar_event_form" model="ir.ui.view">
|
||||
<field name="model">calendar.event</field>
|
||||
<field name="inherit_id" ref="calendar.view_calendar_event_form" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="user_id" position="after">
|
||||
<label for="target_partner_id" string="Sale planner partner" />
|
||||
<field name="target_partner_id" nolabel="1" />
|
||||
</field>
|
||||
<field name="recurrence_id" position="after">
|
||||
<group attrs="{'invisible': [('recurrency', '=', False)]}">
|
||||
<field name="advanced_cycle" />
|
||||
<field
|
||||
name="cycle_number"
|
||||
attrs="{'invisible': [('advanced_cycle', '=', False)]}"
|
||||
/>
|
||||
<field
|
||||
name="cycle_skip"
|
||||
attrs="{'invisible': [('advanced_cycle', '=', False)]}"
|
||||
/>
|
||||
</group>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- TODO: Review to be removed -->
|
||||
<record id="view_calendar_event_kanban" model="ir.ui.view">
|
||||
<field name="name">calendar.event.kanban</field>
|
||||
<field name="model">calendar.event</field>
|
||||
<field name="priority" eval="32" />
|
||||
<field name="arch" type="xml">
|
||||
<kanban
|
||||
class="o_kanban_mobile"
|
||||
default_order="start"
|
||||
quick_create_view="view_sale_planner_calendar_form"
|
||||
on_create="action_sale_planner_calendar_event"
|
||||
>
|
||||
<field name="name" />
|
||||
<field name="target_partner_id" />
|
||||
<field name="attendee_ids" />
|
||||
<field name="categ_ids" />
|
||||
<field name="start" />
|
||||
<field name="start_date" />
|
||||
<field name="target_partner_mobile" />
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_card oe_kanban_global_click">
|
||||
<div class="row mb4">
|
||||
<div class="col-8">
|
||||
<strong><span><t
|
||||
t-esc="record.start.value"
|
||||
/></span></strong>
|
||||
</div>
|
||||
<div class="col-4 text-end">
|
||||
<field
|
||||
name="categ_ids"
|
||||
widget="many2many_tags"
|
||||
options="{'color_field': 'color'}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-8">
|
||||
<field name="target_partner_id" />
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="float-end">
|
||||
<div t-if="record.target_partner_mobile.value">
|
||||
<span title="Mobile">
|
||||
<i class="fa fa-phone" />
|
||||
<field
|
||||
name="target_partner_mobile"
|
||||
widget="phone"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_calendar_event" model="ir.actions.act_window">
|
||||
<field name="name">My Calendar</field>
|
||||
<field name="res_model">calendar.event</field>
|
||||
<field name="view_mode">calendar,tree,kanban,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Schedule a new calendar event
|
||||
</p>
|
||||
</field>
|
||||
<field
|
||||
name="search_view_id"
|
||||
ref="sale_planner_calendar.view_calendar_event_search"
|
||||
/>
|
||||
<field name="context">{
|
||||
'search_default_start_today': 1,
|
||||
'choose_unlink_method': True,
|
||||
}</field>
|
||||
<field
|
||||
name="domain"
|
||||
>[('user_id', '=', uid), ('target_partner_id', '!=', False)]</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="res_config_settings_view_form" model="ir.ui.view">
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="sale.res_config_settings_view_form" />
|
||||
<field name="arch" type="xml">
|
||||
<div data-key="sale_management" position="inside">
|
||||
<h2>Sale Planner Calendar</h2>
|
||||
<div class="row mt16 o_settings_container">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="susbscriptions_backward_days" />
|
||||
<div class="text-muted">
|
||||
Backward days to search documents to update subscriptions
|
||||
</div>
|
||||
<div class="content-group">
|
||||
<div class="mt16">
|
||||
<field
|
||||
name="susbscriptions_backward_days"
|
||||
class="o_light_label"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="sale_planner_forward_months" />
|
||||
<div class="text-muted">
|
||||
Forward months to create calendar events
|
||||
</div>
|
||||
<div class="content-group">
|
||||
<div class="mt16">
|
||||
<field
|
||||
name="sale_planner_forward_months"
|
||||
class="o_light_label"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="sale_planner_mail_to_attendees" />
|
||||
<div class="text-muted">
|
||||
Send invitations to attendees when a planner event is created
|
||||
</div>
|
||||
<div class="content-group">
|
||||
<div class="mt16">
|
||||
<field
|
||||
name="sale_planner_mail_to_attendees"
|
||||
class="o_light_label"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="sale_planner_order_cut_hour" />
|
||||
<div class="text-muted">
|
||||
Time of the next day until which orders of the current day are assigned
|
||||
</div>
|
||||
<div class="content-group">
|
||||
<div class="mt16">
|
||||
<field
|
||||
name="sale_planner_order_cut_hour"
|
||||
class="o_light_label"
|
||||
widget="float_time"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="sale_planner_calendar_max_duration" />
|
||||
<div class="text-muted">
|
||||
Show a warning message when duration is more than this time.
|
||||
Set 00:00 to disable warning.
|
||||
</div>
|
||||
<div class="content-group">
|
||||
<div class="mt16">
|
||||
<field
|
||||
name="sale_planner_calendar_max_duration"
|
||||
class="o_light_label"
|
||||
widget="float_time"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record model="ir.ui.view" id="view_partner_form">
|
||||
<field name="model">res.partner</field>
|
||||
<field name="type">form</field>
|
||||
<field name="inherit_id" ref="base.view_partner_form" />
|
||||
<field name="arch" type="xml">
|
||||
<div name="button_box" position="inside">
|
||||
<button
|
||||
class="oe_stat_button"
|
||||
name="action_calendar_planner"
|
||||
icon="fa-calendar"
|
||||
type="object"
|
||||
>
|
||||
<span class="o_stat_text">Planner</span>
|
||||
</button>
|
||||
</div>
|
||||
<group name="sale" position="inside">
|
||||
<field
|
||||
name="is_sale_planner_contact"
|
||||
attrs="{'invisible': [('parent_id', '=', False)]}"
|
||||
/>
|
||||
</group>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,636 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_sale_planner_calendar_search" model="ir.ui.view">
|
||||
<field name="name">sale.planner.calendar.event</field>
|
||||
<field name="model">calendar.event</field>
|
||||
<field name="priority">40</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search sale planner calendar events">
|
||||
<field
|
||||
name="name"
|
||||
string="Meeting"
|
||||
filter_domain="[('name', 'ilike', self)]"
|
||||
/>
|
||||
<field name="target_partner_id" />
|
||||
<field name="user_id" string="Planner salesperson" />
|
||||
<field name="partner_user_id" />
|
||||
<field name="categ_ids" />
|
||||
<field name="partner_ids" />
|
||||
<field name="description" />
|
||||
<separator />
|
||||
<field name="sale_planner_state" />
|
||||
<filter
|
||||
string="My events"
|
||||
name="my_event_planner"
|
||||
domain="[('user_id','=', uid)]"
|
||||
/>
|
||||
<separator />
|
||||
<filter
|
||||
string="Pending"
|
||||
name="state_pending"
|
||||
domain="[('sale_planner_state','=', 'pending')]"
|
||||
/>
|
||||
<filter
|
||||
string="Done"
|
||||
name="state_done"
|
||||
domain="[('sale_planner_state','=', 'done')]"
|
||||
/>
|
||||
<separator />
|
||||
<filter
|
||||
string="Past"
|
||||
name="planner_overdue"
|
||||
domain="[('start','<=', time.strftime('%%Y-%%m-%%d 00:00:00'))]"
|
||||
/>
|
||||
<filter
|
||||
string="Today"
|
||||
name="planner_today"
|
||||
domain="[('start','>=', time.strftime('%%Y-%%m-%%d 00:00:00')),('start','<',(datetime.date.today() + datetime.timedelta(days=1)).strftime('%%Y-%%m-%%d 00:00:00'))]"
|
||||
/>
|
||||
<filter
|
||||
string="Tomorrow"
|
||||
name="planner_tomorrow"
|
||||
domain="[('start','>', time.strftime('%%Y-%%m-%%d 23:59:59')),('start','<',(datetime.date.today() + datetime.timedelta(days=2)).strftime('%%Y-%%m-%%d 00:00:00'))]"
|
||||
/>
|
||||
<filter
|
||||
string="Future"
|
||||
name="planner_upcoming_all"
|
||||
domain="[('start','>', time.strftime('%%Y-%%m-%%d 23:59:59'))]"
|
||||
/>
|
||||
<separator />
|
||||
<group expand="0" string="Group By">
|
||||
<filter
|
||||
string="Planner salesperson"
|
||||
name="responsible"
|
||||
domain="[]"
|
||||
context="{'group_by': 'user_id'}"
|
||||
/>
|
||||
<filter
|
||||
string="Sale planner partner"
|
||||
name="group_by_target_partner"
|
||||
domain="[]"
|
||||
context="{'group_by': 'target_partner_id'}"
|
||||
/>
|
||||
<filter
|
||||
string="Tags"
|
||||
name="group_by_tags"
|
||||
domain="[]"
|
||||
context="{'group_by': 'categ_ids'}"
|
||||
/>
|
||||
<filter
|
||||
string="Start"
|
||||
name="group_by_start_day"
|
||||
domain="[]"
|
||||
context="{'group_by':'start:day'}"
|
||||
/>
|
||||
<filter
|
||||
string="Rating"
|
||||
name="group_by_planner_rating"
|
||||
domain="[]"
|
||||
context="{'group_by': 'sale_planner_rating'}"
|
||||
/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_sale_planner_calendar_tree" model="ir.ui.view">
|
||||
<field name="name">sale.planner.calendar.event.tree</field>
|
||||
<field name="model">calendar.event</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree js_class="sale_planner_calendar_event_tree">
|
||||
<field name="start" optional="show" />
|
||||
<field name="user_id" string="Planner salesperson" optional="show" />
|
||||
<field name="target_partner_id" optional="show" />
|
||||
<field name="partner_user_id" optional="hide" />
|
||||
<field name="name" optional="show" />
|
||||
<field
|
||||
name="categ_ids"
|
||||
widget="many2many_tags"
|
||||
optional="hide"
|
||||
options="{'color_field': 'color', 'no_create_edit': True}"
|
||||
/>
|
||||
<field name="sale_order_subtotal" optional="show" />
|
||||
<field name="sale_planner_state" optional="show" />
|
||||
<field name="sale_planner_rating" optional="hide" />
|
||||
<field name="sale_planner_currency_id" invisible="1" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_sale_planner_calendar_form" model="ir.ui.view">
|
||||
<field name="name">sale.planner.calendar.event.form</field>
|
||||
<field name="model">calendar.event</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Sale planner calendar event">
|
||||
<sheet>
|
||||
<div class="oe_button_box d-flex" name="button_box">
|
||||
<button
|
||||
string="Done"
|
||||
class="oe_stat_button"
|
||||
name="action_done"
|
||||
type="object"
|
||||
attrs="{'invisible':[('sale_planner_state', 'not in', ['pending'])]}"
|
||||
icon="fa-check"
|
||||
>
|
||||
</button>
|
||||
<button
|
||||
class="oe_stat_button border-end"
|
||||
name="action_cancel"
|
||||
string="Cancel"
|
||||
type="object"
|
||||
attrs="{'invisible':[('sale_planner_state', 'in', ['cancel', 'done'])]}"
|
||||
icon="fa-close"
|
||||
/>
|
||||
<button
|
||||
string="Pending"
|
||||
class="oe_stat_button border-end"
|
||||
name="action_pending"
|
||||
type="object"
|
||||
attrs="{'invisible':[('sale_planner_state', 'in', ['pending'])]}"
|
||||
icon="fa-pencil-square-o"
|
||||
/>
|
||||
<button
|
||||
class="oe_stat_button ms-auto"
|
||||
name="action_open_sale_order"
|
||||
type="object"
|
||||
icon="fa-dollar"
|
||||
>
|
||||
<div class="o_form_field o_stat_info">
|
||||
<span class="o_stat_value">
|
||||
<field name="sale_order_subtotal" />
|
||||
</span>
|
||||
<span class="o_stat_text">Sales</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
class="oe_stat_button"
|
||||
name="action_open_unpaid_invoice"
|
||||
type="object"
|
||||
icon="fa-pencil-square-o"
|
||||
>
|
||||
<div class="o_form_field o_stat_info">
|
||||
<span class="o_stat_value">
|
||||
<field name="invoice_amount_residual" />
|
||||
</span>
|
||||
<span class="o_stat_text">Invoices</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="name" />
|
||||
<field name="target_partner_id" />
|
||||
<!-- TODO: Add attrs="{'invisible': [('user_id', '=', partner_user_id)]}" -->
|
||||
<field name="partner_user_id" />
|
||||
<field name="start" />
|
||||
<field name="user_id" string="Planner salesperson" />
|
||||
<field name="sale_planner_currency_id" invisible="1" />
|
||||
<field name="sale_planner_state" invisible="1" />
|
||||
<field name="sale_order_subtotal" invisible="1" />
|
||||
</group>
|
||||
<group>
|
||||
<field
|
||||
name="alarm_ids"
|
||||
widget="many2many_tags"
|
||||
options="{'no_quick_create': True}"
|
||||
/>
|
||||
<field
|
||||
name="categ_ids"
|
||||
widget="many2many_tags"
|
||||
options="{'color_field': 'color', 'no_create_edit': True}"
|
||||
/>
|
||||
<field name="calendar_issue_type_id" widget="selection" />
|
||||
<field name="comment" />
|
||||
<div class="display-5" colspan="2">
|
||||
<field
|
||||
name="sale_planner_rating"
|
||||
widget="selection_badge"
|
||||
/>
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids" />
|
||||
<field name="message_ids" />
|
||||
</div>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_sale_planner_calendar_issue_form" model="ir.ui.view">
|
||||
<field name="name">sale.planner.calendar.issue.form</field>
|
||||
<field name="model">calendar.event</field>
|
||||
<field name="priority" eval="999" />
|
||||
<field name="arch" type="xml">
|
||||
<form string="Sale planner calendar event">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="target_partner_id" readonly="1" />
|
||||
</group>
|
||||
<group>
|
||||
<field name="calendar_issue_type_id" widget="selection_badge" />
|
||||
</group>
|
||||
<group>
|
||||
<field name="comment" />
|
||||
</group>
|
||||
<footer invisible="context.get('hide_footer', False)">
|
||||
<button
|
||||
name="action_apply_issue"
|
||||
type="object"
|
||||
string="Apply"
|
||||
class="btn-primary"
|
||||
/>
|
||||
<button
|
||||
string="Cancel"
|
||||
class="btn-secondary"
|
||||
special="cancel"
|
||||
/>
|
||||
</footer>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_sale_planner_calendar_issue_tree" model="ir.ui.view">
|
||||
<field name="name">sale.planner.calendar.issue.tree</field>
|
||||
<field name="model">calendar.event</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="start" optional="show" />
|
||||
<field name="user_id" string="Planner salesperson" optional="show" />
|
||||
<field name="target_partner_id" optional="show" />
|
||||
<field name="calendar_issue_type_id" />
|
||||
<field name="comment" />
|
||||
<field name="sale_planner_state" optional="show" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_sale_planner_calendar_issue" model="ir.actions.act_window">
|
||||
<field name="name">Sale planner calendar issue</field>
|
||||
<field name="res_model">calendar.event</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
<field
|
||||
name="context"
|
||||
>{'form_view_ref': 'sale_planner_calendar.view_sale_planner_calendar_issue_form'}</field>
|
||||
</record>
|
||||
|
||||
<record id="action_sale_planner_calendar_issue_tree" model="ir.actions.act_window">
|
||||
<field name="name">Sale planner calendar issue</field>
|
||||
<field name="res_model">calendar.event</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field
|
||||
name="context"
|
||||
>{'form_view_ref': 'sale_planner_calendar.view_sale_planner_calendar_issue_form',
|
||||
'tree_view_ref': 'sale_planner_calendar.view_sale_planner_calendar_issue_tree',
|
||||
'hide_footer': True
|
||||
}</field>
|
||||
</record>
|
||||
|
||||
<record id="view_sale_planner_calendar_rating_form" model="ir.ui.view">
|
||||
<field name="name">sale.planner.calendar.rating.form</field>
|
||||
<field name="model">calendar.event</field>
|
||||
<field name="priority" eval="999" />
|
||||
<field name="arch" type="xml">
|
||||
<form string="Sale planner calendar event">
|
||||
<group>
|
||||
<field name="sale_planner_rating" invisible="1" />
|
||||
<field name="target_partner_id" readonly="1" />
|
||||
</group>
|
||||
<group colspan="4">
|
||||
<label for="comment" />
|
||||
<field colspan="2" name="comment" class="border" nolabel="1" />
|
||||
</group>
|
||||
<div class="display-1 text-center" colspan="2">
|
||||
<field name="sale_planner_rating" widget="selection_badge" />
|
||||
</div>
|
||||
<footer>
|
||||
<!-- TODO: Remove this button when click in badge closes wizard -->
|
||||
<button
|
||||
string="Accept"
|
||||
name="action_set_sale_planner_rating"
|
||||
type="object"
|
||||
class="oe_highlight"
|
||||
/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_sale_planner_calendar_rating" model="ir.actions.act_window">
|
||||
<field name="name">Sale planner calendar rating</field>
|
||||
<field name="res_model">calendar.event</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
<field name="context">
|
||||
{'form_view_ref': 'sale_planner_calendar.view_sale_planner_calendar_rating_form'}
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_sale_planner_calendar_kanban" model="ir.ui.view">
|
||||
<field name="name">sale.planner.calendar.event.kanban</field>
|
||||
<field name="model">calendar.event</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban
|
||||
class="o_kanban_mobile o_sale_planner_calendar_kanban"
|
||||
default_order="start"
|
||||
js_class="sale_planner_calendar_event_kanban"
|
||||
>
|
||||
<field name="name" />
|
||||
<field name="target_partner_id" />
|
||||
<field name="sale_planner_state" />
|
||||
<field name="user_id" />
|
||||
<field name="start" />
|
||||
<field name="sale_ids" />
|
||||
<field name="partner_ref" />
|
||||
<field name="partner_name" />
|
||||
<field name="partner_commercial_name" />
|
||||
<field name="partner_street" />
|
||||
<field name="partner_city" />
|
||||
<field name="partner_mobile" />
|
||||
<field name="partner_contact_name" />
|
||||
<field name="sanitized_partner_mobile" />
|
||||
<field name="location_url" />
|
||||
<field name="calendar_issue_type_id" />
|
||||
<field name="sale_planner_currency_id" />
|
||||
<field name="comment" />
|
||||
<field name="sale_planner_rating" />
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div t-attf-class="oe_kanban_card w-100 oe_kanban_global_click">
|
||||
<div class="row align-items-center">
|
||||
<div class="col col-lg-2 col-xl-2 pe-0 text-center">
|
||||
<div class="row" title="Clock">
|
||||
<div class="col text-center">
|
||||
<i class="fa fa-clock-o fa-2x" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<field name="start" class="fw-bold" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<field
|
||||
name="categ_icons"
|
||||
widget="categ_icons_widget"
|
||||
/>
|
||||
<field
|
||||
name="sale_planner_state"
|
||||
widget="label_selection"
|
||||
class="btn btn-danger btn-lg p-0 pb-1"
|
||||
attrs="{'invisible': [('sale_planner_state', '!=', 'cancel')]}"
|
||||
options="{'classes': {'cancel': 'danger'}}"
|
||||
/>
|
||||
<button
|
||||
name="action_open_rating"
|
||||
type="object"
|
||||
attrs="{'invisible': [('sale_planner_state', '=', 'cancel')]}"
|
||||
title="Change calendar event status by adding a previous rating"
|
||||
t-att-class="'btn p-0 px-1 btn-lg text-capitalize ' + (record.sale_planner_state.raw_value == 'done' and 'btn-success' or 'btn-info')"
|
||||
>
|
||||
<span
|
||||
class="badge pb-2"
|
||||
t-esc="record.sale_planner_state.raw_value"
|
||||
/>
|
||||
<span
|
||||
class="h3"
|
||||
t-esc="record.sale_planner_rating.value"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-lg-4 col-xl-4 pe-0">
|
||||
<div class="row fw-bold">
|
||||
<div
|
||||
class="col-1 text-center p-0"
|
||||
title="Partner"
|
||||
>
|
||||
<i class="fa fa-user fa-fw" />
|
||||
</div>
|
||||
<div class="col">
|
||||
<field name="partner_ref" /> <field
|
||||
name="partner_name"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="col-2"
|
||||
t-if="record.partner_user_id and record.partner_user_id.raw_value != record.user_id.raw_value"
|
||||
>
|
||||
<field
|
||||
name="partner_user_id"
|
||||
widget="many2one_avatar_user"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div
|
||||
class="col-1 text-center p-0"
|
||||
title="Commercial name"
|
||||
>
|
||||
<i class="fa fa-home fa-fw" />
|
||||
</div>
|
||||
<div class="col">
|
||||
<field name="partner_commercial_name" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="row">
|
||||
<div
|
||||
class="col-1 text-center p-0"
|
||||
title="Street"
|
||||
>
|
||||
<i class="fa fa-map-marker fa-fw" />
|
||||
</div>
|
||||
<div class="col-11">
|
||||
<field name="partner_street" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="row">
|
||||
<div
|
||||
class="col-1 text-center p-0"
|
||||
title="City"
|
||||
>
|
||||
<i class="fa fa-building-o fa-fw" />
|
||||
</div>
|
||||
<div class="col-11">
|
||||
<field name="partner_city" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div
|
||||
class="col-1 text-center p-0"
|
||||
title="Mobile"
|
||||
>
|
||||
<i class="fa fa-phone fa-fw" />
|
||||
</div>
|
||||
<div class="col">
|
||||
<t t-if="record.partner_mobile.value">
|
||||
<field
|
||||
name="partner_mobile"
|
||||
widget="phone"
|
||||
/>
|
||||
</t>
|
||||
<t t-if="record.partner_contact_name.value">
|
||||
(<field name="partner_contact_name" />)
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="col col-sm-12 col-md-12 col-lg-6 col-xl-6 mt-xs-3"
|
||||
>
|
||||
<div class="row mt-2 h-100 px-md-3">
|
||||
<div class="col p-1">
|
||||
<button
|
||||
class="btn btn-primary w-100 h-80"
|
||||
title="Info"
|
||||
>
|
||||
<i
|
||||
t-attf-class="fa fa-info fa-fw fa-2x #{record.comment.raw_value ? 'text-warning' : ''}"
|
||||
/>
|
||||
<div>Info</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col p-1">
|
||||
<button
|
||||
class="btn btn-primary w-100 h-80"
|
||||
name="action_open_sale_order"
|
||||
type="object"
|
||||
title="Order"
|
||||
>
|
||||
<i class="fa fa-dollar fa-fw fa-2x" />
|
||||
<div>Order</div>
|
||||
</button>
|
||||
<div
|
||||
t-if="record.sale_order_subtotal.raw_value "
|
||||
class="text-center mt4"
|
||||
>
|
||||
<field
|
||||
class="text-center"
|
||||
name="sale_order_subtotal"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col p-1">
|
||||
<button
|
||||
class="btn btn-primary w-100 h-80"
|
||||
name="action_open_invoices"
|
||||
type="object"
|
||||
title="Invoices"
|
||||
>
|
||||
<i
|
||||
class="fa fa-pencil-square-o fa-fw fa-2x"
|
||||
/>
|
||||
<div class="o_stat_text">Invoices</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col p-1">
|
||||
<button
|
||||
class="btn btn-primary w-100 h-80"
|
||||
name="action_open_unpaid_invoice"
|
||||
type="object"
|
||||
title="Due"
|
||||
>
|
||||
<i
|
||||
class="fa fa-pencil-square-o fa-fw fa-2x"
|
||||
/>
|
||||
<div class="o_stat_text">Due</div>
|
||||
</button>
|
||||
<div
|
||||
t-if="record.invoice_amount_residual.raw_value "
|
||||
class="text-center mt4 text-danger fw-bold"
|
||||
>
|
||||
<field
|
||||
class="text-center"
|
||||
name="invoice_amount_residual"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
t-if="record.sanitized_partner_mobile.value"
|
||||
class="col p-1"
|
||||
>
|
||||
<a
|
||||
role="button"
|
||||
t-att-href="'https://wa.me/' + record.sanitized_partner_mobile.value"
|
||||
class="btn btn-primary w-100 h-80 text-white"
|
||||
title="Whatsapp"
|
||||
target="_blank"
|
||||
aria-label="Whatsapp"
|
||||
>
|
||||
<i class="fa fa-whatsapp fa-fw fa-2x" />
|
||||
<div class="o_stat_text">Whatsapp</div>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
t-if="record.location_url.value"
|
||||
class="col p-1"
|
||||
>
|
||||
<a
|
||||
role="button"
|
||||
t-att-href="'https://www.google.com/maps/search/?api=1&query=' + record.location_url.value"
|
||||
class="btn btn-primary w-100 h-80"
|
||||
title="Location"
|
||||
target="_blank"
|
||||
aria-label="Location"
|
||||
>
|
||||
<i
|
||||
class="fa fa fa-map-marker fa-fw fa-2x"
|
||||
/>
|
||||
<div class="o_stat_text">Location</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col p-1">
|
||||
<button
|
||||
t-attf-class="btn w-100 h-80 #{record.calendar_issue_type_id.raw_value ? 'btn-danger' : 'btn-primary'}"
|
||||
name="action_open_issue"
|
||||
type="object"
|
||||
title="Issue"
|
||||
>
|
||||
<i
|
||||
class="fa fa-exclamation-triangle fa-fw fa-2x"
|
||||
/>
|
||||
<div class="o_stat_text">Issue</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_sale_planner_calendar_event" model="ir.actions.act_window">
|
||||
<field name="name">Sale planner calendar events</field>
|
||||
<field name="res_model">calendar.event</field>
|
||||
<field name="view_mode">kanban,tree,form</field>
|
||||
<field
|
||||
name="search_view_id"
|
||||
ref="sale_planner_calendar.view_sale_planner_calendar_search"
|
||||
/>
|
||||
<field
|
||||
name="context"
|
||||
>{'search_default_my_event_planner': 1, 'search_default_planner_today': 1,
|
||||
'search_default_state_pending': 1,
|
||||
'tree_view_ref':'sale_planner_calendar.view_sale_planner_calendar_tree',
|
||||
'form_view_ref':'sale_planner_calendar.view_sale_planner_calendar_form',
|
||||
'kanban_view_ref':'sale_planner_calendar.view_sale_planner_calendar_kanban'
|
||||
}</field>
|
||||
<field name="domain">[('target_partner_id', '!=', False)]</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_sale_planner_calendar_issue_type_tree" model="ir.ui.view">
|
||||
<field name="name">sale.planner.calendar.issue.type.tree</field>
|
||||
<field name="model">sale.planner.calendar.issue.type</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree editable="bottom">
|
||||
<field name="name" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_sale_planner_calendar_issue_type" model="ir.actions.act_window">
|
||||
<field name="name">Issue types</field>
|
||||
<field name="res_model">sale.planner.calendar.issue.type</field>
|
||||
<field name="view_mode">tree</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<menuitem
|
||||
id="menu_sale_planner_calendar_root"
|
||||
name="Calendar planner"
|
||||
sequence="50"
|
||||
web_icon="sale_planner_calendar,static/description/icon.png"
|
||||
groups="sales_team.group_sale_salesman"
|
||||
/>
|
||||
<menuitem
|
||||
id="menu_sale_planner_calendar_summary_general"
|
||||
name="Planner"
|
||||
parent="menu_sale_planner_calendar_root"
|
||||
sequence="10"
|
||||
/>
|
||||
<!-- For sales managers display summaries list instead of form view -->
|
||||
<menuitem
|
||||
id="menu_sale_planner_calendar_summary_today_non_commercial"
|
||||
parent="menu_sale_planner_calendar_summary_general"
|
||||
action="action_sale_planner_calendar_summary"
|
||||
sequence="5"
|
||||
groups="sales_team.group_sale_salesman_all_leads"
|
||||
/>
|
||||
<menuitem
|
||||
id="menu_sale_planner_calendar_summary_today"
|
||||
parent="menu_sale_planner_calendar_summary_general"
|
||||
action="action_sale_planner_calendar_summary_today"
|
||||
sequence="10"
|
||||
/>
|
||||
<menuitem
|
||||
id="menu_sale_planner_calendar_summary"
|
||||
parent="menu_sale_planner_calendar_summary_general"
|
||||
action="action_sale_planner_calendar_summary"
|
||||
sequence="20"
|
||||
/>
|
||||
<menuitem
|
||||
id="menu_sale_planner_calendar_event"
|
||||
parent="menu_sale_planner_calendar_summary_general"
|
||||
action="action_sale_planner_calendar_event"
|
||||
sequence="30"
|
||||
/>
|
||||
<menuitem
|
||||
id="menu_sale_planner_calendar_customers"
|
||||
parent="menu_sale_planner_calendar_summary_general"
|
||||
action="account.res_partner_action_customer"
|
||||
sequence="40"
|
||||
/>
|
||||
<menuitem
|
||||
id="menu_calendar_event_root"
|
||||
parent="menu_sale_planner_calendar_root"
|
||||
name="Calendar events"
|
||||
sequence="20"
|
||||
/>
|
||||
|
||||
<menuitem
|
||||
id="menu_event_calendar"
|
||||
parent="menu_calendar_event_root"
|
||||
action="action_calendar_event"
|
||||
sequence="10"
|
||||
/>
|
||||
<menuitem
|
||||
id="menu_calendar_event_tree_primary_only"
|
||||
parent="menu_calendar_event_root"
|
||||
action="calendar_event_tree_primary_only_action"
|
||||
sequence="20"
|
||||
/>
|
||||
|
||||
<menuitem
|
||||
id="menu_calendar_event_configuration_root"
|
||||
parent="menu_sale_planner_calendar_root"
|
||||
name="Configuration"
|
||||
sequence="30"
|
||||
/>
|
||||
<menuitem
|
||||
id="menu_sale_planner_calendar_issue_type"
|
||||
parent="menu_calendar_event_configuration_root"
|
||||
action="action_sale_planner_calendar_issue_type"
|
||||
sequence="20"
|
||||
/>
|
||||
<menuitem
|
||||
id="menu_calendar_event_type"
|
||||
parent="menu_calendar_event_configuration_root"
|
||||
action="calendar.action_calendar_event_type"
|
||||
sequence="30"
|
||||
/>
|
||||
<menuitem
|
||||
id="menu_sale_planner_calendar_wizard"
|
||||
parent="menu_calendar_event_configuration_root"
|
||||
action="sale_planner_calendar_wizard_action"
|
||||
sequence="40"
|
||||
/>
|
||||
<menuitem
|
||||
id="menu_sale_planner_calendar_reassign_wiz"
|
||||
parent="menu_calendar_event_configuration_root"
|
||||
action="action_sale_planner_calendar_reassign_wiz"
|
||||
sequence="50"
|
||||
/>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,263 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_sale_planner_calendar_summary_search" model="ir.ui.view">
|
||||
<field name="name">sale.planner.calendar.summary</field>
|
||||
<field name="model">sale.planner.calendar.summary</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search sale planner calendar summary">
|
||||
<field name="state" />
|
||||
<field name="event_type_id" />
|
||||
<filter
|
||||
string="My summaries"
|
||||
name="my_event_planner_summary"
|
||||
domain="[('user_id','=', uid)]"
|
||||
/>
|
||||
<separator />
|
||||
<filter
|
||||
string="Today"
|
||||
name="start_today"
|
||||
domain="[('date','>=', time.strftime('%%Y-%%m-%%d 00:00:00')),('date','<',(datetime.date.today() + datetime.timedelta(days=1)).strftime('%%Y-%%m-%%d 00:00:00'))]"
|
||||
/>
|
||||
<filter
|
||||
string="Tomorrow"
|
||||
name="start_tomorrow"
|
||||
domain="[('date','>', time.strftime('%%Y-%%m-%%d 23:59:59')),('date','<',(datetime.date.today() + datetime.timedelta(days=2)).strftime('%%Y-%%m-%%d 00:00:00'))]"
|
||||
/>
|
||||
<group string="Group By">
|
||||
<filter
|
||||
name="event_type_id"
|
||||
string="Event type"
|
||||
context="{'group_by':'event_type_id'}"
|
||||
/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_sale_planner_calendar_summary_tree" model="ir.ui.view">
|
||||
<field name="name">sale.planner.calendar.summary.tree</field>
|
||||
<field name="model">sale.planner.calendar.summary</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="date" optional="show" />
|
||||
<field name="user_id" optional="show" />
|
||||
<field name="event_type_id" optional="show" />
|
||||
<field name="sale_order_subtotal" optional="show" />
|
||||
<field name="state" optional="show" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_sale_planner_calendar_summary_form" model="ir.ui.view">
|
||||
<field name="name">sale.planner.calendar.summary.form</field>
|
||||
<field name="model">sale.planner.calendar.summary</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Sale planner calendar summary">
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button
|
||||
class="oe_stat_button"
|
||||
name="action_event_planner"
|
||||
type="object"
|
||||
icon="fa-home"
|
||||
string="Events"
|
||||
/>
|
||||
<button
|
||||
class="oe_stat_button"
|
||||
name="action_open_sale_order"
|
||||
type="object"
|
||||
icon="fa-dollar"
|
||||
>
|
||||
<div class="o_form_field o_stat_info">
|
||||
<span class="o_stat_value">
|
||||
<field name="sale_order_subtotal" />
|
||||
</span>
|
||||
<span class="o_stat_text">Subtotal</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
class="oe_stat_button"
|
||||
name="action_open_payment_sheet"
|
||||
type="object"
|
||||
icon="fa-money"
|
||||
string="Payment Sheet"
|
||||
/>
|
||||
<button
|
||||
class="oe_stat_button"
|
||||
name="action_open_issue"
|
||||
type="object"
|
||||
icon="fa-exclamation-triangle"
|
||||
string="Issues"
|
||||
/>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field
|
||||
name="sale_planner_calendar_event_ids"
|
||||
invisible="1"
|
||||
/>
|
||||
<field
|
||||
name="date"
|
||||
attrs="{'readonly': [('sale_planner_calendar_event_ids', '!=', [])]}"
|
||||
/>
|
||||
<field
|
||||
name="user_id"
|
||||
attrs="{'readonly': [('sale_planner_calendar_event_ids', '!=', [])]}"
|
||||
/>
|
||||
<field name="currency_id" invisible="1" />
|
||||
</group>
|
||||
<group>
|
||||
<field name="state" invisible="1" />
|
||||
<field
|
||||
name="event_type_id"
|
||||
widget="selection"
|
||||
attrs="{'readonly': [('sale_planner_calendar_event_ids', '!=', [])]}"
|
||||
/>
|
||||
<field
|
||||
name="company_id"
|
||||
groups="base.group_multi_company"
|
||||
/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Totals">
|
||||
</group>
|
||||
<div name="Totals">
|
||||
<div class="row">
|
||||
<div class="col" />
|
||||
<div class="col h5">Total</div>
|
||||
<div class="col h5">Done</div>
|
||||
<div class="col h5">Effective</div>
|
||||
<div class="col h5">Off planning</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col h5">Events</div>
|
||||
<div class="col"><field name="event_total_count" /></div>
|
||||
<div class="col"><field name="event_done_count" /></div>
|
||||
<div class="col"><field
|
||||
name="event_effective_count"
|
||||
/></div>
|
||||
<div class="col"><field
|
||||
name="event_off_planning_count"
|
||||
/></div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col" />
|
||||
<div class="col h5">Documents</div>
|
||||
<div class="col h5">Amount</div>
|
||||
<div class="col" />
|
||||
<div class="col" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col h5">Orders</div>
|
||||
<div class="col"><field name="sale_order_count" /></div>
|
||||
<div class="col"><field name="sale_order_subtotal" /></div>
|
||||
<div class="col" />
|
||||
<div class="col" />
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col" />
|
||||
<div class="col h5">Documents</div>
|
||||
<div class="col h5">Amount</div>
|
||||
<div class="col" />
|
||||
<div class="col" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col h5">Payments</div>
|
||||
<div class="col"><field name="payment_count" /></div>
|
||||
<div class="col"><field name="payment_amount" /></div>
|
||||
<div class="col" />
|
||||
<div class="col" />
|
||||
</div>
|
||||
</div>
|
||||
<group string="Comment">
|
||||
<field name="comment" nolabel="1" />
|
||||
</group>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids" widget="mail_followers" />
|
||||
<field name="message_ids" widget="mail_thread" />
|
||||
</div>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_sale_planner_calendar_summary_kanban" model="ir.ui.view">
|
||||
<field name="name">sale.planner.calendar.event.kanban</field>
|
||||
<field name="model">sale.planner.calendar.summary</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban
|
||||
class="o_kanban_mobile o_sale_planner_calendar_kanban"
|
||||
default_order="date"
|
||||
>
|
||||
<field name="state" />
|
||||
<field name="user_id" />
|
||||
<field name="date" />
|
||||
<field name="sale_ids" />
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div t-attf-class="oe_kanban_card w-100 oe_kanban_global_click">
|
||||
<div class="row mb4">
|
||||
<div class="col-6">
|
||||
<strong><span><t
|
||||
t-esc="record.date.value"
|
||||
/></span></strong>
|
||||
</div>
|
||||
<div class="col-6 text-end">
|
||||
<field
|
||||
name="state"
|
||||
widget="label_selection"
|
||||
options="{'classes': {'pending': 'info', 'cancel': 'danger', 'done': 'success'}}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<span>Total orders: <field
|
||||
name="sale_order_subtotal"
|
||||
/></span>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="float-end">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
string="New order"
|
||||
name="action_open_sale_order"
|
||||
type="object"
|
||||
>
|
||||
<i class="fa fa-dollar" />
|
||||
<span>Order</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_sale_planner_calendar_summary" model="ir.actions.act_window">
|
||||
<field name="name">Sale planner calendar summary</field>
|
||||
<field name="res_model">sale.planner.calendar.summary</field>
|
||||
<field name="view_mode">tree,form,kanban</field>
|
||||
<field
|
||||
name="context"
|
||||
>{'search_default_start_today': 1, 'search_default_my_event_planner_summary': 1}</field>
|
||||
</record>
|
||||
<record id="action_sale_planner_calendar_summary_today" model="ir.actions.server">
|
||||
<field name="name">Event planner summary today</field>
|
||||
<field name="type">ir.actions.server</field>
|
||||
<field name="state">code</field>
|
||||
<field
|
||||
name="model_id"
|
||||
ref="sale_planner_calendar.model_sale_planner_calendar_summary"
|
||||
/>
|
||||
<field name="code">
|
||||
action = model.action_get_today_summary()
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
from . import sale_invoice_payment
|
||||
from . import sale_planner_calendar_reassign
|
||||
from . import sale_planner_calendar_wizard
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# Copyright 2020 Tecnativa - Carlos Dauden
|
||||
# Copyright 2020 Tecnativa - Sergio Teruel
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class SaleInvoicePaymentWiz(models.TransientModel):
|
||||
_inherit = "sale.invoice.payment.wiz"
|
||||
|
||||
sale_planner_calendar_event_id = fields.Many2one(comodel_name="calendar.event")
|
||||
|
||||
def _prepare_sheet_line_values(self, invoice, amount_pay):
|
||||
values = super()._prepare_sheet_line_values(invoice, amount_pay)
|
||||
values.update(
|
||||
{"sale_planner_calendar_event_id": self.sale_planner_calendar_event_id.id}
|
||||
)
|
||||
return values
|
||||
|
|
@ -0,0 +1,280 @@
|
|||
# Copyright 2021 Tecnativa - Sergio Teruel
|
||||
# Copyright 2021 Tecnativa - Carlos Dauden
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class SalePlannerCalendarReassignWiz(models.TransientModel):
|
||||
_name = "sale.planner.calendar.reassign.wiz"
|
||||
_description = "Sale planner calendar reassign wizard"
|
||||
_rec_name = "user_id"
|
||||
|
||||
user_id = fields.Many2one(
|
||||
comodel_name="res.users",
|
||||
string="Salesperson",
|
||||
default=lambda self: self.env.user,
|
||||
domain="[('share','=',False)]",
|
||||
)
|
||||
event_type_id = fields.Many2one(
|
||||
comodel_name="calendar.event.type", string="Event type"
|
||||
)
|
||||
week_list = fields.Selection(
|
||||
[
|
||||
("mon", "Monday"),
|
||||
("tue", "Tuesday"),
|
||||
("wed", "Wednesday"),
|
||||
("thu", "Thursday"),
|
||||
("fri", "Friday"),
|
||||
("sat", "Saturday"),
|
||||
("sun", "Sunday"),
|
||||
],
|
||||
string="Weekday",
|
||||
)
|
||||
partner_id = fields.Many2one(comodel_name="res.partner")
|
||||
partner_user_id = fields.Many2one(
|
||||
comodel_name="res.users",
|
||||
string="Partner salesperson",
|
||||
domain="[('share','=',False)]",
|
||||
)
|
||||
new_user_id = fields.Many2one(
|
||||
comodel_name="res.users",
|
||||
string="New salesperson",
|
||||
domain="[('share','=',False)]",
|
||||
)
|
||||
new_start = fields.Date()
|
||||
new_end = fields.Date()
|
||||
assign_new_salesperson_to_partner = fields.Boolean()
|
||||
unsuscribe_old_salesperson = fields.Boolean()
|
||||
line_ids = fields.One2many(
|
||||
comodel_name="sale.planner.calendar.reassign.line.wiz",
|
||||
inverse_name="reassign_wiz_id",
|
||||
)
|
||||
new_event_categ_ids = fields.Many2many(
|
||||
comodel_name="calendar.event.type",
|
||||
string="New tags",
|
||||
help="Forces new tags for the specified period. Leave empty to keep current tags.",
|
||||
)
|
||||
|
||||
@api.onchange(
|
||||
"user_id", "event_type_id", "week_list", "partner_user_id", "partner_id"
|
||||
)
|
||||
def onchange_filter_values(self):
|
||||
self.line_ids = False
|
||||
|
||||
def action_get_lines(self):
|
||||
domain = [
|
||||
("recurrency", "=", True),
|
||||
("recurrence_id.until", ">", self.new_start or fields.Date.today()),
|
||||
("is_base_recurrent_event", "=", True),
|
||||
("target_partner_id", "!=", False),
|
||||
]
|
||||
if self.user_id:
|
||||
domain.append(("user_id", "=", self.user_id.id))
|
||||
if self.partner_user_id:
|
||||
domain.append(("target_partner_id.user_id", "=", self.partner_user_id.id))
|
||||
if self.partner_id:
|
||||
domain.append(("target_partner_id", "=", self.partner_id.id))
|
||||
if self.event_type_id:
|
||||
domain.append(("categ_ids", "in", self.event_type_id.ids))
|
||||
if self.week_list:
|
||||
domain.append((self.week_list, "=", True))
|
||||
calendar_events = self.env["calendar.event"].search(domain)
|
||||
self.line_ids = False
|
||||
for calendar_event in calendar_events:
|
||||
self.env["sale.planner.calendar.reassign.line.wiz"].create(
|
||||
{
|
||||
"reassign_wiz_id": self.id,
|
||||
"calendar_event_id": calendar_event.id,
|
||||
# "event_categ_ids": [(6, 0, calendar_event.categ_ids.ids)],
|
||||
"event_user_id": calendar_event.user_id.id,
|
||||
"partner_id": calendar_event.target_partner_id.id,
|
||||
"partner_user_id": calendar_event.partner_user_id.id,
|
||||
"event_start": calendar_event.start,
|
||||
"until": calendar_event.until,
|
||||
}
|
||||
)
|
||||
|
||||
def action_assign_new_values(self):
|
||||
lines = self.line_ids.filtered("selected")
|
||||
lines.new_user_id = self.new_user_id
|
||||
lines.new_event_categ_ids = self.new_event_categ_ids
|
||||
lines.selected = False
|
||||
|
||||
def select_all_lines(self):
|
||||
self.line_ids.selected = True
|
||||
|
||||
def unselect_all_lines(self):
|
||||
self.line_ids.selected = False
|
||||
|
||||
def apply(self):
|
||||
# Not send emails to attendees in copy methods
|
||||
if not self.env.company.sale_planner_mail_to_attendees:
|
||||
self = self.with_context(no_mail_to_attendees=True, dont_notify=True)
|
||||
for line in self.line_ids:
|
||||
if not line.new_user_id:
|
||||
continue
|
||||
if self.assign_new_salesperson_to_partner:
|
||||
line.partner_id.with_context(
|
||||
skip_sale_planner_check=True
|
||||
).user_id = line.new_user_id
|
||||
# If new_start is empty only update partner user_id
|
||||
if not self.new_start:
|
||||
continue
|
||||
old_event = line.calendar_event_id
|
||||
recurrence_events = old_event.recurrence_id.calendar_event_ids
|
||||
if self.new_end:
|
||||
new_base_event_end = recurrence_events.filtered(
|
||||
lambda ce: ce.start.date() >= self.new_end
|
||||
).sorted("start")[:1]
|
||||
partner_ids = (
|
||||
new_base_event_end.partner_ids
|
||||
+ line.event_user_id.partner_id
|
||||
- line.new_user_id.partner_id
|
||||
).ids
|
||||
new_base_event_end.write(
|
||||
{
|
||||
"recurrence_update": "future_events",
|
||||
"user_id": line.event_user_id.id,
|
||||
"partner_ids": [
|
||||
(6, False, partner_ids),
|
||||
],
|
||||
"is_dynamic_end_date": old_event.is_dynamic_end_date,
|
||||
}
|
||||
)
|
||||
new_base_event_start = recurrence_events.filtered(
|
||||
lambda ce: ce.start.date() >= self.new_start
|
||||
).sorted("start")[:1]
|
||||
partner_ids = (
|
||||
new_base_event_start.partner_ids
|
||||
- line.event_user_id.partner_id
|
||||
+ line.new_user_id.partner_id
|
||||
).ids
|
||||
new_base_event_vals = {
|
||||
"recurrence_update": "future_events",
|
||||
"user_id": line.new_user_id.id,
|
||||
"partner_ids": [
|
||||
(6, False, partner_ids),
|
||||
],
|
||||
# Next fields has different behavior if 'self.new_end' field has a
|
||||
# value
|
||||
"is_dynamic_end_date": False
|
||||
if self.new_end
|
||||
else old_event.is_dynamic_end_date,
|
||||
"unsubscribe_date": self.new_end,
|
||||
}
|
||||
if line.new_event_categ_ids:
|
||||
new_base_event_vals["categ_ids"] = [
|
||||
(6, 0, line.new_event_categ_ids.ids)
|
||||
]
|
||||
new_base_event_start.write(new_base_event_vals)
|
||||
|
||||
old_event_vals = {
|
||||
"recurrence_update": "all_events",
|
||||
"is_dynamic_end_date": False,
|
||||
}
|
||||
if self.unsuscribe_old_salesperson and not self.new_end:
|
||||
old_event_vals["unsubscribe_date"] = self.new_start
|
||||
old_event.write(old_event_vals)
|
||||
line.update_subscriptions()
|
||||
self.action_get_lines()
|
||||
|
||||
@api.model
|
||||
def _unsubscribe(self, partner, user):
|
||||
if partner.user_id != user:
|
||||
partner.message_unsubscribe(partner_ids=user.partner_id.ids)
|
||||
unsubscribe_domain = [
|
||||
("message_partner_ids", "in", partner.ids),
|
||||
("user_id", "!=", user.id),
|
||||
"|",
|
||||
("partner_id", "=", partner.id),
|
||||
("partner_shipping_id", "=", partner.id),
|
||||
]
|
||||
self.env["sale.order"].search(unsubscribe_domain).message_unsubscribe(
|
||||
partner_ids=user.partner_id.ids
|
||||
)
|
||||
self.env["account.move"].search(unsubscribe_domain).message_unsubscribe(
|
||||
partner_ids=user.partner_id.ids
|
||||
)
|
||||
|
||||
@api.model
|
||||
def cron_unsubscribe(self, date=None):
|
||||
if date is None:
|
||||
date = fields.Date.today()
|
||||
events = self.env["calendar.event"].search(
|
||||
[
|
||||
("unsubscribe_date", "<=", date),
|
||||
]
|
||||
)
|
||||
for event in events:
|
||||
self._unsubscribe(event.target_partner_id, event.user_id)
|
||||
events.unsubscribe_date = False
|
||||
|
||||
|
||||
class SalePlannerCalendarReassignLineWiz(models.TransientModel):
|
||||
_name = "sale.planner.calendar.reassign.line.wiz"
|
||||
_description = "Sale planner calendar reassign lines wizard"
|
||||
|
||||
reassign_wiz_id = fields.Many2one(comodel_name="sale.planner.calendar.reassign.wiz")
|
||||
selected = fields.Boolean()
|
||||
calendar_event_id = fields.Many2one(comodel_name="calendar.event", readonly=True)
|
||||
event_categ_ids = fields.Many2many(
|
||||
comodel_name="calendar.event.type", related="calendar_event_id.categ_ids"
|
||||
)
|
||||
partner_id = fields.Many2one(comodel_name="res.partner", readonly=True)
|
||||
partner_user_id = fields.Many2one(
|
||||
comodel_name="res.users",
|
||||
string="Partner salesperson",
|
||||
)
|
||||
event_user_id = fields.Many2one(
|
||||
comodel_name="res.users",
|
||||
string="Planner salesperson",
|
||||
readonly=True,
|
||||
domain="[('share','=',False)]",
|
||||
)
|
||||
new_user_id = fields.Many2one(
|
||||
comodel_name="res.users",
|
||||
domain="[('share','=',False)]",
|
||||
)
|
||||
new_event_categ_ids = fields.Many2many(
|
||||
comodel_name="calendar.event.type",
|
||||
string="New tags",
|
||||
help="Forces new tags for the specified period. Leave empty to keep current tags.",
|
||||
)
|
||||
|
||||
event_start = fields.Datetime(readonly=True)
|
||||
until = fields.Datetime(readonly=True)
|
||||
|
||||
def update_subscriptions(self):
|
||||
for line in self:
|
||||
backward_date = fields.Date.today() - timedelta(
|
||||
days=self.env.company.susbscriptions_backward_days
|
||||
)
|
||||
sale_orders = self.env["sale.order"].search(
|
||||
[
|
||||
("date_order", ">=", backward_date),
|
||||
("partner_id", "=", line.partner_id.id),
|
||||
("user_id", "!=", line.new_user_id.id),
|
||||
]
|
||||
)
|
||||
invoices = (
|
||||
self.env["account.move"]
|
||||
.sudo()
|
||||
.search(
|
||||
[
|
||||
("move_type", "in", ["out_invoice", "out_refund"]),
|
||||
("user_id", "!=", line.new_user_id.id),
|
||||
"|",
|
||||
("invoice_date", ">=", backward_date),
|
||||
("payment_state", "!=", "paid"),
|
||||
"|",
|
||||
("partner_id", "=", line.partner_id.id),
|
||||
("partner_shipping_id", "=", line.partner_id.id),
|
||||
]
|
||||
)
|
||||
)
|
||||
for records in [line.partner_id, sale_orders, invoices]:
|
||||
records.message_subscribe(partner_ids=line.new_user_id.partner_id.ids)
|
||||
self.reassign_wiz_id.cron_unsubscribe()
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record model="ir.ui.view" id="view_sale_planner_calendar_reassign_wiz">
|
||||
<field name="name">sale.planner.calendar.reassign.wiz</field>
|
||||
<field name="model">sale.planner.calendar.reassign.wiz</field>
|
||||
<field name="type">form</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<group string="Filter values">
|
||||
<field name="user_id" />
|
||||
<field name="partner_user_id" />
|
||||
<field name="partner_id" />
|
||||
<field name="event_type_id" widget="selection" />
|
||||
<field name="week_list" />
|
||||
<div />
|
||||
<div class="ml-auto mt16">
|
||||
<button
|
||||
name="action_get_lines"
|
||||
type="object"
|
||||
string="Fill events from filter values"
|
||||
icon="fa-refresh"
|
||||
class="btn-light"
|
||||
attrs="{'invisible': [('line_ids', '=', [])]}"
|
||||
/>
|
||||
<button
|
||||
name="action_get_lines"
|
||||
type="object"
|
||||
string="Fill events from filter values"
|
||||
class="btn-primary"
|
||||
icon="fa-refresh"
|
||||
attrs="{'invisible': [('line_ids', '!=', [])]}"
|
||||
/>
|
||||
</div>
|
||||
</group>
|
||||
<group string="New values" col="1">
|
||||
<group>
|
||||
<field name="new_start" />
|
||||
<field name="new_end" />
|
||||
</group>
|
||||
<p class="text-muted">
|
||||
If the new end field is set, in addition to creating the recurring event assigned to the new commercial,
|
||||
a new recurring event will be created from the end date associated with the current commercial.
|
||||
</p>
|
||||
</group>
|
||||
<group />
|
||||
<group string="Optional actions" col="1">
|
||||
<group>
|
||||
<field name="assign_new_salesperson_to_partner" />
|
||||
<field
|
||||
name="unsuscribe_old_salesperson"
|
||||
attrs="{'invisible': [('new_end', '!=', False)]}"
|
||||
/>
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Events to update" col="1">
|
||||
<group>
|
||||
<group>
|
||||
<field name="new_user_id" />
|
||||
</group>
|
||||
<group>
|
||||
<field
|
||||
name="new_event_categ_ids"
|
||||
widget="many2many_tags"
|
||||
/>
|
||||
</group>
|
||||
</group>
|
||||
<div>
|
||||
<button
|
||||
class="oe_link"
|
||||
name="select_all_lines"
|
||||
type="object"
|
||||
string="Select all lines"
|
||||
/>
|
||||
<button
|
||||
class="oe_link"
|
||||
name="unselect_all_lines"
|
||||
type="object"
|
||||
string="Unselect all lines"
|
||||
/>
|
||||
<button
|
||||
class="oe_link ml32"
|
||||
name="action_assign_new_values"
|
||||
type="object"
|
||||
string="Assign new values to selected lines"
|
||||
/>
|
||||
</div>
|
||||
<field name="line_ids" nolabel="1">
|
||||
<tree
|
||||
editable="top"
|
||||
create="false"
|
||||
decoration-info="new_user_id"
|
||||
>
|
||||
<field name="selected" widget="boolean_toggle" />
|
||||
<field name="calendar_event_id" optional="show" />
|
||||
<field
|
||||
name="event_categ_ids"
|
||||
widget="many2many_tags"
|
||||
options="{'color_field': 'color'}"
|
||||
optional="hide"
|
||||
/>
|
||||
<field name="partner_id" />
|
||||
<field name="event_user_id" optional="hide" />
|
||||
<field name="partner_user_id" optional="hide" />
|
||||
<field name="event_start" optional="hide" />
|
||||
<field name="until" optional="hide" />
|
||||
<field name="new_user_id" />
|
||||
<field
|
||||
name="new_event_categ_ids"
|
||||
widget="many2many_tags"
|
||||
optional="hide"
|
||||
/>
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
<div>
|
||||
<p class="text-muted">
|
||||
The changes will be applied to event records with new commercial user filled.
|
||||
</p>
|
||||
<button
|
||||
name="apply"
|
||||
string="Apply changes"
|
||||
class="btn-primary"
|
||||
type="object"
|
||||
/>
|
||||
|
||||
<button string="Cancel" class="oe_link" special="cancel" />
|
||||
</div>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record
|
||||
id="action_sale_planner_calendar_reassign_wiz"
|
||||
model="ir.actions.act_window"
|
||||
>
|
||||
<field name="name">Reassignment of salesperson</field>
|
||||
<field name="res_model">sale.planner.calendar.reassign.wiz</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">inline</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
# Copyright 2021 Tecnativa - Sergio Teruel
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class SalePlannerCalendarWizard(models.TransientModel):
|
||||
_name = "sale.planner.calendar.wizard"
|
||||
_description = "Sale planner calendar Wizard"
|
||||
|
||||
user_id = fields.Many2one(
|
||||
comodel_name="res.users",
|
||||
string="Salesperson",
|
||||
default=lambda self: self.env.user,
|
||||
domain="[('share','=',False)]",
|
||||
)
|
||||
event_type_id = fields.Many2one(
|
||||
comodel_name="calendar.event.type", string="Event type"
|
||||
)
|
||||
week_list = fields.Selection(
|
||||
[
|
||||
("mon", "Monday"),
|
||||
("tue", "Tuesday"),
|
||||
("wed", "Wednesday"),
|
||||
("thu", "Thursday"),
|
||||
("fri", "Friday"),
|
||||
("sat", "Saturday"),
|
||||
("sun", "Sunday"),
|
||||
],
|
||||
string="Weekday",
|
||||
)
|
||||
# Special hack One2many field to manage and save records directly
|
||||
calendar_event_ids = fields.One2many(
|
||||
comodel_name="calendar.event",
|
||||
compute="_compute_calendar_event_ids",
|
||||
readonly=False,
|
||||
)
|
||||
|
||||
@api.depends("user_id", "event_type_id", "week_list")
|
||||
def _compute_calendar_event_ids(self):
|
||||
for rec in self:
|
||||
domain = [
|
||||
("recurrency", "=", True),
|
||||
("recurrence_id.until", ">", fields.Date.today()),
|
||||
("is_base_recurrent_event", "=", True),
|
||||
("target_partner_id", "!=", False),
|
||||
]
|
||||
if rec.user_id:
|
||||
domain.append(("user_id", "=", rec.user_id.id))
|
||||
if rec.event_type_id:
|
||||
domain.append(("categ_ids", "in", rec.event_type_id.ids))
|
||||
if rec.week_list:
|
||||
domain.append(("recurrence_id." + rec.week_list, "=", True))
|
||||
rec.calendar_event_ids = (
|
||||
self.env["calendar.event"].search(domain).sorted("hour")
|
||||
)
|
||||
|
||||
# TODO: Remove when control_panel_hidden works
|
||||
@api.depends("user_id", "event_type_id")
|
||||
def name_get(self):
|
||||
result = []
|
||||
for wiz in self:
|
||||
name = "{} - {}".format(wiz.user_id.name, wiz.event_type_id.name or "Plan")
|
||||
result.append((wiz.id, name))
|
||||
return result
|
||||
|
||||
def apply(self):
|
||||
pass
|
||||
|
||||
def write(self, vals):
|
||||
# Not send emails to attendees for update events
|
||||
if not self.env.company.sale_planner_mail_to_attendees:
|
||||
self = self.with_context(no_mail_to_attendees=True, dont_notify=True)
|
||||
return super().write(vals)
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record model="ir.ui.view" id="view_sale_planner_calendar_wizard">
|
||||
<field name="name">sale.planner.calendar.wizard</field>
|
||||
<field name="model">sale.planner.calendar.wizard</field>
|
||||
<field name="type">form</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="user_id" />
|
||||
<field name="event_type_id" widget="selection" />
|
||||
<field name="week_list" />
|
||||
</group>
|
||||
<field name="calendar_event_ids">
|
||||
<tree
|
||||
editable="top"
|
||||
default_order="hour"
|
||||
create="false"
|
||||
delete="false"
|
||||
>
|
||||
<field name="name" optional="show" />
|
||||
<field name="allday" invisible="1" />
|
||||
<field name="hour" widget="float_time" readonly="0" />
|
||||
<field name="target_partner_id" readonly="1" />
|
||||
<field
|
||||
name="categ_ids"
|
||||
widget="many2many_tags"
|
||||
optional="hide"
|
||||
readonly="1"
|
||||
options="{'color_field': 'color'}"
|
||||
/>
|
||||
<field name="start" optional="hide" readonly="1" />
|
||||
<field name="stop" optional="hide" readonly="1" />
|
||||
<field name="until" optional="hide" readonly="1" />
|
||||
<field
|
||||
name="duration"
|
||||
optional="hide"
|
||||
widget="float_time"
|
||||
/>
|
||||
|
||||
</tree>
|
||||
</field>
|
||||
<button
|
||||
name="apply"
|
||||
string="OK"
|
||||
class="btn-primary"
|
||||
type="object"
|
||||
/>
|
||||
<button string="Cancel" class="btn-default" special="cancel" />
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Loading…
Add table
Add a link
Reference in a new issue