details:   https://code.tryton.org/tryton/commit/9895805863b9
branch:    default
user:      Cédric Krier <[email protected]>
date:      Fri Dec 26 10:57:44 2025 +0100
description:
        Deprecate reduce_ids for SQL_OPERATORS['in']

        Closes #12989 #13897
diffstat:

 modules/account/account.py                           |  29 +++++++----
 modules/account/company.py                           |   5 +-
 modules/account/journal.py                           |   6 +-
 modules/account/move.py                              |  21 +++++---
 modules/account/party.py                             |   5 +-
 modules/account_budget/account.py                    |  11 ++--
 modules/account_deposit/party.py                     |   5 +-
 modules/account_invoice/invoice.py                   |  13 ++--
 modules/account_invoice_history/account.py           |   4 +-
 modules/account_payment/account.py                   |   5 +-
 modules/account_payment/payment.py                   |   8 +-
 modules/account_payment_clearing/payment.py          |   5 +-
 modules/account_payment_sepa/payment.py              |  12 ++--
 modules/account_statement/journal.py                 |  10 ++--
 modules/analytic_budget/analytic_account.py          |   3 +-
 modules/commission/commission.py                     |   5 +-
 modules/marketing_automation/marketing_automation.py |  12 ++--
 modules/marketing_email/marketing.py                 |   5 +-
 modules/product_price_list_cache/product.py          |  15 +++--
 modules/production_work/work.py                      |   5 +-
 modules/project_invoice/invoice.py                   |   4 +-
 modules/project_invoice/project.py                   |  11 ++--
 modules/project_revenue/work.py                      |   8 +-
 modules/purchase/product.py                          |   5 +-
 modules/purchase_history/purchase.py                 |   5 +-
 modules/sale/sale_reporting.py                       |   8 ++-
 modules/sale_history/sale.py                         |   5 +-
 modules/sale_point/sale.py                           |   8 ++-
 modules/sale_promotion_coupon/sale.py                |   5 +-
 modules/stock/move.py                                |  11 ++--
 modules/stock/product.py                             |  14 +++--
 modules/stock/stock_reporting_margin.py              |   5 +-
 modules/stock_forecast/forecast.py                   |   6 +-
 modules/stock_quantity_issue/stock.py                |   3 +-
 modules/stock_shipment_measurements/stock.py         |   8 ++-
 modules/stock_supply/purchase_request.py             |   4 +-
 modules/stock_supply/shipment.py                     |   4 +-
 modules/timesheet/work.py                            |   6 +-
 modules/web_shortener/web.py                         |   4 +-
 trytond/CHANGELOG                                    |   1 +
 trytond/trytond/ir/cron.py                           |   4 +-
 trytond/trytond/ir/note.py                           |   4 +-
 trytond/trytond/ir/trigger.py                        |  11 ++--
 trytond/trytond/model/modelsql.py                    |   1 -
 trytond/trytond/tests/test_tools.py                  |  48 +-------------------
 trytond/trytond/tools/misc.py                        |  48 ++-----------------
 46 files changed, 191 insertions(+), 234 deletions(-)

diffs (1431 lines):

diff -r 65be67251d10 -r 9895805863b9 modules/account/account.py
--- a/modules/account/account.py        Fri Dec 26 10:58:30 2025 +0100
+++ b/modules/account/account.py        Fri Dec 26 10:57:44 2025 +0100
@@ -22,8 +22,8 @@
 from trytond.pyson import Bool, Eval, Id, If, PYSONEncoder
 from trytond.report import Report
 from trytond.tools import (
-    grouped_slice, is_full_text, lstrip_wildcard, pair, reduce_ids,
-    sql_pairing, sqlite_apply_types)
+    grouped_slice, is_full_text, lstrip_wildcard, pair, sql_pairing,
+    sqlite_apply_types)
 from trytond.transaction import Transaction, check_access, inactive_records
 from trytond.wizard import (
     Button, StateAction, StateTransition, StateView, Wizard)
@@ -1019,7 +1019,7 @@
             with transaction.set_context(company=company.id):
                 line_query, fiscalyear_ids = MoveLine.query_get(line)
             for sub_ids in grouped_slice(ids):
-                red_sql = reduce_ids(table_a.id, sub_ids)
+                red_sql = fields.SQL_OPERATORS['in'](table_a.id, sub_ids)
                 if context.get('flat_balance'):
                     query = (table_a
                         .join(line, condition=line.account == table_a.id))
@@ -1092,7 +1092,7 @@
                         Sum(Coalesce(Column(line, name), 0)).as_(name))
                     types.append('NUMERIC')
             for sub_ids in grouped_slice(ids):
