Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
704bf8f
[ADD]pms_fastapi: Added folio endpoint.
jesusVMayor Feb 9, 2026
627cd8c
[IMP]pms_fastapi: Added invoice endpoint.
jesusVMayor Jan 16, 2026
b5d6038
[IMP]pms_fastapi: Improve invoice endpoints.
jesusVMayor Jan 21, 2026
4d9f7ba
[IMP]pms_fastapi: Folio endpoint.
jesusVMayor Feb 9, 2026
95c8630
[ADD]pms_fastapi_verifactu: Added module.
jesusVMayor Feb 9, 2026
96b352b
[IMP]pms_fastapi: Various fixes in invoices and folios endpoints.
jesusVMayor Feb 10, 2026
b2437c0
[IMP]pms_fastapi: Import pyinstrument inside of the profiler.
jesusVMayor Feb 10, 2026
4857d31
[IMP]pms_fastapi: Add tests.
jesusVMayor Feb 10, 2026
0addb3b
Fix github tests repos.yaml
jesusVMayor Feb 10, 2026
91d8390
Fix tests warning
jesusVMayor Feb 10, 2026
7eabf24
[FIX]pms_fastapi: Fix tests.
jesusVMayor Feb 11, 2026
091f950
Verifactu fixes.
jesusVMayor Feb 11, 2026
413e0af
[FIX]pms_fastapi: Various fixes.
jesusVMayor Feb 11, 2026
30d28ea
[IMP]fastapi various fixes.
jesusVMayor Feb 11, 2026
6654141
[FIX]partner_identification_unique: Fix typo in module name.
jesusVMayor Feb 12, 2026
5f4970d
Remove unused external dependency. The jwt dependency is set by auth_…
jesusVMayor Feb 12, 2026
53b56f4
[IMP]*_fastapi: Explicit env dependency to distinguish between public…
jesusVMayor Feb 12, 2026
0bb6f77
[FIX]*_fastapi: Missing dependencies.
jesusVMayor Feb 12, 2026
47b837a
Add roomdoo fastapi conventions IA skill.
jesusVMayor Feb 12, 2026
be9d186
[FIX]*_fastapi: fix tests
jesusVMayor Feb 12, 2026
51b89ab
fix typos.
jesusVMayor Feb 12, 2026
6be1d55
[IMP]pms_fastapi: folio improvements:
jesusVMayor Feb 13, 2026
eb45b28
[IMP]pms_fastapi: Order folios like in the pms view
jesusVMayor Feb 13, 2026
9cc498b
[IMP]pms_fastapi: Added globalsearch to folio endpoint.
jesusVMayor Feb 16, 2026
c09df4e
[FIX]pms_fastapi: /invoices /folios improvements.
jesusVMayor Feb 16, 2026
fc2cc75
[IMP]pms_fastapi: Multi value search in invoice params.
jesusVMayor Feb 18, 2026
0ba6399
[FIX]pms_fastapi: globalSearch in /folios by partner_name.
jesusVMayor Feb 18, 2026
d183186
[FIX]pms_fastapi: Multi search by paymentState fixed.
jesusVMayor Feb 18, 2026
ae3c7c6
[IMP]pms_fastapi: Allow to filter amount with different operators in …
jesusVMayor Feb 20, 2026
0b5ac64
[FIX]pms_fastapi: Fix paymentMethod search.
jesusVMayor Feb 20, 2026
0e849ba
[IMP]roomdoo_fastapi: Added feature flags management.
jesusVMayor Feb 20, 2026
3281b74
Fix paymentMethod search.
jesusVMayor Feb 23, 2026
8d9b86b
[FIX]pms_fastapi_l10n_es: Removed unused schema.
jesusVMayor Feb 23, 2026
787091d
[FIX] pms_fastapi: exclude modified-cancelled reservations from folio…
jesusVMayor Feb 25, 2026
363d1b3
[FIX]pms_fastapi: make the api compatible with fastapi 0.132.0
jesusVMayor Feb 25, 2026
4c8fcbc
[FIX]pms_fastapi: Search correctly by datetime field. By default equa…
jesusVMayor Feb 25, 2026
179d82f
[FIX]pms_fastapi: Fix folio order.
jesusVMayor Mar 4, 2026
f0d042a
[FIX]pms_fastapi: treat invoicing_legacy as paid and fix overdue dete…
jesusVMayor Mar 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions .claude/skills/roomdoo-fastapi-conventions/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
---
name: roomdoo-fastapi-conventions
description: "Conventions and patterns for developing FastAPI endpoints and Pydantic schemas in the Roomdoo project (roomdoo-modules). Use when creating or modifying FastAPI routers, Pydantic schemas, or extending pms_fastapi modules. Covers endpoint naming, helper patterns, data model conventions, CurrencyAmount usage, and module organization."
globs:
- "**/roomdoo-modules/**/routers/**"
- "**/roomdoo-modules/**/schemas/**"
- "**/roomdoo-modules/pms_fastapi*/**"
- "**/roomdoo-modules/roomdoo_fastapi/**"
---

# Roomdoo FastAPI & Pydantic Conventions

## Module Organization

- **`pms_fastapi`**: Base PMS endpoints. No localization.
- **`pms_fastapi_*`**: Extension modules (`auto_install: True`) adding fields/features via `extendable_pydantic`.
- **`roomdoo_fastapi`**: Roomdoo-specific customizations.

### Schema Extension Pattern

```python
class InvoiceSummary(invoice.InvoiceSummary, extends=True):
newField: str | None = Field(None, alias="newField")

@classmethod
def from_account_move(cls, account_move):
res = super().from_account_move(account_move)
res.newField = account_move.some_odoo_field or None
return res
```

## Endpoints (Routers)

### URL Conventions

- Plural (`/invoices`), singular only for single-item resources (`/user`)
- No trailing slash, kebab-case (`/sale-channels`)

### No Business Logic in Endpoints

Endpoints delegate ALL logic to a **helper** (Odoo `AbstractModel`), making it inheritable:

```python
@pms_api_router.get("/invoices", response_model=PagedCollection[InvoiceSummary])
async def list_invoices(env, filters, paging, orderBy):
count, invoices = env["pms_api_invoice.invoice_router.helper"].new()._search(paging, filters, orderBy)
return PagedCollection[InvoiceSummary](
count=count,
items=[InvoiceSummary.from_account_move(inv) for inv in invoices],
)
```

### Helper Pattern

```python
class PmsApiInvoiceRouterHelper(models.AbstractModel):
_name = "pms_api_invoice.invoice_router.helper"

def _get_domain_adapter(self):
return [("move_type", "in", ["out_invoice", "out_refund"])]

@property
def model_adapter(self) -> FilteredModelAdapter[AccountMove]:
return FilteredModelAdapter[AccountMove](self.env, self._get_domain_adapter())

def _search(self, paging, params, order):
return self.model_adapter.search_with_count(
params.to_odoo_domain(self.env), limit=paging.limit,
offset=paging.offset, order=order, context=params.to_odoo_context(self.env),
)
```

Helpers are inherited via `_inherit`: `_inherit = "pms_api_contact.contact_router.helper"`

## Data Models (Pydantic Schemas)

All schemas inherit from `PmsBaseModel` (`odoo.addons.pms_fastapi.schemas.base`).

### Naming

- API fields MUST be camelCase (via `alias`). Python names can be snake_case for Odoo auto-mapping via `_read_odoo_record()`.
Example: `is_agency: bool = Field(False, alias="isAgency")`
- Enum values in camelCase: `inHouse = "inHouse"`, `notPaid = "notPaid"`

### Field Patterns

- **Relational fields** use `id + name` schema: `partnerId: ContactId | None = Field(None, alias="partnerId")`
- **List fields** default to `[]`, never `None`: `phones: list[Phone] = Field(default_factory=list)`
- **Monetary fields** use `CurrencyAmount` type. Set `data["_decimal_places"] = currency.decimal_places` in the factory method. Auto-rounded by `PmsBaseModel` (defaults to 2).

### Conversion Methods (`from_<odoo_model>`)

Factory `@classmethod` named `from_<odoo_model_name>`. Uses `_read_odoo_record()` for basic fields, manual mapping for relational/computed:

```python
class FolioSummary(PmsBaseModel):
totalAmount: CurrencyAmount = Field(0.0, alias="totalAmount")
currency: CurrencySummary | None = None
partnerId: ContactId | None = Field(None, alias="partnerId")

@classmethod
def from_pms_folio(cls, folio):
data = cls._read_odoo_record(folio)
if folio.currency_id:
data["_decimal_places"] = folio.currency_id.decimal_places
data["currency"] = CurrencySummary.from_res_currency(folio.currency_id)
if folio.partner_id:
data["partnerId"] = ContactId.from_res_partner(folio.partner_id)
return cls(**data)
```

