details:   https://code.tryton.org/tryton/commit/bc1c607f913d
branch:    default
user:      Cédric Krier <[email protected]>
date:      Wed Jul 15 09:31:05 2026 +0200
description:
        Use bulk context manager

        Closes #11750
diffstat:

 modules/account/account.py                     |   39 ++---
 modules/account/fiscalyear.py                  |   47 +++---
 modules/account/move.py                        |  154 +++++++++++------------
 modules/account/tax.py                         |   81 ++++++------
 modules/account_asset/asset.py                 |   14 +-
 modules/account_deposit/invoice.py             |   32 ++--
 modules/account_invoice/invoice.py             |  145 ++++++++++-----------
 modules/account_invoice_defer/account.py       |   42 +++---
 modules/account_payment_clearing/payment.py    |   48 +++----
 modules/account_tax_cash/account.py            |   52 ++++----
 modules/commission/commission.py               |   40 ++---
 modules/commission/invoice.py                  |   34 ++--
 modules/product_kit/common.py                  |   22 +-
 modules/production/production.py               |   92 ++++++-------
 modules/production_work/work.py                |    9 +-
 modules/production_work_timesheet/work.py      |   50 +++----
 modules/project/work.py                        |   41 ++---
 modules/purchase_request_quotation/purchase.py |   15 +-
 modules/quality/quality.py                     |   15 +-
 modules/sale_payment/sale.py                   |   11 +-
 modules/sale_rental/sale.py                    |   24 +--
 modules/sale_shipment_cost/sale.py             |   23 +-
 modules/sale_subscription/subscription.py      |   11 +-
 modules/sale_supply_drop_shipment/stock.py     |  143 +++++++++++-----------
 modules/stock/inventory.py                     |  115 ++++++++---------
 modules/stock/location.py                      |   31 ++--
 modules/stock/move.py                          |   13 +-
 modules/stock/period.py                        |   44 +++---
 modules/stock/shipment.py                      |  161 ++++++++++++------------
 modules/stock_consignment/stock.py             |   59 ++++-----
 modules/stock_forecast/forecast.py             |   23 +-
 modules/stock_quantity_early_planning/stock.py |   59 ++++----
 modules/stock_split/stock.py                   |   26 ++--
 modules/stock_supply/purchase_request.py       |   21 +-
 modules/web_shop_shopify/account.py            |   52 ++++----
 modules/web_shop_shopify/product.py            |    8 +-
 trytond/trytond/ir/message.py                  |   50 +++----
 trytond/trytond/ir/module.py                   |   21 +-
 trytond/trytond/ir/session.py                  |   21 +-
 trytond/trytond/ir/translation.py              |  133 ++++++++++----------
 trytond/trytond/ir/trigger.py                  |   14 +-
 trytond/trytond/ir/ui/menu.py                  |   10 +-
 trytond/trytond/ir/ui/view.py                  |   79 +++++------
 trytond/trytond/model/multivalue.py            |    7 +-
 44 files changed, 1022 insertions(+), 1109 deletions(-)

diffs (2873 lines):

diff -r 6fc18060d097 -r bc1c607f913d modules/account/account.py
--- a/modules/account/account.py        Wed Jul 15 09:31:21 2026 +0200
+++ b/modules/account/account.py        Wed Jul 15 09:31:05 2026 +0200
@@ -395,27 +395,26 @@
             self.write(*values)
 
         # Update parent
-        to_save = []
         childs = [self]
-        while childs:
-            for child in childs:
-                if child.template:
-                    if not child.template_override:
-                        if child.template.parent:
-                            # Fallback to current parent
-                            # to keep under the same root
-                            parent = template2type.get(
-                                child.template.parent.id,
-                                child.parent)
-                        else:
-                            parent = None
-                        old_parent = (
-                            child.parent.id if child.parent else None)
-                        if parent != old_parent:
-                            child.parent = parent
-                            to_save.append(child)
-            childs = sum((c.childs for c in childs), ())
-        self.__class__.save(to_save)
+        with self.__class__.bulk_save() as save:
+            while childs:
+                for child in childs:
+                    if child.template:
+                        if not child.template_override:
+                            if child.template.parent:
+                                # Fallback to current parent
+                                # to keep under the same root
+                                parent = template2type.get(
+                                    child.template.parent.id,
+                                    child.parent)
+                            else:
+                                parent = None
+                            old_parent = (
+                                child.parent.id if child.parent else None)
+                            if parent != old_parent:
+                                child.parent = parent
+                                save.push(child)
+                childs = sum((c.childs for c in childs), ())
 
 
 class OpenType(Wizard):
diff -r 6fc18060d097 -r bc1c607f913d modules/account/fiscalyear.py
--- a/modules/account/fiscalyear.py     Wed Jul 15 09:31:21 2026 +0200
+++ b/modules/account/fiscalyear.py     Wed Jul 15 09:31:05 2026 +0200
@@ -236,30 +236,29 @@
         '''
         Create periods for the fiscal years with month interval
         '''
-        Period = Pool().get('account.period')
-        to_create = []
-        for fiscalyear in fiscalyears:
-            period_start_date = fiscalyear.start_date
-            while period_start_date < fiscalyear.end_date:
-                month_offset = 1 if period_start_date.day < end_day else 0
-                period_end_date = (period_start_date
-                    + relativedelta(months=interval - month_offset)
-                    + relativedelta(day=end_day))
-                if period_end_date > fiscalyear.end_date:
-                    period_end_date = fiscalyear.end_date
-                name = period_start_date.strftime('%Y-%m')
-                if name != period_end_date.strftime('%Y-%m'):
-                    name += ' - ' + period_end_date.strftime('%Y-%m')
-                to_create.append({
-                    'name': name,
-                    'start_date': period_start_date,
-                    'end_date': period_end_date,
-                    'fiscalyear': fiscalyear.id,
-                    'type': 'standard',
-                    })
-                period_start_date = period_end_date + relativedelta(days=1)
-        if to_create:
-            Period.create(to_create)
+        pool = Pool()
+        Period = pool.get('account.period')
+        with Period.bulk_create() as create:
+            for fiscalyear in fiscalyears:
+                period_start_date = fiscalyear.start_date
+                while period_start_date < fiscalyear.end_date:
+                    month_offset = 1 if period_start_date.day < end_day else 0
+                    period_end_date = (period_start_date
+                        + relativedelta(months=interval - month_offset)
+                        + relativedelta(day=end_day))
+                    if period_end_date > fiscalyear.end_date:
+                        period_end_date = fiscalyear.end_date
+                    name = period_start_date.strftime('%Y-%m')
+                    if name != period_end_date.strftime('%Y-%m'):
+                        name += ' - ' + period_end_date.strftime('%Y-%m')
+                    create.push({
+                        'name': name,
+                        'start_date': period_start_date,
+                        'end_date': period_end_date,
+                        'fiscalyear': fiscalyear.id,
+                        'type': 'standard',
+                        })
+                    period_start_date = period_end_date + relativedelta(days=1)
 
     @classmethod
     @ModelView.button_action('account.act_create_periods')
diff -r 6fc18060d097 -r bc1c607f913d modules/account/move.py
--- a/modules/account/move.py   Wed Jul 15 09:31:21 2026 +0200
+++ b/modules/account/move.py   Wed Jul 15 09:31:05 2026 +0200
@@ -1617,84 +1617,82 @@
         delegate_to = delegate_to.id if delegate_to else None
 
         reconciliations = []
-        to_post = []
-        for lines in lines_list:
-            if not lines:
-                continue
-            for line in lines:
-                if line.reconciliation:
-                    raise AccessError(
-                        gettext('account.msg_line_already_reconciled',
-                            line=line.rec_name))
-
-            lines = list(lines)
-            reconcile_account = None
-            reconcile_party = None
-            amount = Decimal(0)
-            amount_second_currency = Decimal(0)
-            second_currencies = set()
-            posted = True
-            for line in lines:
-                posted &= line.move.state == 'posted'
-                amount += line.debit - line.credit
-                if not reconcile_account:
-                    reconcile_account = line.account
-                if not reconcile_party:
-                    reconcile_party = line.party
-                if line.amount_second_currency is not None:
-                    amount_second_currency += line.amount_second_currency
-                second_currencies.add(line.second_currency)
-            company = reconcile_account.company
-
-            try:
-                second_currency, = second_currencies
-            except ValueError:
-                amount_second_currency = None
-                second_currency = None
-            if second_currency:
-                writeoff_amount = amount_second_currency
-                writeoff_currency = second_currency
-            else:
-                writeoff_amount = amount
-                writeoff_currency = company.currency
-            if writeoff_amount:
-                if not writeoff:
-                    raise ReconciliationError(gettext(
-                            'account.msg_reconciliation_write_off_missing',
-                            amount=lang.currency(
-                                writeoff_amount, writeoff_currency)))
-                move = cls._get_writeoff_move(
-                    reconcile_account, reconcile_party,
-                    writeoff_amount, writeoff_currency,
-                    writeoff, date=date, description=description)
-                move.save()
-                if posted:
-                    to_post.append(move)
-                for line in move.lines:
-                    if line.account == reconcile_account:
-                        lines.append(line)
-                        amount += line.debit - line.credit
-            if second_currency and amount:
-                move = cls._get_exchange_move(
-                    reconcile_account, reconcile_party, amount, date)
-                move.save()
-                if posted:
-                    to_post.append(move)
-                for line in move.lines:
-                    if line.account == reconcile_account:
-                        lines.append(line)
-                        amount += line.debit - line.credit
-            assert not amount, f"{amount} must be zero"
-            reconciliations.append({
-                    'company': reconcile_account.company,
-                    'lines': [('add', [x.id for x in lines])],
-                    'date': max(filter(None,
-                            (d for l in lines
-                                for d in (l.maturity_date, l.date)))),
-                    'delegate_to': delegate_to,
-                    })
-        if to_post:
-            Move.post(to_post)
+        with Move.bulk_func('post', auto=False) as post:
+            for lines in lines_list:
+                if not lines:
+                    continue
+                for line in lines:
+                    if line.reconciliation:
+                        raise AccessError(
+                            gettext('account.msg_line_already_reconciled',
+                                line=line.rec_name))
+
+                lines = list(lines)
+                reconcile_account = None
+                reconcile_party = None
+                amount = Decimal(0)
+                amount_second_currency = Decimal(0)
+                second_currencies = set()
+                posted = True
+                for line in lines:
+                    posted &= line.move.state == 'posted'
+                    amount += line.debit - line.credit
+                    if not reconcile_account:
+                        reconcile_account = line.account
+                    if not reconcile_party:
+                        reconcile_party = line.party
+                    if line.amount_second_currency is not None:
+                        amount_second_currency += line.amount_second_currency
+                    second_currencies.add(line.second_currency)
+                company = reconcile_account.company
+
+                try:
+                    second_currency, = second_currencies
+                except ValueError:
+                    amount_second_currency = None
+                    second_currency = None
+                if second_currency:
+                    writeoff_amount = amount_second_currency
+                    writeoff_currency = second_currency
+                else:
+                    writeoff_amount = amount
+                    writeoff_currency = company.currency
+                if writeoff_amount:
+                    if not writeoff:
+                        raise ReconciliationError(gettext(
+                                'account.msg_reconciliation_write_off_missing',
+                                amount=lang.currency(
+                                    writeoff_amount, writeoff_currency)))
+                    move = cls._get_writeoff_move(
+                        reconcile_account, reconcile_party,
+                        writeoff_amount, writeoff_currency,
+                        writeoff, date=date, description=description)
+                    move.save()
+                    if posted:
+                        post.push(move)
+                    for line in move.lines:
+                        if line.account == reconcile_account:
+                            lines.append(line)
+                            amount += line.debit - line.credit
+                if second_currency and amount:
+                    move = cls._get_exchange_move(
+                        reconcile_account, reconcile_party, amount, date)
+                    move.save()
+                    if posted:
+                        post.push(move)
+                    for line in move.lines:
+                        if line.account == reconcile_account:
+                            lines.append(line)
+                            amount += line.debit - line.credit
+                assert not amount, f"{amount} must be zero"
+                reconciliations.append({
+                        'company': reconcile_account.company,
+                        'lines': [('add', [x.id for x in lines])],
+                        'date': max(filter(None,
+                                (d for l in lines
+                                    for d in (l.maturity_date, l.date)))),
+                        'delegate_to': delegate_to,
+                        })
         return Reconciliation.create(reconciliations)
 
     @classmethod
diff -r 6fc18060d097 -r bc1c607f913d modules/account/tax.py
--- a/modules/account/tax.py    Wed Jul 15 09:31:21 2026 +0200
+++ b/modules/account/tax.py    Wed Jul 15 09:31:05 2026 +0200
@@ -274,27 +274,26 @@
             cls.write(*values)
 
         # Update parent
