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.
Files changed (26) hide show
  1. odoo_addon_project_task_api-16.0.1.0.0/PKG-INFO +121 -0
  2. odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/CHANGELOG.md +10 -0
  3. odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/README.rst +94 -0
  4. odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/__init__.py +1 -0
  5. odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/__manifest__.py +25 -0
  6. odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/data/fastapi_endpoint.xml +10 -0
  7. odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/models/__init__.py +4 -0
  8. odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/models/fastapi_endpoint.py +17 -0
  9. odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/models/project_project.py +11 -0
  10. odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/routers/__init__.py +1 -0
  11. odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/routers/project_task_router.py +211 -0
  12. odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/schemas/__init__.py +2 -0
  13. odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/schemas/common.py +7 -0
  14. odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/schemas/project_task.py +190 -0
  15. odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/static/description/icon.png +0 -0
  16. odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/tests/__init__.py +1 -0
  17. odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/tests/test_fastapi_endpoint.py +283 -0
  18. odoo_addon_project_task_api-16.0.1.0.0/odoo/addons/project_task_api/views/project_project_view.xml +14 -0
  19. odoo_addon_project_task_api-16.0.1.0.0/odoo_addon_project_task_api.egg-info/PKG-INFO +121 -0
  20. odoo_addon_project_task_api-16.0.1.0.0/odoo_addon_project_task_api.egg-info/SOURCES.txt +24 -0
  21. odoo_addon_project_task_api-16.0.1.0.0/odoo_addon_project_task_api.egg-info/dependency_links.txt +1 -0
  22. odoo_addon_project_task_api-16.0.1.0.0/odoo_addon_project_task_api.egg-info/not-zip-safe +1 -0
  23. odoo_addon_project_task_api-16.0.1.0.0/odoo_addon_project_task_api.egg-info/requires.txt +4 -0
  24. odoo_addon_project_task_api-16.0.1.0.0/odoo_addon_project_task_api.egg-info/top_level.txt +1 -0
  25. odoo_addon_project_task_api-16.0.1.0.0/setup.cfg +4 -0
  26. 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>
@@ -0,0 +1,4 @@
1
+ from . import (
2
+ fastapi_endpoint,
3
+ project_project,
4
+ )
@@ -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,11 @@
1
+ from odoo import fields, models
2
+
3
+
4
+ class ProjectProject(models.Model):
5
+ _inherit = "project.project"
6
+
7
+ code = fields.Char(string="Code")
8
+
9
+ sql_constraints = [
10
+ ("code_uniq", "UNIQUE(code)", "The code must be unique across all projects.")
11
+ ]
@@ -0,0 +1 @@
1
+ from . import project_task_router
@@ -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,2 @@
1
+ from . import common
2
+ from . import project_task
@@ -0,0 +1,7 @@
1
+ from typing import Literal, Annotated
2
+ from pydantic import Field
3
+
4
+
5
+ PositiveInt = Annotated[int, Field(gt=0)]
6
+ PriorityType = Literal["0", "1"]
7
+ KanbanStateType = Literal["normal", "done", "blocked"]
@@ -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
+ )
@@ -0,0 +1 @@
1
+ from . import test_fastapi_endpoint
@@ -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)
@@ -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
@@ -0,0 +1,4 @@
1
+ fastapi
2
+ odoo-addon-fastapi_auth_api_key_sc<16.1dev,>=16.0dev
3
+ odoo<16.1dev,>=16.0a
4
+ pydantic
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ import setuptools
2
+
3
+ setuptools.setup(
4
+ setup_requires=['setuptools-odoo'],
5
+ odoo_addon=True,
6
+ )