-                red_sql = reduce_ids(table.id, sub_ids)
+                red_sql = fields.SQL_OPERATORS['in'](table.id, sub_ids)
                 query = (table.join(line, 'LEFT',
                         condition=line.account == table.id
                         ).select(*columns,
@@ -1531,9 +1531,11 @@
             with transaction.set_context(company=company.id):
                 line_query, fiscalyear_ids = MoveLine.query_get(line)
             for sub_account_ids in grouped_slice(account_ids):
-                account_sql = reduce_ids(table_a.id, sub_account_ids)
+                account_sql = fields.SQL_OPERATORS['in'](
+                    table_a.id, sub_account_ids)
                 for sub_party_ids in grouped_slice(party_ids):
-                    party_sql = reduce_ids(line.party, sub_party_ids)
+                    party_sql = fields.SQL_OPERATORS['in'](
+                        line.party, sub_party_ids)
                     if context.get('flat_balance'):
                         query = (table_a
                             .join(line, condition=line.account == table_a.id))
@@ -1608,9 +1610,11 @@
                 line_query, fiscalyear_ids = MoveLine.query_get(line)
 
             for sub_account_ids in grouped_slice(account_ids):
-                account_sql = reduce_ids(table.id, sub_account_ids)
+                account_sql = fields.SQL_OPERATORS['in'](
+                    table.id, sub_account_ids)
                 for sub_party_ids in grouped_slice(party_ids):
-                    party_sql = reduce_ids(line.party, sub_party_ids)
+                    party_sql = fields.SQL_OPERATORS['in'](
+                        line.party, sub_party_ids)
                     query = (table.join(line, 'LEFT',
                             condition=line.account == table.id
                             ).select(*columns,
@@ -1750,7 +1754,8 @@
         balances = defaultdict(Decimal)
 
         for sub_deferrals in grouped_slice(deferrals):
-            red_sql = reduce_ids(table.id, [d.id for d in sub_deferrals])
+            red_sql = fields.SQL_OPERATORS['in'](
+                table.id, [d.id for d in sub_deferrals])
             query = (table
                 .join(account, condition=table.account == account.id)
                 .join(account_child,
@@ -2487,10 +2492,12 @@
         query = account_party.select(
             account_party.account, account_party.party, account_party.id)
         for sub_account_ids in grouped_slice(account_ids):
-            account_where = reduce_ids(account_party.account, sub_account_ids)
+            account_where = fields.SQL_OPERATORS['in'](
+                account_party.account, sub_account_ids)
             for sub_party_ids in grouped_slice(party_ids):
                 query.where = (account_where
-                    & reduce_ids(account_party.party, sub_party_ids))
+                    & fields.SQL_OPERATORS['in'](
+                        account_party.party, sub_party_ids))
                 cursor.execute(*query)
                 for account, party, id_ in cursor:
                     key = (account, party)
diff -r 65be67251d10 -r 9895805863b9 modules/account/company.py
--- a/modules/account/company.py        Fri Dec 26 10:58:30 2025 +0100
+++ b/modules/account/company.py        Fri Dec 26 10:57:44 2025 +0100
@@ -12,7 +12,7 @@
 from trytond.model.exceptions import AccessError
 from trytond.modules.currency.fields import Monetary
 from trytond.pool import Pool, PoolMeta
-from trytond.tools import grouped_slice, reduce_ids, sqlite_apply_types
+from trytond.tools import grouped_slice, sqlite_apply_types
 from trytond.transaction import Transaction
 
 
@@ -67,7 +67,8 @@
                 today_where = Literal(True)
             for sub_companies in grouped_slice(companies):
                 sub_ids = [p.id for p in sub_companies]
-                company_where = reduce_ids(account.company, sub_ids)
+                company_where = fields.SQL_OPERATORS['in'](
+                    account.company, sub_ids)
                 query = (line
                     .join(account,
                         condition=account.id == line.account)
diff -r 65be67251d10 -r 9895805863b9 modules/account/journal.py
--- a/modules/account/journal.py        Fri Dec 26 10:58:30 2025 +0100
+++ b/modules/account/journal.py        Fri Dec 26 10:57:44 2025 +0100
@@ -14,8 +14,7 @@
 from trytond.pool import Pool
 from trytond.pyson import Eval
 from trytond.tools import (
-    grouped_slice, is_full_text, lstrip_wildcard, reduce_ids,
-    sqlite_apply_types)
+    grouped_slice, is_full_text, lstrip_wildcard, sqlite_apply_types)
 from trytond.transaction import Transaction
 
 STATES = {
@@ -115,7 +114,8 @@
             & (move.company == company.id))
         for sub_journals in grouped_slice(journals):
             sub_journals = list(sub_journals)
-            red_sql = reduce_ids(move.journal, [j.id for j in sub_journals])
+            red_sql = fields.SQL_OPERATORS['in'](
+                move.journal, [j.id for j in sub_journals])
             query = line.join(move, 'LEFT', condition=line.move == move.id
                 ).join(account, 'LEFT', condition=line.account == account.id
                 ).join(account_type, 'LEFT',
diff -r 65be67251d10 -r 9895805863b9 modules/account/move.py
--- a/modules/account/move.py   Fri Dec 26 10:58:30 2025 +0100
+++ b/modules/account/move.py   Fri Dec 26 10:57:44 2025 +0100
@@ -21,8 +21,7 @@
 from trytond.pyson import Bool, Eval, If, PYSONEncoder
 from trytond.report import Report
 from trytond.rpc import RPC
-from trytond.tools import (
-    firstline, grouped_slice, reduce_ids, sqlite_apply_types)
+from trytond.tools import firstline, grouped_slice, sqlite_apply_types
 from trytond.transaction import Transaction, check_access
 from trytond.wizard import (
     Button, StateAction, StateTransition, StateView, Wizard)
@@ -363,7 +362,8 @@
         for company, moves in groupby(moves, key=lambda m: m.company):
             currency = company.currency
             for sub_moves in grouped_slice(list(moves)):
-                red_sql = reduce_ids(line.move, [m.id for m in sub_moves])
+                red_sql = fields.SQL_OPERATORS['in'](
+                    line.move, [m.id for m in sub_moves])
 
                 valid_move_query = line.select(
                     line.move,
@@ -465,7 +465,8 @@
 
                 cursor.execute(*move.select(
                         move.id,
-                        where=reduce_ids(move.id, sub_moves_ids)
+                        where=fields.SQL_OPERATORS['in'](
+                            move.id, sub_moves_ids)
                         & ~Exists(line.select(
                                 line.move,
                                 where=line.move == move.id))))
@@ -480,7 +481,8 @@
 
                 cursor.execute(*line.select(
                         line.move,
-                        where=reduce_ids(line.move, sub_moves_ids),
+                        where=fields.SQL_OPERATORS['in'](
+                            line.move, sub_moves_ids),
                         group_by=line.move,
                         having=Abs(Round(
                                 Sum(line.debit - line.credit),
@@ -496,7 +498,8 @@
 
                 cursor.execute(*line.select(
                         line.id,
-                        where=reduce_ids(line.move, sub_moves_ids)
+                        where=fields.SQL_OPERATORS['in'](
+                            line.move, sub_moves_ids)
                         & (line.debit == Decimal(0))
                         & (line.credit == Decimal(0))
                         & (line.reconciliation == Null)
@@ -864,7 +867,8 @@
                 parties = {l.party for l in sub_lines}
                 if None in parties:
                     party_where |= line.party == parties.discard(None)
-                party_where |= reduce_ids(line.party, map(int, parties))
+                party_where |= fields.SQL_OPERATORS['in'](
+                    line.party, map(int, parties))
                 where &= party_where
 
                 query = (line
@@ -878,7 +882,8 @@
                         where=where))
                 query = query.select(
                     query.id, query.balance.as_('balance'),
-                    where=reduce_ids(query.id, [l.id for l in sub_lines]))
+                    where=fields.SQL_OPERATORS['in'](
+                        query.id, [l.id for l in sub_lines]))
                 if backend.name == 'sqlite':
                     sqlite_apply_types(query, [None, 'NUMERIC'])
                 cursor.execute(*query)
diff -r 65be67251d10 -r 9895805863b9 modules/account/party.py
--- a/modules/account/party.py  Fri Dec 26 10:58:30 2025 +0100
+++ b/modules/account/party.py  Fri Dec 26 10:57:44 2025 +0100
@@ -16,7 +16,7 @@
 from trytond.modules.party.exceptions import EraseError
 from trytond.pool import Pool, PoolMeta
 from trytond.pyson import Eval, If
-from trytond.tools import grouped_slice, reduce_ids, sqlite_apply_types
+from trytond.tools import grouped_slice, sqlite_apply_types
 from trytond.tools import timezone as tz
 from trytond.transaction import Transaction
 
@@ -165,7 +165,8 @@
             columns.append(Coalesce(expressions[name], Decimal()).as_(name))
 
         if parties is not None:
-            party_where = reduce_ids(move_line.party, [p.id for p in parties])
+            party_where = fields.SQL_OPERATORS['in'](
+                move_line.party, [p.id for p in parties])
         else:
             party_where = Literal(True)
 
diff -r 65be67251d10 -r 9895805863b9 modules/account_budget/account.py
--- a/modules/account_budget/account.py Fri Dec 26 10:58:30 2025 +0100
+++ b/modules/account_budget/account.py Fri Dec 26 10:57:44 2025 +0100
@@ -17,7 +17,7 @@
 from trytond.pool import Pool
 from trytond.pyson import Bool, Eval, If
 from trytond.rpc import RPC
-from trytond.tools import grouped_slice, reduce_ids, sqlite_apply_types
+from trytond.tools import grouped_slice, sqlite_apply_types
 from trytond.transaction import Transaction
 from trytond.wizard import (
     Button, StateAction, StateTransition, StateView, Wizard)
@@ -213,7 +213,7 @@
             sqlite_apply_types(query, [None, 'NUMERIC'])
 
         for sub_ids in grouped_slice(ids):
-            query.where = reduce_ids(table.id, sub_ids)
+            query.where = fields.SQL_OPERATORS['in'](table.id, sub_ids)
             cursor.execute(*query)
             amounts.update(cursor)
 
@@ -549,7 +549,7 @@
         line = Line.__table__()
 
         amount = Sum(Coalesce(line.credit, 0) - Coalesce(line.debit, 0))
-        red_sql = reduce_ids(table.id, [r.id for r in records])
+        red_sql = fields.SQL_OPERATORS['in'](table.id, [r.id for r in records])
         periods = Transaction().context.get('periods')
         if not periods:
             periods = [p.id for p in Period.search([
@@ -693,7 +693,7 @@
         line = MoveLine.__table__()
 
         amount = Sum(Coalesce(line.credit, 0) - Coalesce(line.debit, 0))
-        red_sql = reduce_ids(table.id, [r.id for r in records])
+        red_sql = fields.SQL_OPERATORS['in'](table.id, [r.id for r in records])
         periods = Transaction().context.get('periods')
         if not periods:
             periods = [p.id for p in Period.search([
@@ -737,7 +737,8 @@
         for sub_ids in grouped_slice({p.budget_line.id for p in periods}):
             cursor.execute(*table.select(
                     table.budget_line,
-                    where=reduce_ids(table.budget_line, sub_ids),
+                    where=fields.SQL_OPERATORS['in'](
+                        table.budget_line, sub_ids),
                     group_by=table.budget_line,
                     having=Sum(table.ratio) > 1,
                     limit=1))
diff -r 65be67251d10 -r 9895805863b9 modules/account_deposit/party.py
--- a/modules/account_deposit/party.py  Fri Dec 26 10:58:30 2025 +0100
+++ b/modules/account_deposit/party.py  Fri Dec 26 10:57:44 2025 +0100
@@ -12,7 +12,7 @@
 from trytond.modules.currency.fields import Monetary
 from trytond.modules.party.exceptions import EraseError
 from trytond.pool import Pool, PoolMeta
-from trytond.tools import grouped_slice, reduce_ids, sqlite_apply_types
+from trytond.tools import grouped_slice, sqlite_apply_types
 from trytond.transaction import Transaction
 
 
@@ -46,7 +46,8 @@
         line_clause, _ = MoveLine.query_get(line)
 
         for sub_parties in grouped_slice(parties):
-            party_clause = reduce_ids(line.party, [p.id for p in sub_parties])
+            party_clause = fields.SQL_OPERATORS['in'](
+                line.party, [p.id for p in sub_parties])
             query = (line
                 .join(account, condition=account.id == line.account)
                 .join(account_type, condition=account.type == account_type.id)
diff -r 65be67251d10 -r 9895805863b9 modules/account_invoice/invoice.py
--- a/modules/account_invoice/invoice.py        Fri Dec 26 10:58:30 2025 +0100
+++ b/modules/account_invoice/invoice.py        Fri Dec 26 10:57:44 2025 +0100
@@ -32,8 +32,7 @@
 from trytond.report import Report
 from trytond.rpc import RPC
 from trytond.tools import (
-    cached_property, firstline, grouped_slice, reduce_ids, slugify,
-    sqlite_apply_types)
+    cached_property, firstline, grouped_slice, slugify, sqlite_apply_types)
 from trytond.transaction import Transaction
 from trytond.wizard import (
     Button, StateAction, StateReport, StateTransition, StateView, Wizard)
@@ -829,8 +828,9 @@
 
         type_name = cls.tax_amount._field.sql_type().base
         tax = InvoiceTax.__table__()
-        for sub_ids in grouped_slice(invoices_no_cache):
-            red_sql = reduce_ids(tax.invoice, sub_ids)
+        for sub_invoices in grouped_slice(invoices_no_cache):
+            red_sql = fields.SQL_OPERATORS['in'](
+                tax.invoice, map(int, sub_invoices))
             query = (tax.select(tax.invoice,
                     Coalesce(Sum(tax.amount), 0).as_(type_name),
                     where=red_sql,
@@ -885,7 +885,8 @@
         invoice = cls.__table__()
         additional_move = AdditionalMove.__table__()
 
-        red_sql = reduce_ids(invoice.id, invoices)
+        red_sql = fields.SQL_OPERATORS['in'](
+            invoice.id, map(int, invoices))
         query = (invoice
             .join(line,
                 condition=((invoice.move == line.move)
@@ -1152,7 +1153,7 @@
         for sub_invoices in grouped_slice(invoices):
             sub_ids = map(int, sub_invoices)
             cursor.execute(*table.select(table.id, has_cache,
-                    where=reduce_ids(table.id, sub_ids)))
+                    where=fields.SQL_OPERATORS['in'](table.id, sub_ids)))
             result.update(cursor)
         return result
 
diff -r 65be67251d10 -r 9895805863b9 modules/account_invoice_history/account.py
--- a/modules/account_invoice_history/account.py        Fri Dec 26 10:58:30 
2025 +0100
+++ b/modules/account_invoice_history/account.py        Fri Dec 26 10:57:44 
2025 +0100
@@ -7,7 +7,7 @@
 from trytond import backend
 from trytond.model import ModelView, Workflow, fields
 from trytond.pool import Pool, PoolMeta
-from trytond.tools import grouped_slice, reduce_ids, sqlite_apply_types
+from trytond.tools import grouped_slice, sqlite_apply_types
 from trytond.transaction import Transaction
 
 
@@ -55,7 +55,7 @@
                     Greatest(table.numbered_at, party.create_date,
                         address.create_date, identifier.create_date,
                         payment_term.create_date).as_('history_datetime'),
-                    where=reduce_ids(table.id, ids)
+                    where=fields.SQL_OPERATORS['in'](table.id, ids)
                     & (table.numbered_at != Null)
                     & (table.state.in_(cls._history_states()))))
             if backend.name == 'sqlite':
diff -r 65be67251d10 -r 9895805863b9 modules/account_payment/account.py
--- a/modules/account_payment/account.py        Fri Dec 26 10:58:30 2025 +0100
+++ b/modules/account_payment/account.py        Fri Dec 26 10:57:44 2025 +0100
@@ -19,7 +19,7 @@
 from trytond.modules.currency.fields import Monetary
 from trytond.pool import Pool, PoolMeta
 from trytond.pyson import Bool, Eval, Id, If
-from trytond.tools import grouped_slice, reduce_ids
+from trytond.tools import grouped_slice
 from trytond.transaction import Transaction, check_access, without_check_access
 from trytond.wizard import (
     Button, StateAction, StateTransition, StateView, Wizard)
@@ -195,7 +195,8 @@
             for sub_lines in grouped_slice(lines):
                 query.where = (
                     table.account.in_(accounts)
-                    & reduce_ids(table.id, map(int, sub_lines)))
+                    & fields.SQL_OPERATORS['in'](
+                        table.id, map(int, sub_lines)))
                 cursor.execute(*query)
         else:
             cursor.execute(*query)
diff -r 65be67251d10 -r 9895805863b9 modules/account_payment/payment.py
--- a/modules/account_payment/payment.py        Fri Dec 26 10:58:30 2025 +0100
+++ b/modules/account_payment/payment.py        Fri Dec 26 10:57:44 2025 +0100
@@ -22,8 +22,7 @@
 from trytond.pyson import Eval, If
 from trytond.rpc import RPC
 from trytond.tools import (
-    cursor_dict, grouped_slice, reduce_ids, sortable_values,
-    sqlite_apply_types)
+    cursor_dict, grouped_slice, sortable_values, sqlite_apply_types)
 from trytond.transaction import Transaction
 from trytond.wizard import StateAction, Wizard
 
@@ -202,9 +201,10 @@
                 ).as_('payment_not_complete'),
             ]
 
-        for sub_ids in grouped_slice(groups):
+        for sub_groups in grouped_slice(groups):
             query = payment.select(*columns,
-                where=reduce_ids(payment.group, sub_ids),
+                where=fields.SQL_OPERATORS['in'](
+                    payment.group, map(int, sub_groups)),
                 group_by=payment.group)
             if backend.name == 'sqlite':
                 sqlite_apply_types(
diff -r 65be67251d10 -r 9895805863b9 modules/account_payment_clearing/payment.py
--- a/modules/account_payment_clearing/payment.py       Fri Dec 26 10:58:30 
2025 +0100
+++ b/modules/account_payment_clearing/payment.py       Fri Dec 26 10:57:44 
2025 +0100
@@ -13,7 +13,7 @@
 from trytond.modules.account.exceptions import AccountMissing
 from trytond.pool import Pool, PoolMeta
 from trytond.pyson import Bool, Eval, If, TimeDelta
-from trytond.tools import grouped_slice, reduce_ids
+from trytond.tools import grouped_slice
 from trytond.transaction import Transaction
 from trytond.wizard import Button, StateTransition, StateView, Wizard
 
@@ -447,7 +447,8 @@
         for sub_groups in grouped_slice(groups):
             cursor.execute(*payment.select(
                     payment.group, column,
-                    where=reduce_ids(payment.group, sub_groups),
+                    where=fields.SQL_OPERATORS['in'](
+                        payment.group, map(int, sub_groups)),
                     group_by=payment.group))
             result.update(cursor)
         return result
diff -r 65be67251d10 -r 9895805863b9 modules/account_payment_sepa/payment.py
--- a/modules/account_payment_sepa/payment.py   Fri Dec 26 10:58:30 2025 +0100
+++ b/modules/account_payment_sepa/payment.py   Fri Dec 26 10:57:44 2025 +0100
@@ -26,7 +26,7 @@
 from trytond.pyson import Eval, If
 from trytond.report import Report
 from trytond.tools import (
-    cached_property, grouped_slice, is_full_text, lstrip_wildcard, reduce_ids,
+    cached_property, grouped_slice, is_full_text, lstrip_wildcard,
     sortable_values)
 from trytond.transaction import Transaction
 
@@ -786,8 +786,9 @@
         cursor = Transaction().connection.cursor()
 
         has_payments = dict.fromkeys([m.id for m in mandates], False)
-        for sub_ids in grouped_slice(mandates):
-            red_sql = reduce_ids(payment.sepa_mandate, sub_ids)
+        for sub_mandates in grouped_slice(mandates):
+            red_sql = fields.SQL_OPERATORS['in'](
+                payment.sepa_mandate, map(int, sub_mandates))
             cursor.execute(*payment.select(payment.sepa_mandate, Literal(True),
                     where=red_sql,
                     group_by=payment.sepa_mandate))
@@ -803,8 +804,9 @@
         cursor = Transaction().connection.cursor()
 
         is_first = dict.fromkeys([m.id for m in mandates], True)
-        for sub_ids in grouped_slice(mandates):
-            red_sql = reduce_ids(payment.sepa_mandate, sub_ids)
+        for sub_mandates in grouped_slice(mandates):
+            red_sql = fields.SQL_OPERATORS['in'](
+                payment.sepa_mandate, map(int, sub_mandates))
             cursor.execute(*payment.select(
                     payment.sepa_mandate, Literal(False),
                     where=red_sql
diff -r 65be67251d10 -r 9895805863b9 modules/account_statement/journal.py
--- a/modules/account_statement/journal.py      Fri Dec 26 10:58:30 2025 +0100
+++ b/modules/account_statement/journal.py      Fri Dec 26 10:57:44 2025 +0100
@@ -10,8 +10,7 @@
 from trytond.pool import Pool
 from trytond.pyson import Eval
 from trytond.rpc import RPC
-from trytond.tools import (
-    cursor_dict, grouped_slice, reduce_ids, sqlite_apply_types)
+from trytond.tools import cursor_dict, grouped_slice, sqlite_apply_types
 from trytond.transaction import Transaction
 
 
@@ -145,9 +144,10 @@
                 columns.append(
                     FirstValue(statement.date, window=w).as_('last_date'))
             query = statement.select(*columns,
-                    distinct=True,
-                    where=reduce_ids(statement.journal, sub_ids)
-                    & (statement.state != 'cancelled'))
+                distinct=True,
+                where=(fields.SQL_OPERATORS['in'](
+                        statement.journal, sub_ids)
+                    & (statement.state != 'cancelled')))
             if backend.name == 'sqlite':
                 types = [None]
                 if 'last_amount' in names:
diff -r 65be67251d10 -r 9895805863b9 modules/analytic_budget/analytic_account.py
--- a/modules/analytic_budget/analytic_account.py       Fri Dec 26 10:58:30 
2025 +0100
+++ b/modules/analytic_budget/analytic_account.py       Fri Dec 26 10:57:44 
2025 +0100
@@ -8,7 +8,6 @@
     BudgetLineMixin, BudgetMixin, CopyBudgetMixin, CopyBudgetStartMixin)
 from trytond.pool import Pool
 from trytond.pyson import Eval
-from trytond.tools import reduce_ids
 from trytond.transaction import Transaction
 from trytond.wizard import Button, StateAction, StateView, Wizard
 
@@ -179,7 +178,7 @@
         children = cls.__table__()
 
         balance = Sum(Coalesce(line.credit, 0) - Coalesce(line.debit, 0))
-        red_sql = reduce_ids(table.id, [r.id for r in records])
+        red_sql = fields.SQL_OPERATORS['in'](table.id, [r.id for r in records])
         with Transaction().set_context(context):
             query_where = Line.query_get(line)
         return (table
diff -r 65be67251d10 -r 9895805863b9 modules/commission/commission.py
--- a/modules/commission/commission.py  Fri Dec 26 10:58:30 2025 +0100
+++ b/modules/commission/commission.py  Fri Dec 26 10:57:44 2025 +0100
@@ -21,8 +21,7 @@
 from trytond.modules.product import price_digits, round_price
 from trytond.pool import Pool
 from trytond.pyson import Bool, Eval, Id, If
-from trytond.tools import (
-    decistmt, grouped_slice, reduce_ids, sqlite_apply_types)
+from trytond.tools import decistmt, grouped_slice, sqlite_apply_types
 from trytond.transaction import Transaction, check_access
 from trytond.wizard import Button, StateAction, StateView, Wizard
 
@@ -129,7 +128,7 @@
         ids = [a.id for a in agents]
         amounts = dict.fromkeys(ids, None)
         for sub_ids in grouped_slice(ids):
-            where = reduce_ids(commission.agent, sub_ids)
+            where = fields.SQL_OPERATORS['in'](commission.agent, sub_ids)
             where &= commission.invoice_line == Null
             query = commission.select(
                 commission.agent, Sum(commission.amount).as_('pending_amount'),
diff -r 65be67251d10 -r 9895805863b9 
modules/marketing_automation/marketing_automation.py
--- a/modules/marketing_automation/marketing_automation.py      Fri Dec 26 
10:58:30 2025 +0100
+++ b/modules/marketing_automation/marketing_automation.py      Fri Dec 26 
10:57:44 2025 +0100
@@ -26,7 +26,7 @@
 from trytond.pyson import Eval, If, PYSONDecoder, TimeDelta
 from trytond.report import Report, html_to_text, mjml_to_html
 from trytond.sendmail import SMTPDataManager, send_message_transactional
-from trytond.tools import grouped_slice, pairwise_longest, reduce_ids
+from trytond.tools import grouped_slice, pairwise_longest
 from trytond.tools.chart import sparkline
 from trytond.tools.email_ import format_address, has_rcpt, set_from_header
 from trytond.transaction import Transaction
@@ -178,12 +178,13 @@
                 others.append(scenario)
 
         count = {name: defaultdict(int) for name in names}
-        for sub in grouped_slice(others):
+        for sub_others in grouped_slice(others):
             cursor.execute(*record.select(
                     record.scenario,
                     Count(),
                     Count(filter_=record.blocked),
-                    where=reduce_ids(record.scenario, sub),
+                    where=fields.SQL_OPERATORS['in'](
+                        record.scenario, map(int, sub_others)),
                     group_by=record.scenario))
             for id_, all_, blocked in cursor:
                 if 'record_count' in count:
@@ -521,13 +522,14 @@
         cursor = Transaction().connection.cursor()
 
         count = {name: defaultdict(int) for name in names}
-        for sub in grouped_slice(activities):
+        for sub_activities in grouped_slice(activities):
             cursor.execute(*record_activity.select(
                     record_activity.activity,
                     Count(filter_=record_activity.state == 'done'),
                     Count(filter_=record_activity.email_opened),
                     Count(filter_=record_activity.email_clicked),
-                    where=reduce_ids(record_activity.activity, sub),
+                    where=fields.SQL_OPERATORS['in'](
+                        record_activity.activity, map(int, sub_activities)),
                     group_by=record_activity.activity))
             for id_, all_, email_opened, email_clicked in cursor:
                 if 'record_count' in count:
diff -r 65be67251d10 -r 9895805863b9 modules/marketing_email/marketing.py
--- a/modules/marketing_email/marketing.py      Fri Dec 26 10:58:30 2025 +0100
+++ b/modules/marketing_email/marketing.py      Fri Dec 26 10:57:44 2025 +0100
@@ -22,7 +22,7 @@
 from trytond.pyson import Eval
 from trytond.report import Report, get_email, html_to_text, mjml_to_html
 from trytond.sendmail import SMTPDataManager, send_message_transactional
-from trytond.tools import grouped_slice, reduce_ids
+from trytond.tools import grouped_slice
 from trytond.tools.email_ import (
     EmailNotValidError, format_address, normalize_email, set_from_header,
     validate_email)
@@ -259,7 +259,8 @@
             email.list_, Count(), group_by=[email.list_])
         for sub_lists in grouped_slice(lists):
             query.where = (
-                reduce_ids(email.list_, sub_lists)
+                fields.SQL_OPERATORS['in'](
+                    email.list_, map(int, sub_lists))
                 & email.active)
             cursor.execute(*query)
             subscribed.update(cursor)
diff -r 65be67251d10 -r 9895805863b9 modules/product_price_list_cache/product.py
--- a/modules/product_price_list_cache/product.py       Fri Dec 26 10:58:30 
2025 +0100
+++ b/modules/product_price_list_cache/product.py       Fri Dec 26 10:57:44 
2025 +0100
@@ -10,7 +10,7 @@
 from trytond.model import ModelSQL, dualmethod, fields
 from trytond.pool import Pool, PoolMeta
 from trytond.protocols.jsonrpc import JSONDecoder, JSONEncoder
-from trytond.tools import grouped_slice, reduce_ids
+from trytond.tools import grouped_slice
 from trytond.transaction import Transaction
 
 dumps = partial(
@@ -164,20 +164,21 @@
             cursor.execute(*cache.delete())
         elif price_lists and products is None:
             for sub_price_lists in grouped_slice(price_lists):
-                cursor.execute(*cache.delete(where=reduce_ids(
+                cursor.execute(*cache.delete(where=fields.SQL_OPERATORS['in'](
                             cache.price_list, [
                                 p.id for p in sub_price_lists])))
         elif price_lists is None and products:
             for sub_products in grouped_slice(products):
-                cursor.execute(*cache.delete(where=reduce_ids(
+                cursor.execute(*cache.delete(where=fields.SQL_OPERATORS['in'](
                             cache.product, [p.id for p in sub_products])))
         else:
             for sub_products in grouped_slice(products):
                 for sub_price_lists in grouped_slice(price_lists):
-                    cursor.execute(*cache.delete(where=reduce_ids(
-                            cache.price_list, [
-                                p.id for p in sub_price_lists])
-                            & reduce_ids(
+                    cursor.execute(*cache.delete(
+                            where=fields.SQL_OPERATORS['in'](
+                                cache.price_list, [
+                                    p.id for p in sub_price_lists])
+                            & fields.SQL_OPERATORS['in'](
                                 cache.product, [p.id for p in sub_products])))
 
     @classmethod
diff -r 65be67251d10 -r 9895805863b9 modules/production_work/work.py
--- a/modules/production_work/work.py   Fri Dec 26 10:58:30 2025 +0100
+++ b/modules/production_work/work.py   Fri Dec 26 10:57:44 2025 +0100
@@ -19,7 +19,7 @@
 from trytond.modules.product import price_digits, round_price
 from trytond.pool import Pool
 from trytond.pyson import Bool, Eval, If, TimeDelta
-from trytond.tools import grouped_slice, reduce_ids, sqlite_apply_types
+from trytond.tools import grouped_slice, sqlite_apply_types
 from trytond.transaction import Transaction
 
 from .exceptions import PickerError
@@ -250,7 +250,8 @@
         costs = defaultdict(Decimal)
 
         for sub_works in grouped_slice(works):
-            red_sql = reduce_ids(cycle.work, [w.id for w in sub_works])
+            red_sql = fields.SQL_OPERATORS['in'](
+                cycle.work, [w.id for w in sub_works])
             query = cycle.select(
                 cycle.work, Sum(Coalesce(cycle.cost, 0)).as_('cost'),
                 where=red_sql & (cycle.state == 'done'),
diff -r 65be67251d10 -r 9895805863b9 modules/project_invoice/invoice.py
--- a/modules/project_invoice/invoice.py        Fri Dec 26 10:58:30 2025 +0100
+++ b/modules/project_invoice/invoice.py        Fri Dec 26 10:57:44 2025 +0100
@@ -10,7 +10,7 @@
 from trytond.modules.account_invoice.exceptions import (
     InvoiceLineValidationError)
 from trytond.pool import Pool, PoolMeta
-from trytond.tools import grouped_slice, reduce_ids
+from trytond.tools import grouped_slice
 from trytond.transaction import Transaction
 
 
@@ -92,7 +92,7 @@
             ts_line.invoice_line, Sum(ts_line.duration),
             group_by=ts_line.invoice_line)
         for sub_lines in grouped_slice(lines):
-            query.where = reduce_ids(
+            query.where = fields.SQL_OPERATORS['in'](
                 ts_line.invoice_line, map(int, sub_lines))
             cursor.execute(*query)
 
diff -r 65be67251d10 -r 9895805863b9 modules/project_invoice/project.py
--- a/modules/project_invoice/project.py        Fri Dec 26 10:58:30 2025 +0100
+++ b/modules/project_invoice/project.py        Fri Dec 26 10:57:44 2025 +0100
@@ -18,7 +18,7 @@
 from trytond.modules.currency.fields import Monetary
 from trytond.pool import Pool, PoolMeta
 from trytond.pyson import Bool, Eval, Id, If
-from trytond.tools import grouped_slice, reduce_ids, sqlite_apply_types
+from trytond.tools import grouped_slice, sqlite_apply_types
 from trytond.transaction import Transaction
 from trytond.wizard import StateAction, Wizard
 
@@ -114,7 +114,7 @@
         quantities = {}
         for sub_works in grouped_slice(works):
             sub_works = list(sub_works)
-            where = reduce_ids(
+            where = fields.SQL_OPERATORS['in'](
                 table.id, [x.id for x in sub_works if x.invoice_unit_price])
             cursor.execute(*table.join(progress,
                     condition=progress.work == table.id
@@ -162,7 +162,7 @@
         work2currency = {}
         ids2work = dict((w.id, w) for w in works)
         for sub_ids in grouped_slice(ids2work.keys()):
-            where = reduce_ids(table.id, sub_ids)
+            where = fields.SQL_OPERATORS['in'](table.id, sub_ids)
             query = (table.join(progress,
                     condition=progress.work == table.id
                     ).join(invoice_line,
@@ -275,7 +275,8 @@
             group_by=line.work)
         for upto, tworks in upto2tworks.items():
             for sub_ids in grouped_slice(tworks):
-                query.where = (reduce_ids(line.work, sub_ids)
+                query.where = (
+                    fields.SQL_OPERATORS['in'](line.work, sub_ids)
                     & (line.invoice_line == Null))
                 if upto:
                     query.where &= (line.date <= upto)
@@ -318,7 +319,7 @@
         work2currency = {}
         work_ids = [w.id for w in works]
         for sub_ids in grouped_slice(work_ids):
-            where = reduce_ids(table.id, sub_ids)
+            where = fields.SQL_OPERATORS['in'](table.id, sub_ids)
             cursor.execute(*table.join(timesheet_work,
                     condition=(
                         Concat(cls.__name__ + ',', table.id)
diff -r 65be67251d10 -r 9895805863b9 modules/project_revenue/work.py
--- a/modules/project_revenue/work.py   Fri Dec 26 10:58:30 2025 +0100
+++ b/modules/project_revenue/work.py   Fri Dec 26 10:57:44 2025 +0100
@@ -13,7 +13,7 @@
 from trytond.modules.product import price_digits, round_price
 from trytond.pool import Pool, PoolMeta
 from trytond.pyson import Eval
-from trytond.tools import grouped_slice, reduce_ids
+from trytond.tools import grouped_slice
 from trytond.transaction import Transaction
 
 
@@ -95,13 +95,13 @@
 
         work_ids = [w.id for w in works]
         for sub_ids in grouped_slice(work_ids):
-            red_sql = reduce_ids(table.id, sub_ids)
+            where = fields.SQL_OPERATORS['in'](table.id, sub_ids)
             cursor.execute(*table.join(work,
                     condition=(
                         Concat(cls.__name__ + ',', table.id) == work.origin)
                     ).join(line, condition=line.work == work.id
                     ).select(table.id, Sum(line.cost_price * line.duration),
-                    where=red_sql,
+                    where=where,
                     group_by=[table.id]))
             for work_id, cost in cursor:
                 # SQLite stores timedelta as float
@@ -135,7 +135,7 @@
         work2currency = {}
         iline2work = {}
         for sub_ids in grouped_slice(work_ids):
-            where = reduce_ids(table.id, sub_ids)
+            where = fields.SQL_OPERATORS['in'](table.id, sub_ids)
             cursor.execute(*table.join(purchase_line,
                     condition=purchase_line.work == table.id
                     ).join(invoice_line,
diff -r 65be67251d10 -r 9895805863b9 modules/purchase/product.py
--- a/modules/purchase/product.py       Fri Dec 26 10:58:30 2025 +0100
+++ b/modules/purchase/product.py       Fri Dec 26 10:57:44 2025 +0100
@@ -13,8 +13,7 @@
     ProductDeactivatableMixin, price_digits, round_price)
 from trytond.pool import Pool, PoolMeta
 from trytond.pyson import Bool, Eval, If, TimeDelta
-from trytond.tools import (
-    grouped_slice, is_full_text, lstrip_wildcard, reduce_ids)
+from trytond.tools import grouped_slice, is_full_text, lstrip_wildcard
 from trytond.transaction import Transaction
 
 from .exceptions import PurchaseUOMWarning
@@ -160,7 +159,7 @@
                 .join(purchase, condition=line.purchase == purchase.id)
                 .select(
                     Max(line.id),
-                    where=where & reduce_ids(
+                    where=where & fields.SQL_OPERATORS['in'](
                         line.product, map(int, sub_products)),
                     group_by=[line.product]))
             cursor.execute(*query)
diff -r 65be67251d10 -r 9895805863b9 modules/purchase_history/purchase.py
--- a/modules/purchase_history/purchase.py      Fri Dec 26 10:58:30 2025 +0100
+++ b/modules/purchase_history/purchase.py      Fri Dec 26 10:57:44 2025 +0100
@@ -4,7 +4,7 @@
 from trytond.model import ModelView, Workflow, fields
 from trytond.pool import PoolMeta
 from trytond.pyson import Eval
-from trytond.tools import grouped_slice, reduce_ids
+from trytond.tools import grouped_slice
 from trytond.transaction import Transaction
 
 
@@ -49,7 +49,8 @@
             cursor.execute(*table.update(
                     [table.revision],
                     [table.revision + 1],
-                    where=reduce_ids(table.id, sub_purchases)))
+                    where=fields.SQL_OPERATORS['in'](
+                        table.id, map(int, sub_purchases))))
 
         super().draft(purchases)
 
diff -r 65be67251d10 -r 9895805863b9 modules/sale/sale_reporting.py
--- a/modules/sale/sale_reporting.py    Fri Dec 26 10:58:30 2025 +0100
+++ b/modules/sale/sale_reporting.py    Fri Dec 26 10:57:44 2025 +0100
@@ -15,7 +15,7 @@
 from trytond.modules.currency.fields import Monetary
 from trytond.pool import Pool
 from trytond.pyson import Eval, If
-from trytond.tools import grouped_slice, pairwise_longest, reduce_ids
+from trytond.tools import grouped_slice, pairwise_longest
 from trytond.tools.chart import sparkline
 from trytond.transaction import Transaction
 from trytond.wizard import StateAction, StateTransition, Wizard
@@ -517,7 +517,8 @@
         reporting_customer_categories = []
         for sub_ids in grouped_slice(ids):
             sub_ids = list(sub_ids)
-            where = reduce_ids(reporting_product_category.id, sub_ids)
+            where = fields.SQL_OPERATORS['in'](
+                reporting_product_category.id, sub_ids)
             cursor.execute(
                 *reporting_product_category.select(
                     reporting_product_category.id, where=where))
@@ -758,7 +759,8 @@
         reporting_product_categories = []
         for sub_ids in grouped_slice(ids):
             sub_ids = list(sub_ids)
-            where = reduce_ids(reporting_product_category.id, sub_ids)
+            where = fields.SQL_OPERATORS['in'](
+                reporting_product_category.id, sub_ids)
             cursor.execute(
                 *reporting_product_category.select(
                     reporting_product_category.id, where=where))
diff -r 65be67251d10 -r 9895805863b9 modules/sale_history/sale.py
--- a/modules/sale_history/sale.py      Fri Dec 26 10:58:30 2025 +0100
+++ b/modules/sale_history/sale.py      Fri Dec 26 10:57:44 2025 +0100
@@ -4,7 +4,7 @@
 from trytond.model import ModelView, Workflow, fields
 from trytond.pool import PoolMeta
 from trytond.pyson import Eval
-from trytond.tools import grouped_slice, reduce_ids
+from trytond.tools import grouped_slice
 from trytond.transaction import Transaction
 
 
@@ -45,7 +45,8 @@
             cursor.execute(*table.update(
                     [table.revision],
                     [table.revision + 1],
-                    where=reduce_ids(table.id, sub_records)))
+                    where=fields.SQL_OPERATORS['in'](
+                        table.id, map(int, sub_records))))
 
         super().draft(records)
 
diff -r 65be67251d10 -r 9895805863b9 modules/sale_point/sale.py
--- a/modules/sale_point/sale.py        Fri Dec 26 10:58:30 2025 +0100
+++ b/modules/sale_point/sale.py        Fri Dec 26 10:57:44 2025 +0100
@@ -21,7 +21,7 @@
 from trytond.modules.product import price_digits, round_price
 from trytond.pool import Pool
 from trytond.pyson import Bool, Eval, Id, If
-from trytond.tools import grouped_slice, reduce_ids, sqlite_apply_types
+from trytond.tools import grouped_slice, sqlite_apply_types
 from trytond.transaction import Transaction
 from trytond.wizard import Button, StateTransition, StateView, Wizard
 
@@ -865,12 +865,14 @@
                 payment.select(
                     payment.session.as_('session'),
                     Sum(payment.amount).as_('amount'),
-                    where=reduce_ids(payment.session, sub_ids),
+                    where=fields.SQL_OPERATORS['in'](
+                        payment.session, sub_ids),
                     group_by=[payment.session]),
                 transfer.select(
                     transfer.session.as_('session'),
                     Sum(transfer.amount).as_('amount'),
-                    where=reduce_ids(transfer.session, sub_ids),
+                    where=fields.SQL_OPERATORS['in'](
+                        transfer.session, sub_ids),
                     group_by=[transfer.session]),
                 all_=True)
             query = query.select(
diff -r 65be67251d10 -r 9895805863b9 modules/sale_promotion_coupon/sale.py
--- a/modules/sale_promotion_coupon/sale.py     Fri Dec 26 10:58:30 2025 +0100
+++ b/modules/sale_promotion_coupon/sale.py     Fri Dec 26 10:57:44 2025 +0100
@@ -15,7 +15,7 @@
 from trytond.pyson import Eval, If
 from trytond.sql.functions import DateRange
 from trytond.sql.operators import RangeOverlap
-from trytond.tools import grouped_slice, reduce_ids
+from trytond.tools import grouped_slice
 from trytond.transaction import Transaction
 
 from .exceptions import PromotionCouponNumberDatesError
@@ -302,7 +302,8 @@
 
         result = {}
         for sub_numbers in grouped_slice(numbers):
-            query.where = reduce_ids(table.id, map(int, sub_numbers))
+            query.where = fields.SQL_OPERATORS['in'](
+                table.id, map(int, sub_numbers))
             cursor.execute(*query)
             result.update(dict(cursor))
         return result
diff -r 65be67251d10 -r 9895805863b9 modules/stock/move.py
--- a/modules/stock/move.py     Fri Dec 26 10:58:30 2025 +0100
+++ b/modules/stock/move.py     Fri Dec 26 10:57:44 2025 +0100
@@ -19,7 +19,7 @@
 from trytond.modules.product import price_digits, round_price
 from trytond.pool import Pool
 from trytond.pyson import Bool, Eval, Id, If, TimeDelta
-from trytond.tools import cached_property, grouped_slice, reduce_ids
+from trytond.tools import cached_property, grouped_slice
 from trytond.transaction import Transaction, without_check_access
 
 from .exceptions import MoveFutureWarning, MoveOriginWarning
@@ -1331,9 +1331,9 @@
                         sub_location_ids = list(sub_location_ids)
                         table = cls.__table__()
                         query = table.select(Literal(1),
-                            where=(reduce_ids(
+                            where=(fields.SQL_OPERATORS['in'](
                                     table.to_location, sub_location_ids)
-                                | reduce_ids(
+                                | fields.SQL_OPERATORS['in'](
                                     table.from_location, sub_location_ids))
                             & table.product.in_(product_ids)
                             & (table.company == company_id),
@@ -1635,9 +1635,10 @@
                 if PeriodCache:
                     cache_column = Column(period_cache, fieldname)
                 if isinstance(grouping_ids[0], (int, float, Decimal)):
-                    where &= reduce_ids(column, grouping_ids)
+                    where &= fields.SQL_OPERATORS['in'](column, grouping_ids)
                     if PeriodCache:
-                        where_period &= reduce_ids(cache_column, grouping_ids)
+                        where_period &= fields.SQL_OPERATORS['in'](
+                            cache_column, grouping_ids)
                 else:
                     where &= column.in_(grouping_ids)
                     if PeriodCache:
diff -r 65be67251d10 -r 9895805863b9 modules/stock/product.py
--- a/modules/stock/product.py  Fri Dec 26 10:58:30 2025 +0100
+++ b/modules/stock/product.py  Fri Dec 26 10:57:44 2025 +0100
@@ -17,7 +17,7 @@
 from trytond.modules.product import price_digits, round_price
 from trytond.pool import Pool, PoolMeta
 from trytond.pyson import Bool, Eval, If, PYSONEncoder
-from trytond.tools import decistmt, grouped_slice, reduce_ids
+from trytond.tools import decistmt, grouped_slice
 from trytond.tools import timezone as tz
 from trytond.transaction import Transaction, without_check_access
 from trytond.wizard import (
@@ -679,7 +679,8 @@
                     raise AccessError(gettext(
                             'stock.msg_product_quantities_max',
                             max=transaction.database.IN_MAX))
-                product_clause = reduce_ids(product.template, product_template)
+                product_clause = fields.SQL_OPERATORS['in'](
+                    product.template, product_template)
             else:
                 product_clause = product.template == Null
             product_column = Concat('product.template,', product.template)
@@ -695,7 +696,8 @@
                     raise AccessError(gettext(
                             'stock.msg_product_quantities_max',
                             max=transaction.database.IN_MAX))
-                product_clause = reduce_ids(move.product, product)
+                product_clause = fields.SQL_OPERATORS['in'](
+                    move.product, product)
             else:
                 product_clause = move.product == Null
             product_column = Concat('product.product,', move.product)
@@ -945,7 +947,8 @@
                     raise AccessError(gettext(
                             'stock.msg_product_quantities_max',
                             max=transaction.database.IN_MAX))
-                product_clause = reduce_ids(product.template, product_template)
+                product_clause = fields.SQL_OPERATORS['in'](
+                    product.template, product_template)
             else:
                 product_clause = product.template == Null
             product_column = Concat('product.template,', product.template)
@@ -960,7 +963,8 @@
                     raise AccessError(gettext(
                             'stock.msg_product_quantities_max',
                             max=transaction.database.IN_MAX))
-                product_clause = reduce_ids(move.product, product)
+                product_clause = fields.SQL_OPERATORS['in'](
+                    move.product, product)
             else:
                 product_clause = move.product == Null
             product_column = Concat('product.product,', move.product)
diff -r 65be67251d10 -r 9895805863b9 modules/stock/stock_reporting_margin.py
--- a/modules/stock/stock_reporting_margin.py   Fri Dec 26 10:58:30 2025 +0100
+++ b/modules/stock/stock_reporting_margin.py   Fri Dec 26 10:57:44 2025 +0100
@@ -15,7 +15,7 @@
 from trytond.modules.currency.fields import Monetary
 from trytond.pool import Pool
 from trytond.pyson import Eval, If
-from trytond.tools import grouped_slice, pairwise_longest, reduce_ids
+from trytond.tools import grouped_slice, pairwise_longest
 from trytond.tools.chart import sparkline
 from trytond.transaction import Transaction
 
@@ -596,7 +596,8 @@
         reporting_categories = []
         for sub_ids in grouped_slice(ids):
             sub_ids = list(sub_ids)
-            where = reduce_ids(reporting_category.id, sub_ids)
+            where = fields.SQL_OPERATORS['in'](
+                reporting_category.id, sub_ids)
             cursor.execute(
                 *reporting_category.select(reporting_category.id, where=where))
             reporting_categories.extend(r for r, in cursor)
diff -r 65be67251d10 -r 9895805863b9 modules/stock_forecast/forecast.py
--- a/modules/stock_forecast/forecast.py        Fri Dec 26 10:58:30 2025 +0100
+++ b/modules/stock_forecast/forecast.py        Fri Dec 26 10:57:44 2025 +0100
@@ -18,7 +18,7 @@
 from trytond.pyson import Bool, Eval, If
 from trytond.sql.functions import DateRange
 from trytond.sql.operators import RangeOverlap
-from trytond.tools import grouped_slice, reduce_ids
+from trytond.tools import grouped_slice
 from trytond.transaction import Transaction
 from trytond.wizard import Button, StateTransition, StateView, Wizard
 
@@ -357,13 +357,13 @@
             product2line = dict((line.product.id, line) for line in lines)
             product_ids = product2line.keys()
             for sub_ids in grouped_slice(product_ids):
-                red_sql = reduce_ids(move.product, sub_ids)
+                where = fields.SQL_OPERATORS['in'](move.product, sub_ids)
                 cursor.execute(*move.join(location_from,
                         condition=move.from_location == location_from.id
                         ).join(location_to,
                         condition=move.to_location == location_to.id
                         ).select(move.product, Sum(move.internal_quantity),
-                        where=red_sql
+                        where=where
                         & (location_from.left >= forecast.warehouse.left)
                         & (location_from.right <= forecast.warehouse.right)
                         & (location_to.left >= forecast.destination.left)
diff -r 65be67251d10 -r 9895805863b9 modules/stock_quantity_issue/stock.py
--- a/modules/stock_quantity_issue/stock.py     Fri Dec 26 10:58:30 2025 +0100
+++ b/modules/stock_quantity_issue/stock.py     Fri Dec 26 10:57:44 2025 +0100
@@ -249,12 +249,11 @@
             issue.issue_products = []
             issues[issue.origin] = issue
 
-        # Order by id to speedup reduce_ids
         products = Product.search([
                 ('type', 'in', ['goods', 'assets']),
                 ('consumable', '=', False),
                 ],
-            order=[('id', 'ASC')])
+            order=[])
 
         for product in products:
             for warehouse in warehouses:
diff -r 65be67251d10 -r 9895805863b9 
modules/stock_shipment_measurements/stock.py
--- a/modules/stock_shipment_measurements/stock.py      Fri Dec 26 10:58:30 
2025 +0100
+++ b/modules/stock_shipment_measurements/stock.py      Fri Dec 26 10:57:44 
2025 +0100
@@ -13,7 +13,7 @@
 from trytond.modules.company.model import CompanyValueMixin
 from trytond.pool import Pool, PoolMeta
 from trytond.pyson import Bool, Eval, Id
-from trytond.tools import grouped_slice, reduce_ids
+from trytond.tools import grouped_slice
 from trytond.transaction import Transaction
 
 
@@ -285,7 +285,9 @@
             where = query.where
             for sub_shipments in grouped_slice(shipments):
                 query.where = (
-                    where & reduce_ids(table.id, map(int, sub_shipments)))
+                    where
+                    & fields.SQL_OPERATORS['in'](
+                        table.id, map(int, sub_shipments)))
                 cursor.execute(*query)
         else:
             cursor.execute(*query)
@@ -331,7 +333,7 @@
                     if s.state not in states
                     or s.internal_weight is None
                     or s.internal_volume is None)):
-            query.where = reduce_ids(
+            query.where = fields.SQL_OPERATORS['in'](
                 table.id, [s.id for s in sub_shipments])
             cursor.execute(*query)
             for id_, weight, volume in cursor:
diff -r 65be67251d10 -r 9895805863b9 modules/stock_supply/purchase_request.py
--- a/modules/stock_supply/purchase_request.py  Fri Dec 26 10:58:30 2025 +0100
+++ b/modules/stock_supply/purchase_request.py  Fri Dec 26 10:57:44 2025 +0100
@@ -65,12 +65,12 @@
 
         if products is None:
             # fetch goods and assets
-            # ordered by ids to speedup reduce_ids in products_by_location
             products = Product.search([
                     ('type', 'in', ['goods', 'assets']),
                     ('consumable', '=', False),
                     ('purchasable', '=', True),
-                    ], order=[('id', 'ASC')])
+                    ],
+                order=[])
         # aggregate product by minimum supply date
         date2products = defaultdict(list)
         for product in products:
diff -r 65be67251d10 -r 9895805863b9 modules/stock_supply/shipment.py
--- a/modules/stock_supply/shipment.py  Fri Dec 26 10:58:30 2025 +0100
+++ b/modules/stock_supply/shipment.py  Fri Dec 26 10:57:44 2025 +0100
@@ -89,11 +89,11 @@
                     location_ids, with_childs=True,
                     grouping_filter=grouping_filter)
 
-        # ordered by ids to speedup reduce_ids in products_by_location
         if implicit_locations:
             products = Product.search([
                     ('type', 'in', ['goods', 'assets']),
-                    ], order=[('id', 'ASC')])
+                    ],
+                order=[])
             product_ids = [p.id for p in products]
             pbl = get_pbl(None, today, None)
         else:
diff -r 65be67251d10 -r 9895805863b9 modules/timesheet/work.py
--- a/modules/timesheet/work.py Fri Dec 26 10:58:30 2025 +0100
+++ b/modules/timesheet/work.py Fri Dec 26 10:57:44 2025 +0100
@@ -10,7 +10,7 @@
 from trytond.model import ModelSQL, ModelStorage, ModelView, Unique, fields
 from trytond.pool import Pool
 from trytond.pyson import Bool, Eval, If
-from trytond.tools import grouped_slice, reduce_ids
+from trytond.tools import grouped_slice
 from trytond.transaction import Transaction
 
 from .exceptions import CompanyValidationError
@@ -106,9 +106,9 @@
             condition=line.work == table_w.id)
 
         for sub_ids in grouped_slice(ids):
-            red_sql = reduce_ids(table_w.id, sub_ids)
             cursor.execute(*query_table.select(table_w.id, Sum(line.duration),
-                    where=red_sql & where,
+                    where=where
+                    & fields.SQL_OPERATORS['in'](table_w.id, sub_ids),
                     group_by=table_w.id))
             for work_id, duration in cursor:
                 # SQLite uses float for SUM
diff -r 65be67251d10 -r 9895805863b9 modules/web_shortener/web.py
--- a/modules/web_shortener/web.py      Fri Dec 26 10:58:30 2025 +0100
+++ b/modules/web_shortener/web.py      Fri Dec 26 10:57:44 2025 +0100
@@ -10,7 +10,7 @@
 import trytond.config as config
 from trytond.model import ModelSQL, ModelView, fields
 from trytond.pool import Pool
-from trytond.tools import grouped_slice, reduce_ids
+from trytond.tools import grouped_slice
 from trytond.transaction import Transaction
 from trytond.url import http_host
 from trytond.wsgi import Base64Converter
@@ -56,7 +56,7 @@
         for sub_ids in grouped_slice(shortened_urls):
             cursor.execute(*access.select(
                     access.url, Count(access.id),
-                    where=reduce_ids(access.url, sub_ids),
+                    where=fields.SQL_OPERATORS['in'](access.url, sub_ids),
                     group_by=[access.url]))
             counts.update(cursor)
         return counts
diff -r 65be67251d10 -r 9895805863b9 trytond/CHANGELOG
--- a/trytond/CHANGELOG Fri Dec 26 10:58:30 2025 +0100
+++ b/trytond/CHANGELOG Fri Dec 26 10:57:44 2025 +0100
@@ -1,3 +1,4 @@
+* Deprecate ``reduce_ids`` for ``SQL_OPERATORS['in']``
 * Use array for ``in`` operators
 * Update to Psycopg 3
 * Add support for Python 3.14
diff -r 65be67251d10 -r 9895805863b9 trytond/trytond/ir/cron.py
--- a/trytond/trytond/ir/cron.py        Fri Dec 26 10:58:30 2025 +0100
+++ b/trytond/trytond/ir/cron.py        Fri Dec 26 10:57:44 2025 +0100
@@ -22,7 +22,7 @@
 from trytond.pool import Pool
 from trytond.pyson import Eval
 from trytond.status import processing
-from trytond.tools import grouped_slice, reduce_ids
+from trytond.tools import grouped_slice
 from trytond.tools import timezone as tz
 from trytond.transaction import Transaction, TransactionError
 from trytond.worker import run_task
@@ -143,7 +143,7 @@
                     ids = [c.id for c in sub_crons]
                     query = table.select(
                         table.id,
-                        where=reduce_ids(table.id, ids),
+                        where=fields.SQL_OPERATORS['in'](table.id, ids),
                         for_=For('UPDATE'))
                     cursor.execute(*query)
                     not_running = {i for i, in cursor}
diff -r 65be67251d10 -r 9895805863b9 trytond/trytond/ir/note.py
--- a/trytond/trytond/ir/note.py        Fri Dec 26 10:58:30 2025 +0100
+++ b/trytond/trytond/ir/note.py        Fri Dec 26 10:57:44 2025 +0100
@@ -9,7 +9,7 @@
 from trytond.model import ModelSQL, ModelView, fields
 from trytond.pool import Pool
 from trytond.pyson import Eval
-from trytond.tools import grouped_slice, reduce_ids
+from trytond.tools import grouped_slice
 from trytond.transaction import Transaction
 
 from .resource import ResourceMixin, resource_copy
@@ -52,7 +52,7 @@
 
         unread = {}
         for sub_ids in grouped_slice(ids):
-            where = reduce_ids(table.id, sub_ids)
+            where = fields.SQL_OPERATORS['in'](table.id, sub_ids)
             query = table.join(read, 'LEFT',
                 condition=(table.id == read.note)
                 & (read.user == user_id)
diff -r 65be67251d10 -r 9895805863b9 trytond/trytond/ir/trigger.py
--- a/trytond/trytond/ir/trigger.py     Fri Dec 26 10:58:30 2025 +0100
+++ b/trytond/trytond/ir/trigger.py     Fri Dec 26 10:57:44 2025 +0100
@@ -15,7 +15,7 @@
 from trytond.model.exceptions import ValidationError
 from trytond.pool import Pool
 from trytond.pyson import Eval, If, PYSONDecoder, TimeDelta
-from trytond.tools import grouped_slice, reduce_ids
+from trytond.tools import grouped_slice
 from trytond.transaction import Transaction
 
 
@@ -206,10 +206,10 @@
             new_ids = []
             for sub_ids in grouped_slice(ids):
                 sub_ids = list(sub_ids)
-                red_sql = reduce_ids(trigger_log.record_id, sub_ids)
+                where = fields.SQL_OPERATORS['in'](trigger_log.record_id, 
sub_ids)
                 cursor.execute(*trigger_log.select(
                         trigger_log.record_id, Count(),
-                        where=red_sql & (trigger_log.trigger == self.id),
+                        where=where & (trigger_log.trigger == self.id),
                         group_by=trigger_log.record_id))
                 number = dict(cursor)
                 for record_id in sub_ids:
@@ -231,10 +231,11 @@
                 now = datetime.datetime.fromisoformat(now)
             for sub_ids in grouped_slice(ids):
                 sub_ids = list(sub_ids)
-                red_sql = reduce_ids(trigger_log.record_id, sub_ids)
+                where = fields.SQL_OPERATORS['in'](
+                    trigger_log.record_id, sub_ids)
                 cursor.execute(*trigger_log.select(
                         trigger_log.record_id, Max(trigger_log.create_date),
-                        where=(red_sql & (trigger_log.trigger == self.id)),
+                        where=where & (trigger_log.trigger == self.id),
                         group_by=trigger_log.record_id))
                 delay = dict(cursor)
                 for record_id in sub_ids:
diff -r 65be67251d10 -r 9895805863b9 trytond/trytond/model/modelsql.py
--- a/trytond/trytond/model/modelsql.py Fri Dec 26 10:58:30 2025 +0100
+++ b/trytond/trytond/model/modelsql.py Fri Dec 26 10:57:44 2025 +0100
@@ -383,7 +383,6 @@
     @classmethod
     def __setup_indexes__(cls):
         pool = Pool()
-        # Define Range index to optimise with reduce_ids
         for field_name, field in cls._fields.items():
             Targets = []
             if isinstance(field, fields.Many2One):
diff -r 65be67251d10 -r 9895805863b9 trytond/trytond/tests/test_tools.py
--- a/trytond/trytond/tests/test_tools.py       Fri Dec 26 10:58:30 2025 +0100
+++ b/trytond/trytond/tests/test_tools.py       Fri Dec 26 10:57:44 2025 +0100
@@ -6,7 +6,6 @@
 import doctest
 import email.message
 import os
-import sys
 import unittest
 from copy import deepcopy
 from decimal import Decimal
@@ -29,7 +28,7 @@
 from trytond.tools import (
     cached_property, decimal_, email_, escape_wildcard, file_open, firstline,
     grouped_slice, is_full_text, is_instance_method, likify, lstrip_wildcard,
-    pair, pairwise_longest, reduce_domain, reduce_ids, remove_forbidden_chars,
+    pair, pairwise_longest, reduce_domain, remove_forbidden_chars,
     rstrip_wildcard, slugify, sortable_values, sqlite_apply_types,
     strip_wildcard, timezone, unescape_wildcard, unpair)
 from trytond.tools.chart import sparkline
@@ -56,51 +55,6 @@
     'Test tools'
     table = sql.Table('test')
 
-    def test_reduce_ids_empty(self):
-        'Test reduce_ids empty list'
-        self.assertEqual(reduce_ids(self.table.id, []), sql.Literal(False))
-
-    def test_reduce_ids_continue(self):
-        'Test reduce_ids continue list'
-        self.assertEqual(reduce_ids(self.table.id, list(range(10))),
-            sql.operators.Or(((self.table.id >= 0) & (self.table.id <= 9),)))
-
-    def test_reduce_ids_one_hole(self):
-        'Test reduce_ids continue list with one hole'
-        self.assertEqual(reduce_ids(
-                self.table.id, list(range(10)) + list(range(20, 30))),
-            ((self.table.id >= 0) & (self.table.id <= 9))
-            | ((self.table.id >= 20) & (self.table.id <= 29)))
-
-    def test_reduce_ids_short_continue(self):
-        'Test reduce_ids short continue list'
-        self.assertEqual(reduce_ids(self.table.id, list(range(4))),
-            sql.operators.Or((self.table.id.in_(list(range(4))),)))
-
-    def test_reduce_ids_complex(self):
-        'Test reduce_ids complex list'
-        self.assertEqual(reduce_ids(self.table.id,
-                list(range(10)) + list(range(25, 30)) + list(range(15, 20))),
-            (((self.table.id >= 0) & (self.table.id <= 14))
-                | (self.table.id.in_(list(range(25, 30))))))
-
-    def test_reduce_ids_complex_small_continue(self):
-        'Test reduce_ids complex list with small continue'
-        self.assertEqual(reduce_ids(self.table.id,
-                [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 15, 18, 19, 21]),
-            (((self.table.id >= 1) & (self.table.id <= 12))
-                | (self.table.id.in_([15, 18, 19, 21]))))
-
-    @unittest.skipIf(sys.flags.optimize, "assert removed by optimization")
-    def test_reduce_ids_float(self):
-        'Test reduce_ids with integer as float'
-        self.assertEqual(reduce_ids(self.table.id,
-                [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
-                    15.0, 18.0, 19.0, 21.0]),
-            (((self.table.id >= 1.0) & (self.table.id <= 12.0))
-                | (self.table.id.in_([15.0, 18.0, 19.0, 21.0]))))
-        self.assertRaises(AssertionError, reduce_ids, self.table.id, [1.1])
-
     def test_reduce_domain(self):
         'Test reduce_domain'
         clause = ('x', '=', 'x')
diff -r 65be67251d10 -r 9895805863b9 trytond/trytond/tools/misc.py
--- a/trytond/trytond/tools/misc.py     Fri Dec 26 10:58:30 2025 +0100
+++ b/trytond/trytond/tools/misc.py     Fri Dec 26 10:57:44 2025 +0100
@@ -12,14 +12,12 @@
 import types
 import unicodedata
 import warnings
-from array import array
 from collections.abc import Iterable, Sized
 from functools import cache, wraps
 from itertools import chain, islice, tee, zip_longest
 
-from sql import As, Cast, Literal, Select
+from sql import As, Cast, Select
 from sql.conditionals import Case
-from sql.operators import Or
 
 from trytond.const import MODULES_GROUP, OPERATORS
 
@@ -111,45 +109,11 @@
 
 
 def reduce_ids(field, ids):
-    '''
-    Return a small SQL expression for the list of ids and the sql column
-    '''
-    if __debug__:
-        def strict_int(value):
-            assert not isinstance(value, float) or value.is_integer(), \
-                "ids must be integer"
-            return int(value)
-    else:
-        strict_int = int
-    ids = list(map(strict_int, ids))
-    if not ids:
-        return Literal(False)
-    ids.sort()
-    prev = ids.pop(0)
-    continue_list = [prev, prev]
-    discontinue_list = array('l')
-    sql = Or()
-    for i in ids:
-        if i == prev:
-            continue
-        if i != prev + 1:
-            if continue_list[-1] - continue_list[0] < 5:
-                discontinue_list.extend([continue_list[0] + x for x in
-                    range(continue_list[-1] - continue_list[0] + 1)])
-            else:
-                sql.append((field >= continue_list[0])
-                    & (field <= continue_list[-1]))
-            continue_list = []
-        continue_list.append(i)
-        prev = i
-    if continue_list[-1] - continue_list[0] < 5:
-        discontinue_list.extend([continue_list[0] + x for x in
-            range(continue_list[-1] - continue_list[0] + 1)])
-    else:
-        sql.append((field >= continue_list[0]) & (field <= continue_list[-1]))
-    if discontinue_list:
-        sql.append(field.in_(discontinue_list))
-    return sql
+    from trytond.model.fields import SQL_OPERATORS
+    warnings.warn(
+        "reduce_ids is deprecated use trytond.fields.SQL_OPERATORS['in']",
+        DeprecationWarning)
+    return SQL_OPERATORS['in'](field, ids)
 
 
 def reduce_domain(domain):

Reply via email to