-        to_save = []
-        childs = cls.search([
-                ('company', '=', company_id),
-                ('parent', '=', None),
-                ])
-        while childs:
-            for child in childs:
-                if child.template:
-                    if not child.template_override:
-                        if child.template.parent:
-                            parent = template2tax_code.get(
-                                child.template.parent.id)
-                        else:
-                            parent = None
-                        old_parent = (
-                            child.parent.id if child.parent else None)
-                        if parent != old_parent:
-                            child.parent = parent
-                            to_save.append(child)
-            childs = sum((c.childs for c in childs), ())
-        cls.save(to_save)
+        with cls.bulk_save() as save:
+            childs = cls.search([
+                    ('company', '=', company_id),
+                    ('parent', '=', None),
+                    ])
+            while childs:
+                for child in childs:
+                    if child.template:
+                        if not child.template_override:
+                            if child.template.parent:
+                                parent = template2tax_code.get(
+                                    child.template.parent.id)
+                            else:
+                                parent = None
+                            old_parent = (
+                                child.parent.id if child.parent else None)
+                            if parent != old_parent:
+                                child.parent = parent
+                                save.push(child)
+                childs = sum((c.childs for c in childs), ())
 
 
 class TaxCodeLineTemplate(ModelSQL, ModelView):
@@ -1158,26 +1157,26 @@
             cls.write(*values)
 
         # Update parent
-        to_save = []
-        childs = cls.search([
-                ('company', '=', company_id),
-                ('parent', '=', None),
-                ])
-        while childs:
-            for child in childs:
-                if child.template:
-                    if not child.template_override:
-                        if child.template.parent:
-                            parent = template2tax.get(child.template.parent.id)
-                        else:
-                            parent = None
-                        old_parent = (
-                            child.parent.id if child.parent else None)
-                        if parent != old_parent:
-                            child.parent = parent
-                            to_save.append(child)
-            childs = sum((c.childs for c in childs), ())
-        cls.save(to_save)
+        with cls.bulk_save() as save:
+            childs = cls.search([
+                    ('company', '=', company_id),
+                    ('parent', '=', None),
+                    ])
+            while childs:
+                for child in childs:
+                    if child.template:
+                        if not child.template_override:
+                            if child.template.parent:
+                                parent = template2tax.get(
+                                    child.template.parent.id)
+                            else:
+                                parent = None
+                            old_parent = (
+                                child.parent.id if child.parent else None)
+                            if parent != old_parent:
+                                child.parent = parent
+                                save.push(child)
+                childs = sum((c.childs for c in childs), ())
 
 
 class _TaxLine:
diff -r 6fc18060d097 -r bc1c607f913d modules/account_asset/asset.py
--- a/modules/account_asset/asset.py    Wed Jul 15 09:31:21 2026 +0200
+++ b/modules/account_asset/asset.py    Wed Jul 15 09:31:05 2026 +0200
@@ -513,14 +513,14 @@
     @classmethod
     @ModelView.button
     def clear_lines(cls, assets):
-        Line = Pool().get('account.asset.line')
+        pool = Pool()
+        Line = pool.get('account.asset.line')
 
-        lines_to_delete = []
-        for asset in assets:
-            for line in asset.lines:
-                if not line.move or line.move.state != 'posted':
-                    lines_to_delete.append(line)
-        Line.delete(lines_to_delete)
+        with Line.bulk_delete() as delete:
+            for asset in assets:
+                for line in asset.lines:
+                    if not line.move or line.move.state != 'posted':
+                        delete.push(line)
 
     @classmethod
     @ModelView.button_action('account_asset.wizard_update')
diff -r 6fc18060d097 -r bc1c607f913d modules/account_deposit/invoice.py
--- a/modules/account_deposit/invoice.py        Wed Jul 15 09:31:21 2026 +0200
+++ b/modules/account_deposit/invoice.py        Wed Jul 15 09:31:05 2026 +0200
@@ -51,23 +51,21 @@
         else:
             if balance < 0 and total_amount > 0:
                 amount = -min(-balance, total_amount)
-        to_delete = []
-        for line in self.lines:
-            if line.account == account:
-                to_delete.append(line)
-        if amount < 0:
-            line = self._get_deposit_recall_invoice_line(
-                amount, account, description)
-            try:
-                line.sequence = max(l.sequence for l in self.lines
-                    if l.sequence is not None)
-            except ValueError:
-                pass
-            line.save()
-        else:
-            amount = Decimal(0)
-        if to_delete:
-            InvoiceLine.delete(to_delete)
+        with InvoiceLine.bulk_delete(auto=False) as delete:
+            for line in self.lines:
+                if line.account == account:
+                    delete.push(line)
+            if amount < 0:
+                line = self._get_deposit_recall_invoice_line(
+                    amount, account, description)
+                try:
+                    line.sequence = max(l.sequence for l in self.lines
+                        if l.sequence is not None)
+                except ValueError:
+                    pass
+                line.save()
+            else:
+                amount = Decimal(0)
         return amount
 
     def _get_deposit_recall_invoice_line(self, amount, account, description):
diff -r 6fc18060d097 -r bc1c607f913d modules/account_invoice/invoice.py
--- a/modules/account_invoice/invoice.py        Wed Jul 15 09:31:21 2026 +0200
+++ b/modules/account_invoice/invoice.py        Wed Jul 15 09:31:05 2026 +0200
@@ -1213,39 +1213,35 @@
     @dualmethod
     def update_taxes(cls, invoices, exception=False):
         Tax = Pool().get('account.invoice.tax')
-        to_create = []
-        to_delete = []
         to_write = []
-        for invoice in invoices:
-            if invoice.state in ('posted', 'paid', 'cancelled'):
-                continue
-            computed_taxes = dict(invoice._compute_taxes())
-            if not invoice.taxes:
-                to_create.extend(computed_taxes.values())
-            else:
-                tax_keys = set()
-                for tax in invoice.taxes:
-                    if tax.manual:
-                        continue
-                    key = tax._key
-                    if (key not in computed_taxes) or (key in tax_keys):
-                        to_delete.append(tax)
-                        continue
-                    tax_keys.add(key)
-                    if not invoice.currency.is_zero(
-                            computed_taxes[key]['base'] - tax.base):
-                        to_write.extend(([tax], computed_taxes[key]))
-                for key in computed_taxes:
-                    if key not in tax_keys:
-                        to_create.append(computed_taxes[key])
-            if exception and (to_create or to_delete or to_write):
-                raise InvoiceTaxValidationError(
-                    gettext('account_invoice.msg_invoice_tax_invalid',
-                        invoice=invoice.rec_name))
-        if to_create:
-            Tax.create(to_create)
-        if to_delete:
-            Tax.delete(to_delete)
+        with Tax.bulk_create(auto=False) as create, \
+                Tax.bulk_delete(auto=False) as delete:
+            for invoice in invoices:
+                if invoice.state in ('posted', 'paid', 'cancelled'):
+                    continue
+                computed_taxes = dict(invoice._compute_taxes())
+                if not invoice.taxes:
+                    create.extend(computed_taxes.values())
+                else:
+                    tax_keys = set()
+                    for tax in invoice.taxes:
+                        if tax.manual:
+                            continue
+                        key = tax._key
+                        if (key not in computed_taxes) or (key in tax_keys):
+                            delete.push(tax)
+                            continue
+                        tax_keys.add(key)
+                        if not invoice.currency.is_zero(
+                                computed_taxes[key]['base'] - tax.base):
+                            to_write.extend(([tax], computed_taxes[key]))
+                    for key in computed_taxes:
+                        if key not in tax_keys:
+                            create.push(computed_taxes[key])
+                if exception and (create or delete or to_write):
+                    raise InvoiceTaxValidationError(
+                        gettext('account_invoice.msg_invoice_tax_invalid',
+                            invoice=invoice.rec_name))
         if to_write:
             Tax.write(*to_write)
 
@@ -2271,23 +2267,21 @@
     @classmethod
     @ModelView.button
     def process(cls, invoices):
-        to_save = []
         paid = []
         posted = []
-        for invoice in invoices:
-            if invoice.state in {'posted', 'paid'}:
-                if invoice.reconciled:
-                    paid.append(invoice)
-                else:
-                    posted.append(invoice)
-            elif invoice.state == 'cancelled' and invoice.move:
-                if not invoice.reconciled:
-                    if invoice.cancel_move:
-                        invoice.cancel_move = None
-                        invoice.save()
-                        to_save.append(invoice)
-                    posted.append(invoice)
-        cls.save(to_save)
+        with cls.bulk_save() as save:
+            for invoice in invoices:
+                if invoice.state in {'posted', 'paid'}:
+                    if invoice.reconciled:
+                        paid.append(invoice)
+                    else:
+                        posted.append(invoice)
+                elif invoice.state == 'cancelled' and invoice.move:
+                    if not invoice.reconciled:
+                        if invoice.cancel_move:
+                            invoice.cancel_move = None
+                            save.push(invoice)
+                        posted.append(invoice)
         cls.paid(paid)
         cls._post(posted)
 
@@ -2305,36 +2299,33 @@
         Move = pool.get('account.move')
         Line = pool.get('account.move.line')
 
-        cancel_moves = []
-        delete_moves = []
-        to_save = []
-        for invoice in invoices:
-            if invoice.move or invoice.number:
-                if invoice.move and invoice.move.state == 'draft':
-                    delete_moves.append(invoice.move)
-                    delete_moves.extend(invoice.additional_moves)
-                elif not invoice.cancel_move:
-                    if (invoice.type == 'out'
-                            and not invoice.company.cancel_invoice_out):
-                        raise AccessError(
-                            gettext('account_invoice'
-                                '.msg_invoice_customer_cancel_move',
-                                invoice=invoice.rec_name))
-                    if invoice.move:
-                        invoice.cancel_move = invoice.move.cancel()
-                        additional_cancel_moves = [
-                            m.cancel() for m in invoice.additional_moves]
-                        invoice.additional_moves += tuple(
-                            additional_cancel_moves)
-                        to_save.append(invoice)
-                        cancel_moves.append(invoice.cancel_move)
-                        cancel_moves.extend(additional_cancel_moves)
-        if cancel_moves:
-            Move.save(cancel_moves)
-        cls._store_cache(invoices)
-        cls.save(to_save)
-        if delete_moves:
-            Move.delete(delete_moves)
+        with Move.bulk_delete(auto=False) as move_delete, \
+                cls.bulk_save(auto=False) as save:
+            cancel_moves = []
+            for invoice in invoices:
+                if invoice.move or invoice.number:
+                    if invoice.move and invoice.move.state == 'draft':
+                        move_delete.push(invoice.move)
+                        move_delete.extend(invoice.additional_moves)
+                    elif not invoice.cancel_move:
+                        if (invoice.type == 'out'
+                                and not invoice.company.cancel_invoice_out):
+                            raise AccessError(
+                                gettext('account_invoice'
+                                    '.msg_invoice_customer_cancel_move',
+                                    invoice=invoice.rec_name))
+                        if invoice.move:
+                            invoice.cancel_move = invoice.move.cancel()
+                            additional_cancel_moves = [
+                                m.cancel() for m in invoice.additional_moves]
+                            invoice.additional_moves += tuple(
+                                additional_cancel_moves)
+                            save.push(invoice)
+                            cancel_moves.append(invoice.cancel_move)
+                            cancel_moves.extend(additional_cancel_moves)
+            if cancel_moves:
+                Move.save(cancel_moves)
+            cls._store_cache(invoices)
         if cancel_moves:
             Move.post(cancel_moves)
         # Write state before reconcile to prevent invoice to go to paid state
diff -r 6fc18060d097 -r bc1c607f913d modules/account_invoice_defer/account.py
--- a/modules/account_invoice_defer/account.py  Wed Jul 15 09:31:21 2026 +0200
+++ b/modules/account_invoice_defer/account.py  Wed Jul 15 09:31:05 2026 +0200
@@ -225,13 +225,12 @@
     @classmethod
     def close_try(cls, deferrals):
         "Try to close the deferrals if last move has been created"
-        to_close = []
-        for deferral in deferrals:
-            if deferral.moves:
-                last_move = deferral.moves[-1]
-                if last_move.period.end_date >= deferral.end_date:
-                    to_close.append(deferral)
-        cls.close(to_close)
+        with cls.bulk_func('close', auto=False) as close:
+            for deferral in deferrals:
+                if deferral.moves:
+                    last_move = deferral.moves[-1]
+                    if last_move.period.end_date >= deferral.end_date:
+                        close.push(deferral)
 
     @classmethod
     @Workflow.transition('closed')
@@ -285,21 +284,20 @@
                     key=lambda p: p.start_date):
                 moves.append(deferral.get_move(period))
         Move.save(moves)
-        to_save = []
-        for deferral in deferrals:
-            if deferral.moves:
-                last_move = deferral.moves[-1]
-                if last_move.period.end_date >= deferral.end_date:
-                    remainder = deferral.amount_remainder
-                    if remainder:
-                        for line in last_move.lines:
-                            if line.debit:
-                                line.debit -= remainder
-                            else:
-                                line.credit -= remainder
-                        last_move.lines = last_move.lines
-                        to_save.append(last_move)
-        Move.save(to_save)
+        with Move.bulk_save() as save:
+            for deferral in deferrals:
+                if deferral.moves:
+                    last_move = deferral.moves[-1]
+                    if last_move.period.end_date >= deferral.end_date:
+                        remainder = deferral.amount_remainder
+                        if remainder:
+                            for line in last_move.lines:
+                                if line.debit:
+                                    line.debit -= remainder
+                                else:
+                                    line.credit -= remainder
+                            last_move.lines = last_move.lines
+                            save.push(last_move)
         Move.post(moves)
 
     @property