### Search/Filter Schemas

Inherit from `BaseSearch`, implement `to_odoo_domain()` and optionally `to_odoo_context()`:

```python
class InvoiceSearch(BaseSearch):
def to_odoo_domain(self, env) -> list:
domain = []
if self.name:
domain = expression.AND([domain, [("name", "ilike", self.name)]])
return domain
```
8 changes: 8 additions & 0 deletions .github/repos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,11 @@
merges:
- oca 16.0
target: oca 16.0

./deps/account-invoicing:
depth: 1
remotes:
oca: https://github.com/OCA/account-invoicing.git
merges:
- oca 16.0
target: oca 16.0
6 changes: 4 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ exclude: |
# Ignore test files in addons
/tests/samples/.*|
# You don't usually want a bot to modify your legal texts
(LICENSE.*|COPYING.*)
(LICENSE.*|COPYING.*)|
# AI assistant configuration
^\.claude/
default_language_version:
python: python3
node: "16.17.0"
Expand Down Expand Up @@ -91,7 +93,7 @@ repos:
- id: mixed-line-ending
args: ["--fix=lf"]
- repo: https://github.com/acsone/setuptools-odoo
rev: 3.1.8
rev: 3.3.2
hooks:
- id: setuptools-odoo-make-default
- id: setuptools-odoo-get-requirements
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ pip install -r requirements.txt

This approach allows us to work with cutting-edge features and bugfixes while maintaining a clear record of all dependencies for reproducible installations.

## AI Coding Assistants

This repository includes coding conventions and patterns for AI assistants in `.claude/skills/`. These documents help AI tools understand the project's architecture and generate code that follows our standards.

To make a skill available globally (outside the repo context), create a symlink:

```bash
ln -s /path/to/roomdoo-modules/.claude/skills/roomdoo-fastapi-conventions ~/.claude/skills/roomdoo-fastapi-conventions
```

## License

