mirror of
https://github.com/bringout/oca-project.git
synced 2026-04-18 16:01:59 +02:00
Move 124 sale modules to oca-sale, create oca-project with 56 project modules from oca-workflow-process
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
9eb7ae5807
commit
6094c218b2
2332 changed files with 125826 additions and 0 deletions
46
odoo-bringout-oca-project-project_task_code_portal/README.md
Normal file
46
odoo-bringout-oca-project-project_task_code_portal/README.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# Project Task Code Portal
|
||||
|
||||
Odoo addon: project_task_code_portal
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install odoo-bringout-oca-project-project_task_code_portal
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
This addon depends on:
|
||||
- project_task_code
|
||||
|
||||
## Manifest Information
|
||||
|
||||
- **Name**: Project Task Code Portal
|
||||
- **Version**: 16.0.1.0.0
|
||||
- **Category**: Project
|
||||
- **License**: AGPL-3
|
||||
- **Installable**: False
|
||||
|
||||
## Source
|
||||
|
||||
Based on [OCA/project](https://github.com/OCA/project) branch 16.0, addon `project_task_code_portal`.
|
||||
|
||||
## License
|
||||
|
||||
This package maintains the original AGPL-3 license from the upstream Odoo project.
|
||||
|
||||
## Documentation
|
||||
|
||||
- Overview: doc/OVERVIEW.md
|
||||
- Architecture: doc/ARCHITECTURE.md
|
||||
- Models: doc/MODELS.md
|
||||
- Controllers: doc/CONTROLLERS.md
|
||||
- Wizards: doc/WIZARDS.md
|
||||
- Reports: doc/REPORTS.md
|
||||
- Security: doc/SECURITY.md
|
||||
- Install: doc/INSTALL.md
|
||||
- Usage: doc/USAGE.md
|
||||
- Configuration: doc/CONFIGURATION.md
|
||||
- Dependencies: doc/DEPENDENCIES.md
|
||||
- Troubleshooting: doc/TROUBLESHOOTING.md
|
||||
- FAQ: doc/FAQ.md
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
# Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
U[Users] -->|HTTP| V[Views and QWeb Templates]
|
||||
V --> C[Controllers]
|
||||
V --> W[Wizards – Transient Models]
|
||||
C --> M[Models and ORM]
|
||||
W --> M
|
||||
M --> R[Reports]
|
||||
DX[Data XML] --> M
|
||||
S[Security – ACLs and Groups] -. enforces .-> M
|
||||
|
||||
subgraph Project_task_code_portal Module - project_task_code_portal
|
||||
direction LR
|
||||
M:::layer
|
||||
W:::layer
|
||||
C:::layer
|
||||
V:::layer
|
||||
R:::layer
|
||||
S:::layer
|
||||
DX:::layer
|
||||
end
|
||||
|
||||
classDef layer fill:#eef8ff,stroke:#6ea8fe,stroke-width:1px
|
||||
```
|
||||
|
||||
Notes
|
||||
- Views include tree/form/kanban templates and report templates.
|
||||
- Controllers provide website/portal routes when present.
|
||||
- Wizards are UI flows implemented with `models.TransientModel`.
|
||||
- Data XML loads data/demo records; Security defines groups and access.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Configuration
|
||||
|
||||
Refer to Odoo settings for project_task_code_portal. Configure related models, access rights, and options as needed.
|
||||
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# Dependencies
|
||||
|
||||
This addon depends on:
|
||||
|
||||
- [project_task_code](https://github.com/bringout/oca-workflow-process)
|
||||
|
|
@ -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_task_code_portal or install in UI.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# Install
|
||||
|
||||
```bash
|
||||
pip install odoo-bringout-oca-project-project_task_code_portal"
|
||||
# or
|
||||
uv pip install odoo-bringout-oca-project-project_task_code_portal"
|
||||
```
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
# Models
|
||||
|
||||
Detected core models and extensions in project_task_code_portal.
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class project_task
|
||||
```
|
||||
|
||||
Notes
|
||||
- Classes show model technical names; fields omitted for brevity.
|
||||
- Items listed under _inherit are extensions of existing models.
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# Overview
|
||||
|
||||
Packaged Odoo addon: project_task_code_portal. Provides features documented in upstream Odoo 16 under this addon.
|
||||
|
||||
- Source: OCA/OCB 16.0, addon project_task_code_portal
|
||||
- License: LGPL-3
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Reports
|
||||
|
||||
This module does not define custom reports.
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# Security
|
||||
|
||||
This module does not define custom security rules or access controls beyond Odoo defaults.
|
||||
|
||||
Default Odoo security applies:
|
||||
- Base user access through standard groups
|
||||
- Model access inherited from dependencies
|
||||
- No custom row-level security rules
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# Troubleshooting
|
||||
|
||||
- Ensure Python and Odoo environment matches repo guidance.
|
||||
- Check database connectivity and logs if startup fails.
|
||||
- Validate that dependent addons listed in DEPENDENCIES.md are installed.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# Usage
|
||||
|
||||
Start Odoo including this addon (from repo root):
|
||||
|
||||
```bash
|
||||
python3 scripts/nix_odoo_web_server.py --db-name mydb --addon project_task_code_portal
|
||||
```
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Wizards
|
||||
|
||||
This module does not include UI wizards.
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
========================
|
||||
Project Task Code Portal
|
||||
========================
|
||||
|
||||
..
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:25c95f644bf6a4cefd05afc40477e124c9281ea350af91154b20c8e7bbba87b2
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
|
||||
:target: https://odoo-community.org/page/development-status
|
||||
:alt: Beta
|
||||
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
|
||||
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
|
||||
:alt: License: AGPL-3
|
||||
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fproject-lightgray.png?logo=github
|
||||
:target: https://github.com/OCA/project/tree/16.0/project_task_code_portal
|
||||
:alt: OCA/project
|
||||
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
|
||||
:target: https://translation.odoo-community.org/projects/project-16-0/project-16-0-project_task_code_portal
|
||||
:alt: Translate me on Weblate
|
||||
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
|
||||
:target: https://runboat.odoo-community.org/builds?repo=OCA/project&target_branch=16.0
|
||||
:alt: Try me on Runboat
|
||||
|
||||
|badge1| |badge2| |badge3| |badge4| |badge5|
|
||||
|
||||
This module implements task codes in the portal. It allows users to:
|
||||
|
||||
- Use task codes instead of IDs in portal URLs.
|
||||
- Search for tasks by their unique code.
|
||||
- Display task codes in portal task views.
|
||||
|
||||
**Table of contents**
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
|
||||
Use Cases / Context
|
||||
===================
|
||||
|
||||
Business Need
|
||||
-------------
|
||||
|
||||
Task codes provide great flexibility for backend users. However portal
|
||||
users still have to deal with task id's instead of task codes, which can
|
||||
be misleading and create potential issues.
|
||||
|
||||
Approach
|
||||
--------
|
||||
|
||||
This module extends the standard project portal by allowing:
|
||||
|
||||
- Searching for tasks by their unique code.
|
||||
- Displaying the task code in both list and detail views.
|
||||
- Generating reports that include the task code.
|
||||
|
||||
Use Cases
|
||||
---------
|
||||
|
||||
- Clients can directly access a task via a URL containing the task code.
|
||||
- Support teams can quickly locate a task using its unique identifier.
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
No configuration is required.
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
This module will replace the "ID" field with the "Code" in the following
|
||||
portal views:
|
||||
|
||||
- Task list (including the project task list)
|
||||
- Task page
|
||||
- Task search
|
||||
|
||||
It will modify the portal URLs as follows:
|
||||
|
||||
- **Before:** ``https://example.com/my/tasks/<task_id>``
|
||||
- **After:** ``https://example.com/my/tasks/<task_code>``
|
||||
|
||||
Changelog
|
||||
=========
|
||||
|
||||
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `GitHub Issues <https://github.com/OCA/project/issues>`_.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us to smash it by providing a detailed and welcomed
|
||||
`feedback <https://github.com/OCA/project/issues/new?body=module:%20project_task_code_portal%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
|
||||
|
||||
Do not contact contributors directly about support or help with technical issues.
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
Authors
|
||||
-------
|
||||
|
||||
* Cetmix OÜ
|
||||
|
||||
Contributors
|
||||
------------
|
||||
|
||||
- `Cetmix <https://cetmix.com/>`__:
|
||||
|
||||
- Ivan Sokolov
|
||||
- Anatol Mikheev
|
||||
|
||||
Maintainers
|
||||
-----------
|
||||
|
||||
This module is maintained by the OCA.
|
||||
|
||||
.. image:: https://odoo-community.org/logo.png
|
||||
:alt: Odoo Community Association
|
||||
:target: https://odoo-community.org
|
||||
|
||||
OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.
|
||||
|
||||
This module is part of the `OCA/project <https://github.com/OCA/project/tree/16.0/project_task_code_portal>`_ project on GitHub.
|
||||
|
||||
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import controllers
|
||||
from . import models
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# Copyright 2025 Cetmix OÜ
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
{
|
||||
"name": "Project Task Code Portal",
|
||||
"summary": "Use custom task code in customer portal",
|
||||
"version": "16.0.1.0.0",
|
||||
"development_status": "Beta",
|
||||
"category": "Project",
|
||||
"website": "https://github.com/OCA/project",
|
||||
"author": "Cetmix OÜ, Odoo Community Association (OCA)",
|
||||
"license": "AGPL-3",
|
||||
"depends": [
|
||||
"project_task_code",
|
||||
],
|
||||
"data": [
|
||||
"templates/portal_templates.xml",
|
||||
],
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import portal
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
# Copyright (C) 2025 Cetmix OÜ
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import _, http
|
||||
from odoo.exceptions import AccessError, MissingError
|
||||
from odoo.http import request
|
||||
|
||||
from odoo.addons.project.controllers.portal import ProjectCustomerPortal
|
||||
|
||||
|
||||
class PortalProjectTask(ProjectCustomerPortal):
|
||||
def _task_get_searchbar_inputs(self, milestones_allowed):
|
||||
inputs = super()._task_get_searchbar_inputs(milestones_allowed)
|
||||
if "ref" in inputs and "label" in inputs["ref"]:
|
||||
inputs["ref"]["label"] = _("Search in Task code")
|
||||
return inputs
|
||||
|
||||
def _task_get_search_domain(self, search_in, search):
|
||||
domain = super()._task_get_search_domain(search_in, search)
|
||||
if search_in in ("ref", "all"):
|
||||
for i, item in enumerate(domain):
|
||||
if isinstance(item, tuple) and item[0] == "id":
|
||||
domain[i] = ("code", item[1], item[2])
|
||||
break
|
||||
return domain
|
||||
|
||||
def get_accessible_task_by_code(self, task_code, access_token):
|
||||
task_id = (
|
||||
request.env["project.task"]
|
||||
.sudo()
|
||||
.search([("code", "=", task_code)], limit=1)
|
||||
.id
|
||||
)
|
||||
if not task_id:
|
||||
raise MissingError(_("No task with this code."))
|
||||
task_sudo = self._document_check_access("project.task", task_id, access_token)
|
||||
return task_sudo
|
||||
|
||||
@http.route(
|
||||
["/my/tasks/<string:task_code>"], type="http", auth="public", website=True
|
||||
)
|
||||
def portal_my_task(
|
||||
self,
|
||||
task_code,
|
||||
report_type=None,
|
||||
access_token=None,
|
||||
project_sharing=False,
|
||||
**kw
|
||||
):
|
||||
try:
|
||||
task_sudo = self.get_accessible_task_by_code(task_code, 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
|
||||
task_sudo.attachment_ids.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)
|
||||
|
||||
@http.route(
|
||||
"/my/projects/<int:project_id>/task/<string:task_code>",
|
||||
type="http",
|
||||
auth="public",
|
||||
website=True,
|
||||
)
|
||||
def portal_my_project_task(
|
||||
self, project_id=None, task_code=None, access_token=None, **kw
|
||||
):
|
||||
try:
|
||||
project_sudo = self._document_check_access(
|
||||
"project.project", project_id, access_token
|
||||
)
|
||||
task_sudo = self.get_accessible_task_by_code(task_code, access_token)
|
||||
except (AccessError, MissingError):
|
||||
return request.redirect("/my")
|
||||
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)
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * project_task_code_portal
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 16.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: project_task_code_portal
|
||||
#. odoo-python
|
||||
#: code:addons/project_task_code_portal/controllers/portal.py:0
|
||||
#, python-format
|
||||
msgid "No task with this code."
|
||||
msgstr "No task with this code."
|
||||
|
||||
#. module: project_task_code_portal
|
||||
#. odoo-python
|
||||
#: code:addons/project_task_code_portal/controllers/portal.py:0
|
||||
#, python-format
|
||||
msgid "Search in Task code"
|
||||
msgstr "Pretraži in Zadatak code"
|
||||
|
||||
#. module: project_task_code_portal
|
||||
#: model:ir.model,name:project_task_code_portal.model_project_task
|
||||
msgid "Task"
|
||||
msgstr "Zadatak"
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * project_task_code_portal
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 16.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-06-03 14:25+0000\n"
|
||||
"Last-Translator: mymage <stefano.consolaro@mymage.it>\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: it\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 5.10.4\n"
|
||||
|
||||
#. module: project_task_code_portal
|
||||
#. odoo-python
|
||||
#: code:addons/project_task_code_portal/controllers/portal.py:0
|
||||
#, python-format
|
||||
msgid "No task with this code."
|
||||
msgstr "Nessun lavoro con questo codice."
|
||||
|
||||
#. module: project_task_code_portal
|
||||
#. odoo-python
|
||||
#: code:addons/project_task_code_portal/controllers/portal.py:0
|
||||
#, python-format
|
||||
msgid "Search in Task code"
|
||||
msgstr "Ricerca nel codice lavoro"
|
||||
|
||||
#. module: project_task_code_portal
|
||||
#: model:ir.model,name:project_task_code_portal.model_project_task
|
||||
msgid "Task"
|
||||
msgstr "Lavoro"
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * project_task_code_portal
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 16.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: project_task_code_portal
|
||||
#. odoo-python
|
||||
#: code:addons/project_task_code_portal/controllers/portal.py:0
|
||||
#, python-format
|
||||
msgid "No task with this code."
|
||||
msgstr ""
|
||||
|
||||
#. module: project_task_code_portal
|
||||
#. odoo-python
|
||||
#: code:addons/project_task_code_portal/controllers/portal.py:0
|
||||
#, python-format
|
||||
msgid "Search in Task code"
|
||||
msgstr ""
|
||||
|
||||
#. module: project_task_code_portal
|
||||
#: model:ir.model,name:project_task_code_portal.model_project_task
|
||||
msgid "Task"
|
||||
msgstr ""
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import project_task
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
# Copyright (C) 2025 Cetmix OÜ
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class ProjectTask(models.Model):
|
||||
_inherit = "project.task"
|
||||
|
||||
@property
|
||||
def SELF_READABLE_FIELDS(self):
|
||||
return super().SELF_READABLE_FIELDS | {"code"}
|
||||
|
|
@ -0,0 +1 @@
|
|||
No configuration is required.
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
## Business Need
|
||||
Task codes provide great flexibility for backend users. However portal users still have to deal with task id's instead of task codes, which can be misleading and create potential issues.
|
||||
|
||||
## Approach
|
||||
This module extends the standard project portal by allowing:
|
||||
- Searching for tasks by their unique code.
|
||||
- Displaying the task code in both list and detail views.
|
||||
- Generating reports that include the task code.
|
||||
|
||||
## Use Cases
|
||||
- Clients can directly access a task via a URL containing the task code.
|
||||
- Support teams can quickly locate a task using its unique identifier.
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
* [Cetmix](https://cetmix.com/):
|
||||
|
||||
* Ivan Sokolov
|
||||
* Anatol Mikheev
|
||||
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
This module implements task codes in the portal. It allows users to:
|
||||
|
||||
- Use task codes instead of IDs in portal URLs.
|
||||
- Search for tasks by their unique code.
|
||||
- Display task codes in portal task views.
|
||||
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
This module will replace the "ID" field with the "Code" in the following portal views:
|
||||
|
||||
- Task list (including the project task list)
|
||||
- Task page
|
||||
- Task search
|
||||
|
||||
It will modify the portal URLs as follows:
|
||||
|
||||
- **Before:** `https://example.com/my/tasks/<task_id>`
|
||||
- **After:** `https://example.com/my/tasks/<task_code>`
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
|
|
@ -0,0 +1,66 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Project Task Code Portal</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 20px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
h1, h2 {
|
||||
color: #2c3e50;
|
||||
}
|
||||
ul {
|
||||
margin-left: 20px;
|
||||
}
|
||||
code {
|
||||
background-color: #f4f4f4;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Project Task Code Portal</h1>
|
||||
<p>
|
||||
This module implements task codes in the portal by replacing internal task IDs with human-readable codes.
|
||||
It enhances the customer portal by providing clear task references and improving overall usability.
|
||||
</p>
|
||||
|
||||
<h2>Key Features</h2>
|
||||
<ul>
|
||||
<li>Use task codes instead of IDs in portal URLs</li>
|
||||
<li>Search for tasks by their unique code in portal filters</li>
|
||||
<li>Display task codes in both task list and detailed views</li>
|
||||
<li>Maintains backward compatibility with existing ID-based routes</li>
|
||||
<li>Includes comprehensive documentation for configuration and usage</li>
|
||||
</ul>
|
||||
|
||||
<h2>Usage</h2>
|
||||
<p>
|
||||
Once installed and configured, the module modifies portal URLs as follows:
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>Before:</strong> <code>https://example.com/my/tasks/<task_id></code></li>
|
||||
<li><strong>After:</strong> <code>https://example.com/my/tasks/<task_code></code></li>
|
||||
</ul>
|
||||
<p>
|
||||
Portal users can now easily share and reference tasks using meaningful codes rather than numerical IDs.
|
||||
</p>
|
||||
|
||||
<h2>Installation & Configuration</h2>
|
||||
<p>
|
||||
To install the module, copy it into your Odoo <code>addons</code> directory, ensure that all dependencies are met (e.g., <code>project_task_code</code> and <code>portal</code>), and restart the Odoo server.
|
||||
Detailed configuration instructions are provided in the module documentation.
|
||||
</p>
|
||||
|
||||
<h2>Context</h2>
|
||||
<p>
|
||||
This module addresses the need for client-friendly task references in the portal. It was developed to overcome the limitations of using non-intuitive task IDs by introducing unique, human-readable task codes.
|
||||
This results in improved communication, easier task tracking, and enhanced navigation for end users.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!-- Copyright (C) 2025 Cetmix OÜ
|
||||
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
|
||||
<odoo>
|
||||
<template id="portal_tasks_list_override" inherit_id="project.portal_tasks_list">
|
||||
<xpath
|
||||
expr="//td[contains(@class, 'text-start')]/span[@t-esc='task.id']"
|
||||
position="attributes"
|
||||
>
|
||||
<attribute name="t-esc">task.code</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//a[contains(@t-attf-href, '/my/')]" position="attributes">
|
||||
<attribute
|
||||
name="t-attf-href"
|
||||
>/my/#{task_url}/#{task.code}?{{ keep_query() }}</attribute>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="portal_my_task_override" inherit_id="project.portal_my_task">
|
||||
<xpath
|
||||
expr="//small[contains(@class, 'text-muted')]/span[@t-field='task.id']"
|
||||
position="attributes"
|
||||
>
|
||||
<attribute name="t-field">task.code</attribute>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import test_portal
|
||||
|
|
@ -0,0 +1,279 @@
|
|||
# Copyright 2025 Cetmix OÜ
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
import re
|
||||
|
||||
from lxml import html
|
||||
|
||||
from odoo import Command, tools
|
||||
from odoo.tests import tagged
|
||||
|
||||
from odoo.addons.base.tests.common import HttpCaseWithUserPortal
|
||||
from odoo.addons.project.tests.test_access_rights import TestProjectPortalCommon
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class TestPortalTaskCode(TestProjectPortalCommon, HttpCaseWithUserPortal):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestPortalTaskCode, cls).setUpClass()
|
||||
cls.task_1.project_id.privacy_visibility = "portal"
|
||||
task_wizard = cls.env["portal.share"].create(
|
||||
{
|
||||
"res_model": "project.task",
|
||||
"res_id": cls.task_1.id,
|
||||
"partner_ids": [
|
||||
Command.link(cls.partner_portal.id),
|
||||
],
|
||||
}
|
||||
)
|
||||
task_wizard.action_send_mail()
|
||||
|
||||
cls.host = "127.0.0.1"
|
||||
cls.port = tools.config["http_port"]
|
||||
cls.base_url = "http://%s:%d/my/tasks/" % (cls.host, cls.port)
|
||||
cls.url_task_code_pattern = "/my/tasks/{}?"
|
||||
|
||||
def test_portal_tasks_list_access(self):
|
||||
self.authenticate("portal", "portal")
|
||||
response = self.url_open(self.base_url)
|
||||
content = response.content
|
||||
tree = html.fromstring(content)
|
||||
spans = tree.xpath(
|
||||
"//td[contains(@class, 'text-start') and contains(., '#')]//span"
|
||||
)
|
||||
list_tasks_code = [s.text for s in spans]
|
||||
self.assertIn(self.task_1.code, list_tasks_code)
|
||||
link = tree.xpath(f"//td[a/span[contains(text(), '{self.task_1.name}')]]//a")[
|
||||
0
|
||||
].attrib["href"]
|
||||
self.assertEqual(link, self.url_task_code_pattern.format(self.task_1.code))
|
||||
|
||||
def test_portal_task_access(self):
|
||||
self.authenticate("portal", "portal")
|
||||
response = self.url_open(self.base_url + self.task_1.code)
|
||||
content = response.content
|
||||
tree = html.fromstring(content)
|
||||
spans = tree.xpath(
|
||||
"//small[contains(@class, 'text-muted') and contains(@class, 'd-md-inline')]//span"
|
||||
)
|
||||
list_tasks_code = [s.text for s in spans]
|
||||
self.assertIn(self.task_1.code, list_tasks_code)
|
||||
|
||||
def test_portal_task_not_found(self):
|
||||
self.authenticate("portal", "portal")
|
||||
response = self.url_open(self.base_url + "NoCode")
|
||||
home_url = "http://%s:%d/my" % (self.host, self.port)
|
||||
self.assertEqual(response.url, home_url)
|
||||
|
||||
def test_portal_task_search_link_format(self):
|
||||
self.authenticate("portal", "portal")
|
||||
task_code = self.task_1.code
|
||||
query_params = f"?search_in=ref&search={task_code}"
|
||||
response = self.url_open(self.base_url[:-1] + query_params)
|
||||
content = response.content
|
||||
tree = html.fromstring(content)
|
||||
spans = tree.xpath(
|
||||
"//td[contains(@class, 'text-start') and contains(., '#')]//span"
|
||||
)
|
||||
list_tasks_code = [s.text for s in spans]
|
||||
self.assertIn(task_code, list_tasks_code)
|
||||
link = tree.xpath(f"//td[a/span[contains(text(), '{self.task_1.name}')]]//a")[
|
||||
0
|
||||
].attrib["href"]
|
||||
self.assertEqual(
|
||||
link,
|
||||
self.url_task_code_pattern.format(self.task_1.code)[:-1] + query_params,
|
||||
)
|
||||
|
||||
def test_portal_task_report(self):
|
||||
"""Test task report generation through portal."""
|
||||
self.authenticate("portal", "portal")
|
||||
# Check if hr_timesheet module is installed
|
||||
hr_timesheet_installed = bool(
|
||||
self.env["ir.module.module"].search(
|
||||
[("name", "=", "hr_timesheet"), ("state", "=", "installed")]
|
||||
)
|
||||
)
|
||||
response = self.url_open(self.base_url + self.task_1.code + "?report_type=html")
|
||||
if hr_timesheet_installed:
|
||||
# If hr_timesheet is installed, expect successful response
|
||||
# _show_task_report is overridden by hr_timesheet to generate timesheet reports
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn("text/html", response.headers.get("Content-Type", ""))
|
||||
else:
|
||||
# If hr_timesheet is not installed, expect error response
|
||||
# _show_task_report raises MissingError("There is nothing to report.")
|
||||
# This method is to be overriden to report timesheets if the module is installed
|
||||
self.assertEqual(response.status_code, 400)
|
||||
content = response.content
|
||||
tree = html.fromstring(content)
|
||||
error_elements = tree.xpath(
|
||||
"//pre[contains(text(), 'There is nothing to report.')]"
|
||||
)
|
||||
self.assertTrue(
|
||||
error_elements,
|
||||
"Error message 'There is nothing to report.' not found in response",
|
||||
)
|
||||
|
||||
def test_portal_task_project_sharing(self):
|
||||
"""Test project sharing functionality."""
|
||||
self.authenticate("portal", "portal")
|
||||
|
||||
# First, access multiple tasks to build up history
|
||||
other_task = self.env["project.task"].create(
|
||||
{
|
||||
"name": "Other Task",
|
||||
"project_id": self.task_1.project_id.id,
|
||||
}
|
||||
)
|
||||
self.url_open(f"{self.base_url}{other_task.code}")
|
||||
|
||||
# Share the project with portal user
|
||||
project_share_wizard = self.env["project.share.wizard"].create(
|
||||
{
|
||||
"access_mode": "edit",
|
||||
"res_model": "project.project",
|
||||
"res_id": self.task_1.project_id.id,
|
||||
"partner_ids": [Command.link(self.partner_portal.id)],
|
||||
}
|
||||
)
|
||||
project_share_wizard.action_send_mail()
|
||||
|
||||
# Get the sharing link from the most recent mail message
|
||||
message = self.env["mail.message"].search(
|
||||
[
|
||||
("partner_ids", "in", self.partner_portal.id),
|
||||
("model", "=", "project.project"),
|
||||
("res_id", "=", self.task_1.project_id.id),
|
||||
],
|
||||
order="id DESC",
|
||||
limit=1,
|
||||
)
|
||||
|
||||
share_link = str(message.body.split('href="')[1].split('">')[0])
|
||||
match = re.search(
|
||||
r"access_token=([^&]+)&pid=([^&]+)&hash=([^&]*)", share_link
|
||||
)
|
||||
access_token, pid, _hash = match.groups()
|
||||
|
||||
# Access the task with project sharing context
|
||||
url = f"{self.base_url}{self.task_1.code}"
|
||||
|
||||
# Get initial response to extract CSRF token
|
||||
initial_response = self.url_open(url)
|
||||
content = initial_response.text
|
||||
csrf_token = re.search(r'csrf_token: "([^"]+)"', content).group(1)
|
||||
|
||||
# Make the POST request with CSRF token and project_sharing=True
|
||||
response = self.url_open(
|
||||
url=url,
|
||||
data={
|
||||
"csrf_token": csrf_token,
|
||||
"access_token": access_token,
|
||||
"project_sharing": True,
|
||||
"pid": pid,
|
||||
"hash": _hash,
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Now check if the navigation links for previous/next task are not present
|
||||
# This would indicate that the history was reset to only contain the current task
|
||||
content = response.content
|
||||
tree = html.fromstring(content)
|
||||
|
||||
# Check for absence of navigation links (prev/next)
|
||||
# which would be present if history had multiple tasks
|
||||
prev_links = tree.xpath("//a[contains(@class, 'o_portal_pager_previous')]")
|
||||
next_links = tree.xpath("//a[contains(@class, 'o_portal_pager_next')]")
|
||||
|
||||
# If history was reset to only current task, there should be no prev/next links
|
||||
self.assertFalse(
|
||||
prev_links, "Previous task link should not be present if history was reset"
|
||||
)
|
||||
self.assertFalse(
|
||||
next_links, "Next task link should not be present if history was reset"
|
||||
)
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class TestPortalProjectTaskCode(TestProjectPortalCommon, HttpCaseWithUserPortal):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestPortalProjectTaskCode, cls).setUpClass()
|
||||
cls.project_pigs.privacy_visibility = "portal"
|
||||
task_wizard = cls.env["portal.share"].create(
|
||||
{
|
||||
"res_model": "project.project",
|
||||
"res_id": cls.project_pigs.id,
|
||||
"partner_ids": [
|
||||
Command.link(cls.partner_portal.id),
|
||||
],
|
||||
}
|
||||
)
|
||||
task_wizard.action_send_mail()
|
||||
|
||||
cls.host = "127.0.0.1"
|
||||
cls.port = tools.config["http_port"]
|
||||
cls.base_my_url = f"http://{cls.host}:{cls.port}/my"
|
||||
cls.base_projects_url = f"{cls.base_my_url}/projects"
|
||||
|
||||
def test_portal_project_tasks_list_access(self):
|
||||
self.authenticate("portal", "portal")
|
||||
project_id = self.task_1.project_id.id
|
||||
url = f"{self.base_projects_url}/{project_id}"
|
||||
response = self.url_open(url)
|
||||
content = response.content
|
||||
tree = html.fromstring(content)
|
||||
spans = tree.xpath(
|
||||
"//td[contains(@class, 'text-start') and contains(., '#')]//span"
|
||||
)
|
||||
list_tasks_code = [s.text for s in spans]
|
||||
self.assertIn(self.task_1.code, list_tasks_code)
|
||||
link = tree.xpath(f"//td[a/span[contains(text(), '{self.task_1.name}')]]//a")[
|
||||
0
|
||||
].attrib["href"]
|
||||
expected_link = f"/my/projects/{project_id}/task/{self.task_1.code}?"
|
||||
self.assertEqual(link, expected_link)
|
||||
|
||||
def test_portal_my_project_task_ok(self):
|
||||
self.authenticate("portal", "portal")
|
||||
project_id = self.task_1.project_id.id
|
||||
task_code = self.task_1.code
|
||||
url = f"{self.base_projects_url}/{project_id}/task/{task_code}"
|
||||
response = self.url_open(url)
|
||||
content = response.content
|
||||
tree = html.fromstring(content)
|
||||
spans = tree.xpath(
|
||||
"//small[contains(@class, 'text-muted') and contains(@class, 'd-md-inline')]//span"
|
||||
)
|
||||
list_tasks_code = [s.text for s in spans]
|
||||
self.assertIn(self.task_1.code, list_tasks_code)
|
||||
|
||||
def test_portal_my_project_task_not_found(self):
|
||||
self.authenticate("portal", "portal")
|
||||
project_id = self.task_1.project_id.id
|
||||
url = f"{self.base_projects_url}/{project_id}/task/NotExistentCode"
|
||||
response = self.url_open(url)
|
||||
self.assertEqual(response.url, self.base_my_url)
|
||||
|
||||
def test_portal_my_project_task_no_access(self):
|
||||
other_project = self.env["project.project"].create(
|
||||
{
|
||||
"name": "Closed project",
|
||||
"privacy_visibility": "followers",
|
||||
}
|
||||
)
|
||||
task = self.env["project.task"].create(
|
||||
{
|
||||
"name": "Hidden task",
|
||||
"project_id": other_project.id,
|
||||
"code": "HIDDEN-CODE",
|
||||
}
|
||||
)
|
||||
self.authenticate("portal", "portal")
|
||||
url = f"{self.base_projects_url}/{other_project.id}/task/{task.code}"
|
||||
response = self.url_open(url)
|
||||
self.assertEqual(response.url, self.base_my_url)
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
[project]
|
||||
name = "odoo-bringout-oca-project-project_task_code_portal"
|
||||
version = "16.0.0"
|
||||
description = "Project Task Code Portal - Use custom task code in customer portal"
|
||||
authors = [
|
||||
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
|
||||
]
|
||||
dependencies = [
|
||||
"odoo-bringout-oca-project-project_task_code>=16.0.0",
|
||||
"requests>=2.25.1"
|
||||
]
|
||||
readme = "README.md"
|
||||
requires-python = ">= 3.11"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Topic :: Office/Business",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
homepage = "https://github.com/bringout/0"
|
||||
repository = "https://github.com/bringout/0"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.metadata]
|
||||
allow-direct-references = true
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["project_task_code_portal"]
|
||||
|
||||
[tool.rye]
|
||||
managed = true
|
||||
dev-dependencies = [
|
||||
"pytest>=8.4.1",
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue