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):