odoo-addon-project-task-api 16.0.1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- odoo_addon_project_task_api-16.0.1.0.0/PKG-INFO +121 -0
- odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/CHANGELOG.md +10 -0
- odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/README.rst +94 -0
- odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/__init__.py +1 -0
- odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/__manifest__.py +25 -0
- odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/data/fastapi_endpoint.xml +10 -0
- odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/models/__init__.py +4 -0
- odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/models/fastapi_endpoint.py +17 -0
- odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/models/project_project.py +11 -0
- odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/routers/__init__.py +1 -0
- odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/routers/project_task_router.py +211 -0
- odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/schemas/__init__.py +2 -0
- odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/schemas/common.py +7 -0
- odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/schemas/project_task.py +190 -0
- odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/static/description/icon.png +0 -0
- odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/tests/__init__.py +1 -0
- odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/tests/test_fastapi_endpoint.py +283 -0
- odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/views/project_project_view.xml +14 -0
- odoo_addon_project_task_api-16.0.1.0.0/odoo_addon_project_task_api.egg-info/PKG-INFO +121 -0
- odoo_addon_project_task_api-16.0.1.0.0/odoo_addon_project_task_api.egg-info/SOURCES.txt +24 -0
- odoo_addon_project_task_api-16.0.1.0.0/odoo_addon_project_task_api.egg-info/dependency_links.txt +1 -0
- odoo_addon_project_task_api-16.0.1.0.0/odoo_addon_project_task_api.egg-info/not-zip-safe +1 -0
- odoo_addon_project_task_api-16.0.1.0.0/odoo_addon_project_task_api.egg-info/requires.txt +4 -0
- odoo_addon_project_task_api-16.0.1.0.0/odoo_addon_project_task_api.egg-info/top_level.txt +1 -0
- odoo_addon_project_task_api-16.0.1.0.0/setup.cfg +4 -0
- odoo_addon_project_task_api-16.0.1.0.0/setup.py +6 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: odoo-addon-project_task_api
|
|
3
|
+
Version: 16.0.1.0.0
|
|
4
|
+
Summary: API module based on FastAPI to get and create project tasks from external applications
|
|
5
|
+
Home-page: https://git.coopdevs.org/coopdevs/som-connexio/odoo/odoo-somconnexio
|
|
6
|
+
Author: Som Connexió SCCL, Odoo Community Association (OCA)
|
|
7
|
+
Author-email: support@odoo-community.org
|
|
8
|
+
License: AGPL-3
|
|
9
|
+
Classifier: Programming Language :: Python
|
|
10
|
+
Classifier: Framework :: Odoo
|
|
11
|
+
Classifier: Framework :: Odoo :: 16.0
|
|
12
|
+
Classifier: License :: OSI Approved :: GNU Affero General Public License v3
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Requires-Dist: fastapi
|
|
15
|
+
Requires-Dist: odoo-addon-fastapi_auth_api_key_sc<16.1dev,>=16.0dev
|
|
16
|
+
Requires-Dist: odoo<16.1dev,>=16.0a
|
|
17
|
+
Requires-Dist: pydantic
|
|
18
|
+
Dynamic: author
|
|
19
|
+
Dynamic: author-email
|
|
20
|
+
Dynamic: classifier
|
|
21
|
+
Dynamic: description
|
|
22
|
+
Dynamic: home-page
|
|
23
|
+
Dynamic: license
|
|
24
|
+
Dynamic: requires-dist
|
|
25
|
+
Dynamic: requires-python
|
|
26
|
+
Dynamic: summary
|
|
27
|
+
|
|
28
|
+
##################
|
|
29
|
+
Project Task API
|
|
30
|
+
##################
|
|
31
|
+
|
|
32
|
+
This module uses OCAs FAST API module for creating endpoints to get and
|
|
33
|
+
create project tasks from external applications.
|
|
34
|
+
|
|
35
|
+
.. note::
|
|
36
|
+
|
|
37
|
+
This module is made by and for developers.
|
|
38
|
+
|
|
39
|
+
**Table of contents**
|
|
40
|
+
|
|
41
|
+
.. contents::
|
|
42
|
+
:local:
|
|
43
|
+
|
|
44
|
+
***************
|
|
45
|
+
Configuration
|
|
46
|
+
***************
|
|
47
|
+
|
|
48
|
+
As this module is based on `fastapi_auth_api_key_sc`, an API-KEY is
|
|
49
|
+
required to authenticate requests. Read README from
|
|
50
|
+
`fastapi_auth_api_key_sc` for more information about the user related
|
|
51
|
+
API-KEY authentication.
|
|
52
|
+
|
|
53
|
+
*******
|
|
54
|
+
Usage
|
|
55
|
+
*******
|
|
56
|
+
|
|
57
|
+
This module provides FastAPI-based endpoints of project tasks by
|
|
58
|
+
extending `fastapi.endpoint`.
|
|
59
|
+
|
|
60
|
+
The module registers an example model API at the following roots:
|
|
61
|
+
|
|
62
|
+
- GET: `/api/project_task/{id}`
|
|
63
|
+
- POST: `/api/project_task/`
|
|
64
|
+
|
|
65
|
+
The authenticated API uses `API-KEY`
|
|
66
|
+
|
|
67
|
+
************************
|
|
68
|
+
Known issues / Roadmap
|
|
69
|
+
************************
|
|
70
|
+
|
|
71
|
+
There are no known issues for this module.
|
|
72
|
+
|
|
73
|
+
*************
|
|
74
|
+
Bug Tracker
|
|
75
|
+
*************
|
|
76
|
+
|
|
77
|
+
Bugs are tracked on `GitLab Issues
|
|
78
|
+
<https://gitlab.com/somitcoop/erp-research/odoo-helpdesk/-/issues>`_. In
|
|
79
|
+
case of trouble, please check there if your issue has already been
|
|
80
|
+
reported. If you spotted it first, help us smashing it by providing a
|
|
81
|
+
detailed and welcomed feedback.
|
|
82
|
+
|
|
83
|
+
Do not contact contributors directly about support or help with
|
|
84
|
+
technical issues.
|
|
85
|
+
|
|
86
|
+
TODO: Module to be substituted by `the OCA module`_ once this ODOO is
|
|
87
|
+
migrated to the 17th version or further.
|
|
88
|
+
|
|
89
|
+
.. _the oca module: https://github.com/OCA/rest-framework/tree/17.0/fastapi_auth_api_key/
|
|
90
|
+
|
|
91
|
+
*********
|
|
92
|
+
Credits
|
|
93
|
+
*********
|
|
94
|
+
|
|
95
|
+
Authors
|
|
96
|
+
=======
|
|
97
|
+
|
|
98
|
+
- Som Connexio SCCL
|
|
99
|
+
|
|
100
|
+
Contributors
|
|
101
|
+
============
|
|
102
|
+
|
|
103
|
+
- `Som Connexio SCCL <https://somconnexio.coop>`_:
|
|
104
|
+
|
|
105
|
+
- Gerard Funosas <gerard.funosas@somconnexio.coop>
|
|
106
|
+
|
|
107
|
+
Maintainers
|
|
108
|
+
===========
|
|
109
|
+
|
|
110
|
+
This module is maintained by the OCA.
|
|
111
|
+
|
|
112
|
+
.. image:: https://odoo-community.org/logo.png
|
|
113
|
+
:alt: Odoo Community Association
|
|
114
|
+
:target: https://odoo-community.org
|
|
115
|
+
|
|
116
|
+
OCA, or the Odoo Community Association, is a nonprofit organization
|
|
117
|
+
whose mission is to support the collaborative development of Odoo
|
|
118
|
+
features and promote its widespread use.
|
|
119
|
+
|
|
120
|
+
You are welcome to contribute. To learn how please visit
|
|
121
|
+
https://odoo-community.org/page/Contribute.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
All notable changes to this project will be documented in this file.
|
|
3
|
+
|
|
4
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
5
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
## [16.0.1.0.0] - 2026-09-22
|
|
9
|
+
### Added
|
|
10
|
+
- [#1761](https://git.coopdevs.org/coopdevs/som-connexio/odoo/odoo-somconnexio/-/merge_requests/1761) Add project_task_api
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
##################
|
|
2
|
+
Project Task API
|
|
3
|
+
##################
|
|
4
|
+
|
|
5
|
+
This module uses OCAs FAST API module for creating endpoints to get and
|
|
6
|
+
create project tasks from external applications.
|
|
7
|
+
|
|
8
|
+
.. note::
|
|
9
|
+
|
|
10
|
+
This module is made by and for developers.
|
|
11
|
+
|
|
12
|
+
**Table of contents**
|
|
13
|
+
|
|
14
|
+
.. contents::
|
|
15
|
+
:local:
|
|
16
|
+
|
|
17
|
+
***************
|
|
18
|
+
Configuration
|
|
19
|
+
***************
|
|
20
|
+
|
|
21
|
+
As this module is based on `fastapi_auth_api_key_sc`, an API-KEY is
|
|
22
|
+
required to authenticate requests. Read README from
|
|
23
|
+
`fastapi_auth_api_key_sc` for more information about the user related
|
|
24
|
+
API-KEY authentication.
|
|
25
|
+
|
|
26
|
+
*******
|
|
27
|
+
Usage
|
|
28
|
+
*******
|
|
29
|
+
|
|
30
|
+
This module provides FastAPI-based endpoints of project tasks by
|
|
31
|
+
extending `fastapi.endpoint`.
|
|
32
|
+
|
|
33
|
+
The module registers an example model API at the following roots:
|
|
34
|
+
|
|
35
|
+
- GET: `/api/project_task/{id}`
|
|
36
|
+
- POST: `/api/project_task/`
|
|
37
|
+
|
|
38
|
+
The authenticated API uses `API-KEY`
|
|
39
|
+
|
|
40
|
+
************************
|
|
41
|
+
Known issues / Roadmap
|
|
42
|
+
************************
|
|
43
|
+
|
|
44
|
+
There are no known issues for this module.
|
|
45
|
+
|
|
46
|
+
*************
|
|
47
|
+
Bug Tracker
|
|
48
|
+
*************
|
|
49
|
+
|
|
50
|
+
Bugs are tracked on `GitLab Issues
|
|
51
|
+
<https://gitlab.com/somitcoop/erp-research/odoo-helpdesk/-/issues>`_. In
|
|
52
|
+
case of trouble, please check there if your issue has already been
|
|
53
|
+
reported. If you spotted it first, help us smashing it by providing a
|
|
54
|
+
detailed and welcomed feedback.
|
|
55
|
+
|
|
56
|
+
Do not contact contributors directly about support or help with
|
|
57
|
+
technical issues.
|
|
58
|
+
|
|
59
|
+
TODO: Module to be substituted by `the OCA module`_ once this ODOO is
|
|
60
|
+
migrated to the 17th version or further.
|
|
61
|
+
|
|
62
|
+
.. _the oca module: https://github.com/OCA/rest-framework/tree/17.0/fastapi_auth_api_key/
|
|
63
|
+
|
|
64
|
+
*********
|
|
65
|
+
Credits
|
|
66
|
+
*********
|
|
67
|
+
|
|
68
|
+
Authors
|
|
69
|
+
=======
|
|
70
|
+
|
|
71
|
+
- Som Connexio SCCL
|
|
72
|
+
|
|
73
|
+
Contributors
|
|
74
|
+
============
|
|
75
|
+
|
|
76
|
+
- `Som Connexio SCCL <https://somconnexio.coop>`_:
|
|
77
|
+
|
|
78
|
+
- Gerard Funosas <gerard.funosas@somconnexio.coop>
|
|
79
|
+
|
|
80
|
+
Maintainers
|
|
81
|
+
===========
|
|
82
|
+
|
|
83
|
+
This module is maintained by the OCA.
|
|
84
|
+
|
|
85
|
+
.. image:: https://odoo-community.org/logo.png
|
|
86
|
+
:alt: Odoo Community Association
|
|
87
|
+
:target: https://odoo-community.org
|
|
88
|
+
|
|
89
|
+
OCA, or the Odoo Community Association, is a nonprofit organization
|
|
90
|
+
whose mission is to support the collaborative development of Odoo
|
|
91
|
+
features and promote its widespread use.
|
|
92
|
+
|
|
93
|
+
You are welcome to contribute. To learn how please visit
|
|
94
|
+
https://odoo-community.org/page/Contribute.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from . import models, routers, schemas
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
|
2
|
+
{
|
|
3
|
+
"name": "Project Task API",
|
|
4
|
+
"version": "16.0.1.0.0",
|
|
5
|
+
"summary": """
|
|
6
|
+
API module based on FastAPI to get and create project tasks from
|
|
7
|
+
external applications
|
|
8
|
+
""",
|
|
9
|
+
"depends": ["fastapi_auth_api_key_sc", "project"],
|
|
10
|
+
"author": """
|
|
11
|
+
Som Connexió SCCL,
|
|
12
|
+
Odoo Community Association (OCA)
|
|
13
|
+
""",
|
|
14
|
+
"category": "API",
|
|
15
|
+
"website": "https://git.coopdevs.org/coopdevs/som-connexio/odoo/odoo-somconnexio",
|
|
16
|
+
"license": "AGPL-3",
|
|
17
|
+
"data": [
|
|
18
|
+
"data/fastapi_endpoint.xml",
|
|
19
|
+
"views/project_project_view.xml",
|
|
20
|
+
],
|
|
21
|
+
"demo": [],
|
|
22
|
+
"external_dependencies": {"python": ["pydantic", "fastapi"]},
|
|
23
|
+
"application": False,
|
|
24
|
+
"installable": True,
|
|
25
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<odoo>
|
|
3
|
+
<record id="fastapi_project_task_endpoint" model="fastapi.endpoint">
|
|
4
|
+
<field name="name">FastAPi Project Task Endpoint</field>
|
|
5
|
+
<field name="description">Project task endpoint</field>
|
|
6
|
+
<field name="app">fastapi_project_task</field>
|
|
7
|
+
<field name="root_path">/api</field>
|
|
8
|
+
<field name="user_id" ref="fastapi_auth_api_key_sc.fastapi_service_user"/>
|
|
9
|
+
</record>
|
|
10
|
+
</odoo>
|
odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/models/fastapi_endpoint.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from odoo import fields, models
|
|
2
|
+
|
|
3
|
+
from ..routers.project_task_router import project_task_router
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FastapiEndpoint(models.Model):
|
|
7
|
+
_inherit = "fastapi.endpoint"
|
|
8
|
+
|
|
9
|
+
app = fields.Selection(
|
|
10
|
+
selection_add=[("fastapi_project_task", "FastAPi Project Task")],
|
|
11
|
+
ondelete={"fastapi_project_task": "cascade"},
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
def _get_fastapi_routers(self):
|
|
15
|
+
if self.app == "fastapi_project_task":
|
|
16
|
+
return [project_task_router]
|
|
17
|
+
return super()._get_fastapi_routers()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from . import project_task_router
|
odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/routers/project_task_router.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
from typing import Annotated, List
|
|
2
|
+
from fastapi import APIRouter, Depends, HTTPException, status
|
|
3
|
+
|
|
4
|
+
from odoo.addons.base.models.res_partner import Partner
|
|
5
|
+
from odoo.api import Environment
|
|
6
|
+
from odoo.addons.fastapi.dependencies import odoo_env
|
|
7
|
+
|
|
8
|
+
from odoo.addons.fastapi_auth_api_key_sc.dependencies import (
|
|
9
|
+
authenticated_partner_by_api_key,
|
|
10
|
+
serialize_record,
|
|
11
|
+
)
|
|
12
|
+
from ..schemas.project_task import (
|
|
13
|
+
ProjectTaskCreatePayload,
|
|
14
|
+
ProjectTaskPresenter,
|
|
15
|
+
ProjectTaskUpdatePayload,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
project_task_router = APIRouter()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_partner_by_ref(env, ref):
|
|
22
|
+
partner = (
|
|
23
|
+
env["res.partner"]
|
|
24
|
+
.sudo()
|
|
25
|
+
.search(
|
|
26
|
+
[
|
|
27
|
+
("parent_id", "=", False),
|
|
28
|
+
("ref", "=", ref),
|
|
29
|
+
],
|
|
30
|
+
limit=1,
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
if not partner.exists():
|
|
34
|
+
raise HTTPException(
|
|
35
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
36
|
+
detail="Partner with ref {} not found".format(ref),
|
|
37
|
+
)
|
|
38
|
+
return partner
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_project_by_code(env, code):
|
|
42
|
+
project = env["project.project"].sudo().search([("code", "=", code)])
|
|
43
|
+
if not project:
|
|
44
|
+
raise HTTPException(
|
|
45
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
46
|
+
detail="Project record with code {} not found".format(code),
|
|
47
|
+
)
|
|
48
|
+
return project
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@project_task_router.get("/project_tasks", response_model=List[ProjectTaskPresenter])
|
|
52
|
+
def list_project_tasks(
|
|
53
|
+
partner: Annotated[Partner, Depends(authenticated_partner_by_api_key)],
|
|
54
|
+
env: Annotated[Environment, Depends(odoo_env)],
|
|
55
|
+
partner_ref: str | None = None,
|
|
56
|
+
project_id: int | None = None,
|
|
57
|
+
partner_phone: str | None = None,
|
|
58
|
+
) -> ProjectTaskPresenter:
|
|
59
|
+
if not partner_ref and not project_id and not partner_phone:
|
|
60
|
+
raise HTTPException(
|
|
61
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
62
|
+
detail="At least one of the following query parameters must be provided: partner_ref, project_id, partner_phone", # noqa E501
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
domain = []
|
|
66
|
+
if partner_ref:
|
|
67
|
+
partner = get_partner_by_ref(env, partner_ref)
|
|
68
|
+
domain += [("partner_id", "=", partner.id)]
|
|
69
|
+
if project_id:
|
|
70
|
+
domain += [("project_id", "=", project_id)]
|
|
71
|
+
if partner_phone:
|
|
72
|
+
domain += [("partner_phone", "=", partner_phone)]
|
|
73
|
+
|
|
74
|
+
records = env["project.task"].sudo().search(domain)
|
|
75
|
+
if not records:
|
|
76
|
+
raise HTTPException(
|
|
77
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
78
|
+
detail="Project task record not found",
|
|
79
|
+
)
|
|
80
|
+
return [serialize_record(record, ProjectTaskPresenter) for record in records]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@project_task_router.post(
|
|
84
|
+
"/project_tasks",
|
|
85
|
+
response_model=ProjectTaskPresenter,
|
|
86
|
+
status_code=status.HTTP_201_CREATED,
|
|
87
|
+
)
|
|
88
|
+
def create_project_task(
|
|
89
|
+
partner: Annotated[Partner, Depends(authenticated_partner_by_api_key)],
|
|
90
|
+
env: Annotated[Environment, Depends(odoo_env)],
|
|
91
|
+
payload: ProjectTaskCreatePayload,
|
|
92
|
+
) -> ProjectTaskPresenter:
|
|
93
|
+
params = {k: v for k, v in payload.model_dump().items() if v is not None}
|
|
94
|
+
if params.get("partner_ref"):
|
|
95
|
+
partner = get_partner_by_ref(env, params["partner_ref"])
|
|
96
|
+
if not partner.exists():
|
|
97
|
+
raise HTTPException(
|
|
98
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
99
|
+
detail="Partner record with code {} found".format(
|
|
100
|
+
params["partner_ref"]
|
|
101
|
+
),
|
|
102
|
+
)
|
|
103
|
+
project = get_project_by_code(env, params["project_code"])
|
|
104
|
+
params.pop("partner_ref", None)
|
|
105
|
+
params.pop("project_code", None)
|
|
106
|
+
params.update(
|
|
107
|
+
{
|
|
108
|
+
"partner_id": partner.id,
|
|
109
|
+
"project_id": project.id,
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if params.get("properties"):
|
|
114
|
+
project_properties = project.task_properties_definition
|
|
115
|
+
project_properties_names = [prop["name"] for prop in project_properties]
|
|
116
|
+
unknown_properties = set(params["properties"]) - set(project_properties_names)
|
|
117
|
+
if unknown_properties:
|
|
118
|
+
raise HTTPException(
|
|
119
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
120
|
+
detail="Unknown property {} for this task: {}".format(
|
|
121
|
+
"y" if len(unknown_properties) == 1 else "ies",
|
|
122
|
+
", ".join(sorted(unknown_properties)),
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
params["task_properties"] = params["properties"]
|
|
126
|
+
del params["properties"]
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
record = env["project.task"].sudo().create(params)
|
|
130
|
+
except Exception as exc:
|
|
131
|
+
raise HTTPException(
|
|
132
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
133
|
+
detail=str(exc),
|
|
134
|
+
) from exc
|
|
135
|
+
return serialize_record(record, ProjectTaskPresenter)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@project_task_router.get(
|
|
139
|
+
"/project_tasks/{record_id}", response_model=ProjectTaskPresenter
|
|
140
|
+
)
|
|
141
|
+
def get_project_task(
|
|
142
|
+
partner: Annotated[Partner, Depends(authenticated_partner_by_api_key)],
|
|
143
|
+
env: Annotated[Environment, Depends(odoo_env)],
|
|
144
|
+
record_id: int,
|
|
145
|
+
) -> ProjectTaskPresenter:
|
|
146
|
+
record = env["project.task"].sudo().browse(record_id)
|
|
147
|
+
if not record.exists():
|
|
148
|
+
raise HTTPException(
|
|
149
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
150
|
+
detail="Project task record with id {} not found".format(record_id),
|
|
151
|
+
)
|
|
152
|
+
return serialize_record(record, ProjectTaskPresenter)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@project_task_router.put(
|
|
156
|
+
"/project_tasks/{record_id}", response_model=ProjectTaskPresenter
|
|
157
|
+
)
|
|
158
|
+
def update_project_task(
|
|
159
|
+
partner: Annotated[Partner, Depends(authenticated_partner_by_api_key)],
|
|
160
|
+
env: Annotated[Environment, Depends(odoo_env)],
|
|
161
|
+
record_id: int,
|
|
162
|
+
payload: ProjectTaskUpdatePayload,
|
|
163
|
+
) -> ProjectTaskPresenter:
|
|
164
|
+
record = env["project.task"].sudo().browse(record_id)
|
|
165
|
+
if not record.exists():
|
|
166
|
+
raise HTTPException(
|
|
167
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
168
|
+
detail="Project task record with id {} not found".format(record_id),
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
params = {
|
|
172
|
+
k: v
|
|
173
|
+
for k, v in payload.model_dump().items()
|
|
174
|
+
if v is not None and k != "properties"
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if params.get("partner_ref"):
|
|
178
|
+
partner_record = get_partner_by_ref(env, params.pop("partner_ref"))
|
|
179
|
+
params["partner_id"] = partner_record.id
|
|
180
|
+
params.pop("partner_ref", None)
|
|
181
|
+
|
|
182
|
+
if params.get("project_code"):
|
|
183
|
+
project = get_project_by_code(env, params.pop("project_code"))
|
|
184
|
+
params["project_id"] = project.id
|
|
185
|
+
params.pop("project_code", None)
|
|
186
|
+
|
|
187
|
+
if payload.properties:
|
|
188
|
+
current_properties = {
|
|
189
|
+
prop["name"]: prop["value"] for prop in record.task_properties or []
|
|
190
|
+
}
|
|
191
|
+
unknown_properties = set(payload.properties) - set(current_properties)
|
|
192
|
+
if unknown_properties:
|
|
193
|
+
raise HTTPException(
|
|
194
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
195
|
+
detail="Unknown property {} for this task: {}".format(
|
|
196
|
+
"y" if len(unknown_properties) == 1 else "ies",
|
|
197
|
+
", ".join(sorted(unknown_properties)),
|
|
198
|
+
),
|
|
199
|
+
)
|
|
200
|
+
current_properties.update(payload.properties)
|
|
201
|
+
params["task_properties"] = current_properties
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
record.write(params)
|
|
205
|
+
except Exception as exc:
|
|
206
|
+
raise HTTPException(
|
|
207
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
208
|
+
detail=str(exc),
|
|
209
|
+
) from exc
|
|
210
|
+
|
|
211
|
+
return serialize_record(record, ProjectTaskPresenter)
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
from datetime import date
|
|
2
|
+
from typing import Any, Dict, List, Optional
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
5
|
+
|
|
6
|
+
from .common import PriorityType, KanbanStateType, PositiveInt
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ProjectTaskCreatePayload(BaseModel):
|
|
10
|
+
"""
|
|
11
|
+
Output contract for project.task (Odoo ORM response).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
name: str = Field(..., description="Task title (required in Odoo).")
|
|
15
|
+
|
|
16
|
+
project_code: str = Field(..., description="Project code (required).")
|
|
17
|
+
|
|
18
|
+
description: Optional[str] = Field(None, description="Internal task description.")
|
|
19
|
+
|
|
20
|
+
priority: Optional[PriorityType] = Field(
|
|
21
|
+
None, description="Priority (Odoo: '0' Low → '1' High)."
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
partner_ref: Optional[str] = Field(None, description="Assigned partner REF.")
|
|
25
|
+
|
|
26
|
+
partner_phone: Optional[str] = Field(None, description="Phone number.")
|
|
27
|
+
|
|
28
|
+
partner_email: Optional[str] = Field(None, description="Email address.")
|
|
29
|
+
|
|
30
|
+
stage_id: Optional[int] = Field(None, gt=0, description="Kanban stage ID.")
|
|
31
|
+
|
|
32
|
+
date_deadline: Optional[date] = Field(
|
|
33
|
+
None, description="Deadline date (ISO format)."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
tag_ids: Optional[List[PositiveInt]] = Field(
|
|
37
|
+
None, description="List of tag IDs associated with the task."
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
properties: Optional[Dict[str, Any]] = Field(
|
|
41
|
+
None,
|
|
42
|
+
description=(
|
|
43
|
+
"Values for the task's properties (Odoo Properties feature), "
|
|
44
|
+
"keyed by property name. Property names must already be defined "
|
|
45
|
+
"on the task's project."
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
model_config = ConfigDict(
|
|
50
|
+
extra="forbid",
|
|
51
|
+
str_strip_whitespace=True,
|
|
52
|
+
json_schema_extra={
|
|
53
|
+
"example": {
|
|
54
|
+
"name": "Implement authentication",
|
|
55
|
+
"project_code": "code",
|
|
56
|
+
"description": "some text",
|
|
57
|
+
"priority": "1",
|
|
58
|
+
"partner_ref": "1234",
|
|
59
|
+
"partner_phone": "123456789",
|
|
60
|
+
"partner_email": "hello@hello.com",
|
|
61
|
+
"tag_ids": [1, 2],
|
|
62
|
+
"date_deadline": "2026-05-01",
|
|
63
|
+
"properties": {"color": "red"},
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ProjectTaskUpdatePayload(BaseModel):
|
|
70
|
+
"""
|
|
71
|
+
Input contract to partially update a project.task record.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
name: Optional[str] = Field(None, description="Task title.")
|
|
75
|
+
|
|
76
|
+
project_code: Optional[str] = Field(None, description="Project code.")
|
|
77
|
+
|
|
78
|
+
description: Optional[str] = Field(None, description="Internal task description.")
|
|
79
|
+
|
|
80
|
+
priority: Optional[PriorityType] = Field(
|
|
81
|
+
None, description="Priority (Odoo: '0' Low → '1' High)."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
partner_ref: Optional[str] = Field(None, description="Assigned partner REF.")
|
|
85
|
+
|
|
86
|
+
partner_phone: Optional[str] = Field(None, description="Phone number.")
|
|
87
|
+
|
|
88
|
+
partner_email: Optional[str] = Field(None, description="Email address.")
|
|
89
|
+
|
|
90
|
+
stage_id: Optional[int] = Field(None, gt=0, description="Kanban stage ID.")
|
|
91
|
+
|
|
92
|
+
date_deadline: Optional[date] = Field(
|
|
93
|
+
None, description="Deadline date (ISO format)."
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
tag_ids: Optional[List[PositiveInt]] = Field(
|
|
97
|
+
None, description="List of tag IDs associated with the task."
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
properties: Optional[Dict[str, Any]] = Field(
|
|
101
|
+
None,
|
|
102
|
+
description=(
|
|
103
|
+
"Values for the task's properties (Odoo Properties feature), "
|
|
104
|
+
"keyed by property name. Property names must already be defined "
|
|
105
|
+
"on the task's project."
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
model_config = ConfigDict(
|
|
110
|
+
extra="forbid",
|
|
111
|
+
str_strip_whitespace=True,
|
|
112
|
+
json_schema_extra={
|
|
113
|
+
"example": {
|
|
114
|
+
"name": "Implement authentication",
|
|
115
|
+
"description": "some text",
|
|
116
|
+
"priority": "1",
|
|
117
|
+
"partner_phone": "123456789",
|
|
118
|
+
"partner_email": "hello@hello.com",
|
|
119
|
+
"tag_ids": [1, 2],
|
|
120
|
+
"date_deadline": "2026-05-01",
|
|
121
|
+
"properties": {"color": "red"},
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class ProjectTaskPresenter(BaseModel):
|
|
128
|
+
"""
|
|
129
|
+
Output contract for project.task (Odoo ORM response).
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
id: int = Field(..., description="Task ID")
|
|
133
|
+
|
|
134
|
+
name: str = Field(..., description="Task title")
|
|
135
|
+
|
|
136
|
+
description: Optional[str] = Field(None, description="Task description")
|
|
137
|
+
|
|
138
|
+
project_id: int = Field(..., description="Project ID")
|
|
139
|
+
|
|
140
|
+
partner_phone: Optional[str] = Field(None, description="Phone number")
|
|
141
|
+
|
|
142
|
+
partner_email: Optional[str] = Field(None, description="Email address")
|
|
143
|
+
|
|
144
|
+
priority: PriorityType = Field(None, description="Priority level")
|
|
145
|
+
|
|
146
|
+
kanban_state: KanbanStateType = Field(None, description="Kanban state")
|
|
147
|
+
|
|
148
|
+
partner_id: Optional[int] = Field(None, description="Partner ID")
|
|
149
|
+
|
|
150
|
+
stage_id: Optional[int] = Field(None, description="Stage id")
|
|
151
|
+
|
|
152
|
+
date_deadline: Optional[date] = Field(None, description="Deadline")
|
|
153
|
+
|
|
154
|
+
tag_ids: List[PositiveInt] = Field(
|
|
155
|
+
None, description="List of tag IDs associated with the task."
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
task_properties: Optional[List[Dict[str, Any]]] = Field(
|
|
159
|
+
None,
|
|
160
|
+
description=(
|
|
161
|
+
"Values for the task's properties (Odoo Properties feature), "
|
|
162
|
+
"keyed by property name"
|
|
163
|
+
),
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
sequence: int = Field(None, description="Ordering index")
|
|
167
|
+
|
|
168
|
+
model_config = ConfigDict(
|
|
169
|
+
from_attributes=True,
|
|
170
|
+
extra="ignore",
|
|
171
|
+
json_schema_extra={
|
|
172
|
+
"example": {
|
|
173
|
+
"id": 12,
|
|
174
|
+
"name": "Implement authentication",
|
|
175
|
+
"description": "OAuth2 integration",
|
|
176
|
+
"project_code": "2",
|
|
177
|
+
"priority": "3",
|
|
178
|
+
"kanban_state": "normal",
|
|
179
|
+
"partner_ref": "1234",
|
|
180
|
+
"partner_name": "Emma",
|
|
181
|
+
"partner_phone": "123456789",
|
|
182
|
+
"partner_email": "hello@hello.com",
|
|
183
|
+
"stage": "new",
|
|
184
|
+
"date_deadline": "2026-05-01",
|
|
185
|
+
"tags": [1, 2],
|
|
186
|
+
"sequence": 10,
|
|
187
|
+
"task_properties": {"color": "red"},
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
)
|
odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/static/description/icon.png
ADDED
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from . import test_fastapi_endpoint
|
odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/tests/test_fastapi_endpoint.py
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
from fastapi import status
|
|
2
|
+
|
|
3
|
+
from odoo.addons.fastapi_auth_api_key_sc.tests.common import APICommonBaseFastAPICase
|
|
4
|
+
from ..routers.project_task_router import project_task_router
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestFastAPiEndpoint(APICommonBaseFastAPICase):
|
|
8
|
+
@classmethod
|
|
9
|
+
def setUpClass(cls) -> None:
|
|
10
|
+
super().setUpClass()
|
|
11
|
+
cls.default_fastapi_router = project_task_router
|
|
12
|
+
cls.default_fastapi_running_user = cls.env.ref(
|
|
13
|
+
"fastapi_auth_api_key_sc.fastapi_service_user"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
def setUp(self):
|
|
17
|
+
super().setUp()
|
|
18
|
+
self.project_task = self.env.ref("project.project_1_task_1")
|
|
19
|
+
self.url = "project_tasks"
|
|
20
|
+
self.partner = self.env.ref("base.res_partner_12")
|
|
21
|
+
self.partner.ref = "TEST_REF"
|
|
22
|
+
self.project_task.partner_id = self.partner
|
|
23
|
+
|
|
24
|
+
def test_project_task_get(self):
|
|
25
|
+
"""
|
|
26
|
+
Test the GET /project_task/{record_id} endpoint to retrieve
|
|
27
|
+
a project task by its ID.
|
|
28
|
+
"""
|
|
29
|
+
with self._create_test_client() as test_client:
|
|
30
|
+
response = test_client.get(
|
|
31
|
+
f"/{self.url}/{self.project_task.id}", headers=self.api_key_headers()
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
35
|
+
self.assertIsInstance(response.json(), dict)
|
|
36
|
+
self.assertEqual(response.json()["id"], self.project_task.id)
|
|
37
|
+
|
|
38
|
+
def test_project_task_search_project_id(self):
|
|
39
|
+
"""
|
|
40
|
+
Test the GET /project_task/search endpoint can search by project_id
|
|
41
|
+
retrieving a list of all the project tasks from the given project.
|
|
42
|
+
"""
|
|
43
|
+
project = self.project_task.project_id
|
|
44
|
+
|
|
45
|
+
with self._create_test_client() as test_client:
|
|
46
|
+
response = test_client.get(
|
|
47
|
+
f"/{self.url}?project_id={project.id}",
|
|
48
|
+
headers=self.api_key_headers(),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
52
|
+
self.assertIsInstance(response.json(), list)
|
|
53
|
+
self.assertEqual(len(response.json()), len(project.tasks))
|
|
54
|
+
self.assertIn(self.project_task.id, [task["id"] for task in response.json()])
|
|
55
|
+
|
|
56
|
+
def test_project_task_search_partner_ref(self):
|
|
57
|
+
"""
|
|
58
|
+
Test the GET /project_task/search endpoint can search by both project_id
|
|
59
|
+
and partner_ref retrieving a list of all the project tasks from the given
|
|
60
|
+
project and partner.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
with self._create_test_client(env=self.env) as test_client:
|
|
64
|
+
response = test_client.get(
|
|
65
|
+
"/{}?project_id={}&partner_ref={}".format(
|
|
66
|
+
self.url,
|
|
67
|
+
self.project_task.project_id.id,
|
|
68
|
+
self.project_task.partner_id.ref,
|
|
69
|
+
),
|
|
70
|
+
headers=self.api_key_headers(),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
74
|
+
self.assertIsInstance(response.json(), list)
|
|
75
|
+
self.assertEqual(len(response.json()), 1)
|
|
76
|
+
self.assertEqual(response.json()[0]["id"], self.project_task.id)
|
|
77
|
+
|
|
78
|
+
def test_project_task_search_partner_phone(self):
|
|
79
|
+
"""
|
|
80
|
+
Test the GET /project_task/search endpoint can search by partner_phone
|
|
81
|
+
retrieving a list of all the project tasks from the given partner.
|
|
82
|
+
"""
|
|
83
|
+
self.project_task.partner_phone = "699696969"
|
|
84
|
+
|
|
85
|
+
with self._create_test_client(env=self.env) as test_client:
|
|
86
|
+
response = test_client.get(
|
|
87
|
+
"/{}?project_id={}&partner_phone={}".format(
|
|
88
|
+
self.url,
|
|
89
|
+
self.project_task.project_id.id,
|
|
90
|
+
self.project_task.partner_phone,
|
|
91
|
+
),
|
|
92
|
+
headers=self.api_key_headers(),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
96
|
+
self.assertIsInstance(response.json(), list)
|
|
97
|
+
self.assertEqual(len(response.json()), 1)
|
|
98
|
+
self.assertEqual(response.json()[0]["id"], self.project_task.id)
|
|
99
|
+
|
|
100
|
+
def test_project_task_search_without_params(self):
|
|
101
|
+
"""
|
|
102
|
+
Test the GET /project_task/search endpoint returns an error if
|
|
103
|
+
not called by none of the params project_id or partner_ref or partner_phone.
|
|
104
|
+
"""
|
|
105
|
+
with self._create_test_client() as test_client:
|
|
106
|
+
response = test_client.get(f"/{self.url}", headers=self.api_key_headers())
|
|
107
|
+
|
|
108
|
+
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
|
109
|
+
self.assertIsInstance(response.json(), dict)
|
|
110
|
+
self.assertEqual(
|
|
111
|
+
response.json()["detail"],
|
|
112
|
+
"At least one of the following query parameters must be provided: partner_ref, project_id, partner_phone", # noqa: E501
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def test_project_task_create(self):
|
|
116
|
+
"""
|
|
117
|
+
Test the POST /project_task endpoint creates a new project task.
|
|
118
|
+
"""
|
|
119
|
+
code = "TESTCODE"
|
|
120
|
+
self.project_task.project_id.code = code
|
|
121
|
+
payload = {
|
|
122
|
+
"name": "Implement authentication",
|
|
123
|
+
"project_code": code,
|
|
124
|
+
"priority": "1",
|
|
125
|
+
"partner_ref": self.project_task.partner_id.ref,
|
|
126
|
+
"tag_ids": [1, 2],
|
|
127
|
+
"date_deadline": "2026-05-01",
|
|
128
|
+
}
|
|
129
|
+
with self._create_test_client(env=self.env) as test_client:
|
|
130
|
+
response = test_client.post(
|
|
131
|
+
f"/{self.url}", json=payload, headers=self.api_key_headers()
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
|
135
|
+
created = response.json()
|
|
136
|
+
self.assertEqual(created["name"], payload["name"])
|
|
137
|
+
self.assertEqual(created["project_id"], self.project_task.project_id.id)
|
|
138
|
+
self.assertEqual(created["priority"], payload["priority"])
|
|
139
|
+
self.assertEqual(created["tag_ids"], payload["tag_ids"])
|
|
140
|
+
self.assertEqual(created["date_deadline"], payload["date_deadline"])
|
|
141
|
+
|
|
142
|
+
def test_project_task_create_with_properties(self):
|
|
143
|
+
"""
|
|
144
|
+
Test the POST /project_tasks endpoint creates a project task with
|
|
145
|
+
properties.
|
|
146
|
+
"""
|
|
147
|
+
code = "TESTCODE"
|
|
148
|
+
self.project_task.project_id.code = code
|
|
149
|
+
self.project_task.project_id.task_properties_definition = [
|
|
150
|
+
{"name": "color", "string": "Color", "type": "char"},
|
|
151
|
+
{"name": "size", "string": "Size", "type": "char"},
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
payload = {
|
|
155
|
+
"name": "Implement authentication",
|
|
156
|
+
"project_code": code,
|
|
157
|
+
"partner_ref": self.project_task.partner_id.ref,
|
|
158
|
+
"properties": {"color": "red", "size": "large"},
|
|
159
|
+
}
|
|
160
|
+
with self._create_test_client(env=self.env) as test_client:
|
|
161
|
+
response = test_client.post(
|
|
162
|
+
f"/{self.url}", json=payload, headers=self.api_key_headers()
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
|
166
|
+
created = response.json()
|
|
167
|
+
self.assertEqual(created["task_properties"][0]["name"], "color")
|
|
168
|
+
self.assertEqual(
|
|
169
|
+
created["task_properties"][0]["value"], payload["properties"]["color"]
|
|
170
|
+
)
|
|
171
|
+
self.assertEqual(created["task_properties"][1]["name"], "size")
|
|
172
|
+
self.assertEqual(
|
|
173
|
+
created["task_properties"][1]["value"], payload["properties"]["size"]
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def test_project_task_create_project_code_not_found(self):
|
|
177
|
+
"""
|
|
178
|
+
Test the POST /project_tasks endpoint returns a 404 error when no
|
|
179
|
+
project carries the given project_code.
|
|
180
|
+
"""
|
|
181
|
+
code = "MISSINGCODE"
|
|
182
|
+
payload = {
|
|
183
|
+
"name": "Implement authentication",
|
|
184
|
+
"project_code": code,
|
|
185
|
+
"partner_ref": self.project_task.partner_id.ref,
|
|
186
|
+
}
|
|
187
|
+
with self._create_test_client(env=self.env) as test_client:
|
|
188
|
+
response = test_client.post(
|
|
189
|
+
f"/{self.url}", json=payload, headers=self.api_key_headers()
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
|
193
|
+
self.assertEqual(
|
|
194
|
+
response.json()["detail"],
|
|
195
|
+
"Project record with code {} not found".format(code),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
def test_project_task_update(self):
|
|
199
|
+
"""
|
|
200
|
+
Test the PUT /project_task/{record_id} endpoint updates a project
|
|
201
|
+
task's regular fields.
|
|
202
|
+
"""
|
|
203
|
+
payload = {"name": "Updated task name", "priority": "1"}
|
|
204
|
+
with self._create_test_client(env=self.env) as test_client:
|
|
205
|
+
response = test_client.put(
|
|
206
|
+
f"/{self.url}/{self.project_task.id}",
|
|
207
|
+
json=payload,
|
|
208
|
+
headers=self.api_key_headers(),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
212
|
+
updated = response.json()
|
|
213
|
+
self.assertEqual(updated["name"], payload["name"])
|
|
214
|
+
self.assertEqual(updated["priority"], payload["priority"])
|
|
215
|
+
self.assertEqual(self.project_task.name, payload["name"])
|
|
216
|
+
|
|
217
|
+
def test_project_task_update_properties(self):
|
|
218
|
+
"""
|
|
219
|
+
Test the PUT /project_task/{record_id} endpoint saves the given
|
|
220
|
+
properties while keeping the ones not sent untouched.
|
|
221
|
+
"""
|
|
222
|
+
self.project_task.project_id.task_properties_definition = [
|
|
223
|
+
{"name": "esim_activation_code", "string": "Urgent", "type": "char"},
|
|
224
|
+
{"name": "notes", "string": "Notes", "type": "char"},
|
|
225
|
+
]
|
|
226
|
+
self.project_task.task_properties = {
|
|
227
|
+
"esim_activation_code": False,
|
|
228
|
+
"notes": "old note",
|
|
229
|
+
}
|
|
230
|
+
payload = {"properties": {"esim_activation_code": "hello@hello.com"}}
|
|
231
|
+
|
|
232
|
+
with self._create_test_client(env=self.env) as test_client:
|
|
233
|
+
response = test_client.put(
|
|
234
|
+
f"/{self.url}/{self.project_task.id}",
|
|
235
|
+
json=payload,
|
|
236
|
+
headers=self.api_key_headers(),
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
240
|
+
properties = {
|
|
241
|
+
prop["name"]: prop["value"] for prop in self.project_task.task_properties
|
|
242
|
+
}
|
|
243
|
+
self.assertEqual(
|
|
244
|
+
properties["esim_activation_code"],
|
|
245
|
+
payload["properties"]["esim_activation_code"],
|
|
246
|
+
)
|
|
247
|
+
self.assertEqual(properties["notes"], "old note")
|
|
248
|
+
|
|
249
|
+
def test_project_task_update_unknown_property(self):
|
|
250
|
+
"""
|
|
251
|
+
Test the PUT /project_task/{record_id} endpoint returns a 400 error
|
|
252
|
+
when a property is not defined on the task's project.
|
|
253
|
+
"""
|
|
254
|
+
self.project_task.project_id.task_properties_definition = [
|
|
255
|
+
{"name": "urgent", "string": "Urgent", "type": "boolean"},
|
|
256
|
+
]
|
|
257
|
+
self.project_task.task_properties = {"urgent": False}
|
|
258
|
+
|
|
259
|
+
payload = {"properties": {"unknown_prop": True}}
|
|
260
|
+
with self._create_test_client(env=self.env) as test_client:
|
|
261
|
+
response = test_client.put(
|
|
262
|
+
f"/{self.url}/{self.project_task.id}",
|
|
263
|
+
json=payload,
|
|
264
|
+
headers=self.api_key_headers(),
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
|
268
|
+
self.assertIn("unknown_prop", response.json()["detail"])
|
|
269
|
+
|
|
270
|
+
def test_project_task_update_not_found(self):
|
|
271
|
+
"""
|
|
272
|
+
Test the PUT /project_task/{record_id} endpoint returns a 404 error
|
|
273
|
+
when the project task does not exist.
|
|
274
|
+
"""
|
|
275
|
+
payload = {"name": "Updated task name"}
|
|
276
|
+
with self._create_test_client(env=self.env) as test_client:
|
|
277
|
+
response = test_client.put(
|
|
278
|
+
f"/{self.url}/999999",
|
|
279
|
+
json=payload,
|
|
280
|
+
headers=self.api_key_headers(),
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/views/project_project_view.xml
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<odoo>
|
|
2
|
+
<data>
|
|
3
|
+
<record id="view_project_form_inherit" model="ir.ui.view">
|
|
4
|
+
<field name="name">project.project.form.inherit</field>
|
|
5
|
+
<field name="model">project.project</field>
|
|
6
|
+
<field name="inherit_id" ref="project.edit_project"/>
|
|
7
|
+
<field name="arch" type="xml">
|
|
8
|
+
<field name ="user_id" position="before">
|
|
9
|
+
<field name="code"/>
|
|
10
|
+
</field>
|
|
11
|
+
</field>
|
|
12
|
+
</record>
|
|
13
|
+
</data>
|
|
14
|
+
</odoo>
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: odoo-addon-project_task_api
|
|
3
|
+
Version: 16.0.1.0.0
|
|
4
|
+
Summary: API module based on FastAPI to get and create project tasks from external applications
|
|
5
|
+
Home-page: https://git.coopdevs.org/coopdevs/som-connexio/odoo/odoo-somconnexio
|
|
6
|
+
Author: Som Connexió SCCL, Odoo Community Association (OCA)
|
|
7
|
+
Author-email: support@odoo-community.org
|
|
8
|
+
License: AGPL-3
|
|
9
|
+
Classifier: Programming Language :: Python
|
|
10
|
+
Classifier: Framework :: Odoo
|
|
11
|
+
Classifier: Framework :: Odoo :: 16.0
|
|
12
|
+
Classifier: License :: OSI Approved :: GNU Affero General Public License v3
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Requires-Dist: fastapi
|
|
15
|
+
Requires-Dist: odoo-addon-fastapi_auth_api_key_sc<16.1dev,>=16.0dev
|
|
16
|
+
Requires-Dist: odoo<16.1dev,>=16.0a
|
|
17
|
+
Requires-Dist: pydantic
|
|
18
|
+
Dynamic: author
|
|
19
|
+
Dynamic: author-email
|
|
20
|
+
Dynamic: classifier
|
|
21
|
+
Dynamic: description
|
|
22
|
+
Dynamic: home-page
|
|
23
|
+
Dynamic: license
|
|
24
|
+
Dynamic: requires-dist
|
|
25
|
+
Dynamic: requires-python
|
|
26
|
+
Dynamic: summary
|
|
27
|
+
|
|
28
|
+
##################
|
|
29
|
+
Project Task API
|
|
30
|
+
##################
|
|
31
|
+
|
|
32
|
+
This module uses OCAs FAST API module for creating endpoints to get and
|
|
33
|
+
create project tasks from external applications.
|
|
34
|
+
|
|
35
|
+
.. note::
|
|
36
|
+
|
|
37
|
+
This module is made by and for developers.
|
|
38
|
+
|
|
39
|
+
**Table of contents**
|
|
40
|
+
|
|
41
|
+
.. contents::
|
|
42
|
+
:local:
|
|
43
|
+
|
|
44
|
+
***************
|
|
45
|
+
Configuration
|
|
46
|
+
***************
|
|
47
|
+
|
|
48
|
+
As this module is based on `fastapi_auth_api_key_sc`, an API-KEY is
|
|
49
|
+
required to authenticate requests. Read README from
|
|
50
|
+
`fastapi_auth_api_key_sc` for more information about the user related
|
|
51
|
+
API-KEY authentication.
|
|
52
|
+
|
|
53
|
+
*******
|
|
54
|
+
Usage
|
|
55
|
+
*******
|
|
56
|
+
|
|
57
|
+
This module provides FastAPI-based endpoints of project tasks by
|
|
58
|
+
extending `fastapi.endpoint`.
|
|
59
|
+
|
|
60
|
+
The module registers an example model API at the following roots:
|
|
61
|
+
|
|
62
|
+
- GET: `/api/project_task/{id}`
|
|
63
|
+
- POST: `/api/project_task/`
|
|
64
|
+
|
|
65
|
+
The authenticated API uses `API-KEY`
|
|
66
|
+
|
|
67
|
+
************************
|
|
68
|
+
Known issues / Roadmap
|
|
69
|
+
************************
|
|
70
|
+
|
|
71
|
+
There are no known issues for this module.
|
|
72
|
+
|
|
73
|
+
*************
|
|
74
|
+
Bug Tracker
|
|
75
|
+
*************
|
|
76
|
+
|
|
77
|
+
Bugs are tracked on `GitLab Issues
|
|
78
|
+
<https://gitlab.com/somitcoop/erp-research/odoo-helpdesk/-/issues>`_. In
|
|
79
|
+
case of trouble, please check there if your issue has already been
|
|
80
|
+
reported. If you spotted it first, help us smashing it by providing a
|
|
81
|
+
detailed and welcomed feedback.
|
|
82
|
+
|
|
83
|
+
Do not contact contributors directly about support or help with
|
|
84
|
+
technical issues.
|
|
85
|
+
|
|
86
|
+
TODO: Module to be substituted by `the OCA module`_ once this ODOO is
|
|
87
|
+
migrated to the 17th version or further.
|
|
88
|
+
|
|
89
|
+
.. _the oca module: https://github.com/OCA/rest-framework/tree/17.0/fastapi_auth_api_key/
|
|
90
|
+
|
|
91
|
+
*********
|
|
92
|
+
Credits
|
|
93
|
+
*********
|
|
94
|
+
|
|
95
|
+
Authors
|
|
96
|
+
=======
|
|
97
|
+
|
|
98
|
+
- Som Connexio SCCL
|
|
99
|
+
|
|
100
|
+
Contributors
|
|
101
|
+
============
|
|
102
|
+
|
|
103
|
+
- `Som Connexio SCCL <https://somconnexio.coop>`_:
|
|
104
|
+
|
|
105
|
+
- Gerard Funosas <gerard.funosas@somconnexio.coop>
|
|
106
|
+
|
|
107
|
+
Maintainers
|
|
108
|
+
===========
|
|
109
|
+
|
|
110
|
+
This module is maintained by the OCA.
|
|
111
|
+
|
|
112
|
+
.. image:: https://odoo-community.org/logo.png
|
|
113
|
+
:alt: Odoo Community Association
|
|
114
|
+
:target: https://odoo-community.org
|
|
115
|
+
|
|
116
|
+
OCA, or the Odoo Community Association, is a nonprofit organization
|
|
117
|
+
whose mission is to support the collaborative development of Odoo
|
|
118
|
+
features and promote its widespread use.
|
|
119
|
+
|
|
120
|
+
You are welcome to contribute. To learn how please visit
|
|
121
|
+
https://odoo-community.org/page/Contribute.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
setup.py
|
|
2
|
+
odoo/addons/project_task_api/CHANGELOG.md
|
|
3
|
+
odoo/addons/project_task_api/README.rst
|
|
4
|
+
odoo/addons/project_task_api/__init__.py
|
|
5
|
+
odoo/addons/project_task_api/__manifest__.py
|
|
6
|
+
odoo/addons/project_task_api/data/fastapi_endpoint.xml
|
|
7
|
+
odoo/addons/project_task_api/models/__init__.py
|
|
8
|
+
odoo/addons/project_task_api/models/fastapi_endpoint.py
|
|
9
|
+
odoo/addons/project_task_api/models/project_project.py
|
|
10
|
+
odoo/addons/project_task_api/routers/__init__.py
|
|
11
|
+
odoo/addons/project_task_api/routers/project_task_router.py
|
|
12
|
+
odoo/addons/project_task_api/schemas/__init__.py
|
|
13
|
+
odoo/addons/project_task_api/schemas/common.py
|
|
14
|
+
odoo/addons/project_task_api/schemas/project_task.py
|
|
15
|
+
odoo/addons/project_task_api/static/description/icon.png
|
|
16
|
+
odoo/addons/project_task_api/tests/__init__.py
|
|
17
|
+
odoo/addons/project_task_api/tests/test_fastapi_endpoint.py
|
|
18
|
+
odoo/addons/project_task_api/views/project_project_view.xml
|
|
19
|
+
odoo_addon_project_task_api.egg-info/PKG-INFO
|
|
20
|
+
odoo_addon_project_task_api.egg-info/SOURCES.txt
|
|
21
|
+
odoo_addon_project_task_api.egg-info/dependency_links.txt
|
|
22
|
+
odoo_addon_project_task_api.egg-info/not-zip-safe
|
|
23
|
+
odoo_addon_project_task_api.egg-info/requires.txt
|
|
24
|
+
odoo_addon_project_task_api.egg-info/top_level.txt
|
odoo_addon_project_task_api-16.0.1.0.0/odoo_addon_project_task_api.egg-info/dependency_links.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
odoo
|