diff -r 6fc18060d097 -r bc1c607f913d modules/account_payment_clearing/payment.py
--- a/modules/account_payment_clearing/payment.py       Wed Jul 15 09:31:21 
2026 +0200
+++ b/modules/account_payment_clearing/payment.py       Wed Jul 15 09:31:05 
2026 +0200
@@ -79,34 +79,30 @@
 
         result = func(cls, payments, *args, **kwargs)
 
-        to_delete = []
-        to_reconcile = defaultdict(lambda: defaultdict(list))
-        to_unreconcile = []
-        for payment in payments:
-            if payment.clearing_move:
-                if payment.clearing_move.state == 'draft':
-                    to_delete.append(payment.clearing_move)
-                    for line in payment.clearing_move.lines:
-                        if line.reconciliation:
-                            to_unreconcile.append(line.reconciliation)
-                else:
-                    cancel_move = payment.clearing_move.cancel()
-                    for line in (payment.clearing_move.lines
-                            + cancel_move.lines):
-                        if line.reconciliation:
-                            to_unreconcile.append(line.reconciliation)
-                        if line.account.reconcile:
-                            to_reconcile[payment.party][line.account].append(
-                                line)
+        with Move.bulk_delete(auto=False) as delete:
+            to_reconcile = defaultdict(lambda: defaultdict(list))
+            with Reconciliation.bulk_delete(auto=False) as unreconcile:
+                for payment in payments:
+                    if payment.clearing_move:
+                        if payment.clearing_move.state == 'draft':
+                            delete.push(payment.clearing_move)
+                            for line in payment.clearing_move.lines:
+                                if line.reconciliation:
+                                    unreconcile.push(line.reconciliation)
+                        else:
+                            cancel_move = payment.clearing_move.cancel()
+                            for line in (payment.clearing_move.lines
+                                    + cancel_move.lines):
+                                if line.reconciliation:
+                                    unreconcile.push(line.reconciliation)
+                                if line.account.reconcile:
+                                    to_reconcile[payment.party][
+                                        line.account].append(line)
 
-        # Remove clearing_move before delete
-        # in case reconciliation triggers use it.
-        cls.write(payments, {'clearing_move': None})
+                # Remove clearing_move before delete
+                # in case reconciliation triggers use it.
+                cls.write(payments, {'clearing_move': None})
 
-        if to_unreconcile:
-            Reconciliation.delete(to_unreconcile)
-        if to_delete:
-            Move.delete(to_delete)
         for party in to_reconcile:
             for lines in to_reconcile[party].values():
                 Line.reconcile(lines)
diff -r 6fc18060d097 -r bc1c607f913d modules/account_tax_cash/account.py
--- a/modules/account_tax_cash/account.py       Wed Jul 15 09:31:21 2026 +0200
+++ b/modules/account_tax_cash/account.py       Wed Jul 15 09:31:05 2026 +0200
@@ -176,36 +176,36 @@
     def update_cash_basis(cls, lines, ratio, period):
         if not lines:
             return
-        to_save = []
         lines = cls.browse(sorted(
                 lines, key=sortable_values(cls.group_cash_basis_key)))
-        for key, lines in groupby(lines, key=cls.group_cash_basis_key):
-            key = dict(key)
-            if not key['on_cash_basis']:
-                continue
-            lines = list(lines)
-            company = lines[0].company
-            line_no_periods = [l for l in lines if not l.period]
-            if line_no_periods:
-                line_no_period, = line_no_periods
-            else:
-                line_no_period = None
-            total = sum(l.amount for l in lines)
-            amount = total * ratio - sum(l.amount for l in lines if l.period)
-            amount = company.currency.round(amount)
-            if amount:
-                if line_no_period and line_no_period.amount == amount:
-                    line_no_period.period = period
+        with cls.bulk_save() as save:
+            for key, lines in groupby(lines, key=cls.group_cash_basis_key):
+                key = dict(key)
+                if not key['on_cash_basis']:
+                    continue
+                lines = list(lines)
+                company = lines[0].company
+                line_no_periods = [l for l in lines if not l.period]
+                if line_no_periods:
+                    line_no_period, = line_no_periods
                 else:
-                    line = cls(**key, amount=amount)
+                    line_no_period = None
+                total = sum(l.amount for l in lines)
+                amount = total * ratio
+                amount -= sum(l.amount for l in lines if l.period)
+                amount = company.currency.round(amount)
+                if amount:
+                    if line_no_period and line_no_period.amount == amount:
+                        line_no_period.period = period
+                    else:
+                        line = cls(**key, amount=amount)
+                        if line_no_period:
+                            line_no_period.amount -= line.amount
+                        line.period = period
+                        if line.amount:
+                            save.push(line)
                     if line_no_period:
-                        line_no_period.amount -= line.amount
-                    line.period = period
-                    if line.amount:
-                        to_save.append(line)
-                if line_no_period:
-                    to_save.append(line_no_period)
-        cls.save(to_save)
+                        save.push(line_no_period)
 
 
 class Move(metaclass=PoolMeta):
diff -r 6fc18060d097 -r bc1c607f913d modules/commission/commission.py
--- a/modules/commission/commission.py  Wed Jul 15 09:31:21 2026 +0200
+++ b/modules/commission/commission.py  Wed Jul 15 09:31:05 2026 +0200
@@ -473,28 +473,26 @@
         def line_key(c):
             return c._group_to_invoice_line_key()
         commissions.sort(key=invoice_key)
-        invoices = []
-        invoice_lines = []
-        to_save = []
-        for key, commissions in groupby(commissions, key=invoice_key):
-            commissions = list(commissions)
-            key = dict(key)
-            invoice = cls._get_invoice(key)
-            invoices.append(invoice)
+        with cls.bulk_save(auto=False) as save:
+            with InvoiceLine.bulk_save(auto=False) as invoice_line_save, \
+                    Invoice.bulk_save(auto=False) as invoice_save:
+                for key, commissions in groupby(commissions, key=invoice_key):
+                    commissions = list(commissions)
+                    key = dict(key)
+                    invoice = cls._get_invoice(key)
+                    invoice_save.push(invoice)
 
-            commissions.sort(key=line_key)
-            for key, commissions in groupby(commissions, key=line_key):
-                commissions = [c for c in commissions if not c.invoice_line]
-                key = dict(key)
-                invoice_line = cls._get_invoice_line(key, invoice, commissions)
-                invoice_lines.append(invoice_line)
-                for commission in commissions:
-                    commission.invoice_line = invoice_line
-                    to_save.append(commission)
-        Invoice.save(invoices)
-        InvoiceLine.save(invoice_lines)
-        Invoice.update_taxes(invoices)
-        cls.save(to_save)
+                    commissions.sort(key=line_key)
+                    for key, commissions in groupby(commissions, key=line_key):
+                        commissions = [
+                            c for c in commissions if not c.invoice_line]
+                        key = dict(key)
+                        invoice_line = cls._get_invoice_line(
+                            key, invoice, commissions)
+                        invoice_line_save.push(invoice_line)
+                        for commission in commissions:
+                            commission.invoice_line = invoice_line
+                            save.push(commission)
 
         if Move and hasattr(Move, 'update_unit_price'):
             moves = list(set().union(*(c.stock_moves for c in commissions)))
diff -r 6fc18060d097 -r bc1c607f913d modules/commission/invoice.py
--- a/modules/commission/invoice.py     Wed Jul 15 09:31:21 2026 +0200
+++ b/modules/commission/invoice.py     Wed Jul 15 09:31:05 2026 +0200
@@ -29,11 +29,12 @@
 
     @classmethod
     def _post(cls, invoices):
-        # Create commission only the first time the invoice is posted
-        to_commission = [i for i in invoices
-            if i.state not in ['posted', 'paid']]
-        super()._post(invoices)
-        cls.create_commissions(to_commission)
+        with cls.bulk_func('create_commissions', auto=False) as create:
+            # Create commission only the first time the invoice is posted
+            for invoice in invoices:
+                if invoice.state not in {'posted', 'paid'}:
+                    create.push(invoice)
+            super()._post(invoices)
 
     @classmethod
     def create_commissions(cls, invoices):
@@ -98,24 +99,21 @@
         super().cancel(invoices)
         cls.set_commissions_date(invoices_to_set_date)
 
-        to_delete = []
-        to_save = []
-        to_delete += Commission.search([
-                ('invoice_line', '=', None),
-                ('origin.invoice', 'in', invoices_to_revert_commission,
-                    'account.invoice.line'),
-                ])
+        with Commission.bulk_delete() as delete:
+            delete.extend(Commission.search([
+                        ('invoice_line', '=', None),
+                        ('origin.invoice', 'in', invoices_to_revert_commission,
+                            'account.invoice.line'),
+                        ]))
+
         to_cancel = Commission.search([
                 ('invoice_line', '!=', None),
                 ('origin.invoice', 'in', invoices_to_revert_commission,
                     'account.invoice.line'),
                 ])
-        for commission in Commission.copy(to_cancel):
-            commission.amount *= -1
-            to_save.append(commission)
-
-        Commission.delete(to_delete)
-        Commission.save(to_save)
+        Commission.copy(to_cancel, default={
+                'amount': lambda data: -data['amount'],
+                })
 
     def _credit(self, **values):
         values.setdefault('agent', self.agent)
diff -r 6fc18060d097 -r bc1c607f913d modules/product_kit/common.py
--- a/modules/product_kit/common.py     Wed Jul 15 09:31:21 2026 +0200
+++ b/modules/product_kit/common.py     Wed Jul 15 09:31:05 2026 +0200
@@ -96,18 +96,16 @@
         def draft(cls, records):
             pool = Pool()
             Line = pool.get(prefix + '.line')
-            to_delete = []
-            to_save = []
-            for record in records:
-                for line in record.lines:
-                    if line.component_parent:
-                        to_delete.append(line)
-                    elif line.components:
-                        line.components = None
-                        to_save.append(line)
-            Line.save(to_save)
-            super().draft(records)
-            Line.delete(to_delete)
+            with Line.bulk_delete(auto=False) as delete:
+                with Line.bulk_save() as save:
+                    for record in records:
+                        for line in record.lines:
+                            if line.component_parent:
+                                delete.push(line)
+                            elif line.components:
+                                line.components = None
+                                save.push(line)
+                super().draft(records)
 
         @classmethod
         @ModelView.button
diff -r 6fc18060d097 -r bc1c607f913d modules/production/production.py
--- a/modules/production/production.py  Wed Jul 15 09:31:21 2026 +0200
+++ b/modules/production/production.py  Wed Jul 15 09:31:05 2026 +0200
@@ -504,37 +504,36 @@
     def set_moves(cls, productions):
         pool = Pool()
         Move = pool.get('stock.move')
-        to_save = []
-        for production in productions:
-            dates = production._get_move_planned_date()
-            input_date, output_date = dates
-            if not production.bom:
-                if production.product:
-                    move = production._move(
-                        'output', production.product, production.unit,
-                        production.quantity)
-                    move.planned_date = output_date
-                    to_save.append(move)
-                continue
+        with Move.bulk_save() as save:
+            for production in productions:
+                dates = production._get_move_planned_date()
+                input_date, output_date = dates
+                if not production.bom:
+                    if production.product:
+                        move = production._move(
+                            'output', production.product, production.unit,
+                            production.quantity)
+                        move.planned_date = output_date
+                        save.push(move)
+                    continue
 
-            factor = production.bom.compute_factor(
-                production.product, production.quantity, production.unit)
-            for input_ in production.bom.inputs:
-                quantity = input_.compute_quantity(factor)
-                product = input_.product
-                move = production._move(
-                    'input', product, input_.unit, quantity)
-                move.planned_date = input_date
-                to_save.append(input_.prepare_move(production, move))
+                factor = production.bom.compute_factor(
+                    production.product, production.quantity, production.unit)
+                for input_ in production.bom.inputs:
+                    quantity = input_.compute_quantity(factor)
+                    product = input_.product
+                    move = production._move(
+                        'input', product, input_.unit, quantity)
+                    move.planned_date = input_date
+                    save.push(input_.prepare_move(production, move))
 
-            for output in production.bom.outputs:
-                quantity = output.compute_quantity(factor)
-                product = output.product
-                move = production._move(
-                    'output', product, output.unit, quantity)
-                move.planned_date = output_date
-                to_save.append(output.prepare_move(production, move))
-        Move.save(to_save)
+                for output in production.bom.outputs:
+                    quantity = output.compute_quantity(factor)
+                    product = output.product
+                    move = production._move(
+                        'output', product, output.unit, quantity)
+                    move.planned_date = output_date
+                    save.push(output.prepare_move(production, move))
 
     @classmethod
     def set_cost_from_moves(cls):
@@ -745,15 +744,14 @@
         pool = Pool()
         Move = pool.get('stock.move')
 
-        to_draft, to_delete = [], []
-        for production in productions:
-            for move in chain(production.inputs, production.outputs):
-                if move.state != 'cancelled':
-                    to_draft.append(move)
-                else:
-                    to_delete.append(move)
-        Move.draft(to_draft)
-        Move.delete(to_delete)
+        with Move.bulk_delete() as delete, \
+                Move.bulk_func('draft') as draft:
+            for production in productions:
+                for move in chain(production.inputs, production.outputs):
+                    if move.state != 'cancelled':
+                        draft.push(move)
+                    else:
+                        delete.push(move)
 
     @classmethod
     @ModelView.button
@@ -824,16 +822,14 @@
         if Move.assign_try(to_assign):
             cls.assign(productions)
         else:
-            to_assign = []
-            for production in productions:
-                if any(
-                        m.state in {'staging', 'draft'}
-                        for m in production.assign_moves
-                        if m.assignation_required):
-                    continue
-                to_assign.append(production)
-            if to_assign:
-                cls.assign(to_assign)
+            with cls.bulk_func('assign', auto=False) as assign:
+                for production in productions:
+                    if any(
+                            m.state in {'staging', 'draft'}
+                            for m in production.assign_moves
+                            if m.assignation_required):
+                        continue
+                    assign.push(production)
 
     @classmethod
     def _get_reschedule_planned_start_dates_domain(cls, date):
diff -r 6fc18060d097 -r bc1c607f913d modules/production_work/work.py
--- a/modules/production_work/work.py   Wed Jul 15 09:31:21 2026 +0200
+++ b/modules/production_work/work.py   Wed Jul 15 09:31:05 2026 +0200
@@ -197,11 +197,10 @@
         pool = Pool()
         Cycle = pool.get('production.work.cycle')
 
-        to_do = []
-        for work in works:
-            for cycle in work.active_cycles:
-                to_do.append(cycle)
-        Cycle.do(to_do)
+        with Cycle.bulk_func('do', auto=False) as do:
+            for work in works:
+                for cycle in work.active_cycles:
+                    do.push(cycle)
 
     @property
     def _state(self):
diff -r 6fc18060d097 -r bc1c607f913d modules/production_work_timesheet/work.py
--- a/modules/production_work_timesheet/work.py Wed Jul 15 09:31:21 2026 +0200
+++ b/modules/production_work_timesheet/work.py Wed Jul 15 09:31:05 2026 +0200
@@ -97,34 +97,30 @@
         Timesheet = pool.get('timesheet.work')
         Date = pool.get('ir.date')
 
-        to_create = []
-        to_delete = []
         to_write = defaultdict(list)
