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:
Ernad Husremovic 2025-08-30 18:04:10 +02:00
parent 9eb7ae5807
commit 6094c218b2
2332 changed files with 125826 additions and 0 deletions

View 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

View 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_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.

View file

@ -0,0 +1,3 @@
# Configuration
Refer to Odoo settings for project_task_code_portal. Configure related models, access rights, and options as needed.

View 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.

View file

@ -0,0 +1,5 @@
# Dependencies
This addon depends on:
- [project_task_code](https://github.com/bringout/oca-workflow-process)

View 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_task_code_portal or install in UI.

View file

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

View file

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

View file

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

View file

@ -0,0 +1,3 @@
# Reports
This module does not define custom reports.

View file

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

View 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.

View 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_task_code_portal
```

View file

@ -0,0 +1,3 @@
# Wizards
This module does not include UI wizards.

View file

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

View file

@ -0,0 +1,4 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import controllers
from . import models

View file

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

View file

@ -0,0 +1,3 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import portal

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,3 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import project_task

View file

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

View file

@ -0,0 +1 @@
No configuration is required.

View file

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

View file

@ -0,0 +1,5 @@
* [Cetmix](https://cetmix.com/):
* Ivan Sokolov
* Anatol Mikheev

View file

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

View file

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

View file

@ -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/&lt;task_id&gt;</code></li>
<li><strong>After:</strong> <code>https://example.com/my/tasks/&lt;task_code&gt;</code></li>
</ul>
<p>
Portal users can now easily share and reference tasks using meaningful codes rather than numerical IDs.
</p>
<h2>Installation &amp; 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>

View file

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

View file

@ -0,0 +1,3 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import test_portal

View file

@ -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=([^&]+)&amp;pid=([^&]+)&amp;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)

View file

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