Skip to content

Commit a0e9e7d

Browse files
author
Callum Dickinson
committed
Replace supplementary record/manager classes with mixins
Records in Odoo can have some additional fields that are shared across many different record models using model inheritance. To support adding these kinds of shared fields (and related methods) to record/manager classes in the OpenStack Odoo Client library in a more modular way, add support for the use of **mixins** to take advantage of Python's multiple inheritance to add such fields and methods to custom record/manager classes. To make type-hinted record manager mixins possible, a **protocol** class called `RecordManagerProtocol` had to be created documenting the attributes and methods on the `RecordManagerBase` class intended for use by implementing classes. This protocol is used to set the type of the `self` parameter on mixin methods. `RecordManagerBase` now subclasses this protocol. The `NamedRecordManagerBase` and `CodedRecordManagerBase` classes have been reimplemented using mixins to not only utilise this paradigm inside the library itself, but also demonstrate its usage in a simple and practical way for anyone looking to write their own mixins. The `get_by_unique_field` method provided by `RecordManagerWithUniqueFieldBase` has been moved into `RecordManagerBase` to make it available for use in any custom manager class.
1 parent 33e5d51 commit a0e9e7d

36 files changed

+1605
-810
lines changed

changelog.d/13.changed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Replace supplementary record/manager classes with mixins

docs/managers/custom.md

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,230 @@ The following internal attributes are also available for use in methods:
791791
* `_odoo` (`odoorpc.ODOO`) - The OdooRPC connection object
792792
* `_env` (`odoorpc.env.Environment`) - The OdooRPC environment object for the model
793793