-        for work in works:
-            with Transaction().set_context(company=work.company.id):
-                today = Date.today()
-            if work.timesheet_available:
-                ended = work.state in {'done', 'cancelled'}
-                if not work.timesheet_works:
-                    to_create.append({
-                            'origin': str(work),
-                            'company': work.company.id,
-                            'timesheet_end_date': today if ended else None,
-                            })
-                elif ended:
-                    for timesheet in work.timesheet_works:
-                        date = max([today]
-                            + [l.date for l in timesheet.timesheet_lines])
-                        to_write[date].append(timesheet)
-            if (not work.timesheet_available
-                    and work.timesheet_works):
-                if all(not w.timesheet_lines
-                        for w in work.timesheet_works):
-                    to_delete.extend(work.timesheet_works)
-        if to_create:
-            Timesheet.create(to_create)
-        if to_delete:
-            Timesheet.delete(to_delete)
+        with Timesheet.bulk_create() as create, \
+                Timesheet.bulk_delete() as delete:
+            for work in works:
+                with Transaction().set_context(company=work.company.id):
+                    today = Date.today()
+                if work.timesheet_available:
+                    ended = work.state in {'done', 'cancelled'}
+                    if not work.timesheet_works:
+                        create.push({
+                                'origin': str(work),
+                                'company': work.company.id,
+                                'timesheet_end_date': today if ended else None,
+                                })
+                    elif ended:
+                        for timesheet in work.timesheet_works:
+                            date = max([today]
+                                + [l.date for l in timesheet.timesheet_lines])
+                            to_write[date].append(timesheet)
+                if (not work.timesheet_available
+                        and work.timesheet_works):
+                    if all(not w.timesheet_lines
+                            for w in work.timesheet_works):
+                        delete.extend(work.timesheet_works)
         for date, timesheets in list(to_write.items()):
             Timesheet.write(timesheets, {
                     'timesheet_end_date': date,
diff -r 6fc18060d097 -r bc1c607f913d modules/project/work.py
--- a/modules/project/work.py   Wed Jul 15 09:31:21 2026 +0200
+++ b/modules/project/work.py   Wed Jul 15 09:31:05 2026 +0200
@@ -392,21 +392,16 @@
         pool = Pool()
         Timesheet = pool.get('timesheet.work')
 
-        to_create = []
-        to_delete = []
-        for project in projects:
-            if not project.timesheet_works and value:
-                to_create.append({
-                        'origin': str(project),
-                        'company': project.company.id,
-                        })
-            elif project.timesheet_works and not value:
-                to_delete.extend(project.timesheet_works)
-
-        if to_create:
-            Timesheet.create(to_create)
-        if to_delete:
-            Timesheet.delete(to_delete)
+        with Timesheet.bulk_create() as create, \
+                Timesheet.bulk_delete() as delete:
+            for project in projects:
+                if not project.timesheet_works and value:
+                    create.push({
+                            'origin': str(project),
+                            'company': project.company.id,
+                            })
+                elif project.timesheet_works and not value:
+                    delete.extend(project.timesheet_works)
 
     def get_timesheet_date(self, name):
         if self.timesheet_works:
@@ -530,15 +525,13 @@
         default.setdefault(
             'status', lambda data: WorkStatus.get_default_status(data['type']))
         new_works = super().copy(project_works, default=default)
-        to_save = []
-        for work, new_work in zip(project_works, new_works):
-            if work.timesheet_available:
-                new_work.timesheet_available = work.timesheet_available
-                new_work.timesheet_start_date = work.timesheet_start_date
-                new_work.timesheet_end_date = work.timesheet_end_date
-                to_save.append(new_work)
-        if to_save:
-            cls.save(to_save)
+        with cls.bulk_save() as save:
+            for work, new_work in zip(project_works, new_works):
+                if work.timesheet_available:
+                    new_work.timesheet_available = work.timesheet_available
+                    new_work.timesheet_start_date = work.timesheet_start_date
+                    new_work.timesheet_end_date = work.timesheet_end_date
+                    save.push(new_work)
         return new_works
 
     @classmethod
diff -r 6fc18060d097 -r bc1c607f913d 
modules/purchase_request_quotation/purchase.py
--- a/modules/purchase_request_quotation/purchase.py    Wed Jul 15 09:31:21 
2026 +0200
+++ b/modules/purchase_request_quotation/purchase.py    Wed Jul 15 09:31:05 
2026 +0200
@@ -691,15 +691,12 @@
         super().__setup__()
 
     def transition_start(self):
-        to_save = []
-        reqs = [r for r in self.records
-            if not r.purchase_line and r.quotation_lines]
-        to_save = []
-        for req in reqs:
-            if req.best_quotation_line:
-                to_save.append(self.apply_quotation(req))
-        if to_save:
-            self.model.save(to_save)
+        reqs = (r for r in self.records
+            if not r.purchase_line and r.quotation_lines)
+        with self.model.bulk_save() as save:
+            for req in reqs:
+                if req.best_quotation_line:
+                    save.push(self.apply_quotation(req))
         state = super().transition_start()
         return state
 
diff -r 6fc18060d097 -r bc1c607f913d modules/quality/quality.py
--- a/modules/quality/quality.py        Wed Jul 15 09:31:21 2026 +0200
+++ b/modules/quality/quality.py        Wed Jul 15 09:31:05 2026 +0200
@@ -555,14 +555,13 @@
         cls.write([i for i in inspections if not i.processed_at], {
                 'processed_at': dt.datetime.now(),
                 })
-        to_pass, to_fail = [], []
-        for inspection in inspections:
-            if inspection.check():
-                to_pass.append(inspection)
-            else:
-                to_fail.append(inspection)
-        cls.pass_(to_pass)
-        cls.fail(to_fail)
+        with cls.bulk_func('pass_') as pass_, \
+                cls.bulk_func('fail') as fail:
+            for inspection in inspections:
+                if inspection.check():
+                    pass_.push(inspection)
+                else:
+                    fail.push(inspection)
 
     @classmethod
     @ModelView.button
diff -r 6fc18060d097 -r bc1c607f913d modules/sale_payment/sale.py
--- a/modules/sale_payment/sale.py      Wed Jul 15 09:31:21 2026 +0200
+++ b/modules/sale_payment/sale.py      Wed Jul 15 09:31:05 2026 +0200
@@ -91,13 +91,10 @@
                 abs(authorized) >= abs(amount)
                 and (authorized * amount >= 0))
 
-        to_confirm = []
-        for sale in sales:
-            if cover(sale.payment_amount_authorized, sale.amount_to_pay):
-                to_confirm.append(sale)
-        if to_confirm:
-            to_confirm = cls.browse(to_confirm)  # optimize cache
-            cls.confirm(to_confirm)
+        with cls.bulk_func('confirm') as confirm:
+            for sale in sales:
+                if cover(sale.payment_amount_authorized, sale.amount_to_pay):
+                    confirm.push(sale)
 
     @property
     def credit_limit_amount(self):
diff -r 6fc18060d097 -r bc1c607f913d modules/sale_rental/sale.py
--- a/modules/sale_rental/sale.py       Wed Jul 15 09:31:21 2026 +0200
+++ b/modules/sale_rental/sale.py       Wed Jul 15 09:31:05 2026 +0200
@@ -700,14 +700,12 @@
 
     @dualmethod
     def try_picked_up(cls, rentals):
-        to_picked_up = []
-        for rental in rentals:
-            if all(
-                    l.rental_state in {'picked up', 'done'}
-                    for l in rental.lines):
-                to_picked_up.append(rental)
-        if to_picked_up:
-            cls.picked_up(to_picked_up)
+        with cls.bulk_func('picked_up') as picked_up:
+            for rental in rentals:
+                if all(
+                        l.rental_state in {'picked up', 'done'}
+                        for l in rental.lines):
+                    picked_up.push(rental)
 
     @classmethod
     @Workflow.transition('picked up')
@@ -739,12 +737,10 @@
 
     @dualmethod
     def try_done(cls, rentals):
-        to_do = []
-        for rental in rentals:
-            if all(l.rental_state == 'done' for l in rental.lines):
-                to_do.append(rental)
-        if to_do:
-            cls.do(to_do)
+        with cls.bulk_func('do') as do:
+            for rental in rentals:
+                if all(l.rental_state == 'done' for l in rental.lines):
+                    do.push(rental)
 
     @classmethod
     @Workflow.transition('done')
diff -r 6fc18060d097 -r bc1c607f913d modules/sale_shipment_cost/sale.py
--- a/modules/sale_shipment_cost/sale.py        Wed Jul 15 09:31:21 2026 +0200
+++ b/modules/sale_shipment_cost/sale.py        Wed Jul 15 09:31:05 2026 +0200
@@ -258,18 +258,19 @@
 
         super()._process_invoice_fulfillment_states(sales)
 
-        to_save, to_delete, shipments = [], [], set()
-        for sale in sent:
-            if sale.shipment_state != 'sent':
-                to_delete.extend(sale.shipment_costs)
-                shipments.update(sale.shipments)
-        for sale in not_sent:
-            if sale.shipment_state == 'sent':
-                to_save.extend(sale._get_shipment_costs())
-                shipments.update(sale.shipments)
+        shipments = set()
+        with ShipmentCostSale.bulk_delete() as delete:
+            for sale in sent:
+                if sale.shipment_state != 'sent':
+                    for shipment_cost in sale.shipment_costs:
+                        delete.push(shipment_cost)
+                    shipments.update(sale.shipments)
+        with ShipmentCostSale.bulk_save() as save:
+            for sale in not_sent:
+                if sale.shipment_state == 'sent':
+                    save.extend(sale._get_shipment_costs())
+                    shipments.update(sale.shipments)
 
-        ShipmentCostSale.delete(to_delete)
-        ShipmentCostSale.save(to_save)
         ShipmentOut.set_shipment_cost(shipments)
 
     @property
diff -r 6fc18060d097 -r bc1c607f913d modules/sale_subscription/subscription.py
--- a/modules/sale_subscription/subscription.py Wed Jul 15 09:31:21 2026 +0200
+++ b/modules/sale_subscription/subscription.py Wed Jul 15 09:31:05 2026 +0200
@@ -404,12 +404,11 @@
 
     @classmethod
     def process(cls, subscriptions):
-        to_close = []
-        for subscription in subscriptions:
-            if all(l.next_consumption_date is None
-                    for l in subscription.lines):
-                to_close.append(subscription)
-        cls.close(to_close)
+        with cls.bulk_func('close') as close:
+            for subscription in subscriptions:
+                if all(l.next_consumption_date is None
+                        for l in subscription.lines):
+                    close.push(subscription)
 
     @classmethod
     @Workflow.transition('closed')
diff -r 6fc18060d097 -r bc1c607f913d modules/sale_supply_drop_shipment/stock.py
--- a/modules/sale_supply_drop_shipment/stock.py        Wed Jul 15 09:31:21 
2026 +0200
+++ b/modules/sale_supply_drop_shipment/stock.py        Wed Jul 15 09:31:05 
2026 +0200
@@ -392,34 +392,33 @@
         Move = pool.get('stock.move')
         UoM = pool.get('product.uom')
 
-        to_save = []
-        for shipment in shipments:
-            product_cost = defaultdict(int)
-            s_product_qty = defaultdict(int)
-            for s_move in shipment.supplier_moves:
-                if s_move.state == 'cancelled':
-                    continue
-                internal_quantity = Decimal(str(s_move.internal_quantity))
-                product_cost[s_move.product] += (
-                    s_move.get_cost_price() * internal_quantity)
+        with Move.bulk_save() as save:
+            for shipment in shipments:
+                product_cost = defaultdict(int)
+                s_product_qty = defaultdict(int)
+                for s_move in shipment.supplier_moves:
+                    if s_move.state == 'cancelled':
+                        continue
+                    internal_quantity = Decimal(str(s_move.internal_quantity))
+                    product_cost[s_move.product] += (
+                        s_move.get_cost_price() * internal_quantity)
 
-                quantity = UoM.compute_qty(
-                    s_move.unit, s_move.quantity, s_move.product.default_uom,
-                    round=False)
-                s_product_qty[s_move.product] += quantity
+                    quantity = UoM.compute_qty(
+                        s_move.unit, s_move.quantity,
+                        s_move.product.default_uom,
+                        round=False)
+                    s_product_qty[s_move.product] += quantity
 
-            for product, cost in product_cost.items():
-                qty = Decimal(str(s_product_qty[product]))
-                if qty:
-                    product_cost[product] = round_price(cost / qty)
+                for product, cost in product_cost.items():
+                    qty = Decimal(str(s_product_qty[product]))
+                    if qty:
+                        product_cost[product] = round_price(cost / qty)
 
-            for move in shipment.moves:
-                cost_price = product_cost[move.product]
-                if cost_price != move.cost_price:
-                    move.cost_price = cost_price
-                    to_save.append(move)
-        if to_save:
-            Move.save(to_save)
+                for move in shipment.moves:
+                    cost_price = product_cost[move.product]
+                    if cost_price != move.cost_price:
+                        move.cost_price = cost_price
+                        save.push(move)
 
     @classmethod
     @ModelView.button
@@ -429,23 +428,24 @@
         PurchaseLine = pool.get('purchase.line')
         Move = pool.get('stock.move')
 
-        to_save = []
-        for shipment in shipments:
-            for s_move in shipment.supplier_moves:
-                if not isinstance(s_move.origin, PurchaseLine):
-                    continue
-                p_line = s_move.origin
-                for request in p_line.requests:
-                    for sale_line in request.sale_lines:
-                        for c_move in sale_line.moves:
-                            if (c_move.state not in {'cancelled', 'done'}
-                                    and not c_move.shipment
-                                    and c_move.from_location.type == 'drop'):
-                                c_move.shipment = shipment
-                                c_move.origin_drop = s_move
-                                to_save.append(c_move)
-        Move.save(to_save)
-        Move.draft(to_save)
+        with Move.bulk_func('draft', auto=False) as draft, \
+                Move.bulk_func('save', auto=False) as save:
+            for shipment in shipments:
+                for s_move in shipment.supplier_moves:
+                    if not isinstance(s_move.origin, PurchaseLine):
+                        continue
+                    p_line = s_move.origin
+                    for request in p_line.requests:
+                        for sale_line in request.sale_lines:
+                            for c_move in sale_line.moves:
+                                if (c_move.state not in {'cancelled', 'done'}
+                                        and not c_move.shipment
+                                        and c_move.from_location.type
+                                        == 'drop'):
+                                    c_move.shipment = shipment
+                                    c_move.origin_drop = s_move
+                                    save.push(c_move)
+                                    draft.push(c_move)
         cls._synchronize_moves(shipments)
 
     @classmethod
@@ -458,14 +458,14 @@
         Move = pool.get('stock.move')
         Move.do([m for s in shipments for m in s.supplier_moves])
         cls._synchronize_moves(shipments)
-        to_assign, to_delete = [], []
-        for shipment in shipments:
-            for move in shipment.customer_moves:
-                if move.quantity:
-                    to_assign.append(move)
-                else:
-                    to_delete.append(move)
-        Move.delete(to_delete)
+        with Move.bulk_delete(auto=False) as delete:
+            to_assign = []
+            for shipment in shipments:
+                for move in shipment.customer_moves:
+                    if move.quantity:
+                        to_assign.append(move)
+                    else:
+                        delete.push(move)
         Move.assign(to_assign)
 
     @classmethod
@@ -477,15 +477,15 @@
         Move = pool.get('stock.move')
         Date = pool.get('ir.date')
         cls.set_cost(shipments)
-        customer_moves, to_delete = [], []
-        for shipment in shipments:
-            shipment.check_quantity()
-            for move in shipment.customer_moves:
-                if move.quantity:
-                    customer_moves.append(move)
-                else:
-                    to_delete.append(move)
-        Move.delete(to_delete)
+        with Move.bulk_delete() as delete:
+            customer_moves = []
+            for shipment in shipments:
+                shipment.check_quantity()
+                for move in shipment.customer_moves:
+                    if move.quantity:
+                        customer_moves.append(move)
+                    else:
+                        delete.push(move)
         Move.do(customer_moves)
         for company, shipments in groupby(shipments, key=lambda s: s.company):
             with Transaction().set_context(company=company.id):
@@ -617,17 +617,16 @@
         with Transaction().set_context(_stock_move_split_drop=True):
             moves = super().split(quantity, unit, count=count)
         if self.moves_drop:
-            to_save = []
-            moves_drop = list(self.moves_drop)
-            for move in moves:
-                remainder = move.quantity
-                while remainder > 0 and moves_drop:
-                    move_drop = moves_drop.pop(0)
-                    splits = move_drop.split(remainder, move.unit, count=1)
-                    move_drop.origin_drop = move
-                    remainder -= move_drop.quantity
-                    to_save.append(move_drop)
-                    splits.remove(move_drop)
-                    moves_drop.extend(splits)
-            self.__class__.save(to_save)
+            with self.__class__.bulk_save() as save:
+                moves_drop = list(self.moves_drop)
+                for move in moves:
+                    remainder = move.quantity
+                    while remainder > 0 and moves_drop:
+                        move_drop = moves_drop.pop(0)
+                        splits = move_drop.split(remainder, move.unit, count=1)
+                        move_drop.origin_drop = move
+                        remainder -= move_drop.quantity
+                        save.push(move_drop)
+                        splits.remove(move_drop)
+                        moves_drop.extend(splits)
         return moves
diff -r 6fc18060d097 -r bc1c607f913d modules/stock/inventory.py
--- a/modules/stock/inventory.py        Wed Jul 15 09:31:21 2026 +0200
+++ b/modules/stock/inventory.py        Wed Jul 15 09:31:05 2026 +0200
@@ -273,70 +273,69 @@
         Product = pool.get('product.product')
 
         grouping = cls.grouping()
-        to_save, to_delete = [], []
-        for inventory in inventories:
-            # Once done computation is wrong because include created moves
-            if inventory.state == 'done':
-                continue
-            # Compute product quantities
-            with Transaction().set_context(
-                    company=inventory.company.id,
-                    stock_date_end=inventory.date):
-                if fill:
-                    pbl = Product.products_by_location(
-                        [inventory.location.id],
-                        grouping=grouping)
-                else:
-                    product_ids = [l.product.id for l in inventory.lines]
-                    pbl = defaultdict(int)
-                    pbl.update(Product.products_by_location(
+        with Line.bulk_save() as save, \
+                Line.bulk_delete() as delete:
+            for inventory in inventories:
+                # Once done computation is wrong because include created moves
+                if inventory.state == 'done':
+                    continue
+                # Compute product quantities
+                with Transaction().set_context(
+                        company=inventory.company.id,
+                        stock_date_end=inventory.date):
+                    if fill:
+                        pbl = Product.products_by_location(
                             [inventory.location.id],
-                            grouping=grouping,
-                            grouping_filter=(list(product_ids),)))
+                            grouping=grouping)
+                    else:
+                        product_ids = [l.product.id for l in inventory.lines]
+                        pbl = defaultdict(int)
+                        pbl.update(Product.products_by_location(
+                                [inventory.location.id],
+                                grouping=grouping,
+                                grouping_filter=(list(product_ids),)))
 
-            # Update existing lines
-            for line in inventory.lines:
-                if line.product.type != 'goods':
-                    to_delete.append(line)
+                # Update existing lines
+                for line in inventory.lines:
+                    if line.product.type != 'goods':
+                        delete.push(line)
+                        continue
+
+                    key = (inventory.location.id,) + line.unique_key
+                    if key in pbl:
+                        quantity = pbl.pop(key)
+                    else:
+                        quantity = 0.0
+                    line.update_for_complete(quantity)
+                    save.push(line)
+
+                if not fill:
                     continue
 
-                key = (inventory.location.id,) + line.unique_key
-                if key in pbl:
-                    quantity = pbl.pop(key)
-                else:
-                    quantity = 0.0
-                line.update_for_complete(quantity)
-                to_save.append(line)
-
-            if not fill:
-                continue
-
-            product_idx = grouping.index('product') + 1
-            # Index some data
-            product2type = {}
-            product2consumable = {}
-            for product in Product.browse({line[product_idx] for line in pbl}):
-                product2type[product.id] = product.type
-                product2consumable[product.id] = product.consumable
+                product_idx = grouping.index('product') + 1
+                # Index some data
+                product2type = {}
+                product2consumable = {}
+                for product in Product.browse(
+                        {line[product_idx] for line in pbl}):
+                    product2type[product.id] = product.type
+                    product2consumable[product.id] = product.consumable
 
-            # Create lines if needed
-            for key, quantity in pbl.items():
-                product_id = key[product_idx]
-                if (product2type[product_id] != 'goods'
-                        or product2consumable[product_id]):
-                    continue
-                if not quantity:
-                    continue
+                # Create lines if needed
+                for key, quantity in pbl.items():
+                    product_id = key[product_idx]
+                    if (product2type[product_id] != 'goods'
+                            or product2consumable[product_id]):
+                        continue
+                    if not quantity:
+                        continue
 
-                line = Line(
-                    inventory=inventory,
-                    **{fname: key[i] for i, fname in enumerate(grouping, 1)})
-                line.update_for_complete(quantity)
-                to_save.append(line)
-        if to_delete:
-            Line.delete(to_delete)
-        if to_save:
-            Line.save(to_save)
+                    line = Line(
+                        inventory=inventory,
+                        **{fname: key[i]
+                            for i, fname in enumerate(grouping, 1)})
+                    line.update_for_complete(quantity)
+                    save.push(line)
 
     @classmethod
     @ModelView.button_action('stock.wizard_inventory_count')
diff -r 6fc18060d097 -r bc1c607f913d modules/stock/location.py
--- a/modules/stock/location.py Wed Jul 15 09:31:21 2026 +0200
+++ b/modules/stock/location.py Wed Jul 15 09:31:05 2026 +0200
@@ -523,22 +523,21 @@
         '''
         Set the parent of child location of warehouse if not set
         '''
-        to_update = set()
-        to_save = []
-        for location in locations:
-            if location.type == 'warehouse':
-                if not location.input_location.parent:
-                    to_update.add(location.input_location)
-                if not location.output_location.parent:
-                    to_update.add(location.output_location)
-                if not location.storage_location.parent:
-                    to_update.add(location.storage_location)
-                if to_update:
-                    for child_location in to_update:
-                        child_location.parent = location
-                        to_save.append(child_location)
-                    to_update.clear()
-        cls.save(to_save)
+        with cls.bulk_save() as save:
+            to_update = set()
+            for location in locations:
+                if location.type == 'warehouse':
+                    if not location.input_location.parent:
+                        to_update.add(location.input_location)
+                    if not location.output_location.parent:
+                        to_update.add(location.output_location)
+                    if not location.storage_location.parent:
+                        to_update.add(location.storage_location)
+                    if to_update:
+                        for child_location in to_update:
+                            child_location.parent = location
+                            save.push(child_location)
+                        to_update.clear()
 
     @classmethod
     def on_modification(cls, mode, locations, field_names=None):
diff -r 6fc18060d097 -r bc1c607f913d modules/stock/move.py
--- a/modules/stock/move.py     Wed Jul 15 09:31:21 2026 +0200
+++ b/modules/stock/move.py     Wed Jul 15 09:31:05 2026 +0200
@@ -895,13 +895,13 @@
 
         cls.check_origin(moves)
         for key, grouped_moves in groupby(moves, key=cls._cost_price_key):
-            to_save = []
             cost_values = []
             products = set()
             grouped_moves = list(grouped_moves)
             context = dict(key)
             context.update(cls._cost_price_context(grouped_moves))
-            with Transaction().set_context(context):
+            with Transaction().set_context(context), \
+                    cls.bulk_save(auto=False) as save:
                 grouped_moves = cls.browse(grouped_moves)
                 for move in grouped_moves:
                     move.set_effective_date()
@@ -912,9 +912,8 @@
                             # The average computation of product cost price
                             # requires each previous move of the same product
                             # to be saved
-                            cls.save(to_save)
+                            save.flush()
                             set_cost_values(cost_values)
-                            del to_save[:]
                             del cost_values[:]
                             products.clear()
                             # Recompute with unmodified move but including new
@@ -924,7 +923,7 @@
                         cost_values.append(
                             (move.product, cost_price,
                                 move._cost_price_pattern))
-                    to_save.extend(extra_to_save)
+                    save.extend(extra_to_save)
                     if move.cost_price_required and move.cost_price is None:
                         if cost_price is None:
                             cost_price = move.product.get_multivalue(
@@ -932,11 +931,9 @@
                         move.cost_price = cost_price
                     move.state = 'done'
 
-                    to_save.append(move)
+                    save.push(move)
                     products.add(move.product)
 
-                if to_save:
-                    cls.save(to_save)
                 if cost_values:
                     set_cost_values(cost_values)
 
diff -r 6fc18060d097 -r bc1c607f913d modules/stock/period.py
--- a/modules/stock/period.py   Wed Jul 15 09:31:21 2026 +0200
+++ b/modules/stock/period.py   Wed Jul 15 09:31:05 2026 +0200
@@ -148,29 +148,27 @@
 
         for grouping in cls.groupings():
             Cache = cls.get_cache(grouping)
-            to_create = []
-            for period in periods:
-                with Transaction().set_context(
-                        stock_date_end=period.date,
-                        stock_date_start=None,
-                        stock_assign=False,
-                        forecast=False,
-                        stock_destinations=None,
-                        ):
-                    pbl = Product.products_by_location(
-                        [l.id for l in locations], grouping=grouping)
-                for key, quantity in pbl.items():
-                    if quantity:
-                        values = {
-                            'location': key[0],
-                            'period': period.id,
-                            'internal_quantity': quantity,
-                            }
-                        for i, field in enumerate(grouping, 1):
-                            values[field] = key[i]
-                        to_create.append(values)
-            if to_create:
-                Cache.create(to_create)
+            with Cache.bulk_create() as create:
+                for period in periods:
+                    with Transaction().set_context(
+                            stock_date_end=period.date,
+                            stock_date_start=None,
+                            stock_assign=False,
+                            forecast=False,
+                            stock_destinations=None,
+                            ):
+                        pbl = Product.products_by_location(
+                            [l.id for l in locations], grouping=grouping)
+                    for key, quantity in pbl.items():
+                        if quantity:
+                            values = {
+                                'location': key[0],
+                                'period': period.id,
+                                'internal_quantity': quantity,
+                                }
+                            for i, field in enumerate(grouping, 1):
+                                values[field] = key[i]
+                            create.push(values)
 
     @classmethod
     def auto_create(cls):
diff -r 6fc18060d097 -r bc1c607f913d modules/stock/shipment.py
--- a/modules/stock/shipment.py Wed Jul 15 09:31:21 2026 +0200
+++ b/modules/stock/shipment.py Wed Jul 15 09:31:05 2026 +0200
@@ -233,13 +233,13 @@
         Move.write(moves, {
                 'quantity': 0,
                 })
-        to_assign = [
-            s for s in shipments
-            if all(
-                m.state not in {'staging', 'draft'}
-                for m in s.assign_moves if m.assignation_required)]
-        if to_assign:
-            cls.assign(to_assign)
+        with cls.bulk_func('assign', auto=False) as assign:
+            for shipment in shipments:
+                if all(
+                        m.state not in {'staging', 'draft'}
+                        for m in shipment.assign_moves
+                        if m.assignation_required):
+                    assign.push(shipment)
 
     @classmethod
     def _get_assign_domain(cls):
@@ -767,16 +767,21 @@
     @set_employee('received_by')
     def receive(cls, shipments):
         Move = Pool().get('stock.move')
-        Move.do([m for s in shipments for m in s.incoming_moves])
-        Move.delete([m for s in shipments for m in s.inventory_moves
-            if m.state in ('draft', 'cancelled')])
+        with Move.bulk_func('do', auto=False) as do:
+            for shipment in shipments:
+                do.extend(shipment.incoming_moves)
+        with Move.bulk_delete(auto=False) as delete:
+            for shipment in shipments:
+                for move in shipment.inventory_moves:
+                    if move.state in {'draft', 'cancelled'}:
+                        delete.push(move)
         cls.create_inventory_moves(shipments)
         # Set received state to allow done transition
         cls.write(shipments, {'state': 'received'})
-        to_do = [s for s in shipments
-            if s.warehouse_storage == s.warehouse_input]
-        if to_do:
-            cls.do(to_do)
+        with cls.bulk_func('do', auto=False) as do:
+            for shipment in shipments:
+                if shipment.warehouse_storage == shipment.warehouse_input:
+                    do.push(shipment)
 
     @classmethod
     @ModelView.button
@@ -1129,16 +1134,14 @@
         if success:
             cls.assign(shipments)
         else:
-            to_assign = []
-            for shipment in shipments:
-                if any(
-                        m.state in {'staging', 'draft'}
-                        for m in shipment.assign_moves
-                        if m.assignation_required):
-                    continue
-                to_assign.append(shipment)
-            if to_assign:
-                cls.assign(to_assign)
+            with cls.bulk_func('assign', auto=False) as assign:
+                for shipment in shipments:
+                    if any(
+                            m.state in {'staging', 'draft'}
+                            for m in shipment.assign_moves
+                            if m.assignation_required):
+                        continue
+                    assign.push(shipment)
 
     @classmethod
     def _get_reschedule_domain(cls, date):
@@ -1540,25 +1543,25 @@
         else:
             assert all(m.shipment in shipments for m in moves)
         Move.draft(moves)
-        Move.delete([m for s in shipments for m in s.inventory_moves
-                if m.state in ('draft', 'cancelled')])
+        with Move.bulk_delete(auto=False) as delete:
+            for shipment in shipments:
+                for move in shipment.inventory_moves:
+                    if move.state in {'draft', 'cancelled'}:
+                        delete.push(move)
         Move.draft([
                 m for s in shipments for m in s.outgoing_moves
                 if m.state != 'staging'])
 
-        to_create = []
-        for shipment in shipments:
-            if shipment.warehouse_storage == shipment.warehouse_output:
-                # Do not create inventory moves
-                continue
-            for move in shipment.outgoing_moves:
-                if move.state in ('cancelled', 'done'):
+        with Move.bulk_save(auto=False) as save:
+            for shipment in shipments:
+                if shipment.warehouse_storage == shipment.warehouse_output:
+                    # Do not create inventory moves
                     continue
-                inventory_move = shipment._get_inventory_move(move)
-                if inventory_move:
-                    to_create.append(inventory_move)
-        if to_create:
-            Move.save(to_create)
+                for move in shipment.outgoing_moves:
+                    if move.state in {'cancelled', 'done'}:
+                        continue
+                    if inventory_move := shipment._get_inventory_move(move):
+                        save.push(inventory_move)
         cls.set_number(shipments)
 
     def _get_inventory_move(self, move):
@@ -1624,21 +1627,21 @@
     def pack(cls, shipments):
         pool = Pool()
         Move = pool.get('stock.move')
-        outgoing_moves, to_delete = [], []
-        for shipment in shipments:
-            for move in shipment.inventory_moves:
-                if move.state not in {'done', 'cancelled'}:
-                    raise AccessError(
-                        gettext('stock.msg_shipment_pack_inventory_done',
-                            shipment=shipment.rec_name))
-            if shipment.warehouse_storage != shipment.warehouse_output:
-                shipment.check_quantity()
-            for move in shipment.outgoing_moves:
-                if move.quantity:
-                    outgoing_moves.append(move)
-                else:
-                    to_delete.append(move)
-        Move.delete(to_delete)
+        with Move.bulk_delete() as delete:
+            outgoing_moves = []
+            for shipment in shipments:
+                for move in shipment.inventory_moves:
+                    if move.state not in {'done', 'cancelled'}:
+                        raise AccessError(
+                            gettext('stock.msg_shipment_pack_inventory_done',
+                                shipment=shipment.rec_name))
+                if shipment.warehouse_storage != shipment.warehouse_output:
+                    shipment.check_quantity()
+                for move in shipment.outgoing_moves:
+                    if move.quantity:
+                        outgoing_moves.append(move)
+                    else:
+                        delete.push(move)
         Move.assign(outgoing_moves)
 
     @property
@@ -1846,16 +1849,14 @@
         if Move.assign_try(to_assign):
             cls.assign(shipments)
         else:
-            to_assign = []
-            for shipment in shipments:
-                if any(
-                        m.state in {'staging', 'draft'}
-                        for m in shipment.assign_moves
-                        if m.assignation_required):
-                    continue
-                to_assign.append(shipment)
-            if to_assign:
-                cls.assign(to_assign)
+            with cls.bulk_func('assign', auto=False) as assign:
+                for shipment in shipments:
+                    if any(
+                            m.state in {'staging', 'draft'}
+                            for m in shipment.assign_moves
+                            if m.assignation_required):
+                        continue
+                    assign.push(shipment)
 
     @classmethod
     def _get_reschedule_domain(cls, date):
@@ -2228,15 +2229,19 @@
     @Workflow.transition('received')
     @set_employee('received_by')
     def receive(cls, shipments):
-        Move = Pool().get('stock.move')
-        Move.do([m for s in shipments for m in s.incoming_moves])
+        pool = Pool()
+        Move = pool.get('stock.move')
+
+        with Move.bulk_func('do', auto=False) as do:
+            for shipment in shipments:
+                do.extend(shipment.incoming_moves)
         cls.create_inventory_moves(shipments)
         # Set received state to allow done transition
         cls.write(shipments, {'state': 'received'})
-        to_do = [s for s in shipments
-            if s.warehouse_storage == s.warehouse_input]
-        if to_do:
-            cls.do(to_do)
+        with cls.bulk_func('do', auto=False) as do:
+            for shipment in shipments:
+                if shipment.warehouse_storage == shipment.warehouse_input:
+                    do.push(shipment)
 
     @classmethod
     @ModelView.button
@@ -2982,16 +2987,14 @@
         if Move.assign_try(to_assign):
             cls.assign(shipments)
         else:
-            to_assign = []
-            for shipment in shipments:
-                if any(
-                        m.state in {'staging', 'draft'}
-                        for m in shipment.assign_moves
-                        if m.assignation_required):
-                    continue
-                to_assign.append(shipment)
-            if to_assign:
-                cls.assign(to_assign)
+            with cls.bulk_func('assign', auto=False) as assign:
+                for shipment in shipments:
+                    if any(
+                            m.state in {'staging', 'draft'}
+                            for m in shipment.assign_moves
+                            if m.assignation_required):
+                        continue
+                    assign.push(shipment)
 
     @property
     def _move_planned_date(self):
diff -r 6fc18060d097 -r bc1c607f913d modules/stock_consignment/stock.py
--- a/modules/stock_consignment/stock.py        Wed Jul 15 09:31:21 2026 +0200
+++ b/modules/stock_consignment/stock.py        Wed Jul 15 09:31:05 2026 +0200
@@ -62,16 +62,15 @@
         def wrapper(cls, moves):
             pool = Pool()
             InvoiceLine = pool.get('account.invoice.line')
-            to_save = []
             move2line = {}
-            for move in moves:
-                if not move.consignment_invoice_lines:
-                    lines = move.get_invoice_lines_consignment()
-                    if lines:
-                        to_save.extend(lines)
-                        move2line[move] = lines[0]
-            if to_save:
-                InvoiceLine.save(to_save)
+            with InvoiceLine.bulk_save() as save:
+                for move in moves:
+                    if not move.consignment_invoice_lines:
+                        lines = move.get_invoice_lines_consignment()
+                        if lines:
+                            save.extend(lines)
+                            move2line[move] = lines[0]
+            with cls.bulk_save() as save:
                 for move, line in move2line.items():
                     if not move.origin:
                         move.origin = line
@@ -84,7 +83,7 @@
                         move.unit_price = None
                         move.currency = None
                     move.state = original_state
-                cls.save(list(move2line.keys()))
+                    save.push(move)
             return func(cls, moves)
         return wrapper
     return decorator
@@ -96,22 +95,18 @@
         def wrapper(cls, moves):
             pool = Pool()
             InvoiceLine = pool.get('account.invoice.line')
-            lines, to_save = [], set()
-            for move in moves:
-                for invoice_line in move.consignment_invoice_lines:
-                    lines.append(invoice_line)
-                    if move.origin == move:
-                        move.origin = None
-                    to_save.add(move)
-                if (not move.on_change_with_unit_price_required()
-                        and (move.unit_price or move.currency)):
-                    move.unit_price = None
-                    move.currency = None
-                    to_save.add(move)
-            if lines:
-                InvoiceLine.delete(lines)
-            if to_save:
-                cls.save(list(to_save))
+            with InvoiceLine.bulk_delete() as delete, \
+                    cls.bulk_save() as save:
+                for move in moves:
+                    for invoice_line in move.consignment_invoice_lines:
+                        delete.push(invoice_line)
+                        if move.origin == move:
+                            move.origin = None
+                    if (not move.on_change_with_unit_price_required()
+                            and (move.unit_price or move.currency)):
+                        move.unit_price = None
+                        move.currency = None
+                    save.push(move)
             return func(cls, moves)
         return wrapper
     return decorator
@@ -339,13 +334,11 @@
 
         moves = super().copy(moves, default=default)
         if not Transaction().context.get('_stock_move_split'):
-            to_save = []
-            for move in moves:
-                if isinstance(move.origin, InvoiceLine):
-                    move.origin = None
-                    to_save.append(move)
-            if to_save:
-                cls.save(to_save)
+            with cls.bulk_save() as save:
+                for move in moves:
+                    if isinstance(move.origin, InvoiceLine):
+                        move.origin = None
+                        save.push(move)
         return moves
 
 
diff -r 6fc18060d097 -r bc1c607f913d modules/stock_forecast/forecast.py
--- a/modules/stock_forecast/forecast.py        Wed Jul 15 09:31:21 2026 +0200
+++ b/modules/stock_forecast/forecast.py        Wed Jul 15 09:31:05 2026 +0200
@@ -221,12 +221,12 @@
         'Create stock moves for the forecast ids'
         pool = Pool()
         Move = pool.get('stock.move')
-        to_save = []
-        for forecast in forecasts:
-            if forecast.state == 'done':
-                for line in forecast.lines:
-                    to_save.extend(line.get_moves())
-        Move.save(to_save)
+
+        with Move.bulk_save() as save:
+            for forecast in forecasts:
+                if forecast.state == 'done':
+                    for line in forecast.lines:
+                        save.extend(line.get_moves())
 
     @staticmethod
     def delete_moves(forecasts):
@@ -535,14 +535,13 @@
         ForecastLine = pool.get('stock.forecast.line')
 
         product2line = {l.product: l for l in self.record.lines}
-        to_save = []
         # Ensure context is set
         self.ask.products = map(int, self.ask.products)
-        for product in self.ask.products:
-            line = product2line.get(product, ForecastLine())
-            self._fill_line(line, product)
-            to_save.append(line)
-        ForecastLine.save(to_save)
+        with ForecastLine.bulk_save() as save:
+            for product in self.ask.products:
+                line = product2line.get(product, ForecastLine())
+                self._fill_line(line, product)
+                save.push(line)
         return 'end'
 
     def _fill_line(self, line, product):
diff -r 6fc18060d097 -r bc1c607f913d 
modules/stock_quantity_early_planning/stock.py
--- a/modules/stock_quantity_early_planning/stock.py    Wed Jul 15 09:31:21 
2026 +0200
+++ b/modules/stock_quantity_early_planning/stock.py    Wed Jul 15 09:31:05 
2026 +0200
@@ -323,13 +323,12 @@
                         cls._add(parent, plans)
         cls.save(plans.values())
 
-        to_delete = []
-        for plan in cls.browse(plans.values()):
-            if (plan.state == 'open'
-                    and not isinstance(plan.origin, Move)
-                    and plan.earliest_date == plan.planned_date):
-                to_delete.append(plan)
-        cls.delete(to_delete)
+        with cls.bulk_delete() as delete:
+            for plan in cls.browse(plans.values()):
+                if (plan.state == 'open'
+                        and not isinstance(plan.origin, Move)
+                        and plan.earliest_date == plan.planned_date):
+                    delete.push(plan)
 
         # Update early date based on internal incoming requests
         for warehouse in warehouses:
@@ -354,32 +353,30 @@
                     product2in[product][plan.planned_date].append(
                         (quantity, plan))
 
-            to_save = []
-            products = set()
-            for product_plan in product_plans:
-                if product_plan.warehouse != warehouse:
-                    continue
-                product = product_plan.origin.product
-                quantity = product_plan.origin.internal_quantity
-                plans = product2in[product][product_plan.early_date]
-                plans = cls._pick_incoming(quantity, plans)
-                if plans:
-                    incoming_products = {p
-                        for pl in plans
-                        for p, q in pl._incoming_quantities(warehouse)}
-                    if incoming_products & products:
-                        cls.save(to_save)
-                        del to_save[:]
-                        products.clear()
+            with cls.bulk_save(auto=False) as save:
+                products = set()
+                for product_plan in product_plans:
+                    if product_plan.warehouse != warehouse:
+                        continue
+                    product = product_plan.origin.product
+                    quantity = product_plan.origin.internal_quantity
+                    plans = product2in[product][product_plan.early_date]
+                    plans = cls._pick_incoming(quantity, plans)
+                    if plans:
+                        incoming_products = {p
+                            for pl in plans
+                            for p, q in pl._incoming_quantities(warehouse)}
+                        if incoming_products & products:
+                            save.flush()
+                            products.clear()
 
-                    earlier_date = max(p.earlier_date for p in plans)
+                        earlier_date = max(p.earlier_date for p in plans)
 
-                    if (not product_plan.early_date
-                            or product_plan.early_date > earlier_date):
-                        product_plan.early_date = earlier_date
-                        to_save.append(product_plan)
-                        products.add(product)
-            cls.save(to_save)
+                        if (not product_plan.early_date
+                                or product_plan.early_date > earlier_date):
+                            product_plan.early_date = earlier_date
+                            save.push(product_plan)
+                            products.add(product)
 
     @classmethod
     def _get_earlier_date(cls, move, warehouse):
diff -r 6fc18060d097 -r bc1c607f913d modules/stock_split/stock.py
--- a/modules/stock_split/stock.py      Wed Jul 15 09:31:21 2026 +0200
+++ b/modules/stock_split/stock.py      Wed Jul 15 09:31:05 2026 +0200
@@ -107,20 +107,20 @@
         def unit_precision(unit):
             return unit.factor * unit.rounding
 
-        to_save, to_clear = [], []
-        for group in groups:
-            if len(group) <= 1:
-                continue
-            quantity = sum(m.internal_quantity for m in group)
-            unit = min((m.unit for m in group), key=unit_precision)
-            move, *others = group
-            move.quantity = UoM.compute_qty(
-                move.product.default_uom, quantity, unit)
-            move.unit = unit
-            to_save.append(move)
-            to_clear.extend(others)
+        to_clear = []
+        with cls.bulk_save() as save:
+            for group in groups:
+                if len(group) <= 1:
+                    continue
+                quantity = sum(m.internal_quantity for m in group)
+                unit = min((m.unit for m in group), key=unit_precision)
+                move, *others = group
+                move.quantity = UoM.compute_qty(
+                    move.product.default_uom, quantity, unit)
+                move.unit = unit
+                save.push(move)
+                to_clear.extend(others)
         cls.write(to_clear, {'quantity': 0})
-        cls.save(to_save)
 
 
 class SplitMoveStart(ModelView):
diff -r 6fc18060d097 -r bc1c607f913d modules/stock_supply/purchase_request.py
--- a/modules/stock_supply/purchase_request.py  Wed Jul 15 09:31:21 2026 +0200
+++ b/modules/stock_supply/purchase_request.py  Wed Jul 15 09:31:05 2026 +0200
@@ -141,22 +141,23 @@
                 ('company', '=', company.id),
                 ('origin', 'like', 'stock.order_point,%'),
                 ])
-        reqs = [r for r in reqs
-            if r.product in products and r.warehouse in warehouses]
-        cls.delete(reqs)
+        with cls.bulk_delete() as delete:
+            for request in reqs:
+                if (request.product in products
+                        and request.warehouse in warehouses):
+                    delete.push(request)
         new_requests = cls.compare_requests(new_requests, company)
 
         cls.create_requests(new_requests)
 
     @classmethod
     def create_requests(cls, new_requests):
-        to_save = []
-        for new_req in new_requests:
-            if new_req.supply_date == datetime.date.max:
-                new_req.supply_date = None
-            if new_req.computed_quantity > 0:
-                to_save.append(new_req)
-        cls.save(to_save)
+        with cls.bulk_save() as save:
+            for new_req in new_requests:
+                if new_req.supply_date == datetime.date.max:
+                    new_req.supply_date = None
+                if new_req.computed_quantity > 0:
+                    save.push(new_req)
 
     @classmethod
     def compare_requests(cls, new_requests, company):
diff -r 6fc18060d097 -r bc1c607f913d modules/web_shop_shopify/account.py
--- a/modules/web_shop_shopify/account.py       Wed Jul 15 09:31:21 2026 +0200
+++ b/modules/web_shop_shopify/account.py       Wed Jul 15 09:31:05 2026 +0200
@@ -133,33 +133,33 @@
                 refunded[payment] += Decimal(
                     transaction['amountSet']['presentmentMoney']['amount'])
 
-        to_save = []
-        for payment in id2payments.values():
-            if payment.kind == 'payable':
-                amount = refunded[payment]
-            else:
-                amount = captured[payment]
-            if payment.amount != amount:
-                payment.amount = captured[payment]
-                to_save.append(payment)
-        cls.proceed(to_save)
-        cls.save(to_save)
+        with cls.bulk_func('proceed', auto=False) as proceed, \
+                cls.bulk_save() as save:
+            for payment in id2payments.values():
+                if payment.kind == 'payable':
+                    amount = refunded[payment]
+                else:
+                    amount = captured[payment]
+                if payment.amount != amount:
+                    payment.amount = captured[payment]
+                    save.push(payment)
+                    proceed.push(payment)
 
-        to_succeed, to_fail, to_proceed = set(), set(), set()
-        for transaction_id, payment in id2payments.items():
-            amount = captured[payment] + voided[payment] + refunded[payment]
-            if amounts[transaction_id] == amount:
-                if payment.amount:
-                    if payment.state != 'succeeded':
-                        to_succeed.add(payment)
-                else:
-                    if payment.state != 'failed':
-                        to_fail.add(payment)
-            elif payment.state != 'processing':
-                to_proceed.add(payment)
-        cls.fail(to_fail)
-        cls.proceed(to_proceed)
-        cls.succeed(to_succeed)
+        with cls.bulk_func('succeed', auto=False) as succeed, \
+                cls.bulk_func('proceed', auto=False) as proceed, \
+                cls.bulk_func('fail', auto=False) as fail:
+            for transaction_id, payment in id2payments.items():
+                amount = (
+                    captured[payment] + voided[payment] + refunded[payment])
+                if amounts[transaction_id] == amount:
+                    if payment.amount:
+                        if payment.state != 'succeeded':
+                            succeed.push(payment)
+                    else:
+                        if payment.state != 'failed':
+                            fail.push(payment)
+                elif payment.state != 'processing':
+                    proceed.push(payment)
 
         return list(id2payments.values())
 
diff -r 6fc18060d097 -r bc1c607f913d modules/web_shop_shopify/product.py
--- a/modules/web_shop_shopify/product.py       Wed Jul 15 09:31:21 2026 +0200
+++ b/modules/web_shop_shopify/product.py       Wed Jul 15 09:31:05 2026 +0200
@@ -712,11 +712,11 @@
         Identifier = pool.get('web.shop.shopify_identifier')
         callback = super().on_write(images, values)
         if values.keys() & {'image', 'template', 'web_shop'}:
-            to_delete = []
+            delete = Identifier.bulk_delete(auto=False)
             for image in images:
-                to_delete.extend(image.shopify_identifiers)
-            if to_delete:
-                callback.append(lambda: Identifier.delete(to_delete))
+                delete.extend(image.shopify_identifiers)
+            if delete:
+                callback.append(delete.flush)
         return callback
 
     @classmethod
diff -r 6fc18060d097 -r bc1c607f913d trytond/trytond/ir/message.py
--- a/trytond/trytond/ir/message.py     Wed Jul 15 09:31:21 2026 +0200
+++ b/trytond/trytond/ir/message.py     Wed Jul 15 09:31:05 2026 +0200
@@ -125,32 +125,30 @@
                     ]):
             other_translations[translation.res_id].append(translation)
 
-        to_save = []
-        for message in messages:
-            translation = id2translations.get(message.id)
-            if translation:
-                if ((translation.src == message.text)
-                        and (translation.src_plural == message.text_plural)):
-                    continue
-            else:
-                translation = Translation(
-                    lang=INTERNAL_LANG,
-                    type='model',
-                    name=f'{cls.__name__},text',
-                    res_id=message.id)
-            translation.src = message.text
-            translation.src_plural = message.text_plural
-            translation.value = message.text
-            translation.value_1 = message.text_plural
-            to_save.append(translation)
-            for other_translation in other_translations[message.id]:
-                other_translation.src = message.text
-                other_translation.src_plural = message.text_plural
-                other_translation.fuzzy = True
-                to_save.append(other_translation)
-
-        if to_save:
-            Translation.save(to_save)
+        with Translation.bulk_save() as save:
+            for message in messages:
+                translation = id2translations.get(message.id)
+                if translation:
+                    if ((translation.src == message.text)
+                            and (translation.src_plural
+                                == message.text_plural)):
+                        continue
+                else:
+                    translation = Translation(
+                        lang=INTERNAL_LANG,
+                        type='model',
+                        name=f'{cls.__name__},text',
+                        res_id=message.id)
+                translation.src = message.text
+                translation.src_plural = message.text_plural
+                translation.value = message.text
+                translation.value_1 = message.text_plural
+                save.push(translation)
+                for other_translation in other_translations[message.id]:
+                    other_translation.src = message.text
+                    other_translation.src_plural = message.text_plural
+                    other_translation.fuzzy = True
+                    save.push(other_translation)
 
     @classmethod
     def search_rec_name(cls, name, clause):
diff -r 6fc18060d097 -r bc1c607f913d trytond/trytond/ir/module.py
--- a/trytond/trytond/ir/module.py      Wed Jul 15 09:31:21 2026 +0200
+++ b/trytond/trytond/ir/module.py      Wed Jul 15 09:31:05 2026 +0200
@@ -266,18 +266,15 @@
             name2module[name] = cls(name=name, state=cls.default_state())
         cls.save(name2module.values())
 
-        to_save, to_delete = [], []
-        for module in name2module.values():
-            depends = set(get_module_info(module.name).get('depends', []))
-            for dependency in module.dependencies:
-                if dependency.name not in depends:
-                    to_delete.append(dependency)
-            for name in depends - {d.name for d in module.dependencies}:
-                to_save.append(Dependency(name=name, module=module))
-        if to_delete:
-            Dependency.delete(to_delete)
-        if to_save:
-            Dependency.save(to_save)
+        with Dependency.bulk_save(auto=False) as save, \
+                Dependency.bulk_delete(auto=False) as delete:
+            for module in name2module.values():
+                depends = set(get_module_info(module.name).get('depends', []))
+                for dependency in module.dependencies:
+                    if dependency.name not in depends:
+                        delete.push(dependency)
+                for name in depends - {d.name for d in module.dependencies}:
+                    save.push(Dependency(name=name, module=module))
 
 
 class ModuleDependency(ModelSQL, ModelView):
diff -r 6fc18060d097 -r bc1c607f913d trytond/trytond/ir/session.py
--- a/trytond/trytond/ir/session.py     Wed Jul 15 09:31:21 2026 +0200
+++ b/trytond/trytond/ir/session.py     Wed Jul 15 09:31:05 2026 +0200
@@ -91,17 +91,16 @@
                 domain or [],
                 ])
         find, last_reset = None, None
-        to_delete = []
-        for session in sessions:
-            if abs(session.create_date - now) < timeout:
-                if compare_digest(session.key, key):
-                    find = True
-                    last_reset = session.last_modified_at
-            else:
-                if find is None and compare_digest(session.key, key):
-                    find = False
-                to_delete.append(session)
-        cls.delete(to_delete)
+        with cls.bulk_delete() as delete:
+            for session in sessions:
+                if abs(session.create_date - now) < timeout:
+                    if compare_digest(session.key, key):
+                        find = True
+                        last_reset = session.last_modified_at
+                else:
+                    if find is None and compare_digest(session.key, key):
+                        find = False
+                    delete.push(session)
         if find:
             cls._session_reset_cache.set(key, last_reset)
         return find
diff -r 6fc18060d097 -r bc1c607f913d trytond/trytond/ir/translation.py
--- a/trytond/trytond/ir/translation.py Wed Jul 15 09:31:21 2026 +0200
+++ b/trytond/trytond/ir/translation.py Wed Jul 15 09:31:05 2026 +0200
@@ -572,31 +572,30 @@
                         ]):
                 name2translations[translation.name].append(translation)
 
-            to_save, to_delete = [], []
-            for record, value in zip(records, values):
-                translations = name2translations[get_name(record)]
-                if lang == INTERNAL_LANG:
-                    src = value
-                else:
-                    src = getattr(record, field_name)
-                if not translations:
+            with cls.bulk_save() as save, \
+                    cls.bulk_delete() as delete:
+                for record, value in zip(records, values):
+                    translations = name2translations[get_name(record)]
+                    if lang == INTERNAL_LANG:
+                        src = value
+                    else:
+                        src = getattr(record, field_name)
+                    if not translations:
+                        if not src and not value:
+                            continue
+                        translation = cls()
+                        translation.name = get_name(record)
+                        translation.lang = lang
+                        translation.type = ttype
+                        translations.append(translation)
                     if not src and not value:
-                        continue
-                    translation = cls()
-                    translation.name = get_name(record)
-                    translation.lang = lang
-                    translation.type = ttype
-                    translations.append(translation)
-                if not src and not value:
-                    to_delete.extend(translations)
-                else:
-                    for translation in translations:
-                        translation.src = src
-                        translation.value = value
-                        translation.fuzzy = False
-                        to_save.append(translation)
-            cls.save(to_save)
-            cls.delete(to_delete)
+                        delete.extend(translations)
+                    else:
+                        for translation in translations:
+                            translation.src = src
+                            translation.value = value
+                            translation.fuzzy = False
+                            save.push(translation)
             return
 
         Model = pool.get(model_name)
@@ -623,41 +622,40 @@
                         ]):
                 other_translations[translation.res_id].append(translation)
 
-        to_save, to_delete = [], []
-        for record, value in zip(records, values):
-            translations = id2translations[record.id]
-            if lang == Config.get_language():
-                src = value
-            else:
-                src = getattr(record, field_name)
-            if not translations:
+        with cls.bulk_save() as save, \
+                cls.bulk_delete() as delete:
+            for record, value in zip(records, values):
+                translations = id2translations[record.id]
+                if lang == Config.get_language():
+                    src = value
+                else:
+                    src = getattr(record, field_name)
+                if not translations:
+                    if not src and not value:
+                        continue
+                    translation = cls()
+                    translation.name = name
+                    translation.lang = lang
+                    translation.type = ttype
+                    translation.res_id = record.id
+                    translations.append(translation)
+                else:
+                    other_langs = other_translations[record.id]
+                    if not src and not value:
+                        delete.extend(other_langs)
+                    else:
+                        for other_lang in other_langs:
+                            other_lang.src = src
+                            other_lang.fuzzy = True
+                            save.push(other_lang)
                 if not src and not value:
-                    continue
-                translation = cls()
-                translation.name = name
-                translation.lang = lang
-                translation.type = ttype
-                translation.res_id = record.id
-                translations.append(translation)
-            else:
-                other_langs = other_translations[record.id]
-                if not src and not value:
-                    to_delete.extend(other_langs)
+                    delete.extend(translations)
                 else:
-                    for other_lang in other_langs:
-                        other_lang.src = src
-                        other_lang.fuzzy = True
-                        to_save.append(other_lang)
-            if not src and not value:
-                to_delete.extend(translations)
-            else:
-                for translation in translations:
-                    translation.value = value
-                    translation.src = src
-                    translation.fuzzy = False
-                    to_save.append(translation)
-        cls.save(to_save)
-        cls.delete(to_delete)
+                    for translation in translations:
+                        translation.value = value
+                        translation.src = src
+                        translation.fuzzy = False
+                        save.push(translation)
 
     @classmethod
     @without_check_access
@@ -1015,12 +1013,13 @@
         translations |= set(to_save)
 
         if translations:
-            all_translations = set(cls.search([
-                        ('module', '=', module),
-                        ('lang', '=', lang),
-                        ]))
-            translations_to_delete = all_translations - translations
-            cls.delete(list(translations_to_delete))
+            with cls.bulk_delete(auto=False) as delete:
+                all_translations = set(cls.search([
+                            ('module', '=', module),
+                            ('lang', '=', lang),
+                            ]))
+                translations_to_delete = all_translations - translations
+                delete.extend(translations_to_delete)
         return len(translations)
 
     @classmethod
@@ -1639,8 +1638,8 @@
                     where=(translation.lang == lang)
                     & source_clause
                     & translation.type.in_(self._source_types))))
-        if to_create := list(cursor):
-            Translation.create(to_create)
+        with Translation.bulk_create() as create:
+            create.extend(cursor)
 
         if parent_lang:
             columns.append(translation.value)
@@ -1679,8 +1678,8 @@
                 - translation.select(*columns,
                     where=(translation.lang == lang)
                     & translation.type.in_(self._ressource_types))))
-        if to_create := list(cursor):
-            Translation.create(to_create)
+        with Translation.bulk_create() as create:
+            create.extend(cursor)
 
         if parent_lang:
             columns.append(translation.value)
diff -r 6fc18060d097 -r bc1c607f913d trytond/trytond/ir/trigger.py
--- a/trytond/trytond/ir/trigger.py     Wed Jul 15 09:31:21 2026 +0200
+++ b/trytond/trytond/ir/trigger.py     Wed Jul 15 09:31:05 2026 +0200
@@ -249,14 +249,12 @@
         if records:
             getattr(ActionModel, method)(records, self)
         if self.limit_number or self.minimum_time_delay:
-            to_create = []
-            for record in records:
-                to_create.append({
-                        'trigger': self.id,
-                        'record_id': record.id,
-                        })
-            if to_create:
-                TriggerLog.create(to_create)
+            with TriggerLog.bulk_create() as create:
+                for record in records:
+                    create.push({
+                            'trigger': self.id,
+                            'record_id': record.id,
+                            })
 
     @classmethod
     def trigger_time(cls):
diff -r 6fc18060d097 -r bc1c607f913d trytond/trytond/ir/ui/menu.py
--- a/trytond/trytond/ir/ui/menu.py     Wed Jul 15 09:31:21 2026 +0200
+++ b/trytond/trytond/ir/ui/menu.py     Wed Jul 15 09:31:05 2026 +0200
@@ -248,16 +248,14 @@
             return
         Action = pool.get(action_type)
         action = Action(int(action_id))
-        to_create = []
-        for menu in menus:
-            with Transaction().set_context(_timestamp=False):
-                to_create.append({
+        with Transaction().set_context(_timestamp=False), \
+                ActionKeyword.bulk_create() as create:
+            for menu in menus:
+                create.push({
                         'keyword': 'tree_open',
                         'model': str(menu),
                         'action': action.action.id,
                         })
-        if to_create:
-            ActionKeyword.create(to_create)
 
     @classmethod
     def get_favorite(cls, menus, name):
diff -r 6fc18060d097 -r bc1c607f913d trytond/trytond/ir/ui/view.py
--- a/trytond/trytond/ir/ui/view.py     Wed Jul 15 09:31:21 2026 +0200
+++ b/trytond/trytond/ir/ui/view.py     Wed Jul 15 09:31:05 2026 +0200
@@ -618,30 +618,27 @@
             order=[('occurrence', 'DESC')])
 
         fields = copy.deepcopy(fields)
-        to_save = []
-        for tree_width in records:
-            if tree_width.screen_width == screen_width:
-                index = tree_width.occurrence - 1
-                if index < len(fields[tree_width.field]):
-                    width = fields[tree_width.field][index]
-                    fields[tree_width.field][index] = None
-                    if width is not None:
-                        tree_width.width = width
-                        to_save.append(tree_width)
+        with cls.bulk_save() as save:
+            for tree_width in records:
+                if tree_width.screen_width == screen_width:
+                    index = tree_width.occurrence - 1
+                    if index < len(fields[tree_width.field]):
+                        width = fields[tree_width.field][index]
+                        fields[tree_width.field][index] = None
+                        if width is not None:
+                            tree_width.width = width
+                            save.push(tree_width)
 
-        for name, widths in fields.items():
-            for occurrence, width in enumerate(widths, start=1):
-                if width is not None:
-                    to_save.append(cls(
-                            user=user_id,
-                            model=model,
-                            field=name,
-                            occurrence=occurrence,
-                            screen_width=screen_width,
-                            width=width))
-
-        if to_save:
-            cls.save(to_save)
+            for name, widths in fields.items():
+                for occurrence, width in enumerate(widths, start=1):
+                    if width is not None:
+                        save.push(cls(
+                                user=user_id,
+                                model=model,
+                                field=name,
+                                occurrence=occurrence,
+                                screen_width=screen_width,
+                                width=width))
 
     @classmethod
     def reset_width(cls, model, width):
@@ -792,27 +789,25 @@
             order=[('occurrence', 'DESC')])
 
         fields = copy.deepcopy(fields)
-        to_save = []
 
-        for tree_optional in records:
-            index = tree_optional.occurrence - 1
-            if index < len(fields[tree_optional.field]):
-                tree_optional.value = fields[tree_optional.field][index]
-                fields[tree_optional.field][index] = None
-                to_save.append(tree_optional)
+        with cls.bulk_save() as save:
+            for tree_optional in records:
+                index = tree_optional.occurrence - 1
+                if index < len(fields[tree_optional.field]):
+                    tree_optional.value = fields[tree_optional.field][index]
+                    fields[tree_optional.field][index] = None
+                    save.push(tree_optional)
 
-        for name, optionals in fields.items():
-            for occurrence, optional in enumerate(optionals, start=1):
-                if optional is not None:
-                    to_save.append(cls(
-                            view=view,
-                            user=user,
-                            model=view.model,
-                            field=name,
-                            occurrence=occurrence,
-                            value=optional))
-        if to_save:
-            cls.save(to_save)
+            for name, optionals in fields.items():
+                for occurrence, optional in enumerate(optionals, start=1):
+                    if optional is not None:
+                        save.push(cls(
+                                view=view,
+                                user=user,
+                                model=view.model,
+                                field=name,
+                                occurrence=occurrence,
+                                value=optional))
 
 
 class ViewTreeState(
diff -r 6fc18060d097 -r bc1c607f913d trytond/trytond/model/multivalue.py
--- a/trytond/trytond/model/multivalue.py       Wed Jul 15 09:31:21 2026 +0200
+++ b/trytond/trytond/model/multivalue.py       Wed Jul 15 09:31:05 2026 +0200
@@ -100,10 +100,9 @@
     @classmethod
     def _multivalue_setter(cls, records, name, val):
         Value = cls.multivalue_model(name)
-        to_save = []
-        for record in records:
-            to_save.extend(record.set_multivalue(name, val, save=False))
-        Value.save(to_save)
+        with Value.bulk_save() as save:
+            for record in records:
+                save.extend(record.set_multivalue(name, val, save=False))
 
 
 class ValueMixin(MatchMixin, ModelStorage):

Reply via email to