mirror of
https://github.com/bringout/oca-ocb-project.git
synced 2026-04-18 01:42:03 +02:00
Initial commit: Project packages
This commit is contained in:
commit
89613c97b0
753 changed files with 496325 additions and 0 deletions
12
README.md
Normal file
12
README.md
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
# Project
|
||||||
|
|
||||||
|
This repository contains OCA OCB packages for project.
|
||||||
|
|
||||||
|
## Packages Included
|
||||||
|
|
||||||
|
- odoo-bringout-oca-ocb-project
|
||||||
|
- odoo-bringout-oca-ocb-project_hr_expense
|
||||||
|
- odoo-bringout-oca-ocb-project_mail_plugin
|
||||||
|
- odoo-bringout-oca-ocb-project_mrp
|
||||||
|
- odoo-bringout-oca-ocb-project_purchase
|
||||||
|
- odoo-bringout-oca-ocb-project_timesheet_holidays
|
||||||
54
odoo-bringout-oca-ocb-project/README.md
Normal file
54
odoo-bringout-oca-ocb-project/README.md
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
# Project
|
||||||
|
|
||||||
|
Odoo addon: project
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install odoo-bringout-oca-ocb-project
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
This addon depends on:
|
||||||
|
- analytic
|
||||||
|
- base_setup
|
||||||
|
- mail
|
||||||
|
- portal
|
||||||
|
- rating
|
||||||
|
- resource
|
||||||
|
- web
|
||||||
|
- web_tour
|
||||||
|
- digest
|
||||||
|
|
||||||
|
## Manifest Information
|
||||||
|
|
||||||
|
- **Name**: Project
|
||||||
|
- **Version**: 1.3
|
||||||
|
- **Category**: Services/Project
|
||||||
|
- **License**: LGPL-3
|
||||||
|
- **Installable**: True
|
||||||
|
|
||||||
|
## Source
|
||||||
|
|
||||||
|
Based on [OCA/OCB](https://github.com/OCA/OCB) branch 16.0, addon `project`.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This package maintains the original LGPL-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
|
||||||
32
odoo-bringout-oca-ocb-project/doc/ARCHITECTURE.md
Normal file
32
odoo-bringout-oca-ocb-project/doc/ARCHITECTURE.md
Normal file
|
|
@ -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 Project Module - project
|
||||||
|
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.
|
||||||
3
odoo-bringout-oca-ocb-project/doc/CONFIGURATION.md
Normal file
3
odoo-bringout-oca-ocb-project/doc/CONFIGURATION.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Configuration
|
||||||
|
|
||||||
|
Refer to Odoo settings for project. Configure related models, access rights, and options as needed.
|
||||||
17
odoo-bringout-oca-ocb-project/doc/CONTROLLERS.md
Normal file
17
odoo-bringout-oca-ocb-project/doc/CONTROLLERS.md
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Controllers
|
||||||
|
|
||||||
|
HTTP routes provided by this module.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant U as User/Client
|
||||||
|
participant C as Module Controllers
|
||||||
|
participant O as ORM/Views
|
||||||
|
|
||||||
|
U->>C: HTTP GET/POST (routes)
|
||||||
|
C->>O: ORM operations, render templates
|
||||||
|
O-->>U: HTML/JSON/PDF
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes
|
||||||
|
- See files in controllers/ for route definitions.
|
||||||
13
odoo-bringout-oca-ocb-project/doc/DEPENDENCIES.md
Normal file
13
odoo-bringout-oca-ocb-project/doc/DEPENDENCIES.md
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Dependencies
|
||||||
|
|
||||||
|
This addon depends on:
|
||||||
|
|
||||||
|
- [analytic](../../odoo-bringout-oca-ocb-analytic)
|
||||||
|
- [base_setup](../../odoo-bringout-oca-ocb-base_setup)
|
||||||
|
- [mail](../../odoo-bringout-oca-ocb-mail)
|
||||||
|
- [portal](../../odoo-bringout-oca-ocb-portal)
|
||||||
|
- [rating](../../odoo-bringout-oca-ocb-rating)
|
||||||
|
- [resource](../../odoo-bringout-oca-ocb-resource)
|
||||||
|
- [web](../../odoo-bringout-oca-ocb-web)
|
||||||
|
- [web_tour](../../odoo-bringout-oca-ocb-web_tour)
|
||||||
|
- [digest](../../odoo-bringout-oca-ocb-digest)
|
||||||
4
odoo-bringout-oca-ocb-project/doc/FAQ.md
Normal file
4
odoo-bringout-oca-ocb-project/doc/FAQ.md
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
# FAQ
|
||||||
|
|
||||||
|
- Q: Which Odoo version? A: 16.0 (OCA/OCB packaged).
|
||||||
|
- Q: How to enable? A: Start server with --addon project or install in UI.
|
||||||
7
odoo-bringout-oca-ocb-project/doc/INSTALL.md
Normal file
7
odoo-bringout-oca-ocb-project/doc/INSTALL.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install odoo-bringout-oca-ocb-project"
|
||||||
|
# or
|
||||||
|
uv pip install odoo-bringout-oca-ocb-project"
|
||||||
|
```
|
||||||
29
odoo-bringout-oca-ocb-project/doc/MODELS.md
Normal file
29
odoo-bringout-oca-ocb-project/doc/MODELS.md
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Models
|
||||||
|
|
||||||
|
Detected core models and extensions in project.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class project_collaborator
|
||||||
|
class project_milestone
|
||||||
|
class project_project
|
||||||
|
class project_project_stage
|
||||||
|
class project_tags
|
||||||
|
class project_task
|
||||||
|
class project_task_recurrence
|
||||||
|
class project_task_stage_personal
|
||||||
|
class project_task_type
|
||||||
|
class project_update
|
||||||
|
class res_company
|
||||||
|
class account_analytic_account
|
||||||
|
class digest_digest
|
||||||
|
class ir_ui_menu
|
||||||
|
class mail_message
|
||||||
|
class res_company
|
||||||
|
class res_config_settings
|
||||||
|
class res_partner
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes
|
||||||
|
- Classes show model technical names; fields omitted for brevity.
|
||||||
|
- Items listed under _inherit are extensions of existing models.
|
||||||
6
odoo-bringout-oca-ocb-project/doc/OVERVIEW.md
Normal file
6
odoo-bringout-oca-ocb-project/doc/OVERVIEW.md
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
# Overview
|
||||||
|
|
||||||
|
Packaged Odoo addon: project. Provides features documented in upstream Odoo 16 under this addon.
|
||||||
|
|
||||||
|
- Source: OCA/OCB 16.0, addon project
|
||||||
|
- License: LGPL-3
|
||||||
32
odoo-bringout-oca-ocb-project/doc/REPORTS.md
Normal file
32
odoo-bringout-oca-ocb-project/doc/REPORTS.md
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Reports
|
||||||
|
|
||||||
|
Report definitions and templates in project.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class ReportProjectTaskUser
|
||||||
|
Model <|-- ReportProjectTaskUser
|
||||||
|
class ReportProjectTaskBurndownChart
|
||||||
|
AbstractModel <|-- ReportProjectTaskBurndownChart
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Reports
|
||||||
|
|
||||||
|
### Analytical/Dashboard Reports
|
||||||
|
- **Tasks Analysis** (Analysis/Dashboard)
|
||||||
|
- **Burndown Chart** (Analysis/Dashboard)
|
||||||
|
|
||||||
|
|
||||||
|
## Report Files
|
||||||
|
|
||||||
|
- **__init__.py** (Python logic)
|
||||||
|
- **project_report.py** (Python logic)
|
||||||
|
- **project_report_views.xml** (XML template/definition)
|
||||||
|
- **project_task_burndown_chart_report.py** (Python logic)
|
||||||
|
- **project_task_burndown_chart_report_views.xml** (XML template/definition)
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Named reports above are accessible through Odoo's reporting menu
|
||||||
|
- Python files define report logic and data processing
|
||||||
|
- XML files contain report templates, definitions, and formatting
|
||||||
|
- Reports are integrated with Odoo's printing and email systems
|
||||||
45
odoo-bringout-oca-ocb-project/doc/SECURITY.md
Normal file
45
odoo-bringout-oca-ocb-project/doc/SECURITY.md
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
# Security
|
||||||
|
|
||||||
|
Access control and security definitions in project.
|
||||||
|
|
||||||
|
## Access Control Lists (ACLs)
|
||||||
|
|
||||||
|
Model access permissions defined in:
|
||||||
|
- **[ir.model.access.csv](../project/security/ir.model.access.csv)**
|
||||||
|
- 43 model access rules
|
||||||
|
|
||||||
|
## Record Rules
|
||||||
|
|
||||||
|
Row-level security rules defined in:
|
||||||
|
|
||||||
|
## Security Groups & Configuration
|
||||||
|
|
||||||
|
Security groups and permissions defined in:
|
||||||
|
- **[ir.model.access.xml](../project/security/ir.model.access.xml)**
|
||||||
|
- **[project_security.xml](../project/security/project_security.xml)**
|
||||||
|
- 8 security groups defined
|
||||||
|
|
||||||
|
```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](../project/security/ir.model.access.csv)**
|
||||||
|
- Model access permissions (CRUD rights)
|
||||||
|
- **[ir.model.access.xml](../project/security/ir.model.access.xml)**
|
||||||
|
- Security groups, categories, and XML-based rules
|
||||||
|
- **[project_security.xml](../project/security/project_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
|
||||||
5
odoo-bringout-oca-ocb-project/doc/TROUBLESHOOTING.md
Normal file
5
odoo-bringout-oca-ocb-project/doc/TROUBLESHOOTING.md
Normal file
|
|
@ -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.
|
||||||
7
odoo-bringout-oca-ocb-project/doc/USAGE.md
Normal file
7
odoo-bringout-oca-ocb-project/doc/USAGE.md
Normal file
|
|
@ -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 project
|
||||||
|
```
|
||||||
9
odoo-bringout-oca-ocb-project/doc/WIZARDS.md
Normal file
9
odoo-bringout-oca-ocb-project/doc/WIZARDS.md
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# Wizards
|
||||||
|
|
||||||
|
Transient models exposed as UI wizards in project.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class ProjectShareWizard
|
||||||
|
class ProjectTaskTypeDelete
|
||||||
|
```
|
||||||
69
odoo-bringout-oca-ocb-project/project/README.md
Normal file
69
odoo-bringout-oca-ocb-project/project/README.md
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
Project Management
|
||||||
|
------------------
|
||||||
|
|
||||||
|
### Infinitely flexible. Incredibly easy to use.
|
||||||
|
|
||||||
|
|
||||||
|
Odoo's collaborative and realtime <a href="https://www.odoo.com/app/project">open source project management</a>
|
||||||
|
helps your team get work done. Keep track of everything, from the big picture
|
||||||
|
to the minute details, from the customer contract to the billing.
|
||||||
|
|
||||||
|
Designed to Fit Your Own Process
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
Organize projects around your own processes. Work on tasks and issues using the
|
||||||
|
kanban view, schedule tasks using the gantt chart and control deadlines in the
|
||||||
|
calendar view. Every project may have its own stages, allowing teams to
|
||||||
|
optimize their job.
|
||||||
|
|
||||||
|
Easy to Use
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Get organized as fast as you can think. The easy-to-use interface takes no time
|
||||||
|
to learn, and every action is instantaneous, so there’s nothing standing
|
||||||
|
between you and your sweet productive flow.
|
||||||
|
|
||||||
|
Work Together
|
||||||
|
-------------
|
||||||
|
|
||||||
|
### Real-time chats, document sharing, email integration
|
||||||
|
|
||||||
|
Use the chatter to communicate with your team or customers and share comments
|
||||||
|
and documents on tasks and issues. Integrate discussion fast with the email
|
||||||
|
integration.
|
||||||
|
|
||||||
|
Talk to other users or customers with the website live chat feature.
|
||||||
|
|
||||||
|
Collaborative Writing
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
### The power of etherpad, inside your tasks
|
||||||
|
|
||||||
|
Collaboratively edit the same specifications or meeting minutes right inside
|
||||||
|
the application. The integrated etherpad feature allows several people to
|
||||||
|
work on the same tasks, at the same time.
|
||||||
|
|
||||||
|
This is very efficient for scrum meetings, meeting minutes or complex
|
||||||
|
specifications. Every user has their own color and you can replay the whole
|
||||||
|
creation of the content.
|
||||||
|
|
||||||
|
Get Work Done
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Get alerts on followed events to stay up to date with what interests you. Use
|
||||||
|
instant green/red visual indicators to scan through what has been done and what
|
||||||
|
requires your attention.
|
||||||
|
|
||||||
|
Timesheets, Contracts & Invoicing
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
|
Projects are automatically integrated with customer contracts, allowing you to
|
||||||
|
invoice based on time & materials and record timesheets easily.
|
||||||
|
|
||||||
|
Track Issues
|
||||||
|
------------
|
||||||
|
|
||||||
|
Single out the issues that arise in a project in order to have a better focus
|
||||||
|
on resolving them. Integrate customer interaction on every issue and get
|
||||||
|
accurate reports on your team's performance.
|
||||||
|
|
||||||
43
odoo-bringout-oca-ocb-project/project/__init__.py
Normal file
43
odoo-bringout-oca-ocb-project/project/__init__.py
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from . import controllers
|
||||||
|
from . import models
|
||||||
|
from . import report
|
||||||
|
from . import wizard
|
||||||
|
from . import populate
|
||||||
|
|
||||||
|
from odoo import api, SUPERUSER_ID
|
||||||
|
from odoo.tools.sql import create_index
|
||||||
|
|
||||||
|
|
||||||
|
def _check_exists_collaborators_for_project_sharing(env):
|
||||||
|
""" Check if it exists at least a collaborator in a shared project
|
||||||
|
|
||||||
|
If it is the case we need to active the portal rules added only for this feature.
|
||||||
|
"""
|
||||||
|
collaborator = env['project.collaborator'].search([], limit=1)
|
||||||
|
if collaborator:
|
||||||
|
# Then we need to enable the access rights linked to project sharing for the portal user
|
||||||
|
env['project.collaborator']._toggle_project_sharing_portal_rules(True)
|
||||||
|
|
||||||
|
|
||||||
|
def _project_post_init(cr, registry):
|
||||||
|
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||||
|
_check_exists_collaborators_for_project_sharing(env)
|
||||||
|
|
||||||
|
# Index to improve the performance of burndown chart.
|
||||||
|
project_task_stage_field_id = env['ir.model.fields']._get_ids('project.task').get('stage_id')
|
||||||
|
create_index(
|
||||||
|
cr,
|
||||||
|
'mail_tracking_value_mail_message_id_old_value_integer_task_stage',
|
||||||
|
env['mail.tracking.value']._table,
|
||||||
|
['mail_message_id', 'old_value_integer'],
|
||||||
|
where=f'field={project_task_stage_field_id}'
|
||||||
|
)
|
||||||
|
|
||||||
|
def _project_uninstall_hook(cr, registry):
|
||||||
|
"""Since the m2m table for the project share wizard's `partner_ids` field is not dropped at uninstall, it is
|
||||||
|
necessary to ensure it is emptied, else re-installing the module will fail due to foreign keys constraints."""
|
||||||
|
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||||
|
env['project.share.wizard'].search([("partner_ids", "!=", False)]).partner_ids = False
|
||||||
226
odoo-bringout-oca-ocb-project/project/__manifest__.py
Normal file
226
odoo-bringout-oca-ocb-project/project/__manifest__.py
Normal file
|
|
@ -0,0 +1,226 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
{
|
||||||
|
'name': 'Project',
|
||||||
|
'version': '1.3',
|
||||||
|
'website': 'https://www.odoo.com/app/project',
|
||||||
|
'category': 'Services/Project',
|
||||||
|
'sequence': 45,
|
||||||
|
'summary': 'Organize and plan your projects',
|
||||||
|
'depends': [
|
||||||
|
'analytic',
|
||||||
|
'base_setup',
|
||||||
|
'mail',
|
||||||
|
'portal',
|
||||||
|
'rating',
|
||||||
|
'resource',
|
||||||
|
'web',
|
||||||
|
'web_tour',
|
||||||
|
'digest',
|
||||||
|
],
|
||||||
|
'data': [
|
||||||
|
'security/project_security.xml',
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'security/ir.model.access.xml',
|
||||||
|
'data/digest_data.xml',
|
||||||
|
'report/project_report_views.xml',
|
||||||
|
'report/project_task_burndown_chart_report_views.xml',
|
||||||
|
'views/analytic_views.xml',
|
||||||
|
'views/digest_views.xml',
|
||||||
|
'views/rating_rating_views.xml',
|
||||||
|
'views/project_update_views.xml',
|
||||||
|
'views/project_update_templates.xml',
|
||||||
|
'views/project_project_stage_views.xml',
|
||||||
|
'wizard/project_share_wizard_views.xml',
|
||||||
|
'views/project_collaborator_views.xml',
|
||||||
|
'views/project_views.xml',
|
||||||
|
'views/project_milestone_views.xml',
|
||||||
|
'views/res_partner_views.xml',
|
||||||
|
'views/res_config_settings_views.xml',
|
||||||
|
'views/mail_activity_views.xml',
|
||||||
|
'views/project_sharing_views.xml',
|
||||||
|
'views/project_portal_templates.xml',
|
||||||
|
'views/project_task_templates.xml',
|
||||||
|
'views/project_sharing_templates.xml',
|
||||||
|
'data/ir_cron_data.xml',
|
||||||
|
'data/mail_message_subtype_data.xml',
|
||||||
|
'data/mail_template_data.xml',
|
||||||
|
'data/project_data.xml',
|
||||||
|
'wizard/project_task_type_delete_views.xml',
|
||||||
|
],
|
||||||
|
'demo': [
|
||||||
|
'data/mail_template_demo.xml',
|
||||||
|
'data/project_demo.xml',
|
||||||
|
],
|
||||||
|
'installable': True,
|
||||||
|
'application': True,
|
||||||
|
'post_init_hook': '_project_post_init',
|
||||||
|
'uninstall_hook': '_project_uninstall_hook',
|
||||||
|
'assets': {
|
||||||
|
'web.assets_backend': [
|
||||||
|
'project/static/src/css/project.css',
|
||||||
|
'project/static/src/utils/**/*',
|
||||||
|
'project/static/src/services/**/*',
|
||||||
|
'project/static/src/components/**/*',
|
||||||
|
'project/static/src/views/**/*',
|
||||||
|
'project/static/src/js/project_activity.js',
|
||||||
|
'project/static/src/js/project_control_panel.js',
|
||||||
|
'project/static/src/js/project_graph_view.js',
|
||||||
|
'project/static/src/js/project_pivot_view.js',
|
||||||
|
'project/static/src/js/project_rating_graph_view.js',
|
||||||
|
'project/static/src/js/project_rating_pivot_view.js',
|
||||||
|
'project/static/src/js/project_task_kanban_examples.js',
|
||||||
|
'project/static/src/js/tours/project.js',
|
||||||
|
'project/static/src/js/widgets/*',
|
||||||
|
'project/static/src/scss/project_dashboard.scss',
|
||||||
|
'project/static/src/scss/project_form.scss',
|
||||||
|
'project/static/src/scss/project_widgets.scss',
|
||||||
|
'project/static/src/xml/**/*',
|
||||||
|
],
|
||||||
|
'web.assets_frontend': [
|
||||||
|
'project/static/src/scss/portal_rating.scss',
|
||||||
|
'project/static/src/scss/project_sharing_frontend.scss',
|
||||||
|
'project/static/src/js/portal_rating.js',
|
||||||
|
],
|
||||||
|
'web.qunit_suite_tests': [
|
||||||
|
'project/static/src/project_sharing/components/portal_file_input/portal_file_input.js',
|
||||||
|
'project/static/tests/**/*.js',
|
||||||
|
],
|
||||||
|
'web.assets_tests': [
|
||||||
|
'project/static/tests/tours/**/*',
|
||||||
|
],
|
||||||
|
'project.webclient': [
|
||||||
|
('include', 'web._assets_helpers'),
|
||||||
|
('include', 'web._assets_backend_helpers'),
|
||||||
|
|
||||||
|
'web/static/src/scss/pre_variables.scss',
|
||||||
|
'web/static/lib/bootstrap/scss/_variables.scss',
|
||||||
|
|
||||||
|
('include', 'web._assets_bootstrap'),
|
||||||
|
|
||||||
|
'base/static/src/css/modules.css',
|
||||||
|
|
||||||
|
'web/static/src/core/utils/transitions.scss',
|
||||||
|
'web/static/src/core/**/*',
|
||||||
|
'web/static/src/search/**/*',
|
||||||
|
'web/static/src/webclient/icons.scss', # variables required in list_controller.scss
|
||||||
|
'web/static/src/views/*.js',
|
||||||
|
'web/static/src/views/*.xml',
|
||||||
|
'web/static/src/views/*.scss',
|
||||||
|
'web/static/src/views/fields/**/*',
|
||||||
|
('remove', 'web/static/src/views/fields/journal_dashboard_graph/**/*'), # only works with graph view in assets
|
||||||
|
'web/static/src/views/form/**/*',
|
||||||
|
'web/static/src/views/kanban/**/*',
|
||||||
|
'web/static/src/views/list/**/*',
|
||||||
|
'web/static/src/views/view_button/**/*',
|
||||||
|
'web/static/src/views/view_dialogs/**/*',
|
||||||
|
'web/static/src/views/widgets/**/*',
|
||||||
|
'web/static/src/webclient/**/*',
|
||||||
|
('remove', 'web/static/src/webclient/navbar/navbar.scss'), # already in assets_common
|
||||||
|
('remove', 'web/static/src/webclient/clickbot/clickbot.js'), # lazy loaded
|
||||||
|
('remove', 'web/static/src/views/form/button_box/*.scss'),
|
||||||
|
|
||||||
|
# remove the report code and whitelist only what's needed
|
||||||
|
('remove', 'web/static/src/webclient/actions/reports/**/*'),
|
||||||
|
'web/static/src/webclient/actions/reports/*.js',
|
||||||
|
'web/static/src/webclient/actions/reports/*.xml',
|
||||||
|
|
||||||
|
'web/static/src/env.js',
|
||||||
|
|
||||||
|
'web/static/lib/jquery.scrollTo/jquery.scrollTo.js',
|
||||||
|
'web/static/lib/py.js/lib/py.js',
|
||||||
|
'web/static/lib/py.js/lib/py_extras.js',
|
||||||
|
'web/static/lib/jquery.ba-bbq/jquery.ba-bbq.js',
|
||||||
|
|
||||||
|
'web/static/src/legacy/scss/fields.scss',
|
||||||
|
'web/static/src/legacy/scss/views.scss',
|
||||||
|
'web/static/src/legacy/scss/form_view.scss',
|
||||||
|
'web/static/src/legacy/scss/list_view.scss',
|
||||||
|
'web/static/src/legacy/scss/kanban_dashboard.scss',
|
||||||
|
'web/static/src/legacy/scss/kanban_examples_dialog.scss',
|
||||||
|
'web/static/src/legacy/scss/kanban_column_progressbar.scss',
|
||||||
|
'web/static/src/legacy/scss/kanban_view.scss',
|
||||||
|
|
||||||
|
'base/static/src/scss/res_partner.scss',
|
||||||
|
|
||||||
|
# Form style should be computed before
|
||||||
|
'web/static/src/views/form/button_box/*.scss',
|
||||||
|
|
||||||
|
'web/static/src/legacy/action_adapters.js',
|
||||||
|
'web/static/src/legacy/debug_manager.js',
|
||||||
|
'web/static/src/legacy/legacy_service_provider.js',
|
||||||
|
'web/static/src/legacy/legacy_client_actions.js',
|
||||||
|
'web/static/src/legacy/legacy_dialog.js',
|
||||||
|
'web/static/src/legacy/legacy_load_views.js',
|
||||||
|
'web/static/src/legacy/legacy_promise_error_handler.js',
|
||||||
|
'web/static/src/legacy/legacy_rpc_error_handler.js',
|
||||||
|
'web/static/src/legacy/root_widget.js',
|
||||||
|
'web/static/src/legacy/legacy_setup.js',
|
||||||
|
'web/static/src/legacy/root_widget.js',
|
||||||
|
'web/static/src/legacy/backend_utils.js',
|
||||||
|
'web/static/src/legacy/utils.js',
|
||||||
|
'web/static/src/legacy/web_client.js',
|
||||||
|
'web/static/src/legacy/js/_deprecated/data.js',
|
||||||
|
'web/static/src/legacy/js/chrome/*',
|
||||||
|
'web/static/src/legacy/js/components/*',
|
||||||
|
'web/static/src/legacy/js/control_panel/*',
|
||||||
|
'web/static/src/legacy/js/core/domain.js',
|
||||||
|
'web/static/src/legacy/js/core/mvc.js',
|
||||||
|
'web/static/src/legacy/js/core/py_utils.js',
|
||||||
|
'web/static/src/legacy/js/core/context.js',
|
||||||
|
'web/static/src/legacy/js/core/misc.js',
|
||||||
|
'web/static/src/legacy/js/fields/abstract_field.js',
|
||||||
|
'web/static/src/legacy/js/fields/abstract_field_owl.js',
|
||||||
|
'web/static/src/legacy/js/_deprecated/basic_fields.js',
|
||||||
|
'web/static/src/legacy/js/fields/basic_fields.js',
|
||||||
|
'web/static/src/legacy/js/fields/basic_fields_owl.js',
|
||||||
|
'web/static/src/legacy/js/fields/field_utils.js',
|
||||||
|
'web/static/src/legacy/js/fields/relational_fields.js',
|
||||||
|
'web/static/src/legacy/js/fields/special_fields.js',
|
||||||
|
'web/static/src/legacy/js/fields/field_registry.js',
|
||||||
|
'web/static/src/legacy/js/fields/field_registry_owl.js',
|
||||||
|
'web/static/src/legacy/js/fields/field_utils.js',
|
||||||
|
'web/static/src/legacy/js/fields/field_wrapper.js',
|
||||||
|
'web/static/src/legacy/js/views/sample_server.js',
|
||||||
|
'web/static/src/legacy/js/views/abstract_model.js',
|
||||||
|
'web/static/src/legacy/js/views/basic/basic_model.js',
|
||||||
|
'web/static/src/legacy/js/views/action_model.js',
|
||||||
|
'web/static/src/legacy/js/views/view_utils.js',
|
||||||
|
'web/static/src/legacy/js/services/data_manager.js',
|
||||||
|
'web/static/src/legacy/js/services/session.js',
|
||||||
|
'web/static/src/legacy/js/tools/tools.js',
|
||||||
|
'web/static/src/legacy/js/views/**/*',
|
||||||
|
'web/static/src/legacy/js/widgets/data_export.js',
|
||||||
|
'web/static/src/legacy/js/widgets/date_picker.js',
|
||||||
|
'web/static/src/legacy/js/widgets/domain_selector_dialog.js',
|
||||||
|
'web/static/src/legacy/js/widgets/domain_selector.js',
|
||||||
|
'web/static/src/legacy/js/widgets/model_field_selector.js',
|
||||||
|
'web/static/src/legacy/js/widgets/model_field_selector_popover.js',
|
||||||
|
'web/static/src/legacy/js/env.js',
|
||||||
|
'web/static/src/legacy/js/model.js',
|
||||||
|
'web/static/src/legacy/js/owl_compatibility.js',
|
||||||
|
|
||||||
|
'web_editor/static/src/components/**/*',
|
||||||
|
'web_editor/static/src/scss/web_editor.common.scss',
|
||||||
|
'web_editor/static/src/scss/web_editor.backend.scss',
|
||||||
|
|
||||||
|
'web_editor/static/src/js/wysiwyg/dialog.js',
|
||||||
|
'web_editor/static/src/js/frontend/loader.js',
|
||||||
|
'web_editor/static/src/js/backend/**/*',
|
||||||
|
'web_editor/static/src/xml/backend.xml',
|
||||||
|
|
||||||
|
'mail/static/src/scss/variables/*.scss',
|
||||||
|
'mail/static/src/widgets/**/*.scss',
|
||||||
|
|
||||||
|
'project/static/src/components/project_task_name_with_subtask_count_char_field/*',
|
||||||
|
'project/static/src/views/project_task_form/*.scss',
|
||||||
|
|
||||||
|
'project/static/src/project_sharing/search/favorite_menu/custom_favorite_item.xml',
|
||||||
|
'project/static/src/project_sharing/**/*',
|
||||||
|
'web/static/src/start.js',
|
||||||
|
'web/static/src/legacy/legacy_setup.js',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
}
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,5 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from . import portal
|
||||||
|
from . import project_sharing_chatter
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
530
odoo-bringout-oca-ocb-project/project/controllers/portal.py
Normal file
530
odoo-bringout-oca-ocb-project/project/controllers/portal.py
Normal file
|
|
@ -0,0 +1,530 @@
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
|
from operator import itemgetter
|
||||||
|
from markupsafe import Markup
|
||||||
|
|
||||||
|
from odoo import conf, http, _
|
||||||
|
from odoo.exceptions import AccessError, MissingError
|
||||||
|
from odoo.http import request
|
||||||
|
from odoo.addons.portal.controllers.portal import CustomerPortal, pager as portal_pager
|
||||||
|
from odoo.tools import groupby as groupbyelem
|
||||||
|
|
||||||
|
from odoo.osv.expression import OR, AND
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectCustomerPortal(CustomerPortal):
|
||||||
|
|
||||||
|
def _prepare_home_portal_values(self, counters):
|
||||||
|
values = super()._prepare_home_portal_values(counters)
|
||||||
|
if 'project_count' in counters:
|
||||||
|
values['project_count'] = request.env['project.project'].search_count([]) \
|
||||||
|
if request.env['project.project'].check_access_rights('read', raise_exception=False) else 0
|
||||||
|
if 'task_count' in counters:
|
||||||
|
values['task_count'] = request.env['project.task'].search_count([('project_id', '!=', False)]) \
|
||||||
|
if request.env['project.task'].check_access_rights('read', raise_exception=False) else 0
|
||||||
|
return values
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# My Project
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
def _project_get_page_view_values(self, project, access_token, page=1, date_begin=None, date_end=None, sortby=None, search=None, search_in='content', groupby=None, **kwargs):
|
||||||
|
# default filter by value
|
||||||
|
domain = [('project_id', '=', project.id)]
|
||||||
|
# pager
|
||||||
|
url = "/my/projects/%s" % project.id
|
||||||
|
values = self._prepare_tasks_values(page, date_begin, date_end, sortby, search, search_in, groupby, url, domain, su=bool(access_token))
|
||||||
|
# adding the access_token to the pager's url args,
|
||||||
|
# so we are not prompted for loging when switching pages
|
||||||
|
# if access_token is None, the arg is not present in the URL
|
||||||
|
values['pager']['url_args']['access_token'] = access_token
|
||||||
|
pager = portal_pager(**values['pager'])
|
||||||
|
|
||||||
|
values.update(
|
||||||
|
grouped_tasks=values['grouped_tasks'](pager['offset']),
|
||||||
|
page_name='project',
|
||||||
|
pager=pager,
|
||||||
|
project=project,
|
||||||
|
task_url=f'projects/{project.id}/task',
|
||||||
|
)
|
||||||
|
# default value is set to 'project' in _prepare_tasks_values, so we have to set it to 'none' here.
|
||||||
|
if not groupby:
|
||||||
|
values['groupby'] = 'none'
|
||||||
|
|
||||||
|
return self._get_page_view_values(project, access_token, values, 'my_projects_history', False, **kwargs)
|
||||||
|
|
||||||
|
def _prepare_project_domain(self):
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _prepare_searchbar_sortings(self):
|
||||||
|
return {
|
||||||
|
'date': {'label': _('Newest'), 'order': 'create_date desc'},
|
||||||
|
'name': {'label': _('Name'), 'order': 'name'},
|
||||||
|
}
|
||||||
|
|
||||||
|
@http.route(['/my/projects', '/my/projects/page/<int:page>'], type='http', auth="user", website=True)
|
||||||
|
def portal_my_projects(self, page=1, date_begin=None, date_end=None, sortby=None, **kw):
|
||||||
|
values = self._prepare_portal_layout_values()
|
||||||
|
Project = request.env['project.project']
|
||||||
|
domain = self._prepare_project_domain()
|
||||||
|
|
||||||
|
searchbar_sortings = self._prepare_searchbar_sortings()
|
||||||
|
if not sortby or sortby not in searchbar_sortings:
|
||||||
|
sortby = 'date'
|
||||||
|
order = searchbar_sortings[sortby]['order']
|
||||||
|
|
||||||
|
if date_begin and date_end:
|
||||||
|
domain += [('create_date', '>', date_begin), ('create_date', '<=', date_end)]
|
||||||
|
|
||||||
|
# projects count
|
||||||
|
project_count = Project.search_count(domain)
|
||||||
|
# pager
|
||||||
|
pager = portal_pager(
|
||||||
|
url="/my/projects",
|
||||||
|
url_args={'date_begin': date_begin, 'date_end': date_end, 'sortby': sortby},
|
||||||
|
total=project_count,
|
||||||
|
page=page,
|
||||||
|
step=self._items_per_page
|
||||||
|
)
|
||||||
|
|
||||||
|
# content according to pager and archive selected
|
||||||
|
projects = Project.search(domain, order=order, limit=self._items_per_page, offset=pager['offset'])
|
||||||
|
request.session['my_projects_history'] = projects.ids[:100]
|
||||||
|
|
||||||
|
values.update({
|
||||||
|
'date': date_begin,
|
||||||
|
'date_end': date_end,
|
||||||
|
'projects': projects,
|
||||||
|
'page_name': 'project',
|
||||||
|
'default_url': '/my/projects',
|
||||||
|
'pager': pager,
|
||||||
|
'searchbar_sortings': searchbar_sortings,
|
||||||
|
'sortby': sortby
|
||||||
|
})
|
||||||
|
return request.render("project.portal_my_projects", values)
|
||||||
|
|
||||||
|
@http.route(['/my/project/<int:project_id>',
|
||||||
|
'/my/project/<int:project_id>/page/<int:page>',
|
||||||
|
'/my/project/<int:project_id>/task/<int:task_id>',
|
||||||
|
'/my/project/<int:project_id>/project_sharing'], type='http', auth="public")
|
||||||
|
def portal_project_routes_outdated(self, **kwargs):
|
||||||
|
""" Redirect the outdated routes to the new routes. """
|
||||||
|
return request.redirect(request.httprequest.full_path.replace('/my/project/', '/my/projects/'))
|
||||||
|
|
||||||
|
@http.route(['/my/task',
|
||||||
|
'/my/task/page/<int:page>',
|
||||||
|
'/my/task/<int:task_id>'], type='http', auth='public')
|
||||||
|
def portal_my_task_routes_outdated(self, **kwargs):
|
||||||
|
""" Redirect the outdated routes to the new routes. """
|
||||||
|
return request.redirect(request.httprequest.full_path.replace('/my/task', '/my/tasks'))
|
||||||
|
|
||||||
|
@http.route(['/my/projects/<int:project_id>', '/my/projects/<int:project_id>/page/<int:page>'], type='http', auth="public", website=True)
|
||||||
|
def portal_my_project(self, project_id=None, access_token=None, page=1, date_begin=None, date_end=None, sortby=None, search=None, search_in='content', groupby=None, task_id=None, **kw):
|
||||||
|
try:
|
||||||
|
project_sudo = self._document_check_access('project.project', project_id, access_token)
|
||||||
|
except (AccessError, MissingError):
|
||||||
|
return request.redirect('/my')
|
||||||
|
if project_sudo.collaborator_count and project_sudo.with_user(request.env.user)._check_project_sharing_access():
|
||||||
|
values = {'project_id': project_id}
|
||||||
|
if task_id:
|
||||||
|
values['task_id'] = task_id
|
||||||
|
return request.render("project.project_sharing_portal", values)
|
||||||
|
project_sudo = project_sudo if access_token else project_sudo.with_user(request.env.user)
|
||||||
|
values = self._project_get_page_view_values(project_sudo, access_token, page, date_begin, date_end, sortby, search, search_in, groupby, **kw)
|
||||||
|
return request.render("project.portal_my_project", values)
|
||||||
|
|
||||||
|
def _prepare_project_sharing_session_info(self, project, task=None):
|
||||||
|
session_info = request.env['ir.http'].session_info()
|
||||||
|
user_context = dict(request.env.context) if request.session.uid else {}
|
||||||
|
mods = conf.server_wide_modules or []
|
||||||
|
if request.env.lang:
|
||||||
|
lang = request.env.lang
|
||||||
|
session_info['user_context']['lang'] = lang
|
||||||
|
# Update Cache
|
||||||
|
user_context['lang'] = lang
|
||||||
|
lang = user_context.get("lang")
|
||||||
|
translation_hash = request.env['ir.http'].get_web_translations_hash(mods, lang)
|
||||||
|
cache_hashes = {
|
||||||
|
"translations": translation_hash,
|
||||||
|
}
|
||||||
|
|
||||||
|
project_company = project.company_id
|
||||||
|
session_info.update(
|
||||||
|
cache_hashes=cache_hashes,
|
||||||
|
action_name='project.project_sharing_project_task_action',
|
||||||
|
project_id=project.id,
|
||||||
|
user_companies={
|
||||||
|
'current_company': project_company.id,
|
||||||
|
'allowed_companies': {
|
||||||
|
project_company.id: {
|
||||||
|
'id': project_company.id,
|
||||||
|
'name': project_company.name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
# FIXME: See if we prefer to give only the currency that the portal user just need to see the correct information in project sharing
|
||||||
|
currencies=request.env['ir.http'].get_currencies(),
|
||||||
|
)
|
||||||
|
if task:
|
||||||
|
session_info['open_task_action'] = task.action_project_sharing_open_task()
|
||||||
|
return session_info
|
||||||
|
|
||||||
|
@http.route("/my/projects/<int:project_id>/project_sharing", type="http", auth="user", methods=['GET'])
|
||||||
|
def render_project_backend_view(self, project_id, task_id=None):
|
||||||
|
project = request.env['project.project'].sudo().browse(project_id)
|
||||||
|
if not project.exists() or not project.with_user(request.env.user)._check_project_sharing_access():
|
||||||
|
return request.not_found()
|
||||||
|
task = task_id and request.env['project.task'].browse(int(task_id))
|
||||||
|
return request.render(
|
||||||
|
'project.project_sharing_embed',
|
||||||
|
{'session_info': self._prepare_project_sharing_session_info(project, task)},
|
||||||
|
)
|
||||||
|
|
||||||
|
@http.route('/my/projects/<int:project_id>/task/<int:task_id>', type='http', auth='public', website=True)
|
||||||
|
def portal_my_project_task(self, project_id=None, task_id=None, access_token=None, **kw):
|
||||||
|
try:
|
||||||
|
project_sudo = self._document_check_access('project.project', project_id, access_token)
|
||||||
|
except (AccessError, MissingError):
|
||||||
|
return request.redirect('/my')
|
||||||
|
Task = request.env['project.task']
|
||||||
|
if access_token:
|
||||||
|
Task = Task.sudo()
|
||||||
|
task_sudo = Task.search([('project_id', '=', project_id), ('id', '=', task_id)], limit=1).sudo()
|
||||||
|
task_sudo.attachment_ids.generate_access_token()
|
||||||
|
values = self._task_get_page_view_values(task_sudo, access_token, project=project_sudo, **kw)
|
||||||
|
values['project'] = project_sudo
|
||||||
|
return request.render("project.portal_my_task", values)
|
||||||
|
|
||||||
|
@http.route('/my/projects/<int:project_id>/task/<int:task_id>/subtasks', type='http', auth='user', methods=['GET'], website=True)
|
||||||
|
def portal_my_project_subtasks(self, project_id, task_id, page=1, date_begin=None, date_end=None, sortby=None, filterby=None, search=None, search_in='content', groupby=None, **kw):
|
||||||
|
try:
|
||||||
|
project_sudo = self._document_check_access('project.project', project_id)
|
||||||
|
task_sudo = request.env['project.task'].search([('project_id', '=', project_id), ('id', '=', task_id)]).sudo()
|
||||||
|
task_domain = [('id', 'child_of', task_id), ('id', '!=', task_id)]
|
||||||
|
searchbar_filters = self._get_my_tasks_searchbar_filters([('id', '=', task_sudo.project_id.id)], task_domain)
|
||||||
|
|
||||||
|
if not filterby:
|
||||||
|
filterby = 'all'
|
||||||
|
domain = searchbar_filters.get(filterby, searchbar_filters.get('all'))['domain']
|
||||||
|
|
||||||
|
values = self._prepare_tasks_values(page, date_begin, date_end, sortby, search, search_in, groupby, url=f'/my/projects/{project_id}/task/{task_id}/subtasks', domain=AND([task_domain, domain]))
|
||||||
|
values['page_name'] = 'project_subtasks'
|
||||||
|
|
||||||
|
# pager
|
||||||
|
pager_vals = values['pager']
|
||||||
|
pager_vals['url_args'].update(filterby=filterby)
|
||||||
|
pager = portal_pager(**pager_vals)
|
||||||
|
|
||||||
|
values.update({
|
||||||
|
'project': project_sudo,
|
||||||
|
'task': task_sudo,
|
||||||
|
'grouped_tasks': values['grouped_tasks'](pager['offset']),
|
||||||
|
'pager': pager,
|
||||||
|
'searchbar_filters': OrderedDict(sorted(searchbar_filters.items())),
|
||||||
|
'filterby': filterby,
|
||||||
|
})
|
||||||
|
return request.render("project.portal_my_tasks", values)
|
||||||
|
except (AccessError, MissingError):
|
||||||
|
return request.not_found()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# My Task
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
def _task_get_page_view_values(self, task, access_token, **kwargs):
|
||||||
|
project = kwargs.get('project')
|
||||||
|
if project:
|
||||||
|
project_accessible = True
|
||||||
|
page_name = 'project_task'
|
||||||
|
history = 'my_project_tasks_history'
|
||||||
|
else:
|
||||||
|
page_name = 'task'
|
||||||
|
history = 'my_tasks_history'
|
||||||
|
try:
|
||||||
|
project_accessible = bool(task.project_id.id and self._document_check_access('project.project', task.project_id.id))
|
||||||
|
except (AccessError, MissingError):
|
||||||
|
project_accessible = False
|
||||||
|
values = {
|
||||||
|
'page_name': page_name,
|
||||||
|
'task': task,
|
||||||
|
'user': request.env.user,
|
||||||
|
'project_accessible': project_accessible,
|
||||||
|
'task_link_section': [],
|
||||||
|
}
|
||||||
|
|
||||||
|
values = self._get_page_view_values(task, access_token, values, history, False, **kwargs)
|
||||||
|
if project:
|
||||||
|
values['project_id'] = project.id
|
||||||
|
history = request.session.get('my_project_tasks_history', [])
|
||||||
|
try:
|
||||||
|
current_task_index = history.index(task.id)
|
||||||
|
except ValueError:
|
||||||
|
return values
|
||||||
|
|
||||||
|
total_task = len(history)
|
||||||
|
task_url = f"{task.project_id.access_url}/task/%s?model=project.project&res_id={values['user'].id}&access_token={access_token}"
|
||||||
|
|
||||||
|
values['prev_record'] = current_task_index != 0 and task_url % history[current_task_index - 1]
|
||||||
|
values['next_record'] = current_task_index < total_task - 1 and task_url % history[current_task_index + 1]
|
||||||
|
|
||||||
|
return values
|
||||||
|
|
||||||
|
def _task_get_searchbar_sortings(self, milestones_allowed):
|
||||||
|
values = {
|
||||||
|
'date': {'label': _('Newest'), 'order': 'create_date desc', 'sequence': 1},
|
||||||
|
'name': {'label': _('Title'), 'order': 'name', 'sequence': 2},
|
||||||
|
'project': {'label': _('Project'), 'order': 'project_id, stage_id', 'sequence': 3},
|
||||||
|
'stage': {'label': _('Stage'), 'order': 'stage_id, project_id', 'sequence': 5},
|
||||||
|
'status': {'label': _('Status'), 'order': 'kanban_state', 'sequence': 6},
|
||||||
|
'priority': {'label': _('Priority'), 'order': 'priority desc', 'sequence': 8},
|
||||||
|
'date_deadline': {'label': _('Deadline'), 'order': 'date_deadline asc', 'sequence': 9},
|
||||||
|
'update': {'label': _('Last Stage Update'), 'order': 'date_last_stage_update desc', 'sequence': 11},
|
||||||
|
}
|
||||||
|
if milestones_allowed:
|
||||||
|
values['milestone'] = {'label': _('Milestone'), 'order': 'milestone_id', 'sequence': 7}
|
||||||
|
return values
|
||||||
|
|
||||||
|
def _task_get_searchbar_groupby(self, milestones_allowed):
|
||||||
|
values = {
|
||||||
|
'none': {'input': 'none', 'label': _('None'), 'order': 1},
|
||||||
|
'project': {'input': 'project', 'label': _('Project'), 'order': 2},
|
||||||
|
'stage': {'input': 'stage', 'label': _('Stage'), 'order': 4},
|
||||||
|
'status': {'input': 'status', 'label': _('Status'), 'order': 5},
|
||||||
|
'priority': {'input': 'priority', 'label': _('Priority'), 'order': 7},
|
||||||
|
'customer': {'input': 'customer', 'label': _('Customer'), 'order': 10},
|
||||||
|
}
|
||||||
|
if milestones_allowed:
|
||||||
|
values['milestone'] = {'input': 'milestone', 'label': _('Milestone'), 'order': 6}
|
||||||
|
return dict(sorted(values.items(), key=lambda item: item[1]["order"]))
|
||||||
|
|
||||||
|
def _task_get_groupby_mapping(self):
|
||||||
|
return {
|
||||||
|
'project': 'project_id',
|
||||||
|
'stage': 'stage_id',
|
||||||
|
'customer': 'partner_id',
|
||||||
|
'milestone': 'milestone_id',
|
||||||
|
'priority': 'priority',
|
||||||
|
'status': 'kanban_state',
|
||||||
|
}
|
||||||
|
|
||||||
|
def _task_get_order(self, order, groupby):
|
||||||
|
groupby_mapping = self._task_get_groupby_mapping()
|
||||||
|
field_name = groupby_mapping.get(groupby, '')
|
||||||
|
if not field_name:
|
||||||
|
return order
|
||||||
|
return '%s, %s' % (field_name, order)
|
||||||
|
|
||||||
|
def _task_get_searchbar_inputs(self, milestones_allowed):
|
||||||
|
values = {
|
||||||
|
'all': {'input': 'all', 'label': _('Search in All'), 'order': 1},
|
||||||
|
'content': {'input': 'content', 'label': Markup(_('Search <span class="nolabel"> (in Content)</span>')), 'order': 1},
|
||||||
|
'ref': {'input': 'ref', 'label': _('Search in Ref'), 'order': 1},
|
||||||
|
'project': {'input': 'project', 'label': _('Search in Project'), 'order': 2},
|
||||||
|
'users': {'input': 'users', 'label': _('Search in Assignees'), 'order': 3},
|
||||||
|
'stage': {'input': 'stage', 'label': _('Search in Stages'), 'order': 4},
|
||||||
|
'status': {'input': 'status', 'label': _('Search in Status'), 'order': 5},
|
||||||
|
'priority': {'input': 'priority', 'label': _('Search in Priority'), 'order': 7},
|
||||||
|
'message': {'input': 'message', 'label': _('Search in Messages'), 'order': 11},
|
||||||
|
}
|
||||||
|
if milestones_allowed:
|
||||||
|
values['milestone'] = {'input': 'milestone', 'label': _('Search in Milestone'), 'order': 6}
|
||||||
|
|
||||||
|
return dict(sorted(values.items(), key=lambda item: item[1]["order"]))
|
||||||
|
|
||||||
|
def _task_get_search_domain(self, search_in, search):
|
||||||
|
search_domain = []
|
||||||
|
if search_in in ('content', 'all'):
|
||||||
|
search_domain.append([('name', 'ilike', search)])
|
||||||
|
search_domain.append([('description', 'ilike', search)])
|
||||||
|
if search_in in ('customer', 'all'):
|
||||||
|
search_domain.append([('partner_id', 'ilike', search)])
|
||||||
|
if search_in in ('message', 'all'):
|
||||||
|
search_domain.append([('message_ids.body', 'ilike', search)])
|
||||||
|
if search_in in ('stage', 'all'):
|
||||||
|
search_domain.append([('stage_id', 'ilike', search)])
|
||||||
|
if search_in in ('project', 'all'):
|
||||||
|
search_domain.append([('project_id', 'ilike', search)])
|
||||||
|
if search_in in ('ref', 'all'):
|
||||||
|
search_domain.append([('id', 'ilike', search)])
|
||||||
|
if search_in in ('milestone', 'all'):
|
||||||
|
search_domain.append([('milestone_id', 'ilike', search)])
|
||||||
|
if search_in in ('users', 'all'):
|
||||||
|
user_ids = request.env['res.users'].sudo().search([('name', 'ilike', search)])
|
||||||
|
search_domain.append([('user_ids', 'in', user_ids.ids)])
|
||||||
|
if search_in in ('priority', 'all'):
|
||||||
|
search_domain.append([('priority', 'ilike', search == 'normal' and '0' or '1')])
|
||||||
|
if search_in in ('status', 'all'):
|
||||||
|
search_domain.append([
|
||||||
|
('kanban_state', 'ilike', 'normal' if search == 'In Progress' else 'done' if search == 'Ready' else 'blocked' if search == 'Blocked' else search)
|
||||||
|
])
|
||||||
|
return OR(search_domain)
|
||||||
|
|
||||||
|
def _prepare_tasks_values(self, page, date_begin, date_end, sortby, search, search_in, groupby, url="/my/tasks", domain=None, su=False):
|
||||||
|
values = self._prepare_portal_layout_values()
|
||||||
|
|
||||||
|
Task = request.env['project.task']
|
||||||
|
milestone_domain = AND([domain, [('allow_milestones', '=', 'True')]])
|
||||||
|
milestones_allowed = Task.sudo().search_count(milestone_domain, limit=1) == 1
|
||||||
|
searchbar_sortings = dict(sorted(self._task_get_searchbar_sortings(milestones_allowed).items(),
|
||||||
|
key=lambda item: item[1]["sequence"]))
|
||||||
|
searchbar_inputs = self._task_get_searchbar_inputs(milestones_allowed)
|
||||||
|
searchbar_groupby = self._task_get_searchbar_groupby(milestones_allowed)
|
||||||
|
|
||||||
|
if not domain:
|
||||||
|
domain = []
|
||||||
|
if not su and Task.check_access_rights('read'):
|
||||||
|
domain = AND([domain, request.env['ir.rule']._compute_domain(Task._name, 'read')])
|
||||||
|
Task_sudo = Task.sudo()
|
||||||
|
|
||||||
|
# default sort by value
|
||||||
|
if not sortby or sortby not in searchbar_sortings or (sortby == 'milestone' and not milestones_allowed):
|
||||||
|
sortby = 'date'
|
||||||
|
order = searchbar_sortings[sortby]['order']
|
||||||
|
|
||||||
|
# default group by value
|
||||||
|
if not groupby or (groupby == 'milestone' and not milestones_allowed):
|
||||||
|
groupby = 'project'
|
||||||
|
|
||||||
|
if date_begin and date_end:
|
||||||
|
domain += [('create_date', '>', date_begin), ('create_date', '<=', date_end)]
|
||||||
|
|
||||||
|
# search reset if needed
|
||||||
|
if not milestones_allowed and search_in == 'milestone':
|
||||||
|
search_in = 'all'
|
||||||
|
# search
|
||||||
|
if search and search_in:
|
||||||
|
domain += self._task_get_search_domain(search_in, search)
|
||||||
|
|
||||||
|
# content according to pager and archive selected
|
||||||
|
order = self._task_get_order(order, groupby)
|
||||||
|
|
||||||
|
def get_grouped_tasks(pager_offset):
|
||||||
|
tasks = Task_sudo.search(domain, order=order, limit=self._items_per_page, offset=pager_offset)
|
||||||
|
request.session['my_project_tasks_history' if url.startswith('/my/projects') else 'my_tasks_history'] = tasks.ids[:100]
|
||||||
|
|
||||||
|
tasks_project_allow_milestone = tasks.filtered(lambda t: t.allow_milestones)
|
||||||
|
tasks_no_milestone = tasks - tasks_project_allow_milestone
|
||||||
|
|
||||||
|
groupby_mapping = self._task_get_groupby_mapping()
|
||||||
|
group = groupby_mapping.get(groupby)
|
||||||
|
if group:
|
||||||
|
if group == 'milestone_id':
|
||||||
|
grouped_tasks = [Task_sudo.concat(*g) for k, g in groupbyelem(tasks_project_allow_milestone, itemgetter(group))]
|
||||||
|
|
||||||
|
if not grouped_tasks:
|
||||||
|
if tasks_no_milestone:
|
||||||
|
grouped_tasks = [tasks_no_milestone]
|
||||||
|
else:
|
||||||
|
if grouped_tasks[len(grouped_tasks) - 1][0].milestone_id and tasks_no_milestone:
|
||||||
|
grouped_tasks.append(tasks_no_milestone)
|
||||||
|
else:
|
||||||
|
grouped_tasks[len(grouped_tasks) - 1] |= tasks_no_milestone
|
||||||
|
|
||||||
|
else:
|
||||||
|
grouped_tasks = [Task_sudo.concat(*g) for k, g in groupbyelem(tasks, itemgetter(group))]
|
||||||
|
else:
|
||||||
|
grouped_tasks = [tasks] if tasks else []
|
||||||
|
|
||||||
|
task_states = dict(Task_sudo._fields['kanban_state']._description_selection(request.env))
|
||||||
|
if sortby == 'status':
|
||||||
|
if groupby == 'none' and grouped_tasks:
|
||||||
|
grouped_tasks[0] = grouped_tasks[0].sorted(lambda tasks: task_states.get(tasks.kanban_state))
|
||||||
|
else:
|
||||||
|
grouped_tasks.sort(key=lambda tasks: task_states.get(tasks[0].kanban_state))
|
||||||
|
return grouped_tasks
|
||||||
|
|
||||||
|
values.update({
|
||||||
|
'date': date_begin,
|
||||||
|
'date_end': date_end,
|
||||||
|
'grouped_tasks': get_grouped_tasks,
|
||||||
|
'allow_milestone': milestones_allowed,
|
||||||
|
'page_name': 'task',
|
||||||
|
'default_url': url,
|
||||||
|
'task_url': 'tasks',
|
||||||
|
'pager': {
|
||||||
|
"url": url,
|
||||||
|
"url_args": {'date_begin': date_begin, 'date_end': date_end, 'sortby': sortby, 'groupby': groupby, 'search_in': search_in, 'search': search},
|
||||||
|
"total": Task_sudo.search_count(domain),
|
||||||
|
"page": page,
|
||||||
|
"step": self._items_per_page
|
||||||
|
},
|
||||||
|
'searchbar_sortings': searchbar_sortings,
|
||||||
|
'searchbar_groupby': searchbar_groupby,
|
||||||
|
'searchbar_inputs': searchbar_inputs,
|
||||||
|
'search_in': search_in,
|
||||||
|
'search': search,
|
||||||
|
'sortby': sortby,
|
||||||
|
'groupby': groupby,
|
||||||
|
})
|
||||||
|
return values
|
||||||
|
|
||||||
|
def _get_my_tasks_searchbar_filters(self, project_domain=None, task_domain=None):
|
||||||
|
searchbar_filters = {
|
||||||
|
'all': {'label': _('All'), 'domain': [('project_id', '!=', False)]},
|
||||||
|
}
|
||||||
|
|
||||||
|
# extends filterby criteria with project the customer has access to
|
||||||
|
projects = request.env['project.project'].search(project_domain or [])
|
||||||
|
for project in projects:
|
||||||
|
searchbar_filters.update({
|
||||||
|
str(project.id): {'label': project.name, 'domain': [('project_id', '=', project.id)]}
|
||||||
|
})
|
||||||
|
|
||||||
|
# extends filterby criteria with project (criteria name is the project id)
|
||||||
|
# Note: portal users can't view projects they don't follow
|
||||||
|
project_groups = request.env['project.task'].read_group(AND([[('project_id', 'not in', projects.ids)], task_domain or []]),
|
||||||
|
['project_id'], ['project_id'])
|
||||||
|
for group in project_groups:
|
||||||
|
proj_id = group['project_id'][0] if group['project_id'] else False
|
||||||
|
proj_name = group['project_id'][1] if group['project_id'] else _('Others')
|
||||||
|
searchbar_filters.update({
|
||||||
|
str(proj_id): {'label': proj_name, 'domain': [('project_id', '=', proj_id)]}
|
||||||
|
})
|
||||||
|
return searchbar_filters
|
||||||
|
|
||||||
|
@http.route(['/my/tasks', '/my/tasks/page/<int:page>'], type='http', auth="user", website=True)
|
||||||
|
def portal_my_tasks(self, page=1, date_begin=None, date_end=None, sortby=None, filterby=None, search=None, search_in='content', groupby=None, **kw):
|
||||||
|
searchbar_filters = self._get_my_tasks_searchbar_filters()
|
||||||
|
|
||||||
|
if not filterby:
|
||||||
|
filterby = 'all'
|
||||||
|
domain = searchbar_filters.get(filterby, searchbar_filters.get('all'))['domain']
|
||||||
|
|
||||||
|
values = self._prepare_tasks_values(page, date_begin, date_end, sortby, search, search_in, groupby, domain=domain)
|
||||||
|
|
||||||
|
# pager
|
||||||
|
pager_vals = values['pager']
|
||||||
|
pager_vals['url_args'].update(filterby=filterby)
|
||||||
|
pager = portal_pager(**pager_vals)
|
||||||
|
|
||||||
|
values.update({
|
||||||
|
'grouped_tasks': values['grouped_tasks'](pager['offset']),
|
||||||
|
'pager': pager,
|
||||||
|
'searchbar_filters': OrderedDict(sorted(searchbar_filters.items())),
|
||||||
|
'filterby': filterby,
|
||||||
|
})
|
||||||
|
return request.render("project.portal_my_tasks", values)
|
||||||
|
|
||||||
|
def _show_task_report(self, task_sudo, report_type, download):
|
||||||
|
# This method is to be overriden to report timesheets if the module is installed.
|
||||||
|
# The route should not be called if at least hr_timesheet is not installed
|
||||||
|
raise MissingError(_('There is nothing to report.'))
|
||||||
|
|
||||||
|
@http.route(['/my/tasks/<int:task_id>'], type='http', auth="public", website=True)
|
||||||
|
def portal_my_task(self, task_id, report_type=None, access_token=None, project_sharing=False, **kw):
|
||||||
|
try:
|
||||||
|
task_sudo = self._document_check_access('project.task', task_id, access_token)
|
||||||
|
except (AccessError, MissingError):
|
||||||
|
return request.redirect('/my')
|
||||||
|
|
||||||
|
if report_type in ('pdf', 'html', 'text'):
|
||||||
|
return self._show_task_report(task_sudo, report_type, download=kw.get('download'))
|
||||||
|
|
||||||
|
# ensure attachment are accessible with access token inside template
|
||||||
|
for attachment in task_sudo.attachment_ids:
|
||||||
|
attachment.generate_access_token()
|
||||||
|
if project_sharing is True:
|
||||||
|
# Then the user arrives to the stat button shown in form view of project.task and the portal user can see only 1 task
|
||||||
|
# so the history should be reset.
|
||||||
|
request.session['my_tasks_history'] = task_sudo.ids
|
||||||
|
values = self._task_get_page_view_values(task_sudo, access_token, **kw)
|
||||||
|
return request.render("project.portal_my_task", values)
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from werkzeug.exceptions import Forbidden
|
||||||
|
|
||||||
|
from odoo.http import request, route
|
||||||
|
|
||||||
|
from odoo.addons.portal.controllers.mail import PortalChatter
|
||||||
|
from .portal import ProjectCustomerPortal
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectSharingChatter(PortalChatter):
|
||||||
|
def _check_project_access_and_get_token(self, project_id, res_model, res_id, token):
|
||||||
|
""" Check if the chatter in project sharing can be accessed
|
||||||
|
|
||||||
|
If the portal user is in the project sharing, then we do not have the access token of the task
|
||||||
|
but we can have the one of the project (if the user accessed to the project sharing views via the shared link).
|
||||||
|
So, we need to check if the chatter is for a task and if the res_id is a task
|
||||||
|
in the project shared. Then, if we had the project token and this one is the one in the project
|
||||||
|
then we return the token of the task to continue the portal chatter process.
|
||||||
|
If we do not have any token, then we need to check if the portal user is a follower of the project shared.
|
||||||
|
If it is the case, then we give the access token of the task.
|
||||||
|
"""
|
||||||
|
project_sudo = ProjectCustomerPortal._document_check_access(self, 'project.project', project_id, token)
|
||||||
|
can_access = project_sudo and res_model == 'project.task' and project_sudo.with_user(request.env.user)._check_project_sharing_access()
|
||||||
|
task = None
|
||||||
|
if can_access:
|
||||||
|
task = request.env['project.task'].sudo().search([('id', '=', res_id), ('project_id', '=', project_sudo.id)])
|
||||||
|
if not can_access or not task:
|
||||||
|
raise Forbidden()
|
||||||
|
return task[task._mail_post_token_field]
|
||||||
|
|
||||||
|
# ============================================================ #
|
||||||
|
# Note concerning the methods portal_chatter_(init/post/fetch)
|
||||||
|
# ============================================================ #
|
||||||
|
#
|
||||||
|
# When the project is shared to a portal user with the edit rights,
|
||||||
|
# he has the read/write access to the related tasks. So it could be
|
||||||
|
# possible to call directly the message_post method on a task.
|
||||||
|
#
|
||||||
|
# This change is considered as safe, as we only willingly expose
|
||||||
|
# records, for some assumed fields only, and this feature is
|
||||||
|
# optional and opt-in. (like the public employee model for example).
|
||||||
|
# It doesn't allow portal users to access other models, like
|
||||||
|
# a timesheet or an invoice.
|
||||||
|
#
|
||||||
|
# It could seem odd to use those routes, and converting the project
|
||||||
|
# access token into the task access token, as the user has actually
|
||||||
|
# access to the records.
|
||||||
|
#
|
||||||
|
# However, it has been decided that it was the less hacky way to
|
||||||
|
# achieve this, as:
|
||||||
|
#
|
||||||
|
# - We're reusing the existing routes, that convert all the data
|
||||||
|
# into valid arguments for the methods we use (message_post, ...).
|
||||||
|
# That way, we don't have to reinvent the wheel, duplicating code
|
||||||
|
# from mail/portal that surely will lead too desynchronization
|
||||||
|
# and inconsistencies over the time.
|
||||||
|
#
|
||||||
|
# - We don't define new routes, to do the exact same things than portal,
|
||||||
|
# considering that the portal user can use message_post for example
|
||||||
|
# because he has access to the record.
|
||||||
|
# Let's suppose that we remove this in a future development, those
|
||||||
|
# new routes won't be valid anymore.
|
||||||
|
#
|
||||||
|
# - We could have reused the mail widgets, as we already reuse the
|
||||||
|
# form/list/kanban views, etc. However, we only want to display
|
||||||
|
# the messages and allow to post. We don't need the next activities
|
||||||
|
# the followers system, etc. This required to override most of the
|
||||||
|
# mail.thread basic methods, without being sure that this would
|
||||||
|
# work with other installed applications or customizations
|
||||||
|
|
||||||
|
@route()
|
||||||
|
def portal_chatter_init(self, res_model, res_id, domain=False, limit=False, **kwargs):
|
||||||
|
project_sharing_id = kwargs.get('project_sharing_id')
|
||||||
|
if project_sharing_id:
|
||||||
|
# if there is a token in `kwargs` then it should be the access_token of the project shared
|
||||||
|
token = self._check_project_access_and_get_token(project_sharing_id, res_model, res_id, kwargs.get('token'))
|
||||||
|
if token:
|
||||||
|
del kwargs['project_sharing_id']
|
||||||
|
kwargs['token'] = token
|
||||||
|
return super().portal_chatter_init(res_model, res_id, domain=domain, limit=limit, **kwargs)
|
||||||
|
|
||||||
|
@route()
|
||||||
|
def portal_chatter_post(self, res_model, res_id, message, attachment_ids=None, attachment_tokens=None, **kw):
|
||||||
|
project_sharing_id = kw.get('project_sharing_id')
|
||||||
|
if project_sharing_id:
|
||||||
|
token = self._check_project_access_and_get_token(project_sharing_id, res_model, res_id, kw.get('token'))
|
||||||
|
if token:
|
||||||
|
del kw['project_sharing_id']
|
||||||
|
kw['token'] = token
|
||||||
|
return super().portal_chatter_post(res_model, res_id, message, attachment_ids=attachment_ids, attachment_tokens=attachment_tokens, **kw)
|
||||||
|
|
||||||
|
@route()
|
||||||
|
def portal_message_fetch(self, res_model, res_id, domain=False, limit=10, offset=0, **kw):
|
||||||
|
project_sharing_id = kw.get('project_sharing_id')
|
||||||
|
if project_sharing_id:
|
||||||
|
token = self._check_project_access_and_get_token(project_sharing_id, res_model, res_id, kw.get('token'))
|
||||||
|
if token is not None:
|
||||||
|
kw['token'] = token # Update token (either string which contains token value or False)
|
||||||
|
return super().portal_message_fetch(res_model, res_id, domain=domain, limit=limit, offset=offset, **kw)
|
||||||
41
odoo-bringout-oca-ocb-project/project/data/digest_data.xml
Normal file
41
odoo-bringout-oca-ocb-project/project/data/digest_data.xml
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<record id="digest.digest_digest_default" model="digest.digest">
|
||||||
|
<field name="kpi_project_task_opened">True</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<data>
|
||||||
|
<record id="digest_tip_project_0" model="digest.tip">
|
||||||
|
<field name="name">Tip: Customize tasks and stages according to the project</field>
|
||||||
|
<field name="sequence">1200</field>
|
||||||
|
<field name="group_id" ref="project.group_project_manager"/>
|
||||||
|
<field name="tip_description" type="html">
|
||||||
|
<div>
|
||||||
|
<p class="tip_title">Tip: Customize tasks and stages according to the project</p>
|
||||||
|
<p class="tip_content">Customize how tasks are named according to the project and create tailor made status messages for each step of the workflow. It helps to document your workflow: what should be done at which step.</p>
|
||||||
|
<img src="https://download.odoocdn.com/digests/project/static/src/img/project-custom-tasks.gif" class="illustration_border" />
|
||||||
|
</div>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="digest_tip_project_1" model="digest.tip">
|
||||||
|
<field name="name">Tip: Create tasks from incoming emails</field>
|
||||||
|
<field name="sequence">1300</field>
|
||||||
|
<field name="group_id" ref="project.group_project_user"/>
|
||||||
|
<field name="tip_description" type="html">
|
||||||
|
<div>
|
||||||
|
<t t-set="project_record" t-value="object.env['project.project'].search([('alias_name', '!=', False)], limit=1, order='sequence asc')"/>
|
||||||
|
<p class="tip_title">Tip: Create tasks from incoming emails</p>
|
||||||
|
<t t-if="project_record and project_record.alias_domain">
|
||||||
|
<p class="tip_content">Emails sent to <a t-attf-href="mailto:{{project_record.alias_value}}" target="_blank" style="color: #875a7b; text-decoration: none;"><t t-out="project_record.alias_value" /></a> will generate tasks in your <t t-out="project_record.name"></t> project.</p>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<p class="tip_content">Create tasks by sending an email to the email address of your project.</p>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
21
odoo-bringout-oca-ocb-project/project/data/ir_cron_data.xml
Normal file
21
odoo-bringout-oca-ocb-project/project/data/ir_cron_data.xml
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo noupdate="1">
|
||||||
|
<record id="ir_cron_rating_project" model="ir.cron">
|
||||||
|
<field name="name">Project: Send rating</field>
|
||||||
|
<field name="model_id" ref="project.model_project_project"/>
|
||||||
|
<field name="state">code</field>
|
||||||
|
<field name="code">model._send_rating_all()</field>
|
||||||
|
<field name="interval_type">days</field>
|
||||||
|
<field name="numbercall">-1</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="ir_cron_recurring_tasks" model="ir.cron">
|
||||||
|
<field name="name">Project: Create Recurring Tasks</field>
|
||||||
|
<field name="model_id" ref="project.model_project_task_recurrence"/>
|
||||||
|
<field name="state">code</field>
|
||||||
|
<field name="code">model._cron_create_recurring_tasks()</field>
|
||||||
|
<field name="interval_type">days</field>
|
||||||
|
<field name="numbercall">-1</field>
|
||||||
|
<field name="nextcall" eval="(DateTime.now().replace(hour=3, minute=0) + timedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')" />
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo noupdate="1">
|
||||||
|
<!-- Task-related subtypes for messaging / Chatter -->
|
||||||
|
<record id="mt_task_new" model="mail.message.subtype">
|
||||||
|
<field name="name">Task Created</field>
|
||||||
|
<field name="res_model">project.task</field>
|
||||||
|
<field name="default" eval="False"/>
|
||||||
|
<field name="hidden" eval="True"/>
|
||||||
|
<field name="description">Task Created</field>
|
||||||
|
</record>
|
||||||
|
<record id="mt_task_stage" model="mail.message.subtype">
|
||||||
|
<field name="name">Stage Changed</field>
|
||||||
|
<field name="res_model">project.task</field>
|
||||||
|
<field name="default" eval="False"/>
|
||||||
|
<field name="description">Stage changed</field>
|
||||||
|
</record>
|
||||||
|
<record id="mt_task_blocked" model="mail.message.subtype">
|
||||||
|
<field name="name">Task Blocked</field>
|
||||||
|
<field name="res_model">project.task</field>
|
||||||
|
<field name="default" eval="False"/>
|
||||||
|
<field name="description">Task blocked</field>
|
||||||
|
</record>
|
||||||
|
<record id="mt_task_ready" model="mail.message.subtype">
|
||||||
|
<field name="name">Task Ready</field>
|
||||||
|
<field name="res_model">project.task</field>
|
||||||
|
<field name="default" eval="False"/>
|
||||||
|
<field name="description">Task ready for Next Stage</field>
|
||||||
|
</record>
|
||||||
|
<record id="mt_task_progress" model="mail.message.subtype">
|
||||||
|
<field name="name">Task in Progress</field>
|
||||||
|
<field name="res_model">project.task</field>
|
||||||
|
<field name="default" eval="False"/>
|
||||||
|
</record>
|
||||||
|
<record id="mt_task_rating" model="mail.message.subtype">
|
||||||
|
<field name="name">Task Rating</field>
|
||||||
|
<field name="res_model">project.task</field>
|
||||||
|
<field name="default" eval="False"/>
|
||||||
|
</record>
|
||||||
|
<record id="mt_task_dependency_change" model="mail.message.subtype">
|
||||||
|
<field name="name">Task Dependency Changes</field>
|
||||||
|
<field name="res_model">project.task</field>
|
||||||
|
<field name="default" eval="False"/>
|
||||||
|
<field name="hidden" eval="True"/>
|
||||||
|
</record>
|
||||||
|
<!-- Update-related subtypes for messaging / Chatter -->
|
||||||
|
<record id="mt_update_create" model="mail.message.subtype">
|
||||||
|
<field name="name">Update Created</field>
|
||||||
|
<field name="res_model">project.update</field>
|
||||||
|
<field name="default" eval="False"/>
|
||||||
|
<field name="description">Update Created</field>
|
||||||
|
<field name="hidden" eval="True"/>
|
||||||
|
</record>
|
||||||
|
<!-- Project-related subtypes for messaging / Chatter -->
|
||||||
|
<record id="mt_project_stage_change" model="mail.message.subtype">
|
||||||
|
<field name="name">Project Stage Changed</field>
|
||||||
|
<field name="sequence">9</field>
|
||||||
|
<field name="res_model">project.project</field>
|
||||||
|
<field name="default" eval="False"/>
|
||||||
|
<field name="hidden" eval="True"/>
|
||||||
|
</record>
|
||||||
|
<record id="mt_project_task_new" model="mail.message.subtype">
|
||||||
|
<field name="name">Task Created</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="res_model">project.project</field>
|
||||||
|
<field name="default" eval="False"/>
|
||||||
|
<field name="parent_id" ref="mt_task_new"/>
|
||||||
|
<field name="relation_field">project_id</field>
|
||||||
|
</record>
|
||||||
|
<record id="mt_project_task_blocked" model="mail.message.subtype">
|
||||||
|
<field name="name">Task Blocked</field>
|
||||||
|
<field name="sequence">11</field>
|
||||||
|
<field name="res_model">project.project</field>
|
||||||
|
<field name="default" eval="False"/>
|
||||||
|
<field name="parent_id" ref="mt_task_blocked"/>
|
||||||
|
<field name="relation_field">project_id</field>
|
||||||
|
</record>
|
||||||
|
<record id="mt_project_task_ready" model="mail.message.subtype">
|
||||||
|
<field name="name">Task Ready</field>
|
||||||
|
<field name="sequence">12</field>
|
||||||
|
<field name="res_model">project.project</field>
|
||||||
|
<field name="default" eval="False"/>
|
||||||
|
<field name="parent_id" ref="mt_task_ready"/>
|
||||||
|
<field name="relation_field">project_id</field>
|
||||||
|
</record>
|
||||||
|
<record id="mt_project_task_stage" model="mail.message.subtype">
|
||||||
|
<field name="name">Task Stage Changed</field>
|
||||||
|
<field name="sequence">13</field>
|
||||||
|
<field name="res_model">project.project</field>
|
||||||
|
<field name="default" eval="False"/>
|
||||||
|
<field name="parent_id" ref="mt_task_stage"/>
|
||||||
|
<field name="relation_field">project_id</field>
|
||||||
|
</record>
|
||||||
|
<record id="mt_project_task_rating" model="mail.message.subtype">
|
||||||
|
<field name="name">Task Rating</field>
|
||||||
|
<field name="sequence">14</field>
|
||||||
|
<field name="res_model">project.project</field>
|
||||||
|
<field name="default" eval="True"/>
|
||||||
|
<field name="parent_id" ref="mt_task_rating"/>
|
||||||
|
<field name="relation_field">project_id</field>
|
||||||
|
</record>
|
||||||
|
<record id="mt_project_task_dependency_change" model="mail.message.subtype">
|
||||||
|
<field name="name">Task Dependency Changes</field>
|
||||||
|
<field name="sequence">15</field>
|
||||||
|
<field name="res_model">project.project</field>
|
||||||
|
<field name="default" eval="False"/>
|
||||||
|
<field name="parent_id" ref="mt_task_dependency_change"/>
|
||||||
|
<field name="relation_field">project_id</field>
|
||||||
|
<field name="hidden" eval="True"/>
|
||||||
|
</record>
|
||||||
|
<record id="mt_project_update_create" model="mail.message.subtype">
|
||||||
|
<field name="name">Update Created</field>
|
||||||
|
<field name="sequence">16</field>
|
||||||
|
<field name="res_model">project.project</field>
|
||||||
|
<field name="default" eval="False"/>
|
||||||
|
<field name="parent_id" ref="mt_update_create"/>
|
||||||
|
<field name="relation_field">project_id</field>
|
||||||
|
<field name="hidden" eval="True"/>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<!-- Sample stage-related template -->
|
||||||
|
<record id="mail_template_data_project_task" model="mail.template">
|
||||||
|
<field name="name">Project: Request Acknowledgment</field>
|
||||||
|
<field name="model_id" ref="project.model_project_task"/>
|
||||||
|
<field name="subject">Reception of {{ object.name }}</field>
|
||||||
|
<field name="use_default_to" eval="True"/>
|
||||||
|
<field name="description">Set this template on a project's stage to automate email when tasks reach stages</field>
|
||||||
|
<field name="body_html" type="html">
|
||||||
|
<div>
|
||||||
|
Dear <t t-out="object.partner_id.name or 'customer'">Brandon Freeman</t>,<br/>
|
||||||
|
Thank you for your enquiry.<br />
|
||||||
|
If you have any questions, please let us know.
|
||||||
|
<br/><br/>
|
||||||
|
Thank you,
|
||||||
|
<t t-if="user.signature">
|
||||||
|
<br />
|
||||||
|
<t t-out="user.signature or ''">--<br/>Mitchell Admin</t>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</field>
|
||||||
|
<field name="lang">{{ object.partner_id.lang }}</field>
|
||||||
|
<field name="auto_delete" eval="True"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Mail sent to request a rating for a task -->
|
||||||
|
<record id="rating_project_request_email_template" model="mail.template">
|
||||||
|
<field name="name">Project: Task Rating Request</field>
|
||||||
|
<field name="model_id" ref="project.model_project_task"/>
|
||||||
|
<field name="subject">{{ object.project_id.company_id.name }}: Satisfaction Survey</field>
|
||||||
|
<field name="email_from">{{ (object._rating_get_operator().email_formatted if object._rating_get_operator() else user.email_formatted) }}</field>
|
||||||
|
<field name="partner_to" >{{ object._rating_get_partner().id }}</field>
|
||||||
|
<field name="description">Set this template on a project stage to request feedback from your customers. Enable the "customer ratings" feature on the project</field>
|
||||||
|
<field name="body_html" type="html">
|
||||||
|
<div>
|
||||||
|
<t t-set="access_token" t-value="object._rating_get_access_token()"/>
|
||||||
|
<t t-set="partner" t-value="object._rating_get_partner()"/>
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" width="590" style="width:100%; margin:0px auto;">
|
||||||
|
<tbody>
|
||||||
|
<tr><td valign="top" style="font-size: 13px;">
|
||||||
|
<t t-if="partner.name">
|
||||||
|
Hello <t t-out="partner.name or ''">Brandon Freeman</t>,<br/><br/>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
Hello,<br/><br/>
|
||||||
|
</t>
|
||||||
|
Please take a moment to rate our services related to the task "<strong t-out="object.name or ''">Planning and budget</strong>"
|
||||||
|
<t t-if="object._rating_get_operator().name">
|
||||||
|
assigned to <strong t-out="object._rating_get_operator().name or ''">Mitchell Admin</strong>.<br/>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
.<br/>
|
||||||
|
</t>
|
||||||
|
</td></tr>
|
||||||
|
<tr><td style="text-align: center;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" width="590" summary="o_mail_notification" style="width:100%; margin: 32px 0px 32px 0px;">
|
||||||
|
<tr><td style="font-size: 13px;">
|
||||||
|
<strong>Tell us how you feel about our service</strong><br/>
|
||||||
|
<span style="font-size: 12px; opacity: 0.5; color: #454748;">(click on one of these smileys)</span>
|
||||||
|
</td></tr>
|
||||||
|
<tr><td style="font-size: 13px;">
|
||||||
|
<table style="width:100%;text-align:center;margin-top:2rem;">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a t-attf-href="/rate/{{ access_token }}/5">
|
||||||
|
<img alt="Satisfied" src="/rating/static/src/img/rating_5.png" title="Satisfied"/>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a t-attf-href="/rate/{{ access_token }}/3">
|
||||||
|
<img alt="Okay" src="/rating/static/src/img/rating_3.png" title="Okay"/>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a t-attf-href="/rate/{{ access_token }}/1">
|
||||||
|
<img alt="Dissatisfied" src="/rating/static/src/img/rating_1.png" title="Dissatisfied"/>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</td></tr>
|
||||||
|
<tr><td valign="top" style="font-size: 13px;">
|
||||||
|
We appreciate your feedback. It helps us to improve continuously.
|
||||||
|
<t t-if="object.project_id.rating_status == 'stage'">
|
||||||
|
<br/><br/><span style="margin: 0px 0px 0px 0px; font-size: 12px; opacity: 0.5; color: #454748;">This customer survey has been sent because your task has been moved to the stage <b t-out="object.stage_id.name or ''">In progress</b></span>
|
||||||
|
</t>
|
||||||
|
<t t-if="object.project_id.rating_status == 'periodic'">
|
||||||
|
<br/><span style="margin: 0px 0px 0px 0px; font-size: 12px; opacity: 0.5; color: #454748;">This customer survey is sent <b t-out="object.project_id.rating_status_period or ''">Weekly</b> as long as the task is in the <b t-out="object.stage_id.name or ''">In progress</b> stage.</span>
|
||||||
|
</t>
|
||||||
|
</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</field>
|
||||||
|
<field name="lang">{{ object._rating_get_partner().lang }}</field>
|
||||||
|
<field name="auto_delete" eval="True"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- You have been assigned email -->
|
||||||
|
<template id="project_message_user_assigned">
|
||||||
|
<span>Dear <t t-esc="assignee_name"/>,</span>
|
||||||
|
<br/><br/>
|
||||||
|
<span style="margin-top: 8px;">You have been assigned to the <t t-esc="model_description or 'document'"/> <t t-esc="object.display_name"/>.</span>
|
||||||
|
<br/>
|
||||||
|
</template>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<record id="project_done_email_template" model="mail.template">
|
||||||
|
<field name="name">Project: Project Completed</field>
|
||||||
|
<field name="model_id" ref="project.model_project_project"/>
|
||||||
|
<field name="subject">Project status - {{ object.name }}</field>
|
||||||
|
<field name="email_from">{{ (object.partner_id.email_formatted if object.partner_id else user.email_formatted) }}</field>
|
||||||
|
<field name="partner_to" >{{ object.partner_id.id }}</field>
|
||||||
|
<field name="description">Set on project's stages to inform customers when a project reaches that stage</field>
|
||||||
|
<field name="body_html" type="html">
|
||||||
|
<div>
|
||||||
|
Dear <t t-out="object.partner_id.name or 'customer'">Brandon Freeman</t>,<br/>
|
||||||
|
It is my pleasure to let you know that we have successfully completed the project "<strong t-out="object.name or ''">Renovations</strong>".
|
||||||
|
<t t-if="user.signature">
|
||||||
|
<br />
|
||||||
|
<t t-out="user.signature or ''">--<br/>Mitchell Admin</t>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
<br/><span style="margin: 0px 0px 0px 0px; font-size: 12px; opacity: 0.5; color: #454748;" groups="project.group_project_stages">You are receiving this email because your project has been moved to the stage <b t-out="object.stage_id.name or ''">Done</b></span>
|
||||||
|
</field>
|
||||||
|
<field name="lang">{{ object.partner_id.lang }}</field>
|
||||||
|
<field name="auto_delete" eval="True"/>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
25
odoo-bringout-oca-ocb-project/project/data/project_data.xml
Normal file
25
odoo-bringout-oca-ocb-project/project/data/project_data.xml
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- Project Stages -->
|
||||||
|
<record id="project_project_stage_0" model="project.project.stage">
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="name">To Do</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="project_project_stage_1" model="project.project.stage">
|
||||||
|
<field name="sequence">15</field>
|
||||||
|
<field name="name">In Progress</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="project_project_stage_2" model="project.project.stage">
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="name">Done</field>
|
||||||
|
<field name="fold" eval="True"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="project_project_stage_3" model="project.project.stage">
|
||||||
|
<field name="sequence">25</field>
|
||||||
|
<field name="name">Canceled</field>
|
||||||
|
<field name="fold" eval="True"/>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
1336
odoo-bringout-oca-ocb-project/project/data/project_demo.xml
Normal file
1336
odoo-bringout-oca-ocb-project/project/data/project_demo.xml
Normal file
File diff suppressed because it is too large
Load diff
16
odoo-bringout-oca-ocb-project/project/doc/changelog.rst
Normal file
16
odoo-bringout-oca-ocb-project/project/doc/changelog.rst
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
.. _changelog:
|
||||||
|
|
||||||
|
Changelog
|
||||||
|
=========
|
||||||
|
|
||||||
|
`trunk (saas-2)`
|
||||||
|
----------------
|
||||||
|
|
||||||
|
- Stage/state update
|
||||||
|
|
||||||
|
- ``project.task``: removed inheritance from ``base_stage`` class and removed
|
||||||
|
``state`` field. Added ``date_last_stage_update`` field holding last stage_id
|
||||||
|
modification. Updated reports.
|
||||||
|
- ``project.task.type``: removed ``state`` field.
|
||||||
|
|
||||||
|
- Removed ``project.task.reevaluate`` wizard.
|
||||||
22
odoo-bringout-oca-ocb-project/project/doc/index.rst
Normal file
22
odoo-bringout-oca-ocb-project/project/doc/index.rst
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
=====================
|
||||||
|
Project DevDoc
|
||||||
|
=====================
|
||||||
|
|
||||||
|
Project module documentation
|
||||||
|
===================================
|
||||||
|
|
||||||
|
Documentation topics
|
||||||
|
''''''''''''''''''''
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
|
||||||
|
stage_status.rst
|
||||||
|
|
||||||
|
Changelog
|
||||||
|
'''''''''
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
|
||||||
|
changelog.rst
|
||||||
55
odoo-bringout-oca-ocb-project/project/doc/stage_status.rst
Normal file
55
odoo-bringout-oca-ocb-project/project/doc/stage_status.rst
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
.. _stage_status:
|
||||||
|
|
||||||
|
Stage and Status
|
||||||
|
================
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0 saas-2 state/stage cleaning
|
||||||
|
|
||||||
|
Stage
|
||||||
|
+++++
|
||||||
|
|
||||||
|
This revision removed the concept of state on project.task objects. The ``state``
|
||||||
|
field has been totally removed and replaced by stages, using ``stage_id``. The
|
||||||
|
following models are impacted:
|
||||||
|
|
||||||
|
- ``project.task`` now use only stages. However a convention still exists about
|
||||||
|
'New' stage. A task is consdered as ``new`` when it has the following
|
||||||
|
properties:
|
||||||
|
|
||||||
|
- ``stage_id and stage_id.sequence = 1``
|
||||||
|
|
||||||
|
- ``project.task.type`` do not have any ``state`` field anymore.
|
||||||
|
- ``project.task.report`` do not have any ``state`` field anymore.
|
||||||
|
|
||||||
|
By default a newly created task is in a new stage. It means that it will
|
||||||
|
fetch the stage having ``sequence = 1``. Stage mangement is done using the
|
||||||
|
kanban view or the clikable statusbar. It is not done using buttons anymore.
|
||||||
|
|
||||||
|
Stage analysis
|
||||||
|
++++++++++++++
|
||||||
|
|
||||||
|
Stage analysis can be performed using the newly introduced ``date_last_stage_update``
|
||||||
|
datetime field. This field is updated everytime ``stage_id`` is updated.
|
||||||
|
|
||||||
|
``project.task.report`` model also uses the ``date_last_stage_update`` field.
|
||||||
|
This allows to group and analyse the time spend in the various stages.
|
||||||
|
|
||||||
|
Open / Assignment date
|
||||||
|
+++++++++++++++++++++++
|
||||||
|
|
||||||
|
The ``date_open`` field meaning has been updated. It is now set when the ``user_id``
|
||||||
|
(responsible) is set. It is therefore the assignment date.
|
||||||
|
|
||||||
|
Subtypes
|
||||||
|
++++++++
|
||||||
|
|
||||||
|
The following subtypes are triggered on ``project.task``:
|
||||||
|
|
||||||
|
- ``mt_task_new``: new tasks. Condition: ``obj.stage_id and obj.stage_id.sequence == 1``
|
||||||
|
- ``mt_task_stage``: stage changed. Condition: ``obj.stage_id and obj.stage_id.sequence != 1``
|
||||||
|
- ``mt_task_assigned``: user assigned. condition: ``obj.user_id and obj.user_id.id``
|
||||||
|
- ``mt_task_blocked``: kanban state blocked. Condition: ``obj.kanban_state == 'blocked'``
|
||||||
|
|
||||||
|
|
||||||
|
Those subtypes are also available on the ``project.project`` model and are used
|
||||||
|
for the auto subscription.
|
||||||
6213
odoo-bringout-oca-ocb-project/project/i18n/af.po
Normal file
6213
odoo-bringout-oca-ocb-project/project/i18n/af.po
Normal file
File diff suppressed because it is too large
Load diff
6209
odoo-bringout-oca-ocb-project/project/i18n/am.po
Normal file
6209
odoo-bringout-oca-ocb-project/project/i18n/am.po
Normal file
File diff suppressed because it is too large
Load diff
6515
odoo-bringout-oca-ocb-project/project/i18n/ar.po
Normal file
6515
odoo-bringout-oca-ocb-project/project/i18n/ar.po
Normal file
File diff suppressed because it is too large
Load diff
6269
odoo-bringout-oca-ocb-project/project/i18n/az.po
Normal file
6269
odoo-bringout-oca-ocb-project/project/i18n/az.po
Normal file
File diff suppressed because it is too large
Load diff
6222
odoo-bringout-oca-ocb-project/project/i18n/be.po
Normal file
6222
odoo-bringout-oca-ocb-project/project/i18n/be.po
Normal file
File diff suppressed because it is too large
Load diff
6299
odoo-bringout-oca-ocb-project/project/i18n/bg.po
Normal file
6299
odoo-bringout-oca-ocb-project/project/i18n/bg.po
Normal file
File diff suppressed because it is too large
Load diff
6218
odoo-bringout-oca-ocb-project/project/i18n/bs.po
Normal file
6218
odoo-bringout-oca-ocb-project/project/i18n/bs.po
Normal file
File diff suppressed because it is too large
Load diff
6587
odoo-bringout-oca-ocb-project/project/i18n/ca.po
Normal file
6587
odoo-bringout-oca-ocb-project/project/i18n/ca.po
Normal file
File diff suppressed because it is too large
Load diff
6560
odoo-bringout-oca-ocb-project/project/i18n/cs.po
Normal file
6560
odoo-bringout-oca-ocb-project/project/i18n/cs.po
Normal file
File diff suppressed because it is too large
Load diff
6363
odoo-bringout-oca-ocb-project/project/i18n/da.po
Normal file
6363
odoo-bringout-oca-ocb-project/project/i18n/da.po
Normal file
File diff suppressed because it is too large
Load diff
6617
odoo-bringout-oca-ocb-project/project/i18n/de.po
Normal file
6617
odoo-bringout-oca-ocb-project/project/i18n/de.po
Normal file
File diff suppressed because it is too large
Load diff
3126
odoo-bringout-oca-ocb-project/project/i18n/el.po
Normal file
3126
odoo-bringout-oca-ocb-project/project/i18n/el.po
Normal file
File diff suppressed because it is too large
Load diff
4071
odoo-bringout-oca-ocb-project/project/i18n/en_GB.po
Normal file
4071
odoo-bringout-oca-ocb-project/project/i18n/en_GB.po
Normal file
File diff suppressed because it is too large
Load diff
6596
odoo-bringout-oca-ocb-project/project/i18n/es.po
Normal file
6596
odoo-bringout-oca-ocb-project/project/i18n/es.po
Normal file
File diff suppressed because it is too large
Load diff
3995
odoo-bringout-oca-ocb-project/project/i18n/es_BO.po
Normal file
3995
odoo-bringout-oca-ocb-project/project/i18n/es_BO.po
Normal file
File diff suppressed because it is too large
Load diff
4033
odoo-bringout-oca-ocb-project/project/i18n/es_CL.po
Normal file
4033
odoo-bringout-oca-ocb-project/project/i18n/es_CL.po
Normal file
File diff suppressed because it is too large
Load diff
4660
odoo-bringout-oca-ocb-project/project/i18n/es_CO.po
Normal file
4660
odoo-bringout-oca-ocb-project/project/i18n/es_CO.po
Normal file
File diff suppressed because it is too large
Load diff
4004
odoo-bringout-oca-ocb-project/project/i18n/es_CR.po
Normal file
4004
odoo-bringout-oca-ocb-project/project/i18n/es_CR.po
Normal file
File diff suppressed because it is too large
Load diff
4655
odoo-bringout-oca-ocb-project/project/i18n/es_DO.po
Normal file
4655
odoo-bringout-oca-ocb-project/project/i18n/es_DO.po
Normal file
File diff suppressed because it is too large
Load diff
4662
odoo-bringout-oca-ocb-project/project/i18n/es_EC.po
Normal file
4662
odoo-bringout-oca-ocb-project/project/i18n/es_EC.po
Normal file
File diff suppressed because it is too large
Load diff
6590
odoo-bringout-oca-ocb-project/project/i18n/es_MX.po
Normal file
6590
odoo-bringout-oca-ocb-project/project/i18n/es_MX.po
Normal file
File diff suppressed because it is too large
Load diff
4035
odoo-bringout-oca-ocb-project/project/i18n/es_PE.po
Normal file
4035
odoo-bringout-oca-ocb-project/project/i18n/es_PE.po
Normal file
File diff suppressed because it is too large
Load diff
3992
odoo-bringout-oca-ocb-project/project/i18n/es_PY.po
Normal file
3992
odoo-bringout-oca-ocb-project/project/i18n/es_PY.po
Normal file
File diff suppressed because it is too large
Load diff
4008
odoo-bringout-oca-ocb-project/project/i18n/es_VE.po
Normal file
4008
odoo-bringout-oca-ocb-project/project/i18n/es_VE.po
Normal file
File diff suppressed because it is too large
Load diff
6501
odoo-bringout-oca-ocb-project/project/i18n/et.po
Normal file
6501
odoo-bringout-oca-ocb-project/project/i18n/et.po
Normal file
File diff suppressed because it is too large
Load diff
4008
odoo-bringout-oca-ocb-project/project/i18n/eu.po
Normal file
4008
odoo-bringout-oca-ocb-project/project/i18n/eu.po
Normal file
File diff suppressed because it is too large
Load diff
6478
odoo-bringout-oca-ocb-project/project/i18n/fa.po
Normal file
6478
odoo-bringout-oca-ocb-project/project/i18n/fa.po
Normal file
File diff suppressed because it is too large
Load diff
6603
odoo-bringout-oca-ocb-project/project/i18n/fi.po
Normal file
6603
odoo-bringout-oca-ocb-project/project/i18n/fi.po
Normal file
File diff suppressed because it is too large
Load diff
6601
odoo-bringout-oca-ocb-project/project/i18n/fr.po
Normal file
6601
odoo-bringout-oca-ocb-project/project/i18n/fr.po
Normal file
File diff suppressed because it is too large
Load diff
3979
odoo-bringout-oca-ocb-project/project/i18n/gl.po
Normal file
3979
odoo-bringout-oca-ocb-project/project/i18n/gl.po
Normal file
File diff suppressed because it is too large
Load diff
6217
odoo-bringout-oca-ocb-project/project/i18n/gu.po
Normal file
6217
odoo-bringout-oca-ocb-project/project/i18n/gu.po
Normal file
File diff suppressed because it is too large
Load diff
6395
odoo-bringout-oca-ocb-project/project/i18n/he.po
Normal file
6395
odoo-bringout-oca-ocb-project/project/i18n/he.po
Normal file
File diff suppressed because it is too large
Load diff
6237
odoo-bringout-oca-ocb-project/project/i18n/hi.po
Normal file
6237
odoo-bringout-oca-ocb-project/project/i18n/hi.po
Normal file
File diff suppressed because it is too large
Load diff
6320
odoo-bringout-oca-ocb-project/project/i18n/hr.po
Normal file
6320
odoo-bringout-oca-ocb-project/project/i18n/hr.po
Normal file
File diff suppressed because it is too large
Load diff
6325
odoo-bringout-oca-ocb-project/project/i18n/hu.po
Normal file
6325
odoo-bringout-oca-ocb-project/project/i18n/hu.po
Normal file
File diff suppressed because it is too large
Load diff
6209
odoo-bringout-oca-ocb-project/project/i18n/hy.po
Normal file
6209
odoo-bringout-oca-ocb-project/project/i18n/hy.po
Normal file
File diff suppressed because it is too large
Load diff
6537
odoo-bringout-oca-ocb-project/project/i18n/id.po
Normal file
6537
odoo-bringout-oca-ocb-project/project/i18n/id.po
Normal file
File diff suppressed because it is too large
Load diff
6226
odoo-bringout-oca-ocb-project/project/i18n/is.po
Normal file
6226
odoo-bringout-oca-ocb-project/project/i18n/is.po
Normal file
File diff suppressed because it is too large
Load diff
6580
odoo-bringout-oca-ocb-project/project/i18n/it.po
Normal file
6580
odoo-bringout-oca-ocb-project/project/i18n/it.po
Normal file
File diff suppressed because it is too large
Load diff
6347
odoo-bringout-oca-ocb-project/project/i18n/ja.po
Normal file
6347
odoo-bringout-oca-ocb-project/project/i18n/ja.po
Normal file
File diff suppressed because it is too large
Load diff
3988
odoo-bringout-oca-ocb-project/project/i18n/ka.po
Normal file
3988
odoo-bringout-oca-ocb-project/project/i18n/ka.po
Normal file
File diff suppressed because it is too large
Load diff
4014
odoo-bringout-oca-ocb-project/project/i18n/kab.po
Normal file
4014
odoo-bringout-oca-ocb-project/project/i18n/kab.po
Normal file
File diff suppressed because it is too large
Load diff
6249
odoo-bringout-oca-ocb-project/project/i18n/km.po
Normal file
6249
odoo-bringout-oca-ocb-project/project/i18n/km.po
Normal file
File diff suppressed because it is too large
Load diff
6424
odoo-bringout-oca-ocb-project/project/i18n/ko.po
Normal file
6424
odoo-bringout-oca-ocb-project/project/i18n/ko.po
Normal file
File diff suppressed because it is too large
Load diff
3029
odoo-bringout-oca-ocb-project/project/i18n/lb.po
Normal file
3029
odoo-bringout-oca-ocb-project/project/i18n/lb.po
Normal file
File diff suppressed because it is too large
Load diff
6240
odoo-bringout-oca-ocb-project/project/i18n/lo.po
Normal file
6240
odoo-bringout-oca-ocb-project/project/i18n/lo.po
Normal file
File diff suppressed because it is too large
Load diff
6307
odoo-bringout-oca-ocb-project/project/i18n/lt.po
Normal file
6307
odoo-bringout-oca-ocb-project/project/i18n/lt.po
Normal file
File diff suppressed because it is too large
Load diff
6253
odoo-bringout-oca-ocb-project/project/i18n/lv.po
Normal file
6253
odoo-bringout-oca-ocb-project/project/i18n/lv.po
Normal file
File diff suppressed because it is too large
Load diff
4168
odoo-bringout-oca-ocb-project/project/i18n/mk.po
Normal file
4168
odoo-bringout-oca-ocb-project/project/i18n/mk.po
Normal file
File diff suppressed because it is too large
Load diff
6226
odoo-bringout-oca-ocb-project/project/i18n/ml.po
Normal file
6226
odoo-bringout-oca-ocb-project/project/i18n/ml.po
Normal file
File diff suppressed because it is too large
Load diff
6297
odoo-bringout-oca-ocb-project/project/i18n/mn.po
Normal file
6297
odoo-bringout-oca-ocb-project/project/i18n/mn.po
Normal file
File diff suppressed because it is too large
Load diff
6219
odoo-bringout-oca-ocb-project/project/i18n/ms.po
Normal file
6219
odoo-bringout-oca-ocb-project/project/i18n/ms.po
Normal file
File diff suppressed because it is too large
Load diff
6234
odoo-bringout-oca-ocb-project/project/i18n/nb.po
Normal file
6234
odoo-bringout-oca-ocb-project/project/i18n/nb.po
Normal file
File diff suppressed because it is too large
Load diff
6582
odoo-bringout-oca-ocb-project/project/i18n/nl.po
Normal file
6582
odoo-bringout-oca-ocb-project/project/i18n/nl.po
Normal file
File diff suppressed because it is too large
Load diff
6209
odoo-bringout-oca-ocb-project/project/i18n/no.po
Normal file
6209
odoo-bringout-oca-ocb-project/project/i18n/no.po
Normal file
File diff suppressed because it is too large
Load diff
6590
odoo-bringout-oca-ocb-project/project/i18n/pl.po
Normal file
6590
odoo-bringout-oca-ocb-project/project/i18n/pl.po
Normal file
File diff suppressed because it is too large
Load diff
6218
odoo-bringout-oca-ocb-project/project/i18n/project.pot
Normal file
6218
odoo-bringout-oca-ocb-project/project/i18n/project.pot
Normal file
File diff suppressed because it is too large
Load diff
6438
odoo-bringout-oca-ocb-project/project/i18n/pt.po
Normal file
6438
odoo-bringout-oca-ocb-project/project/i18n/pt.po
Normal file
File diff suppressed because it is too large
Load diff
6578
odoo-bringout-oca-ocb-project/project/i18n/pt_BR.po
Normal file
6578
odoo-bringout-oca-ocb-project/project/i18n/pt_BR.po
Normal file
File diff suppressed because it is too large
Load diff
6422
odoo-bringout-oca-ocb-project/project/i18n/ro.po
Normal file
6422
odoo-bringout-oca-ocb-project/project/i18n/ro.po
Normal file
File diff suppressed because it is too large
Load diff
6532
odoo-bringout-oca-ocb-project/project/i18n/ru.po
Normal file
6532
odoo-bringout-oca-ocb-project/project/i18n/ru.po
Normal file
File diff suppressed because it is too large
Load diff
6299
odoo-bringout-oca-ocb-project/project/i18n/sk.po
Normal file
6299
odoo-bringout-oca-ocb-project/project/i18n/sk.po
Normal file
File diff suppressed because it is too large
Load diff
6307
odoo-bringout-oca-ocb-project/project/i18n/sl.po
Normal file
6307
odoo-bringout-oca-ocb-project/project/i18n/sl.po
Normal file
File diff suppressed because it is too large
Load diff
6209
odoo-bringout-oca-ocb-project/project/i18n/sq.po
Normal file
6209
odoo-bringout-oca-ocb-project/project/i18n/sq.po
Normal file
File diff suppressed because it is too large
Load diff
6439
odoo-bringout-oca-ocb-project/project/i18n/sr.po
Normal file
6439
odoo-bringout-oca-ocb-project/project/i18n/sr.po
Normal file
File diff suppressed because it is too large
Load diff
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue