Skip to content

Commit 07b65c6

Browse files
committed
[MIG] base_geoengine: Migration to 19.0
1 parent a460a1b commit 07b65c6

File tree

15 files changed

+276
-82
lines changed

15 files changed

+276
-82
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ jobs:
4242
makepot: "true"
4343
services:
4444
postgres:
45-
image: postgres:13
45+
image: postgis/postgis:13-3.4
4646
env:
4747
POSTGRES_USER: odoo
4848
POSTGRES_PASSWORD: odoo

base_geoengine/README.rst

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ Geospatial support for Odoo
2121
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
2222
:alt: License: AGPL-3
2323
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fgeospatial-lightgray.png?logo=github
24-
:target: https://github.com/OCA/geospatial/tree/18.0/base_geoengine
24+
:target: https://github.com/OCA/geospatial/tree/19.0/base_geoengine
2525
:alt: OCA/geospatial
2626
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
27-
:target: https://translation.odoo-community.org/projects/geospatial-18-0/geospatial-18-0-base_geoengine
27+
:target: https://translation.odoo-community.org/projects/geospatial-19-0/geospatial-19-0-base_geoengine
2828
:alt: Translate me on Weblate
2929
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
30-
:target: https://runboat.odoo-community.org/builds?repo=OCA/geospatial&target_branch=18.0
30+
:target: https://runboat.odoo-community.org/builds?repo=OCA/geospatial&target_branch=19.0
3131
:alt: Try me on Runboat
3232

3333
|badge1| |badge2| |badge3| |badge4| |badge5|
@@ -306,7 +306,7 @@ Bug Tracker
306306
Bugs are tracked on `GitHub Issues <https://github.com/OCA/geospatial/issues>`_.
307307
In case of trouble, please check there if your issue has already been reported.
308308
If you spotted it first, help us to smash it by providing a detailed and welcomed
309-
`feedback <https://github.com/OCA/geospatial/issues/new?body=module:%20base_geoengine%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
309+
`feedback <https://github.com/OCA/geospatial/issues/new?body=module:%20base_geoengine%0Aversion:%2019.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
310310

311311
Do not contact contributors directly about support or help with technical issues.
312312

@@ -363,6 +363,6 @@ OCA, or the Odoo Community Association, is a nonprofit organization whose
363363
mission is to support the collaborative development of Odoo features and
364364
promote its widespread use.
365365

366-
This module is part of the `OCA/geospatial <https://github.com/OCA/geospatial/tree/18.0/base_geoengine>`_ project on GitHub.
366+
This module is part of the `OCA/geospatial <https://github.com/OCA/geospatial/tree/19.0/base_geoengine>`_ project on GitHub.
367367

368368
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

base_geoengine/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
from . import geo_convertion_helper
55
from . import geo_operators
66
from .geo_db import init_postgis
7+
from . import domains

base_geoengine/__manifest__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
55
{
66
"name": "Geospatial support for Odoo",
7-
"version": "18.0.1.0.1",
7+
"version": "19.0.1.0.1",
88
"category": "GeoBI",
99
"author": "Camptocamp,ACSONE SA/NV,Odoo Community Association (OCA)",
1010
"license": "AGPL-3",

base_geoengine/domains.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import contextlib
2+
import logging
3+
import warnings
4+
5+
from odoo.fields import Domain
6+
from odoo.models import BaseModel
7+
from odoo.orm import domains
8+
from odoo.orm.domains import (
9+
CONDITION_OPERATORS,
10+
NEGATIVE_CONDITION_OPERATORS,
11+
SQL,
12+
DomainCondition,
13+
OptimizationLevel,
14+
Query,
15+
)
16+
from odoo.orm.identifiers import NewId
17+
18+
_logger = logging.getLogger(__name__)
19+
20+
GEO_OPERATORS = frozenset(
21+
[
22+
"geo_greater",
23+
"geo_lesser",
24+
"geo_equal",
25+
"geo_touch",
26+
"geo_within",
27+
"geo_contains",
28+
"geo_intersect",
29+
]
30+
)
31+
32+
33+
def checked(self) -> DomainCondition:
34+
"""Validate `self` and return it if correct, otherwise raise an exception."""
35+
if not isinstance(self.field_expr, str) or not self.field_expr:
36+
self._raise("Empty field name", error=TypeError)
37+
operator = self.operator.lower()
38+
if operator != self.operator:
39+
warnings.warn(
40+
(
41+
f"Deprecated since 19.0, the domain condition "
42+
f"{(self.field_expr, self.operator, self.value)!r} "
43+
f"should have a lower-case operator"
44+
),
45+
DeprecationWarning,
46+
# <MOD>
47+
stacklevel=2,
48+
# </MOD>
49+
)
50+
return DomainCondition(self.field_expr, operator, self.value).checked()
51+
if operator not in CONDITION_OPERATORS:
52+
# <MOD>
53+
if operator not in GEO_OPERATORS:
54+
# </MOD>
55+
self._raise("Invalid operator")
56+
57+
# check already the consistency for domain manipulation
58+
# these are common mistakes and optimizations,
59+
# do them here to avoid recreating the domain
60+
# - NewId is not a value
61+
# - records are not accepted, use values
62+
# - Query and Domain values should be using a relational operator
63+
# <MOD>
64+
# from .models import BaseModel # noqa: PLC0415
65+
# </MOD>
66+
67+
value = self.value
68+
if value is None:
69+
value = False
70+
elif isinstance(value, NewId):
71+
_logger.warning(
72+
"Domains don't support NewId, use .ids instead, for %r",
73+
(self.field_expr, self.operator, self.value),
74+
)
75+
operator = "not in" if operator in NEGATIVE_CONDITION_OPERATORS else "in"
76+
value = []
77+
elif isinstance(value, BaseModel):
78+
_logger.warning(
79+
"The domain condition %r should not have a value which is a model",
80+
(self.field_expr, self.operator, self.value),
81+
)
82+
value = value.ids
83+
elif isinstance(value, (Domain, Query, SQL)) and operator not in (
84+
"any",
85+
"not any",
86+
"any!",
87+
"not any!",
88+
"in",
89+
"not in",
90+
):
91+
# accept SQL object in the right part for simple operators
92+
# use case: compare 2 fields
93+
_logger.warning(
94+
"The domain condition %r should use the 'any' or 'not any' operator.",
95+
(self.field_expr, self.operator, self.value),
96+
)
97+
if value is not self.value:
98+
return DomainCondition(self.field_expr, operator, value)
99+
return self
100+
101+
102+
def _to_sql(self, model: BaseModel, alias: str, query: Query) -> SQL:
103+
"""Enhanced _to_sql that handles geospatial operators."""
104+
field_expr, operator, value = self.field_expr, self.operator, self.value
105+
106+
# Only handle geospatial operators here, delegate everything else to original method
107+
if operator in GEO_OPERATORS:
108+
# Ensure geospatial conditions are fully optimized
109+
assert self._opt_level >= OptimizationLevel.FULL, (
110+
"Must fully optimize before generating the query "
111+
f"{(field_expr, operator, value)}"
112+
)
113+
114+
field = self._field(model)
115+
model._check_field_access(field, "read")
116+
return field.condition_to_sql(field_expr, operator, value, model, alias, query)
117+
118+
# For all other operators, use the original method
119+
return original__to_sql(self, model, alias, query)
120+
121+
122+
def _optimize_step(self, model: BaseModel, level: OptimizationLevel) -> Domain:
123+
"""Optimization step for geospatial operators."""
124+
# For geospatial operators, we need to handle them specially during optimization
125+
# If this is a geospatial operator, mark it as optimized at FULL level
126+
if self.operator in GEO_OPERATORS:
127+
# Perform basic validation and normalization
128+
with contextlib.suppress(Exception):
129+
field = self._field(model)
130+
# Basic geospatial operator validation
131+
if hasattr(field, "geo_type"): # It's a geospatial field
132+
# Create optimized version with FULL level
133+
optimized = DomainCondition(self.field_expr, self.operator, self.value)
134+
object.__setattr__(optimized, "_opt_level", OptimizationLevel.FULL)
135+
return optimized
136+
137+
# Fall back to original optimization for non-geo operators
138+
return original__optimize_step(self, model, level)
139+
140+
141+
# Store original methods before monkey patching
142+
original__optimize_step = DomainCondition._optimize_step
143+
original__to_sql = DomainCondition._to_sql
144+
145+
DomainCondition.checked = checked
146+
DomainCondition._to_sql = _to_sql
147+
DomainCondition._optimize_step = _optimize_step
148+
149+
domains.CONDITION_OPERATORS = domains.CONDITION_OPERATORS.union(GEO_OPERATORS)

base_geoengine/expressions.py

Lines changed: 45 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
# Copyright 2023 ACSONE SA/NV
22
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
33

4+
import logging
45
import random
56
import string
67

8+
from odoo import fields
9+
from odoo.fields import Domain
710
from odoo.models import BaseModel
8-
from odoo.osv import expression
9-
from odoo.osv.expression import TERM_OPERATORS
1011
from odoo.tools import SQL, Query
1112

1213
from .fields import GeoField
1314
from .geo_operators import GeoOperator
1415

15-
original___condition_to_sql = BaseModel._condition_to_sql
16+
logger = logging.getLogger(__name__)
17+
18+
original___condition_to_sql = fields.Field._condition_to_sql
1619

1720
GEO_OPERATORS = {
1821
"geo_greater": ">",
@@ -23,6 +26,7 @@
2326
"geo_contains": "ST_Contains",
2427
"geo_intersect": "ST_Intersects",
2528
}
29+
2630
GEO_SQL_OPERATORS = {
2731
"geo_greater": SQL(">"),
2832
"geo_lesser": SQL("<"),
@@ -32,23 +36,23 @@
3236
"geo_contains": SQL("ST_Contains"),
3337
"geo_intersect": SQL("ST_Intersects"),
3438
}
35-
term_operators_list = list(TERM_OPERATORS)
36-
for op in GEO_OPERATORS:
37-
term_operators_list.append(op)
38-
39-
expression.TERM_OPERATORS = tuple(term_operators_list)
40-
expression.SQL_OPERATORS.update(GEO_SQL_OPERATORS)
4139

4240

4341
def _condition_to_sql(
44-
self, alias: str, fname: str, operator: str, value, query: Query
42+
self,
43+
field_expr: str,
44+
operator: str,
45+
value,
46+
model: BaseModel,
47+
alias: str,
48+
query: Query,
4549
) -> SQL:
4650
"""
4751
This method has been monkey patched in order to be able to include
4852
geo_operators into the Odoo search method.
4953
"""
5054
if operator in GEO_OPERATORS.keys():
51-
current_field = self._fields.get(fname)
55+
current_field = model._fields.get(field_expr)
5256
current_operator = GeoOperator(current_field)
5357
if current_field and isinstance(current_field, GeoField):
5458
params = []
@@ -59,9 +63,9 @@ def _condition_to_sql(
5963
sub_queries = []
6064
for key in ref_search:
6165
i = key.rfind(".")
62-
rel_model = key[0:i]
66+
rel_model_name = key[0:i]
6367
rel_col = key[i + 1 :]
64-
rel_model = self.env[rel_model]
68+
rel_model = model.env[rel_model_name]
6569
# we compute the attributes search on spatial rel
6670
if ref_search[key]:
6771
rel_alias = (
@@ -75,42 +79,52 @@ def _condition_to_sql(
7579
active_test=True,
7680
alias=rel_alias,
7781
)
78-
self._apply_ir_rules(rel_query, "read")
82+
model._check_field_access(current_field, "read")
7983
if operator == "geo_equal":
8084
rel_query.add_where(
81-
f'"{alias}"."{fname}" {GEO_OPERATORS[operator]} '
85+
f'"{alias}"."{field_expr}" {GEO_OPERATORS[operator]} '
8286
f"{rel_alias}.{rel_col}"
8387
)
8488
elif operator in ("geo_greater", "geo_lesser"):
8589
rel_query.add_where(
86-
f"ST_Area({alias}.{fname}) {GEO_OPERATORS[operator]} "
90+
f"ST_Area({alias}.{field_expr}) "
91+
f"{GEO_OPERATORS[operator]} "
8792
f"ST_Area({rel_alias}.{rel_col})"
8893
)
8994
else:
9095
rel_query.add_where(
91-
f'{GEO_OPERATORS[operator]}("{alias}"."{fname}", '
96+
f'{GEO_OPERATORS[operator]}("{alias}"."{field_expr}", '
9297
f"{rel_alias}.{rel_col})"
9398
)
9499

95-
subquery, subparams = rel_query.subselect("1")
100+
subquery_sql = rel_query.subselect("1")
96101
sub_query_mogrified = (
97-
self.env.cr.mogrify(subquery, subparams)
102+
model.env.cr.mogrify(subquery_sql.code, subquery_sql.params)
98103
.decode("utf-8")
99104
.replace(f"'{rel_model._table}'", f'"{rel_model._table}"')
100105
.replace("%", "%%")
101106
)
102107
sub_queries.append(f"EXISTS({sub_query_mogrified})")
103-
query = " AND ".join(sub_queries)
108+
query_str = " AND ".join(sub_queries)
104109
else:
105-
query = get_geo_func(
106-
current_operator, operator, fname, value, params, self._table
110+
query_str = get_geo_func(
111+
current_operator, operator, field_expr, value, params, model._table
107112
)
108-
return SQL(query, *params)
113+
return SQL(query_str, *params)
109114
return original___condition_to_sql(
110-
self, alias=alias, fname=fname, operator=operator, value=value, query=query
115+
self,
116+
field_expr=field_expr,
117+
operator=operator,
118+
value=value,
119+
model=model,
120+
alias=alias,
121+
query=query,
111122
)
112123

113124

125+
fields.Field._condition_to_sql = _condition_to_sql
126+
127+
114128
def get_geo_func(current_operator, operator, left, value, params, table):
115129
"""
116130
This method will call the SQL query corresponding to the requested geo operator
@@ -149,8 +163,12 @@ def where_calc(model, domain, active_test=True, alias=None):
149163

150164
query = Query(model.env, alias, model._table)
151165
if domain:
152-
return expression.expression(domain, model, alias=alias, query=query).query
153-
return query
166+
# In Odoo 19, create Domain object and use its _to_sql method
167+
domain_obj = Domain(domain)
168+
optimized_domain = domain_obj.optimize_full(model)
169+
sql_condition = optimized_domain._to_sql(model, alias, query)
170+
query.add_where(sql_condition)
154171

172+
return query
155173

156-
BaseModel._condition_to_sql = _condition_to_sql
174+
return query

0 commit comments

Comments
 (0)