794+
## Mixins
795+
796+
Python supports [multiple inheritance](https://docs.python.org/3/tutorial/classes.html#multiple-inheritance)
797+
when creating new classes. A common use case for multiple inheritance is to extend
798+
functionality of a class through the use of *mixin classes*, which are minimal
799+
classes that only consist of supplementary attributes and methods, that get added
800+
to other classes through subclassing.
801+
802+
The OpenStack Odoo Client library for Python supports the use of mixin classes
803+
to add functionality to custom record and manager classes in a modular way.
804+
Multiple mixins can be added to record and manager classes to allow mixing and
805+
matching additional functionality as required.
806+
807+
### Using Mixins
808+
809+
To extend the functionality of your custom record and manager classes,
810+
append the mixins for the record class and/or record manager class
811+
**AFTER** the inheritance for `RecordBase` and `RecordManagerBase`.
812+
You also need to specify the **same** type arguments to the mixins as
813+
is already being done for `RecordBase` and `RecordManagerBase`.
814+
815+
```python
816+
from __future__ import annotations
817+
818+
from openstack_odooclient import (
819+
NamedRecordManagerMixin,
820+
NamedRecordMixin,
821+
RecordBase,
822+
RecordManagerBase,
823+
)
824+
825+
class CustomRecord(
826+
RecordBase["CustomRecordManager"],
827+
NamedRecordMixin["CustomRecordManager"],
828+
):
829+
custom_field: str
830+
"""Description of the field."""
831+
832+
class CustomRecordManager(
833+
RecordManagerBase[CustomRecord],
834+
NamedRecordManagerMixin[CustomRecord],
835+
):
836+
env_name = "custom.record"
837+
record_class = CustomRecord
838+
```
839+
840+
That's all that needs to be done. The additional attributes and/or methods
841+
should now be available on your record and manager objects.
842+
843+
The following mixins are provided with the Odoo Client library.
844+
845+
#### Named Records
846+
847+
If your record model has a unique `name` field on it (of `str` type),
848+
you can use the `NamedRecordMixin` and `NamedRecordManagerMixin` mixins
849+
to define the `name` field on the record class, and add the
850+
`get_by_name` method to your custom record manager class.
851+
852+
```python
853+
from __future__ import annotations
854+
855+
from openstack_odooclient import (
856+
NamedRecordManagerMixin,
857+
NamedRecordMixin,
858+
RecordBase,
859+
RecordManagerBase,
860+
)
861+
862+
class CustomRecord(
863+
RecordBase["CustomRecordManager"],
864+
NamedRecordMixin["CustomRecordManager"],
865+
):
866+
custom_field: str
867+
"""Description of the field."""
868+
869+
# Added by NamedRecordMixin:
870+
#
871+
# name: str
872+
# """The unique name of the record."""
873+
874+
class CustomRecordManager(
875+
RecordManagerBase[CustomRecord],
876+
NamedRecordManagerMixin[CustomRecord],
877+
):
878+
env_name = "custom.record"
879+
record_class = CustomRecord
880+
881+
# Added by NamedRecordManagerMixin:
882+
#
883+
# def get_by_name(...):
884+
# ...
885+
```
886+
887+
For more information on using record managers with unique `name` fields,
888+
see [Named Record Managers](index.md#named-record-managers).
889+
890+
#### Coded Records
891+
892+
If your record model has a unique `code` field on it (of `str` type),
893+
you can use the `CodedRecordMixin` and `CodedRecordManagerMixin` mixins
894+
to define the `code` field on the record class, and add the
895+
`get_by_code` method to your custom record manager class.
896+
897+
```python
898+
from __future__ import annotations
899+
900+
from openstack_odooclient import (
901+
CodedRecordManagerMixin,
902+
CodedRecordMixin,
903+
RecordBase,
904+
RecordManagerBase,
905+
)
906+
907+
class CustomRecord(
908+
RecordBase["CustomRecordManager"],
909+
CodedRecordMixin["CustomRecordManager"],
910+
):
911+
custom_field: str
912+
"""Description of the field."""
913+
914+
# Added by CodedRecordMixin:
915+
#
916+
# code: str
917+
# """The unique name for this record."""
918+
919+
class CustomRecordManager(
920+
RecordManagerBase[CustomRecord],
921+
CodedRecordManagerMixin[CustomRecord],
922+
):
923+
env_name = "custom.record"
924+
record_class = CustomRecord
925+
926+
# Added by CodedRecordManagerMixin:
927+
#
928+
# def get_by_code(...):
929+
# ...
930+
```
931+
932+
For more information on using record managers with unique `code` fields,
933+
see [Coded Record Managers](index.md#coded-record-managers).
934+
935+
### Creating Mixins
936+
937+
It is possible to create your own custom mixins to incorporate into
938+
custom record and manager classes.
939+
940+
There are two mixin types: **record mixins** and **record manager mixins**.
941+
942+
#### Record Mixins
943+
944+
Record mixins are used to add custom fields and methods to record classes.
945+
946+
Here is the full implementation of `NamedRecordMixin` as an example
947+
of a mixin for a record class, that simply adds the `name` field:
948+
949+
```python
950+
from __future__ import annotations
951+
952+
from typing import Generic
953+
954+
from openstack_odooclient import RM, RecordProtocol
955+
956+
class NamedRecordMixin(RecordProtocol[RM], Generic[RM]):
957+
name: str
958+
"""The unique name of the record."""
959+
```
960+
961+
A record mixin consists of a class that subclasses `RecordProtocol[RM]`
962+
(where `RM` is the type variable for a record manager class) to get the type
963+
hints for a record class' common fields and methods. `Generic[RM]` is also
964+
subclassed to make the mixin itself a generic class, to allow `RM` to be
965+
passed when creating a record class with the mixin.
966+
967+
Once you have the class, simply define any fields and methods you'd like
968+
to add.
969+
970+
You can then use the mixin as shown in [Using Mixins](#using-mixins).
971+
972+
When defining custom methods, in addition to accessing fields/methods
973+
defined within the mixin, fields/methods from the `RecordBase` class
974+
are also available:
975+
976+
```python
977+
from __future__ import annotations
978+
979+
from typing import Generic
980+
981+
from openstack_odooclient import RM, RecordProtocol
982+
983+
class NamedRecordMixin(RecordProtocol[RM], Generic[RM]):
984+
name: str
985+
"""The unique name of the record."""
986+
987+
def custom_method(self) -> None:
988+
self.name # str
989+
self._env.custom_method(self.id)
990+
```
991+
992+
#### Record Manager Mixins
993+
994+
Record manager mixins are expected to be mainly used to add custom methods
995+
to a record manager class.
996+
997+
```python
998+
from __future__ import annotations
999+
1000+
from typing import Generic
1001+
1002+
from openstack_odooclient import R, RecordManagerProtocol
1003+
1004+
class NamedRecordManagerMixin(RecordManagerProtocol[R], Generic[R]):
1005+
def custom_method(self, record: int | R) -> None:
1006+
self._env.custom_method( # self._env available from RecordManagerBase
1007+
record if isinstance(record, int) else record.id,
1008+
)
1009+
```
1010+
1011+
A record manager mixin consists of a class that subclasses
1012+
`RecordManagerProtocol[R]` (where `R` is the type variable for a record class)
1013+
to get the type hints for a record manager class' common attributes and
1014+
methods. `Generic[R]` is also subclassed to make the mixin itself a generic
1015+
class, to allow `R` to be passed when creating a record manager class
1016+
with the mixin.
1017+
7941018
## Extending Existing Record Types
7951019

7961020
The Odoo Client library provides *limited* support for extending the built-in record types.

docs/managers/index.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -873,9 +873,7 @@ The managers for these record types have additional methods for querying records
873873
* [Currencies](currency.md)
874874
* [OpenStack Customer Groups](customer-group.md)
875875
* [OpenStack Grant Types](grant-type.md)
876-
* [Partner Categories](partner-category.md)
877876
* [Pricelists](pricelist.md)
878-
* [Product Categories](product-category.md)
879877
* [OpenStack Reseller Tiers](reseller-tier.md)
880878
* [Sale Orders](sale-order.md)
881879
* [OpenStack Support Subscription Types](support-subscription-type.md)

docs/managers/partner-category.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ name: str
9999

100100
The name of the partner category.
101101

102+
Not guaranteed to be unique, even under the same parent category.
103+
102104
### `parent_id`
103105

104106
```python

docs/managers/product-category.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,9 @@ The complete product category tree.
8989
name: str
9090
```
9191

92-
Name of the product category.
92+
The name of the product category.
93+
94+
Not guaranteed to be unique, even under the same parent category.
9395

9496
### `parent_id`
9597

docs/managers/voucher-code.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ until it expires.
212212
name: str
213213
```
214214

215-
The unique name of this voucher code.
215+
The automatically generated name of this voucher code.
216216

217217
This uses the code specified in the record as-is.
218218

openstack_odooclient/__init__.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,8 @@
1616
from __future__ import annotations
1717

1818
from .base.client import ClientBase
19-
from .base.record import FieldAlias, ModelRef, RecordBase
20-
from .base.record_manager import RecordManagerBase
21-
from .base.record_manager_coded import CodedRecordManagerBase
22-
from .base.record_manager_named import NamedRecordManagerBase
23-
from .base.record_manager_with_unique_field import (
24-
RecordManagerWithUniqueFieldBase,
25-
)
19+
from .base.record import FieldAlias, ModelRef, RecordBase, RecordProtocol
20+
from .base.record_manager import RecordManagerBase, RecordManagerProtocol
2621
from .client import Client
2722
from .exceptions import (
2823
ClientError,
@@ -77,6 +72,7 @@
7772
VolumeDiscountRangeManager,
7873
)
7974
from .managers.voucher_code import VoucherCode, VoucherCodeManager
75+
from .mixins.named_record import NamedRecordManagerMixin, NamedRecordMixin
8076

8177
__all__ = [
8278
"AccountMove",
@@ -86,7 +82,6 @@
8682
"Client",
8783
"ClientBase",
8884
"ClientError",
89-
"CodedRecordManagerBase",
9085
"Company",
9186
"CompanyManager",
9287
"Credit",
@@ -106,7 +101,8 @@
106101
"GrantTypeManager",
107102
"ModelRef",
108103
"MultipleRecordsFoundError",
109-
"NamedRecordManagerBase",
104+
"NamedRecordManagerMixin",
105+
"NamedRecordMixin",
110106
"Partner",
111107
"PartnerCategory",
112108
"PartnerCategoryManager",
@@ -123,8 +119,9 @@
123119
"ProjectManager",
124120
"RecordBase",
125121
"RecordManagerBase",
126-
"RecordManagerWithUniqueFieldBase",
122+
"RecordManagerProtocol",
127123
"RecordNotFoundError",
124+
"RecordProtocol",
128125
"ReferralCode",
129126
"ReferralCodeManager",
130127
"Reseller",
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Copyright (C) 2025 Catalyst Cloud Limited
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12+
# implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
from __future__ import annotations
17+
18+
from .base import RM, RecordBase, RecordProtocol, RM_co
19+
from .types import FieldAlias, ModelRef
20+
21+
__all__ = [
22+
"RM",
23+
"FieldAlias",
24+
"ModelRef",
25+
"RM_co",
26+
"RecordBase",
27+
"RecordProtocol",
28+
]

0 commit comments

Comments
 (0)