This project is licensed under the [GNU Affero General Public License v3.0](LICENSE).
Expand Down
2 changes: 1 addition & 1 deletion l10n_es_aeat_partner_identification/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"l10n_es_aeat",
"pms_l10n_es",
"partner_identification_map_partner_field",
"parnter_identification_unique",
"partner_identification_unique",
],
"data": ["data/res_partner_id_category.xml"],
"installable": True,
Expand Down
2 changes: 0 additions & 2 deletions parnter_identification_unique/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +0,0 @@
from . import models
from . import wizard
14 changes: 3 additions & 11 deletions parnter_identification_unique/__manifest__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
{
"name": "Partner identification unique",
"author": "Commitsun, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/l10n-spain",
"category": "Generic Modules/Property Management System",
"version": "16.0.2.0.0",
"name": "Partner identification unique (deprecated - renamed)",
"version": "16.0.3.0.0",
"license": "AGPL-3",
"depends": [
"pms_partner_identification",
"base_vat",
"partner_identification_map_partner_field",
],
"data": [],
"depends": ["base"],
"installable": True,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from openupgradelib import openupgrade


@openupgrade.migrate()
def migrate(env, version):
openupgrade.logged_query(
env.cr,
"DELETE FROM ir_model_data WHERE module = 'parnter_identification_unique'",
)
2 changes: 2 additions & 0 deletions partner_identification_unique/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import models
from . import wizard
15 changes: 15 additions & 0 deletions partner_identification_unique/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "Partner identification unique",
"author": "Commitsun, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/l10n-spain",
"category": "Generic Modules/Property Management System",
"version": "16.0.2.0.0",
"license": "AGPL-3",
"depends": [
"pms_partner_identification",
"base_vat",
"partner_identification_map_partner_field",
],
"data": [],
"installable": True,
}
2 changes: 1 addition & 1 deletion pms_api_rest/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"pms_partner_type_residence",
],
"external_dependencies": {
"python": ["jwt", "simplejson", "marshmallow", "jose"],
"python": ["simplejson", "marshmallow", "jose"],
},
"data": [
"security/ir.model.access.csv",
Expand Down
6 changes: 5 additions & 1 deletion pms_fastapi/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,19 @@
"category": "Generic Modules/Property Management System",
"license": "AGPL-3",
"depends": [
"fastapi_auth_jwt",
"extendable_fastapi",
"auth_jwt_login",
"partner_firstname",
"phone_validation",
"account_payment_partner",
"pms_api_rest", # temporal
"pms_l10n_es", # temporal
"parnter_identification_unique",
"partner_identification_unique",
],
"external_dependencies": {
"python": ["pyinstrument"],
},
"data": [
"security/pms_fastapi_groups.xml",
"data/res_users.xml",
Expand Down
12 changes: 11 additions & 1 deletion pms_fastapi/dependencies.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
from enum import Enum
from typing import Annotated

from fastapi import HTTPException, Query
from fastapi import Depends, HTTPException, Query

from odoo.api import Environment

from odoo.addons.fastapi.dependencies import odoo_env
from odoo.addons.fastapi_auth_jwt.dependencies import AuthJwtOdooEnv

PublicEnv = Annotated[Environment, Depends(odoo_env)]
AuthenticatedEnv = Annotated[
Environment, Depends(AuthJwtOdooEnv(validator_name="api_pms"))
]


def create_order_dependency(
Expand Down
4 changes: 4 additions & 0 deletions pms_fastapi/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
from . import fastapi_endpoint
from . import res_partner
from . import id_number_category
from . import account_move
from . import account_move_line
from . import account_partial_reconcile
from . import account_payment_method_line
85 changes: 85 additions & 0 deletions pms_fastapi/models/account_move.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from odoo import fields, models


class AccountMove(models.Model):
_inherit = "account.move"

payment_method_ids = fields.Many2many(
comodel_name="account.payment.method",
string="Payment Methods",
compute="_compute_payment_method_ids",
search="_search_payment_method_ids",
)
has_overdue_payments = fields.Boolean(
string="Has Overdue Payments",
compute="_compute_has_overdue_payments",
search="_search_has_overdue_payments",
)
min_overdue_date = fields.Date(
string="Minimum Overdue Payment Date", compute="_compute_min_overdue_date"
)

def _compute_payment_method_ids(self):
for move in self:
receivable_lines = move.line_ids.filtered(
lambda line: line.account_id.account_type
in ("asset_receivable", "liability_payable")
)
debit_methods = receivable_lines.matched_debit_ids.credit_move_id.payment_id.payment_method_line_id.payment_method_id # noqa: E501
credit_methods = receivable_lines.matched_credit_ids.debit_move_id.payment_id.payment_method_line_id.payment_method_id # noqa: E501
move.payment_method_ids = debit_methods | credit_methods

def _search_payment_method_ids(self, operator, value):
if operator == "=":
value = [value]
elif operator != "in":
raise NotImplementedError(
f"Operator '{operator}' is not supported for payment_method_ids search."
)
# Use raw SQL to avoid full scans on account.move.line.
# Start from account.partial.reconcile (much smaller) and join through
# indexed FK columns to resolve payment method → invoice move directly.
self.env.cr.execute(
"""
SELECT DISTINCT aml_inv.move_id
FROM account_partial_reconcile apr
JOIN account_move_line aml_pay ON aml_pay.id = apr.credit_move_id
JOIN account_payment ap ON ap.id = aml_pay.payment_id
JOIN account_payment_method_line apml ON apml.id = ap.payment_method_line_id
JOIN account_move_line aml_inv ON aml_inv.id = apr.debit_move_id
WHERE apml.payment_method_id = ANY(%s)
UNION
SELECT DISTINCT aml_inv.move_id
FROM account_partial_reconcile apr
JOIN account_move_line aml_pay ON aml_pay.id = apr.debit_move_id
JOIN account_payment ap ON ap.id = aml_pay.payment_id
JOIN account_payment_method_line apml ON apml.id = ap.payment_method_line_id
JOIN account_move_line aml_inv ON aml_inv.id = apr.credit_move_id
WHERE apml.payment_method_id = ANY(%s)
""",
(value, value),
)
move_ids = [row[0] for row in self.env.cr.fetchall()]
if not move_ids:
return [("id", "=", False)]
return [("id", "in", move_ids)]

def _compute_min_overdue_date(self):
for move in self:
overdue_lines = move.line_ids.filtered("is_overdue")
move.min_overdue_date = (
min(overdue_lines.mapped("date_maturity")) if overdue_lines else False
)

def _compute_has_overdue_payments(self):
for move in self:
move.has_overdue_payments = any(line.is_overdue for line in move.line_ids)

def _search_has_overdue_payments(self, operator, value):
if operator != "=":
raise NotImplementedError(
"Only '=' operator is supported for has_overdue_payments search."
)
if value:
return [("line_ids.is_overdue", "=", True)]
return []
Loading