From 234db70d861a7bc9c6620f9ef6a24dbe98eabb98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Voet=20=28ryv=29?= Date: Tue, 21 Feb 2023 12:34:22 +0000 Subject: [PATCH] [IMP] *: Use the new API of `_read_group` for backend use Part-of: odoo/odoo#110737 --- addons/account/models/account_account.py | 15 +-- .../models/account_journal_dashboard.py | 12 +- .../account/models/account_reconcile_model.py | 4 +- addons/account/models/company.py | 8 +- addons/account/models/partner.py | 8 +- .../populate/account_bank_statement.py | 13 +- .../account_debit_note/models/account_move.py | 4 +- addons/account_fleet/models/fleet_vehicle.py | 4 +- .../account_payment/models/account_payment.py | 6 +- addons/analytic/models/analytic_account.py | 23 ++-- addons/calendar/models/calendar_recurrence.py | 7 +- addons/crm/models/crm_lead.py | 17 +-- addons/crm/models/crm_lost_reason.py | 4 +- addons/crm/models/crm_team.py | 20 ++- addons/crm/models/res_partner.py | 10 +- addons/crm/models/utm.py | 4 +- .../models/crm_iap_lead_mining_request.py | 15 +-- addons/crm_livechat/models/chatbot_script.py | 9 +- .../data_recycle/models/data_recycle_model.py | 4 +- addons/digest/models/digest.py | 7 +- addons/event_booth/models/event_event.py | 15 +-- addons/event_booth_sale/models/sale_order.py | 13 +- addons/event_crm/models/event_event.py | 4 +- addons/event_sale/models/event_event.py | 56 ++------ addons/event_sale/models/sale_order.py | 6 +- addons/fleet/models/fleet_vehicle.py | 24 ++-- addons/fleet/models/fleet_vehicle_model.py | 4 +- .../fleet/models/fleet_vehicle_model_brand.py | 4 +- .../models/gamification_challenge.py | 9 +- .../gamification/models/gamification_goal.py | 28 ++-- .../models/gamification_karma_rank.py | 4 +- addons/hr/models/hr_department.py | 8 +- addons/hr/models/hr_job.py | 4 +- addons/hr/models/hr_plan.py | 4 +- addons/hr_attendance/models/hr_employee.py | 6 - addons/hr_contract/models/resource.py | 4 +- .../hr_contract/models/resource_resource.py | 4 +- addons/hr_expense/models/hr_department.py | 4 +- addons/hr_expense/models/hr_expense.py | 19 ++- addons/hr_fleet/models/employee.py | 4 +- .../models/fleet_vehicle_assignation_log.py | 4 +- addons/hr_holidays/models/hr_department.py | 12 +- addons/hr_holidays/models/hr_employee.py | 10 +- addons/hr_holidays/models/hr_leave.py | 15 +-- .../models/hr_leave_accrual_plan.py | 8 +- .../hr_holidays/models/hr_leave_allocation.py | 14 +- addons/hr_holidays/models/hr_leave_type.py | 12 +- addons/hr_holidays/models/res_users.py | 5 +- addons/hr_holidays/models/resource.py | 8 +- addons/hr_recruitment/models/hr_applicant.py | 4 +- addons/hr_recruitment/models/hr_department.py | 12 +- addons/hr_recruitment/models/hr_job.py | 12 +- .../models/hr_recruitment_stage.py | 4 +- addons/hr_recruitment/models/res_users.py | 8 +- addons/hr_timesheet/controllers/portal.py | 10 +- addons/hr_timesheet/models/hr_employee.py | 6 +- addons/hr_timesheet/models/project_project.py | 27 ++-- addons/hr_timesheet/models/project_task.py | 7 +- addons/hr_timesheet/populate/hr_timesheet.py | 20 +-- .../wizard/hr_employee_delete_wizard.py | 4 +- .../tests/test_timesheet_attendance.py | 11 +- .../hr_work_entry_holidays/models/hr_leave.py | 10 +- addons/im_livechat/models/chatbot_script.py | 6 +- .../im_livechat/models/chatbot_script_step.py | 21 ++- addons/im_livechat/models/digest.py | 4 +- .../im_livechat/models/im_livechat_channel.py | 10 +- addons/link_tracker/models/link_tracker.py | 15 +-- addons/link_tracker/models/utm.py | 4 +- addons/loyalty/models/loyalty_program.py | 4 +- addons/loyalty/models/res_partner.py | 11 +- addons/lunch/models/lunch_product_category.py | 4 +- addons/mail/models/mail_activity.py | 34 +++-- addons/mail/models/mail_channel.py | 4 +- addons/mail/models/mail_message.py | 8 +- addons/mail/models/mail_thread.py | 6 +- addons/mail_group/models/mail_group.py | 23 ++-- addons/maintenance/models/maintenance.py | 8 +- addons/mass_mailing/models/mailing.py | 4 +- addons/mass_mailing/models/utm_campaign.py | 32 ++--- .../models/mailing_mailing.py | 4 +- .../models/mailing_mailing.py | 8 +- .../models/mailing_mailing.py | 27 ++-- addons/mass_mailing_sms/models/utm.py | 33 +++-- addons/mrp/models/mrp_production.py | 25 ++-- addons/mrp/models/mrp_routing.py | 4 +- addons/mrp/models/mrp_workcenter.py | 36 +++--- addons/mrp/models/mrp_workorder.py | 4 +- addons/mrp/models/product.py | 4 +- addons/mrp/models/stock_orderpoint.py | 12 +- addons/mrp/models/stock_picking.py | 24 ++-- addons/mrp/report/mrp_report_bom_structure.py | 6 +- addons/mrp/report/stock_forecasted.py | 8 +- .../mrp_subcontracting/models/res_partner.py | 24 ++-- .../onboarding/models/onboarding_progress.py | 6 +- addons/payment/models/payment_transaction.py | 4 +- addons/payment/models/res_partner.py | 5 +- addons/point_of_sale/models/pos_order.py | 14 +- addons/point_of_sale/models/pos_session.py | 19 ++- addons/point_of_sale/models/res_partner.py | 10 +- addons/pos_loyalty/models/loyalty_card.py | 4 +- addons/pos_loyalty/models/loyalty_program.py | 4 +- addons/pos_loyalty/models/pos_session.py | 14 +- addons/pos_restaurant/models/pos_config.py | 4 +- addons/pos_sale/models/crm_team.py | 4 +- addons/product/models/product_category.py | 4 +- addons/product/models/product_pricelist.py | 11 +- addons/project/controllers/portal.py | 11 +- .../models/account_analytic_account.py | 4 +- addons/project/models/project_milestone.py | 40 +++--- addons/project/models/project_project.py | 28 ++-- addons/project/models/project_task.py | 18 ++- .../project/models/project_task_recurrence.py | 6 +- addons/project/models/project_task_type.py | 4 +- addons/project/models/res_partner.py | 10 +- addons/project/populate/project.py | 6 +- .../project_task_burndown_chart_report.py | 2 +- addons/project_mrp/models/project_project.py | 11 +- .../models/project_project.py | 16 +-- .../models/project_task.py | 10 +- .../models/resource_calendar_leaves.py | 37 +++--- .../tests/test_timesheet_global_time_off.py | 10 +- addons/purchase/models/product.py | 4 +- addons/purchase/models/res_partner.py | 30 ++--- addons/purchase_stock/models/product.py | 15 +-- addons/rating/models/rating_mixin.py | 36 +++--- addons/rating/models/rating_parent_mixin.py | 19 ++- addons/sale/models/crm_team.py | 16 +-- addons/sale/models/product_product.py | 8 +- addons/sale/models/res_partner.py | 22 ++-- addons/sale/models/sale_order.py | 7 +- addons/sale/models/sale_order_line.py | 25 ++-- addons/sale/models/utm_campaign.py | 4 +- addons/sale_expense/models/sale_order.py | 4 +- addons/sale_loyalty/models/loyalty_card.py | 4 +- addons/sale_loyalty/models/loyalty_program.py | 7 +- addons/sale_margin/models/sale_order.py | 6 +- addons/sale_mrp/models/sale_order.py | 9 +- addons/sale_project/models/project.py | 34 +++-- .../models/res_config_settings.py | 9 +- addons/sale_project/models/sale_order.py | 4 +- addons/sale_project/models/sale_order_line.py | 20 +-- .../models/sale_order_line.py | 4 +- .../sale_purchase/models/sale_order_line.py | 4 +- addons/sale_timesheet/models/account_move.py | 18 ++- addons/sale_timesheet/models/project.py | 34 ++--- .../models/project_sale_line_employee_map.py | 8 +- addons/sale_timesheet/models/sale_order.py | 14 +- .../wizard/project_create_sale_order.py | 6 +- .../models/sale_order_line.py | 10 +- addons/spreadsheet_account/models/account.py | 6 +- addons/stock/models/product.py | 95 ++++++-------- addons/stock/models/stock_location.py | 26 ++-- addons/stock/models/stock_lot.py | 9 +- addons/stock/models/stock_move.py | 25 ++-- addons/stock/models/stock_orderpoint.py | 32 +++-- addons/stock/models/stock_picking.py | 21 ++- addons/stock/models/stock_quant.py | 24 ++-- addons/stock/models/stock_scrap.py | 12 +- addons/stock/models/stock_warehouse.py | 8 +- addons/stock/report/stock_forecasted.py | 14 +- addons/stock/tests/test_report.py | 120 +++++++++--------- .../stock/tests/test_report_stock_quantity.py | 33 ++--- .../stock/wizard/stock_replenishment_info.py | 4 +- addons/stock_account/models/product.py | 18 ++- addons/stock_delivery/models/stock_picking.py | 25 +--- .../models/stock_quant_package.py | 18 +-- .../tests/test_stock_landed_costs_purchase.py | 6 +- .../models/stock_picking.py | 21 +-- addons/survey/models/res_partner.py | 4 +- addons/survey/models/survey_survey.py | 48 +++---- addons/web/controllers/export.py | 2 +- addons/web/models/models.py | 16 +-- addons/website/models/website_visitor.py | 22 ++-- addons/website_blog/controllers/main.py | 36 +++--- .../models/crm_reveal_rule.py | 4 +- .../controllers/main.py | 6 +- .../models/res_partner.py | 19 +-- addons/website_customer/controllers/main.py | 6 +- .../website_event/models/website_visitor.py | 15 +-- .../models/event_event.py | 4 +- .../website_event_meet/models/event_event.py | 8 +- .../website_event_track/models/event_event.py | 4 +- .../website_event_track/models/event_track.py | 6 +- .../models/website_visitor.py | 6 +- .../controllers/community.py | 8 +- .../controllers/website_forum.py | 14 +- addons/website_forum/models/forum_forum.py | 17 ++- addons/website_forum/models/forum_post.py | 6 +- addons/website_forum/models/res_users.py | 9 +- addons/website_mail/controllers/main.py | 12 +- addons/website_membership/controllers/main.py | 2 +- addons/website_sale/controllers/backend.py | 76 ++++++----- addons/website_sale/models/crm_team.py | 17 +-- .../models/website_snippet_filter.py | 4 +- addons/website_sale/models/website_visitor.py | 13 +- .../models/product_template.py | 4 +- .../models/slide_channel.py | 13 +- addons/website_slides/models/res_partner.py | 4 +- addons/website_slides/models/slide_channel.py | 57 +++------ addons/website_slides/models/slide_slide.py | 84 ++++-------- odoo/addons/base/models/ir_attachment.py | 18 +-- .../test_new_api/tests/test_new_fields.py | 2 +- odoo/models.py | 10 +- 203 files changed, 1269 insertions(+), 1579 deletions(-) diff --git a/addons/account/models/account_account.py b/addons/account/models/account_account.py index fb3717771eb..7968edd4209 100644 --- a/addons/account/models/account_account.py +++ b/addons/account/models/account_account.py @@ -26,13 +26,12 @@ class AccountAccount(models.Model): def _check_account_type_unique_current_year_earning(self): result = self._read_group( domain=[('account_type', '=', 'equity_unaffected')], - fields=['company_id', 'ids:array_agg(id)'], groupby=['company_id'], + aggregates=['id:recordset'], + having=[('__count', '>', 1)], ) - for res in result: - if res.get('company_id_count', 0) >= 2: - account_unaffected_earnings = self.browse(res['ids']) - raise ValidationError(_('You cannot have more than one account with "Current Year Earnings" as type. (accounts: %s)', [a.code for a in account_unaffected_earnings])) + for _company, account_unaffected_earnings in result: + raise ValidationError(_('You cannot have more than one account with "Current Year Earnings" as type. (accounts: %s)', [a.code for a in account_unaffected_earnings])) name = fields.Char(string="Account Name", required=True, index='trigram', tracking=True, translate=True) currency_id = fields.Many2one('res.currency', string='Account Currency', tracking=True, @@ -354,11 +353,11 @@ class AccountAccount(models.Model): def _compute_current_balance(self): balances = { - read['account_id'][0]: read['balance'] - for read in self.env['account.move.line']._read_group( + account.id: balance + for account, balance in self.env['account.move.line']._read_group( domain=[('account_id', 'in', self.ids), ('parent_state', '=', 'posted')], - fields=['balance', 'account_id'], groupby=['account_id'], + aggregates=['balance:sum'], ) } for record in self: diff --git a/addons/account/models/account_journal_dashboard.py b/addons/account/models/account_journal_dashboard.py index a7a246fed47..1fa680debd8 100644 --- a/addons/account/models/account_journal_dashboard.py +++ b/addons/account/models/account_journal_dashboard.py @@ -120,11 +120,11 @@ class account_journal(models.Model): def _compute_entries_count(self): res = { - r['journal_id'][0]: r['journal_id_count'] - for r in self.env['account.move']._read_group( + journal.id: count + for journal, count in self.env['account.move']._read_group( domain=[('journal_id', 'in', self.ids)], - fields=['journal_id'], groupby=['journal_id'], + aggregates=['__count'], ) } for journal in self: @@ -310,11 +310,11 @@ class account_journal(models.Model): :type domain: list[tuple] """ res = { - r['journal_id'][0]: r['journal_id_count'] - for r in self.env[model]._read_group( + journal.id: count + for journal, count in self.env[model]._read_group( domain=[('journal_id', 'in', self.ids)] + domain, - fields=['journal_id'], groupby=['journal_id'], + aggregates=['__count'], ) } for journal in self: diff --git a/addons/account/models/account_reconcile_model.py b/addons/account/models/account_reconcile_model.py index ad46f3bb738..c7255e73058 100644 --- a/addons/account/models/account_reconcile_model.py +++ b/addons/account/models/account_reconcile_model.py @@ -285,8 +285,8 @@ class AccountReconcileModel(models.Model): return action def _compute_number_entries(self): - data = self.env['account.move.line']._read_group([('reconcile_model_id', 'in', self.ids)], ['reconcile_model_id'], 'reconcile_model_id') - mapped_data = dict([(d['reconcile_model_id'][0], d['reconcile_model_id_count']) for d in data]) + data = self.env['account.move.line']._read_group([('reconcile_model_id', 'in', self.ids)], ['reconcile_model_id'], ['__count']) + mapped_data = {reconcile_model.id: count for reconcile_model, count in data} for model in self: model.number_entries = mapped_data.get(model.id, 0) diff --git a/addons/account/models/company.py b/addons/account/models/company.py index 928ed8c6d20..196da68566c 100644 --- a/addons/account/models/company.py +++ b/addons/account/models/company.py @@ -209,11 +209,11 @@ class ResCompany(models.Model): @api.depends('fiscal_position_ids.foreign_vat') def _compute_multi_vat_foreign_country(self): company_to_foreign_vat_country = { - val['company_id'][0]: val['country_ids'] - for val in self.env['account.fiscal.position'].read_group( + company.id: country_ids + for company, country_ids in self.env['account.fiscal.position']._read_group( domain=[('company_id', 'in', self.ids), ('foreign_vat', '!=', False)], - fields=['country_ids:array_agg(country_id)'], - groupby='company_id', + groupby=['company_id'], + aggregates=['country_id:array_agg'], ) } for company in self: diff --git a/addons/account/models/partner.py b/addons/account/models/partner.py index ff27a602e2b..f1fabf4d233 100644 --- a/addons/account/models/partner.py +++ b/addons/account/models/partner.py @@ -398,9 +398,9 @@ class ResPartner(models.Model): ('state', 'not in', ['draft', 'cancel']), ('move_type', 'in', ('out_invoice', 'out_refund')), ] - price_totals = self.env['account.invoice.report'].read_group(domain, ['price_subtotal'], ['partner_id']) + price_totals = self.env['account.invoice.report']._read_group(domain, ['partner_id'], ['price_subtotal:sum']) for partner, child_ids in all_partners_and_children.items(): - partner.total_invoiced = sum(price['price_subtotal'] for price in price_totals if price['partner_id'][0] in child_ids) + partner.total_invoiced = sum(price_subtotal_sum for partner, price_subtotal_sum in price_totals if partner.id in child_ids) def _compute_journal_item_count(self): AccountMoveLine = self.env['account.move.line'] @@ -526,8 +526,8 @@ class ResPartner(models.Model): ) def _compute_bank_count(self): - bank_data = self.env['res.partner.bank']._read_group([('partner_id', 'in', self.ids)], ['partner_id'], ['partner_id']) - mapped_data = dict([(bank['partner_id'][0], bank['partner_id_count']) for bank in bank_data]) + bank_data = self.env['res.partner.bank']._read_group([('partner_id', 'in', self.ids)], ['partner_id'], ['__count']) + mapped_data = {partner.id: count for partner, count in bank_data} for partner in self: partner.bank_account_count = mapped_data.get(partner.id, 0) diff --git a/addons/account/populate/account_bank_statement.py b/addons/account/populate/account_bank_statement.py index 5c7cfe44424..8d69a6281ab 100644 --- a/addons/account/populate/account_bank_statement.py +++ b/addons/account/populate/account_bank_statement.py @@ -25,16 +25,15 @@ class AccountBankStatement(models.Model): """ rand = populate.Random('account_bank_statement+Populate') - read_group_res = self.env['account.bank.statement.line'].read_group( + read_group_res = self.env['account.bank.statement.line']._read_group( [('statement_id', '=', False)], - ['ids:array_agg(id)'], ['journal_id'], + ['id:array_agg'], ) bank_statement_vals_list = [] - for res in read_group_res: - available_ids = res['ids'] - nb_ids = len(available_ids) + for journal, ids in read_group_res: + nb_ids = len(ids) while nb_ids > 0: batch_size = min(rand.randint(1, 19), nb_ids) nb_ids -= batch_size @@ -46,8 +45,8 @@ class AccountBankStatement(models.Model): bank_statement_vals_list.append({ 'name': f"statement_{len(bank_statement_vals_list) + 1}", - 'journal_id': res['journal_id'][0], - 'line_ids': [Command.set(res['ids'])], + 'journal_id': journal.id, + 'line_ids': [Command.set(ids)], }) return self.env['account.bank.statement'].create(bank_statement_vals_list) diff --git a/addons/account_debit_note/models/account_move.py b/addons/account_debit_note/models/account_move.py index ae247e86195..432a7c8f631 100644 --- a/addons/account_debit_note/models/account_move.py +++ b/addons/account_debit_note/models/account_move.py @@ -14,8 +14,8 @@ class AccountMove(models.Model): @api.depends('debit_note_ids') def _compute_debit_count(self): debit_data = self.env['account.move']._read_group([('debit_origin_id', 'in', self.ids)], - ['debit_origin_id'], ['debit_origin_id']) - data_map = {datum['debit_origin_id'][0]: datum['debit_origin_id_count'] for datum in debit_data} + ['debit_origin_id'], ['__count']) + data_map = {debit_origin.id: count for debit_origin, count in debit_data} for inv in self: inv.debit_note_count = data_map.get(inv.id, 0.0) diff --git a/addons/account_fleet/models/fleet_vehicle.py b/addons/account_fleet/models/fleet_vehicle.py index 56bc280a4a7..f610d32ce41 100644 --- a/addons/account_fleet/models/fleet_vehicle.py +++ b/addons/account_fleet/models/fleet_vehicle.py @@ -22,10 +22,10 @@ class FleetVehicle(models.Model): ('parent_state', '!=', 'cancel'), ('move_id.move_type', 'in', self.env['account.move'].get_purchase_types()) ], - fields=['vehicle_id', 'move_id:array_agg'], groupby=['vehicle_id'], + aggregates=['move_id:array_agg'], ) - vehicle_move_mapping = {move['vehicle_id'][0]: set(move['move_id']) for move in moves} + vehicle_move_mapping = {vehicle.id: set(move_ids) for vehicle, move_ids in moves} for vehicle in self: vehicle.account_move_ids = [Command.set(vehicle_move_mapping.get(vehicle.id, []))] vehicle.bill_count = len(vehicle.account_move_ids) diff --git a/addons/account_payment/models/account_payment.py b/addons/account_payment/models/account_payment.py index e858ed27347..3bc076527ff 100644 --- a/addons/account_payment/models/account_payment.py +++ b/addons/account_payment/models/account_payment.py @@ -86,10 +86,10 @@ class AccountPayment(models.Model): ('source_payment_id', 'in', self.ids), ('payment_transaction_id.operation', '=', 'refund') ], - fields=['source_payment_id'], - groupby=['source_payment_id'] + groupby=['source_payment_id'], + aggregates=['__count'] ) - data = {x['source_payment_id'][0]: x['source_payment_id_count'] for x in rg_data} + data = {source_payment.id: count for source_payment, count in rg_data} for payment in self: payment.refunds_count = data.get(payment.id, 0) diff --git a/addons/analytic/models/analytic_account.py b/addons/analytic/models/analytic_account.py index b97caae1b79..04ddfd21db1 100644 --- a/addons/analytic/models/analytic_account.py +++ b/addons/analytic/models/analytic_account.py @@ -150,7 +150,6 @@ class AccountAnalyticAccount(models.Model): @api.depends('line_ids.amount') def _compute_debit_credit_balance(self): - Curr = self.env['res.currency'] analytic_line_obj = self.env['account.analytic.line'] domain = [ ('account_id', 'in', self.ids), @@ -162,27 +161,25 @@ class AccountAnalyticAccount(models.Model): domain.append(('date', '<=', self._context['to_date'])) user_currency = self.env.company.currency_id - credit_groups = analytic_line_obj.read_group( + credit_groups = analytic_line_obj._read_group( domain=domain + [('amount', '>=', 0.0)], - fields=['account_id', 'currency_id', 'amount'], groupby=['account_id', 'currency_id'], - lazy=False, + aggregates=['amount:sum'], ) data_credit = defaultdict(float) - for l in credit_groups: - data_credit[l['account_id'][0]] += Curr.browse(l['currency_id'][0])._convert( - l['amount'], user_currency, self.env.company, fields.Date.today()) + for account, currency, amount_sum in credit_groups: + data_credit[account.id] += currency._convert( + amount_sum, user_currency, self.env.company, fields.Date.today()) - debit_groups = analytic_line_obj.read_group( + debit_groups = analytic_line_obj._read_group( domain=domain + [('amount', '<', 0.0)], - fields=['account_id', 'currency_id', 'amount'], groupby=['account_id', 'currency_id'], - lazy=False, + aggregates=['amount:sum'], ) data_debit = defaultdict(float) - for l in debit_groups: - data_debit[l['account_id'][0]] += Curr.browse(l['currency_id'][0])._convert( - l['amount'], user_currency, self.env.company, fields.Date.today()) + for account, currency, amount_sum in debit_groups: + data_debit[account.id] += currency._convert( + amount_sum, user_currency, self.env.company, fields.Date.today()) for account in self: account.debit = abs(data_debit.get(account.id, 0.0)) diff --git a/addons/calendar/models/calendar_recurrence.py b/addons/calendar/models/calendar_recurrence.py index 5887a731ee9..6c55081f666 100644 --- a/addons/calendar/models/calendar_recurrence.py +++ b/addons/calendar/models/calendar_recurrence.py @@ -164,11 +164,8 @@ class RecurrenceRule(models.Model): @api.depends('calendar_event_ids.start') def _compute_dtstart(self): - groups = self.env['calendar.event'].read_group([('recurrence_id', 'in', self.ids)], ['start:min'], ['recurrence_id']) - start_mapping = { - group['recurrence_id'][0]: group['start'] - for group in groups - } + groups = self.env['calendar.event']._read_group([('recurrence_id', 'in', self.ids)], ['recurrence_id'], ['start:min']) + start_mapping = {recurrence.id: start_min for recurrence, start_min in groups} for recurrence in self: recurrence.dtstart = start_mapping.get(recurrence.id) diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index 85dba55598c..62c573bc666 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -538,13 +538,10 @@ class Lead(models.Model): lead.recurring_revenue_prorated = (lead.recurring_revenue or 0.0) * (lead.probability or 0) / 100.0 def _compute_calendar_event_count(self): - if self.ids: - meeting_data = self.env['calendar.event'].sudo()._read_group([ - ('opportunity_id', 'in', self.ids) - ], ['opportunity_id'], ['opportunity_id']) - mapped_data = {m['opportunity_id'][0]: m['opportunity_id_count'] for m in meeting_data} - else: - mapped_data = dict() + meeting_data = self.env['calendar.event'].sudo()._read_group([ + ('opportunity_id', 'in', self.ids) + ], ['opportunity_id'], ['__count']) + mapped_data = {opportunity.id: count for opportunity, count in meeting_data} for lead in self: lead.calendar_event_count = mapped_data.get(lead.id, 0) @@ -826,11 +823,11 @@ class Lead(models.Model): activity_asc = any('my_activity_date_deadline asc' in item for item in order_items) my_lead_activities = self.env['mail.activity']._read_group( [('res_model', '=', self._name), ('user_id', '=', self.env.uid)], - ['res_id', 'date_deadline:min'], ['res_id'], - orderby='date_deadline ASC' + ['date_deadline:min'], + order='date_deadline:min ASC, res_id', ) - my_lead_mapping = dict((item['res_id'], item['date_deadline']) for item in my_lead_activities) + my_lead_mapping = dict(my_lead_activities) my_lead_ids = list(my_lead_mapping.keys()) my_lead_domain = expression.AND([[('id', 'in', my_lead_ids)], domain]) my_lead_order = ', '.join(item for item in order_items if 'my_activity_date_deadline' not in item) diff --git a/addons/crm/models/crm_lost_reason.py b/addons/crm/models/crm_lost_reason.py index c44ab4d8745..171f7cf7318 100644 --- a/addons/crm/models/crm_lost_reason.py +++ b/addons/crm/models/crm_lost_reason.py @@ -16,9 +16,9 @@ class LostReason(models.Model): lead_data = self.env['crm.lead'].with_context(active_test=False)._read_group( [('lost_reason_id', 'in', self.ids)], ['lost_reason_id'], - ['lost_reason_id'] + ['__count'], ) - mapped_data = dict((data['lost_reason_id'][0], data['lost_reason_id_count']) for data in lead_data) + mapped_data = {lost_reason.id: count for lost_reason, count in lead_data} for reason in self: reason.leads_count = mapped_data.get(reason.id, 0) diff --git a/addons/crm/models/crm_team.py b/addons/crm/models/crm_team.py index 1223a4b7d90..575fe878102 100644 --- a/addons/crm/models/crm_team.py +++ b/addons/crm/models/crm_team.py @@ -77,8 +77,8 @@ class Team(models.Model): ('team_id', 'in', self.ids), ('type', '=', 'lead'), ('user_id', '=', False), - ], ['team_id'], ['team_id']) - counts = {datum['team_id'][0]: datum['team_id_count'] for datum in leads_data} + ], ['team_id'], ['__count']) + counts = {team.id: count for team, count in leads_data} for team in self: team.lead_unassigned_count = counts.get(team.id, 0) @@ -92,12 +92,10 @@ class Team(models.Model): ('team_id', 'in', self.ids), ('probability', '<', 100), ('type', '=', 'opportunity'), - ], ['expected_revenue:sum', 'team_id'], ['team_id']) - counts = {datum['team_id'][0]: datum['team_id_count'] for datum in opportunity_data} - amounts = {datum['team_id'][0]: datum['expected_revenue'] for datum in opportunity_data} + ], ['team_id'], ['__count', 'expected_revenue:sum']) + counts_amounts = {team.id: (count, expected_revenue_sum) for team, count, expected_revenue_sum in opportunity_data} for team in self: - team.opportunities_count = counts.get(team.id, 0) - team.opportunities_amount = amounts.get(team.id, 0) + team.opportunities_count, team.opportunities_amount = counts_amounts.get(team.id, (0, 0)) def _compute_opportunities_overdue_data(self): opportunity_data = self.env['crm.lead']._read_group([ @@ -105,12 +103,10 @@ class Team(models.Model): ('probability', '<', 100), ('type', '=', 'opportunity'), ('date_deadline', '<', fields.Date.to_string(fields.Datetime.now())) - ], ['expected_revenue', 'team_id'], ['team_id']) - counts = {datum['team_id'][0]: datum['team_id_count'] for datum in opportunity_data} - amounts = {datum['team_id'][0]: (datum['expected_revenue']) for datum in opportunity_data} + ], ['team_id'], ['__count', 'expected_revenue:sum']) + counts_amounts = {team.id: (count, expected_revenue_sum) for team, count, expected_revenue_sum in opportunity_data} for team in self: - team.opportunities_overdue_count = counts.get(team.id, 0) - team.opportunities_overdue_amount = amounts.get(team.id, 0) + team.opportunities_overdue_count, team.opportunities_overdue_amount = counts_amounts.get(team.id, (0, 0)) @api.onchange('use_leads', 'use_opportunities') def _onchange_use_leads_opportunities(self): diff --git a/addons/crm/models/res_partner.py b/addons/crm/models/res_partner.py index e332a79172d..a6df4c7ba51 100644 --- a/addons/crm/models/res_partner.py +++ b/addons/crm/models/res_partner.py @@ -51,15 +51,15 @@ class Partner(models.Model): opportunity_data = self.env['crm.lead'].with_context(active_test=False)._read_group( domain=[('partner_id', 'in', all_partners.ids)], - fields=['partner_id'], groupby=['partner_id'] + groupby=['partner_id'], aggregates=['__count'] ) + self_ids = set(self._ids) self.opportunity_count = 0 - for group in opportunity_data: - partner = self.browse(group['partner_id'][0]) + for partner, count in opportunity_data: while partner: - if partner in self: - partner.opportunity_count += group['partner_id_count'] + if partner.id in self_ids: + partner.opportunity_count += count partner = partner.parent_id def action_view_opportunity(self): diff --git a/addons/crm/models/utm.py b/addons/crm/models/utm.py index b92dcad6fe2..c22be1c3d28 100644 --- a/addons/crm/models/utm.py +++ b/addons/crm/models/utm.py @@ -16,8 +16,8 @@ class UtmCampaign(models.Model): def _compute_crm_lead_count(self): lead_data = self.env['crm.lead'].with_context(active_test=False)._read_group([ ('campaign_id', 'in', self.ids)], - ['campaign_id'], ['campaign_id']) - mapped_data = {datum['campaign_id'][0]: datum['campaign_id_count'] for datum in lead_data} + ['campaign_id'], ['__count']) + mapped_data = {campaign.id: count for campaign, count in lead_data} for campaign in self: campaign.crm_lead_count = mapped_data.get(campaign.id, 0) diff --git a/addons/crm_iap_mine/models/crm_iap_lead_mining_request.py b/addons/crm_iap_mine/models/crm_iap_lead_mining_request.py index 75d1454a4ed..a8da6565b55 100644 --- a/addons/crm_iap_mine/models/crm_iap_lead_mining_request.py +++ b/addons/crm_iap_mine/models/crm_iap_lead_mining_request.py @@ -98,15 +98,12 @@ class CRMLeadMiningRequest(models.Model): @api.depends('lead_ids.lead_mining_request_id') def _compute_lead_count(self): - if self.ids: - leads_data = self.env['crm.lead']._read_group( - [('lead_mining_request_id', 'in', self.ids)], - ['lead_mining_request_id'], ['lead_mining_request_id']) - else: - leads_data = [] - mapped_data = dict( - (m['lead_mining_request_id'][0], m['lead_mining_request_id_count']) - for m in leads_data) + leads_data = self.env['crm.lead']._read_group( + [('lead_mining_request_id', 'in', self.ids)], + ['lead_mining_request_id'], ['__count']) + mapped_data = { + lead_mining_request.id: count + for lead_mining_request, count in leads_data} for request in self: request.lead_count = mapped_data.get(request.id, 0) diff --git a/addons/crm_livechat/models/chatbot_script.py b/addons/crm_livechat/models/chatbot_script.py index 8fff92a94dd..07e50d705a6 100644 --- a/addons/crm_livechat/models/chatbot_script.py +++ b/addons/crm_livechat/models/chatbot_script.py @@ -11,12 +11,9 @@ class ChatbotScript(models.Model): string='Generated Lead Count', compute='_compute_lead_count') def _compute_lead_count(self): - mapped_leads = {} - if self.ids: - leads_data = self.env['crm.lead'].with_context(active_test=False).sudo()._read_group( - [('source_id', 'in', self.mapped('source_id').ids)], ['source_id'], ['source_id']) - mapped_leads = {lead['source_id'][0]: lead['source_id_count'] for lead in leads_data} - + leads_data = self.env['crm.lead'].with_context(active_test=False).sudo()._read_group( + [('source_id', 'in', self.mapped('source_id').ids)], ['source_id'], ['__count']) + mapped_leads = {source.id: count for source, count in leads_data} for script in self: script.lead_count = mapped_leads.get(script.source_id.id, 0) diff --git a/addons/data_recycle/models/data_recycle_model.py b/addons/data_recycle/models/data_recycle_model.py index 6c91da62f27..d4511ce6df6 100644 --- a/addons/data_recycle/models/data_recycle_model.py +++ b/addons/data_recycle/models/data_recycle_model.py @@ -95,8 +95,8 @@ class DataRecycleModel(models.Model): count_data = self.env['data_recycle.record']._read_group( [('recycle_model_id', 'in', self.ids)], ['recycle_model_id'], - ['recycle_model_id']) - counts = {cd['recycle_model_id'][0]: cd['recycle_model_id_count'] for cd in count_data} + ['__count']) + counts = {recycle_model.id: count for recycle_model, count in count_data} for model in self: model.records_to_recycle_count = counts[model.id] if model.id in counts else 0 diff --git a/addons/digest/models/digest.py b/addons/digest/models/digest.py index 897cde34878..25e0260d4c0 100644 --- a/addons/digest/models/digest.py +++ b/addons/digest/models/digest.py @@ -416,14 +416,11 @@ class Digest(models.Model): values = self.env[model]._read_group( domain=base_domain, - fields=[f'{sum_field}:sum'] if sum_field else None, groupby=['company_id'], + aggregates=[f'{sum_field}:sum'] if sum_field else ['__count'], ) - values_per_company = { - value['company_id'][0]: value[sum_field] if sum_field else value['company_id_count'] - for value in values - } + values_per_company = {company.id: agg for company, agg in values} for digest in self: company = digest.company_id or self.env.company digest[digest_kpi_field] = values_per_company.get(company.id, 0) diff --git a/addons/event_booth/models/event_event.py b/addons/event_booth/models/event_event.py index d07b9b69ae4..207be9185c7 100644 --- a/addons/event_booth/models/event_event.py +++ b/addons/event_booth/models/event_event.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from collections import defaultdict from odoo import api, fields, models from odoo import Command @@ -55,16 +56,14 @@ class Event(models.Model): def _get_booth_stat_count(self): elements = self.env['event.booth'].sudo()._read_group( [('event_id', 'in', self.ids)], - ['event_id', 'state'], ['event_id', 'state'], lazy=False + ['event_id', 'state'], ['__count'] ) - elements_total_count = dict() + elements_total_count = defaultdict(int) elements_available_count = dict() - for element in elements: - event_id = element['event_id'][0] - if element['state'] == 'available': - elements_available_count[event_id] = element['__count'] - elements_total_count.setdefault(event_id, 0) - elements_total_count[event_id] += element['__count'] + for event, state, count in elements: + if state == 'available': + elements_available_count[event.id] = count + elements_total_count[event.id] += count return elements_available_count, elements_total_count @api.depends('event_booth_ids', 'event_booth_ids.state') diff --git a/addons/event_booth_sale/models/sale_order.py b/addons/event_booth_sale/models/sale_order.py index 2cb8428f5aa..7996163dfb8 100644 --- a/addons/event_booth_sale/models/sale_order.py +++ b/addons/event_booth_sale/models/sale_order.py @@ -12,14 +12,11 @@ class SaleOrder(models.Model): @api.depends('event_booth_ids') def _compute_event_booth_count(self): - if self.ids: - slot_data = self.env['event.booth']._read_group( - [('sale_order_id', 'in', self.ids)], - ['sale_order_id'], ['sale_order_id'] - ) - slot_mapped = dict((data['sale_order_id'][0], data['sale_order_id_count']) for data in slot_data) - else: - slot_mapped = dict() + slot_data = self.env['event.booth']._read_group( + [('sale_order_id', 'in', self.ids)], + ['sale_order_id'], ['__count'], + ) + slot_mapped = {sale_order.id: count for sale_order, count in slot_data} for so in self: so.event_booth_count = slot_mapped.get(so.id, 0) diff --git a/addons/event_crm/models/event_event.py b/addons/event_crm/models/event_event.py index 6cede59f82c..4c2656f1cb0 100644 --- a/addons/event_crm/models/event_event.py +++ b/addons/event_crm/models/event_event.py @@ -18,8 +18,8 @@ class EventEvent(models.Model): def _compute_lead_count(self): lead_data = self.env['crm.lead']._read_group( [('event_id', 'in', self.ids)], - ['event_id'], ['event_id'] + ['event_id'], ['__count'], ) - mapped_data = {item['event_id'][0]: item['event_id_count'] for item in lead_data} + mapped_data = {event.id: count for event, count in lead_data} for event in self: event.lead_count = mapped_data.get(event.id, 0) diff --git a/addons/event_sale/models/event_event.py b/addons/event_sale/models/event_event.py index 9ead7cb5b73..4ed906ab6ca 100644 --- a/addons/event_sale/models/event_event.py +++ b/addons/event_sale/models/event_event.py @@ -31,50 +31,22 @@ class Event(models.Model): have to do one conversion per sale.order (and a sale.order is created every time we sell a single event ticket). """ date_now = fields.Datetime.now() - sale_price_by_event = {} - if self.ids: - event_subtotals = self.env['sale.order.line']._read_group( - [('event_id', 'in', self.ids), - ('price_subtotal', '!=', 0)], - ['event_id', 'currency_id', 'price_subtotal:sum'], - ['event_id', 'currency_id'], - lazy=False + event_subtotals = self.env['sale.order.line']._read_group( + [('event_id', 'in', self.ids), + ('price_subtotal', '!=', 0)], + ['event_id', 'currency_id'], + ['price_subtotal:sum'], + ) + + self.sale_price_subtotal = 0 + for event, currency, sum_price_subtotal in event_subtotals: + event.sale_price_subtotal += event.currency_id._convert( + sum_price_subtotal, + currency, + event.company_id, + date_now, ) - company_by_event = { - event._origin.id or event.id: event.company_id - for event in self - } - - currency_by_event = { - event._origin.id or event.id: event.currency_id - for event in self - } - - currency_by_id = { - currency.id: currency - for currency in self.env['res.currency'].browse( - [event_subtotal['currency_id'][0] for event_subtotal in event_subtotals] - ) - } - - for event_subtotal in event_subtotals: - price_subtotal = event_subtotal['price_subtotal'] - event_id = event_subtotal['event_id'][0] - currency_id = event_subtotal['currency_id'][0] - sale_price = currency_by_event[event_id]._convert( - price_subtotal, - currency_by_id[currency_id], - company_by_event[event_id], - date_now) - if event_id in sale_price_by_event: - sale_price_by_event[event_id] += sale_price - else: - sale_price_by_event[event_id] = sale_price - - for event in self: - event.sale_price_subtotal = sale_price_by_event.get(event._origin.id or event.id, 0) - def action_view_linked_orders(self): """ Redirects to the orders linked to the current events """ sale_order_action = self.env["ir.actions.actions"]._for_xml_id("sale.action_orders") diff --git a/addons/event_sale/models/sale_order.py b/addons/event_sale/models/sale_order.py index e689fce03a3..a79fa9218b1 100644 --- a/addons/event_sale/models/sale_order.py +++ b/addons/event_sale/models/sale_order.py @@ -44,12 +44,10 @@ class SaleOrder(models.Model): sale_orders_data = self.env['event.registration']._read_group( [('sale_order_id', 'in', self.ids), ('state', '!=', 'cancel')], - ['sale_order_id'], ['sale_order_id'] + ['sale_order_id'], ['__count'], ) attendee_count_data = { - sale_order_data['sale_order_id'][0]: - sale_order_data['sale_order_id_count'] - for sale_order_data in sale_orders_data + sale_order.id: count for sale_order, count in sale_orders_data } for sale_order in self: sale_order.attendee_count = attendee_count_data.get(sale_order.id, 0) diff --git a/addons/fleet/models/fleet_vehicle.py b/addons/fleet/models/fleet_vehicle.py index 3f243a26a8a..33d810e33eb 100644 --- a/addons/fleet/models/fleet_vehicle.py +++ b/addons/fleet/models/fleet_vehicle.py @@ -167,24 +167,24 @@ class FleetVehicle(models.Model): LogService = self.env['fleet.vehicle.log.services'].with_context(active_test=False) LogContract = self.env['fleet.vehicle.log.contract'].with_context(active_test=False) History = self.env['fleet.vehicle.assignation.log'] - odometers_data = Odometer.read_group([('vehicle_id', 'in', self.ids)], ['vehicle_id'], ['vehicle_id']) - services_data = LogService.read_group([('vehicle_id', 'in', self.ids)], ['vehicle_id', 'active'], ['vehicle_id', 'active'], lazy=False) - logs_data = LogContract.read_group([('vehicle_id', 'in', self.ids), ('state', '!=', 'closed')], ['vehicle_id', 'active'], ['vehicle_id', 'active'], lazy=False) - histories_data = History.read_group([('vehicle_id', 'in', self.ids)], ['vehicle_id'], ['vehicle_id']) + odometers_data = Odometer._read_group([('vehicle_id', 'in', self.ids)], ['vehicle_id'], ['__count']) + services_data = LogService._read_group([('vehicle_id', 'in', self.ids)], ['vehicle_id', 'active'], ['__count']) + logs_data = LogContract._read_group([('vehicle_id', 'in', self.ids), ('state', '!=', 'closed')], ['vehicle_id', 'active'], ['__count']) + histories_data = History._read_group([('vehicle_id', 'in', self.ids)], ['vehicle_id'], ['__count']) mapped_odometer_data = defaultdict(lambda: 0) mapped_service_data = defaultdict(lambda: defaultdict(lambda: 0)) mapped_log_data = defaultdict(lambda: defaultdict(lambda: 0)) mapped_history_data = defaultdict(lambda: 0) - for odometer_data in odometers_data: - mapped_odometer_data[odometer_data['vehicle_id'][0]] = odometer_data['vehicle_id_count'] - for service_data in services_data: - mapped_service_data[service_data['vehicle_id'][0]][service_data['active']] = service_data['__count'] - for log_data in logs_data: - mapped_log_data[log_data['vehicle_id'][0]][log_data['active']] = log_data['__count'] - for history_data in histories_data: - mapped_history_data[history_data['vehicle_id'][0]] = history_data['vehicle_id_count'] + for vehicle, count in odometers_data: + mapped_odometer_data[vehicle.id] = count + for vehicle, active, count in services_data: + mapped_service_data[vehicle.id][active] = count + for vehicle, active, count in logs_data: + mapped_log_data[vehicle.id][active] = count + for vehicle, count in histories_data: + mapped_history_data[vehicle.id] = count for vehicle in self: vehicle.odometer_count = mapped_odometer_data[vehicle.id] diff --git a/addons/fleet/models/fleet_vehicle_model.py b/addons/fleet/models/fleet_vehicle_model.py index 4ca671416cc..65af874e23e 100644 --- a/addons/fleet/models/fleet_vehicle_model.py +++ b/addons/fleet/models/fleet_vehicle_model.py @@ -64,9 +64,9 @@ class FleetVehicleModel(models.Model): def _compute_vehicle_count(self): group = self.env['fleet.vehicle']._read_group( - [('model_id', 'in', self.ids)], ['id', 'model_id'], groupby='model_id', lazy=False, + [('model_id', 'in', self.ids)], ['model_id'], aggregates=['__count'], ) - count_by_model = {entry['model_id'][0]: entry['__count'] for entry in group} + count_by_model = {model.id: count for model, count in group} for model in self: model.vehicle_count = count_by_model.get(model.id, 0) diff --git a/addons/fleet/models/fleet_vehicle_model_brand.py b/addons/fleet/models/fleet_vehicle_model_brand.py index 5aebba8f7ab..f293076cfb8 100644 --- a/addons/fleet/models/fleet_vehicle_model_brand.py +++ b/addons/fleet/models/fleet_vehicle_model_brand.py @@ -18,8 +18,8 @@ class FleetVehicleModelBrand(models.Model): def _compute_model_count(self): model_data = self.env['fleet.vehicle.model']._read_group([ ('brand_id', 'in', self.ids), - ], ['brand_id'], ['brand_id']) - models_brand = {x['brand_id'][0]: x['brand_id_count'] for x in model_data} + ], ['brand_id'], ['__count']) + models_brand = {brand.id: count for brand, count in model_data} for record in self: record.model_count = models_brand.get(record.id, 0) diff --git a/addons/gamification/models/gamification_challenge.py b/addons/gamification/models/gamification_challenge.py index 1c4fd0dcd38..5156253a789 100644 --- a/addons/gamification/models/gamification_challenge.py +++ b/addons/gamification/models/gamification_challenge.py @@ -666,15 +666,14 @@ class Challenge(models.Model): challenge_ended = force or end_date == fields.Date.to_string(yesterday) if challenge.reward_id and (challenge_ended or challenge.reward_realtime): # not using start_date as intemportal goals have a start date but no end_date - reached_goals = self.env['gamification.goal'].read_group([ + reached_goals = self.env['gamification.goal']._read_group([ ('challenge_id', '=', challenge.id), ('end_date', '=', end_date), ('state', '=', 'reached') - ], fields=['user_id'], groupby=['user_id']) - for reach_goals_user in reached_goals: - if reach_goals_user['user_id_count'] == len(challenge.line_ids): + ], groupby=['user_id'], aggregates=['__count']) + for user, count in reached_goals: + if count == len(challenge.line_ids): # the user has succeeded every assigned goal - user = self.env['res.users'].browse(reach_goals_user['user_id'][0]) if challenge.reward_realtime: badges = self.env['gamification.badge.user'].search_count([ ('challenge_id', '=', challenge.id), diff --git a/addons/gamification/models/gamification_goal.py b/addons/gamification/models/gamification_goal.py index 83d90888912..e8997ebddc1 100644 --- a/addons/gamification/models/gamification_goal.py +++ b/addons/gamification/models/gamification_goal.py @@ -185,30 +185,18 @@ class Goal(models.Model): subquery_domain.append((field_date_name, '<=', end_date)) if definition.computation_mode == 'count': - value_field_name = field_name + '_count' - if field_name == 'id': - # grouping on id does not work and is similar to search anyway - users = Obj.search(subquery_domain) - user_values = [{'id': user.id, value_field_name: 1} for user in users] - else: - user_values = Obj.read_group(subquery_domain, fields=[field_name], groupby=[field_name]) + user_values = Obj._read_group(subquery_domain, groupby=[field_name], aggregates=['__count']) else: # sum value_field_name = definition.field_id.name - if field_name == 'id': - user_values = Obj.search_read(subquery_domain, fields=['id', value_field_name]) - else: - user_values = Obj.read_group(subquery_domain, fields=[field_name, "%s:sum" % value_field_name], groupby=[field_name]) + user_values = Obj._read_group(subquery_domain, groupby=[field_name], aggregates=[f'{value_field_name}:sum']) - # user_values has format of read_group: [{'partner_id': 42, 'partner_id_count': 3},...] + # user_values has format of _read_group: [(, ), ...] for goal in [g for g in goals if g.id in query_goals]: - for user_value in user_values: - queried_value = field_name in user_value and user_value[field_name] or False - if isinstance(queried_value, tuple) and len(queried_value) == 2 and isinstance(queried_value[0], int): - queried_value = queried_value[0] + for field_value, aggregate in user_values: + queried_value = field_value.id if isinstance(field_value, models.Model) else field_value if queried_value == query_goals[goal.id]: - new_value = user_value.get(value_field_name, goal.current) - goals_to_write.update(goal._get_write_values(new_value)) + goals_to_write.update(goal._get_write_values(aggregate)) else: for goal in goals: @@ -223,8 +211,8 @@ class Goal(models.Model): if definition.computation_mode == 'sum': field_name = definition.field_id.name - res = Obj.read_group(domain, [field_name], []) - new_value = res and res[0][field_name] or 0.0 + res = Obj._read_group(domain, [], [field_name]) + new_value = res[0][0] or 0.0 else: # computation mode = count new_value = Obj.search_count(domain) diff --git a/addons/gamification/models/gamification_karma_rank.py b/addons/gamification/models/gamification_karma_rank.py index 8b91f2d6f70..ca7994bf004 100644 --- a/addons/gamification/models/gamification_karma_rank.py +++ b/addons/gamification/models/gamification_karma_rank.py @@ -26,8 +26,8 @@ class KarmaRank(models.Model): @api.depends('user_ids') def _compute_rank_users_count(self): - requests_data = self.env['res.users']._read_group([('rank_id', '!=', False)], ['rank_id'], ['rank_id']) - requests_mapped_data = dict((data['rank_id'][0], data['rank_id_count']) for data in requests_data) + requests_data = self.env['res.users']._read_group([('rank_id', '!=', False)], ['rank_id'], ['__count']) + requests_mapped_data = {rank.id: count for rank, count in requests_data} for rank in self: rank.rank_users_count = requests_mapped_data.get(rank.id, 0) diff --git a/addons/hr/models/hr_department.py b/addons/hr/models/hr_department.py index ad4adce326a..75d64c0ff9c 100644 --- a/addons/hr/models/hr_department.py +++ b/addons/hr/models/hr_department.py @@ -54,14 +54,14 @@ class Department(models.Model): department.master_department_id = int(department.parent_path.split('/')[0]) def _compute_total_employee(self): - emp_data = self.env['hr.employee']._read_group([('department_id', 'in', self.ids)], ['department_id'], ['department_id']) - result = dict((data['department_id'][0], data['department_id_count']) for data in emp_data) + emp_data = self.env['hr.employee']._read_group([('department_id', 'in', self.ids)], ['department_id'], ['__count']) + result = {department.id: count for department, count in emp_data} for department in self: department.total_employee = result.get(department.id, 0) def _compute_plan_count(self): - plans_data = self.env['hr.plan']._read_group([('department_id', 'in', self.ids)], ['department_id'], ['department_id']) - plans_count = {x['department_id'][0]: x['department_id_count'] for x in plans_data} + plans_data = self.env['hr.plan']._read_group([('department_id', 'in', self.ids)], ['department_id'], ['__count']) + plans_count = {department.id: count for department, count in plans_data} for department in self: department.plans_count = plans_count.get(department.id, 0) diff --git a/addons/hr/models/hr_job.py b/addons/hr/models/hr_job.py index 089a16cf2b9..98bb50a52eb 100644 --- a/addons/hr/models/hr_job.py +++ b/addons/hr/models/hr_job.py @@ -37,8 +37,8 @@ class Job(models.Model): @api.depends('no_of_recruitment', 'employee_ids.job_id', 'employee_ids.active') def _compute_employees(self): - employee_data = self.env['hr.employee']._read_group([('job_id', 'in', self.ids)], ['job_id'], ['job_id']) - result = dict((data['job_id'][0], data['job_id_count']) for data in employee_data) + employee_data = self.env['hr.employee']._read_group([('job_id', 'in', self.ids)], ['job_id'], ['__count']) + result = {job.id: count for job, count in employee_data} for job in self: job.no_of_employee = result.get(job.id, 0) job.expected_employees = result.get(job.id, 0) + job.no_of_recruitment diff --git a/addons/hr/models/hr_plan.py b/addons/hr/models/hr_plan.py index e120fdddd48..2e994a46a26 100644 --- a/addons/hr/models/hr_plan.py +++ b/addons/hr/models/hr_plan.py @@ -21,7 +21,7 @@ class HrPlan(models.Model): @api.depends('plan_activity_type_ids') def _compute_steps_count(self): - activity_type_data = self.env['hr.plan.activity.type']._read_group([('plan_id', 'in', self.ids)], ['plan_id'], ['plan_id']) - steps_count = {x['plan_id'][0]: x['plan_id_count'] for x in activity_type_data} + activity_type_data = self.env['hr.plan.activity.type']._read_group([('plan_id', 'in', self.ids)], ['plan_id'], ['__count']) + steps_count = {plan.id: count for plan, count in activity_type_data} for plan in self: plan.steps_count = steps_count.get(plan.id, 0) diff --git a/addons/hr_attendance/models/hr_employee.py b/addons/hr_attendance/models/hr_employee.py index 25375e8f8a8..ccf47b2b584 100644 --- a/addons/hr_attendance/models/hr_employee.py +++ b/addons/hr_attendance/models/hr_employee.py @@ -197,12 +197,6 @@ class HrEmployee(models.Model): 'Your attendances have probably been modified manually by human resources.') % {'empl_name': self.sudo().name, }) return attendance - @api.model - def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True): - if 'pin' in groupby or 'pin' in self.env.context.get('group_by', '') or self.env.context.get('no_group_by'): - raise exceptions.UserError(_('Such grouping is not allowed.')) - return super(HrEmployee, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy) - def _compute_presence_icon(self): res = super()._compute_presence_icon() # All employee must chek in or check out. Everybody must have an icon diff --git a/addons/hr_contract/models/resource.py b/addons/hr_contract/models/resource.py index 52316b92636..a6d7677c1cb 100644 --- a/addons/hr_contract/models/resource.py +++ b/addons/hr_contract/models/resource.py @@ -32,8 +32,8 @@ class ResourceCalendar(models.Model): count_data = self.env['hr.contract']._read_group( [('resource_calendar_id', 'in', self.ids)], ['resource_calendar_id'], - ['resource_calendar_id']) - mapped_counts = {cd['resource_calendar_id'][0]: cd['resource_calendar_id_count'] for cd in count_data} + ['__count']) + mapped_counts = {resource_calendar.id: count for resource_calendar, count in count_data} for calendar in self: calendar.contracts_count = mapped_counts.get(calendar.id, 0) diff --git a/addons/hr_contract/models/resource_resource.py b/addons/hr_contract/models/resource_resource.py index c2c9f9fb2c9..680a8b2436e 100644 --- a/addons/hr_contract/models/resource_resource.py +++ b/addons/hr_contract/models/resource_resource.py @@ -17,7 +17,7 @@ class ResourceResource(models.Model): calendars_within_period_per_resource = defaultdict(lambda: defaultdict(Intervals)) # keys are [resource id:integer][calendar:self.env['resource.calendar']] # Employees that have ever had an active contract employee_ids_with_active_contracts = { - contract['employee_id'][0] for contract in + employee.id for [employee] in self.env['hr.contract']._read_group( domain=[ ('employee_id', 'in', self.employee_id.ids), @@ -25,7 +25,7 @@ class ResourceResource(models.Model): '|', ('state', '=', 'close'), '&', ('state', '=', 'draft'), ('kanban_state', '=', 'done') ], - fields=['employee_id'], groupby=['employee_id'] + groupby=['employee_id'], ) } resource_without_contract = self.filtered( diff --git a/addons/hr_expense/models/hr_department.py b/addons/hr_expense/models/hr_department.py index 85f7e8c2bf9..b426002a33a 100644 --- a/addons/hr_expense/models/hr_department.py +++ b/addons/hr_expense/models/hr_department.py @@ -8,8 +8,8 @@ class HrDepartment(models.Model): _inherit = 'hr.department' def _compute_expense_sheets_to_approve(self): - expense_sheet_data = self.env['hr.expense.sheet']._read_group([('department_id', 'in', self.ids), ('state', '=', 'submit')], ['department_id'], ['department_id']) - result = dict((data['department_id'][0], data['department_id_count']) for data in expense_sheet_data) + expense_sheet_data = self.env['hr.expense.sheet']._read_group([('department_id', 'in', self.ids), ('state', '=', 'submit')], ['department_id'], ['__count']) + result = {department.id: count for department, count in expense_sheet_data} for department in self: department.expense_sheets_to_approve_count = result.get(department.id, 0) diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index 68f6c149eef..fd4fdf57291 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -215,8 +215,8 @@ class HrExpense(models.Model): expense.label_convert_rate = rate_txt def _compute_attachment_number(self): - attachment_data = self.env['ir.attachment']._read_group([('res_model', '=', 'hr.expense'), ('res_id', 'in', self.ids)], ['res_id'], ['res_id']) - attachment = dict((data['res_id'], data['res_id_count']) for data in attachment_data) + attachment_data = self.env['ir.attachment']._read_group([('res_model', '=', 'hr.expense'), ('res_id', 'in', self.ids)], ['res_id'], ['__count']) + attachment = dict(attachment_data) for expense in self: expense.attachment_number = attachment.get(expense._origin.id, 0) @@ -611,17 +611,16 @@ class HrExpense(models.Model): if not self.env.user.employee_ids: return expense_state target_currency = self.env.company.currency_id - expenses = self.read_group( + expenses = self._read_group( [ ('employee_id', 'in', self.env.user.employee_ids.ids), ('payment_mode', '=', 'own_account'), ('state', 'in', ['draft', 'reported', 'approved']) - ], ['total_amount', 'currency_id', 'state'], ['state', 'currency_id'], lazy=False) - for expense in expenses: - state = expense['state'] - currency = self.env['res.currency'].browse(expense['currency_id'][0]) if expense['currency_id'] else target_currency + ], ['state', 'currency_id'], ['total_amount:sum']) + for state, currency, total_amount_sum in expenses: + currency = currency or target_currency amount = currency._convert( - expense['total_amount'], target_currency, self.env.company, fields.Date.today()) + total_amount_sum, target_currency, self.env.company, fields.Date.today()) expense_state[state]['amount'] += amount return expense_state @@ -975,8 +974,8 @@ class HrExpenseSheet(models.Model): @api.depends('expense_line_ids') def _compute_expense_number(self): - read_group_result = self.env['hr.expense']._read_group([('sheet_id', 'in', self.ids)], ['sheet_id'], ['sheet_id']) - result = dict((data['sheet_id'][0], data['sheet_id_count']) for data in read_group_result) + read_group_result = self.env['hr.expense']._read_group([('sheet_id', 'in', self.ids)], ['sheet_id'], ['__count']) + result = {sheet.id: count for sheet, count in read_group_result} for sheet in self: sheet.expense_number = result.get(sheet.id, 0) diff --git a/addons/hr_fleet/models/employee.py b/addons/hr_fleet/models/employee.py index de98725a292..9f78917e7d3 100644 --- a/addons/hr_fleet/models/employee.py +++ b/addons/hr_fleet/models/employee.py @@ -43,8 +43,8 @@ class Employee(models.Model): def _compute_employee_cars_count(self): rg = self.env['fleet.vehicle.assignation.log']._read_group([ ('driver_employee_id', 'in', self.ids), - ], ['driver_employee_id'], ['driver_employee_id']) - cars_count = {r['driver_employee_id'][0]: r['driver_employee_id_count'] for r in rg} + ], ['driver_employee_id'], ['__count']) + cars_count = {driver_employee.id: count for driver_employee, count in rg} for employee in self: employee.employee_cars_count = cars_count.get(employee.id, 0) diff --git a/addons/hr_fleet/models/fleet_vehicle_assignation_log.py b/addons/hr_fleet/models/fleet_vehicle_assignation_log.py index adf1dfee883..d0e68065194 100644 --- a/addons/hr_fleet/models/fleet_vehicle_assignation_log.py +++ b/addons/hr_fleet/models/fleet_vehicle_assignation_log.py @@ -21,8 +21,8 @@ class FleetVehicleAssignationLog(models.Model): def _compute_attachment_number(self): attachment_data = self.env['ir.attachment']._read_group([ ('res_model', '=', 'fleet.vehicle.assignation.log'), - ('res_id', 'in', self.ids)], ['res_id'], ['res_id']) - attachment = dict((data['res_id'], data['res_id_count']) for data in attachment_data) + ('res_id', 'in', self.ids)], ['res_id'], ['__count']) + attachment = dict(attachment_data) for doc in self: doc.attachment_number = attachment.get(doc.id, 0) diff --git a/addons/hr_holidays/models/hr_department.py b/addons/hr_holidays/models/hr_department.py index b08dbee9b40..725641f7802 100644 --- a/addons/hr_holidays/models/hr_department.py +++ b/addons/hr_holidays/models/hr_department.py @@ -28,19 +28,19 @@ class Department(models.Model): leave_data = Requests._read_group( [('department_id', 'in', self.ids), ('state', '=', 'confirm')], - ['department_id'], ['department_id']) + ['department_id'], ['__count']) allocation_data = Allocations._read_group( [('department_id', 'in', self.ids), ('state', '=', 'confirm')], - ['department_id'], ['department_id']) + ['department_id'], ['__count']) absence_data = Requests._read_group( [('department_id', 'in', self.ids), ('state', 'not in', ['cancel', 'refuse']), ('date_from', '<=', today_end), ('date_to', '>=', today_start)], - ['department_id'], ['department_id']) + ['department_id'], ['__count']) - res_leave = dict((data['department_id'][0], data['department_id_count']) for data in leave_data) - res_allocation = dict((data['department_id'][0], data['department_id_count']) for data in allocation_data) - res_absence = dict((data['department_id'][0], data['department_id_count']) for data in absence_data) + res_leave = {department.id: count for department, count in leave_data} + res_allocation = {department.id: count for department, count in allocation_data} + res_absence = {department.id: count for department, count in absence_data} for department in self: department.leave_to_approve_count = res_leave.get(department.id, 0) diff --git a/addons/hr_holidays/models/hr_employee.py b/addons/hr_holidays/models/hr_employee.py index a525f031c52..51354129fe2 100644 --- a/addons/hr_holidays/models/hr_employee.py +++ b/addons/hr_holidays/models/hr_employee.py @@ -88,13 +88,13 @@ class HrEmployeeBase(models.AbstractModel): ('state', '=', 'validate'), ('date_from', '<=', current_date), ('date_to', '>=', current_date), - ], ['number_of_days:sum', 'employee_id'], ['employee_id']) - rg_results = dict((d['employee_id'][0], {"employee_id_count": d['employee_id_count'], "number_of_days": d['number_of_days']}) for d in data) + ], ['employee_id'], ['__count', 'number_of_days:sum']) + rg_results = {employee.id: (count, days) for employee, count, days in data} for employee in self: - result = rg_results.get(employee.id) - employee.allocation_count = float_round(result['number_of_days'], precision_digits=2) if result else 0.0 + count, days = rg_results.get(employee.id, (0, 0)) + employee.allocation_count = float_round(days, precision_digits=2) employee.allocation_display = "%g" % employee.allocation_count - employee.allocations_count = result['employee_id_count'] if result else 0.0 + employee.allocations_count = count def _compute_allocation_remaining_display(self): allocations = self.env['hr.leave.allocation'].search([('employee_id', 'in', self.ids)]) diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py index a68f94c01e8..d6858f3a7ad 100644 --- a/addons/hr_holidays/models/hr_leave.py +++ b/addons/hr_holidays/models/hr_leave.py @@ -1067,12 +1067,6 @@ class HolidaysRequest(models.Model): def _get_mail_redirect_suggested_company(self): return self.holiday_status_id.company_id - @api.model - def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True): - if not self.user_has_groups('hr_holidays.group_hr_holidays_user') and 'private_name' in groupby: - raise UserError(_('Such grouping is not allowed.')) - return super(HolidaysRequest, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy) - #################################################### # Business methods #################################################### @@ -1705,13 +1699,12 @@ class HolidaysRequest(models.Model): ('display_type', '=', False), ('day_period', '!=', 'lunch'), ] - attendances = self.env['resource.calendar.attendance'].read_group(domain, - ['ids:array_agg(id)', 'hour_from:min(hour_from)', 'hour_to:max(hour_to)', - 'week_type', 'dayofweek', 'day_period'], - ['week_type', 'dayofweek', 'day_period'], lazy=False) + attendances = self.env['resource.calendar.attendance']._read_group(domain, + ['week_type', 'dayofweek', 'day_period'], + ['hour_from:min', 'hour_to:max']) # Must be sorted by dayofweek ASC and day_period DESC - attendances = sorted([DummyAttendance(group['hour_from'], group['hour_to'], group['dayofweek'], group['day_period'], group['week_type']) for group in attendances], key=lambda att: (att.dayofweek, att.day_period != 'morning')) + attendances = sorted([DummyAttendance(hour_from, hour_to, dayofweek, day_period, week_type) for week_type, dayofweek, day_period, hour_from, hour_to in attendances], key=lambda att: (att.dayofweek, att.day_period != 'morning')) default_value = DummyAttendance(0, 0, 0, 'morning', False) diff --git a/addons/hr_holidays/models/hr_leave_accrual_plan.py b/addons/hr_holidays/models/hr_leave_accrual_plan.py index b7fe973199d..372ca9ed24b 100644 --- a/addons/hr_holidays/models/hr_leave_accrual_plan.py +++ b/addons/hr_holidays/models/hr_leave_accrual_plan.py @@ -39,10 +39,10 @@ class AccrualPlan(models.Model): def _compute_level_count(self): level_read_group = self.env['hr.leave.accrual.level']._read_group( [('accrual_plan_id', 'in', self.ids)], - fields=['accrual_plan_id'], groupby=['accrual_plan_id'], + aggregates=['__count'], ) - mapped_count = {group['accrual_plan_id'][0]: group['accrual_plan_id_count'] for group in level_read_group} + mapped_count = {accrual_plan.id: count for accrual_plan, count in level_read_group} for plan in self: plan.level_count = mapped_count.get(plan.id, 0) @@ -50,10 +50,10 @@ class AccrualPlan(models.Model): def _compute_employee_count(self): allocations_read_group = self.env['hr.leave.allocation']._read_group( [('accrual_plan_id', 'in', self.ids)], - ['accrual_plan_id', 'employee_count:count_distinct(employee_id)'], ['accrual_plan_id'], + ['employee_id:count_distinct'], ) - allocations_dict = {res['accrual_plan_id'][0]: res['employee_count'] for res in allocations_read_group} + allocations_dict = {accrual_plan.id: count for accrual_plan, count in allocations_read_group} for plan in self: plan.employees_count = allocations_dict.get(plan.id, 0) diff --git a/addons/hr_holidays/models/hr_leave_allocation.py b/addons/hr_holidays/models/hr_leave_allocation.py index a782311e021..9058e0f1524 100644 --- a/addons/hr_holidays/models/hr_leave_allocation.py +++ b/addons/hr_holidays/models/hr_leave_allocation.py @@ -313,14 +313,12 @@ class HolidaysAllocation(models.Model): @api.depends('holiday_status_id', 'allocation_type', 'number_of_hours_display', 'number_of_days_display', 'date_to') def _compute_from_holiday_status_id(self): accrual_allocations = self.filtered(lambda alloc: alloc.allocation_type == 'accrual' and not alloc.accrual_plan_id and alloc.holiday_status_id) - accruals_dict = {} - if accrual_allocations: - accruals_read_group = self.env['hr.leave.accrual.plan'].read_group( - [('time_off_type_id', 'in', accrual_allocations.holiday_status_id.ids)], - ['time_off_type_id', 'ids:array_agg(id)'], - ['time_off_type_id'], - ) - accruals_dict = {res['time_off_type_id'][0]: res['ids'] for res in accruals_read_group} + accruals_read_group = self.env['hr.leave.accrual.plan']._read_group( + [('time_off_type_id', 'in', accrual_allocations.holiday_status_id.ids)], + ['time_off_type_id'], + ['id:array_agg'], + ) + accruals_dict = {time_off_type.id: ids for time_off_type, ids in accruals_read_group} for allocation in self: allocation.number_of_days = allocation.number_of_days_display if allocation.type_request_unit == 'hour': diff --git a/addons/hr_holidays/models/hr_leave_type.py b/addons/hr_holidays/models/hr_leave_type.py index 31921a676a3..77461e1bbba 100644 --- a/addons/hr_holidays/models/hr_leave_type.py +++ b/addons/hr_holidays/models/hr_leave_type.py @@ -496,9 +496,9 @@ class HolidaysType(models.Model): grouped_res = self.env['hr.leave.allocation']._read_group( domain, ['holiday_status_id'], - ['holiday_status_id'], + ['__count'], ) - grouped_dict = dict((data['holiday_status_id'][0], data['holiday_status_id_count']) for data in grouped_res) + grouped_dict = {holiday_status.id: count for holiday_status, count in grouped_res} for allocation in self: allocation.allocation_count = grouped_dict.get(allocation.id, 0) @@ -514,15 +514,15 @@ class HolidaysType(models.Model): grouped_res = self.env['hr.leave']._read_group( domain, ['holiday_status_id'], - ['holiday_status_id'], + ['__count'], ) - grouped_dict = dict((data['holiday_status_id'][0], data['holiday_status_id_count']) for data in grouped_res) + grouped_dict = {holiday_status.id: count for holiday_status, count in grouped_res} for allocation in self: allocation.group_days_leave = grouped_dict.get(allocation.id, 0) def _compute_accrual_count(self): - accrual_allocations = self.env['hr.leave.accrual.plan']._read_group([('time_off_type_id', 'in', self.ids)], ['time_off_type_id'], ['time_off_type_id']) - mapped_data = dict((data['time_off_type_id'][0], data['time_off_type_id_count']) for data in accrual_allocations) + accrual_allocations = self.env['hr.leave.accrual.plan']._read_group([('time_off_type_id', 'in', self.ids)], ['time_off_type_id'], ['__count']) + mapped_data = {time_off_type.id: count for time_off_type, count in accrual_allocations} for leave_type in self: leave_type.accrual_count = mapped_data.get(leave_type.id, 0) diff --git a/addons/hr_holidays/models/res_users.py b/addons/hr_holidays/models/res_users.py index efcebcf0347..557bf2285e6 100644 --- a/addons/hr_holidays/models/res_users.py +++ b/addons/hr_holidays/models/res_users.py @@ -66,11 +66,10 @@ class User(models.Model): if not any(u.has_group(approver_group) for u in self): return - res = self.env['hr.employee'].read_group( + res = self.env['hr.employee']._read_group( [('leave_manager_id', 'in', self.ids)], - ['leave_manager_id'], ['leave_manager_id']) - responsibles_to_remove_ids = set(self.ids) - {x['leave_manager_id'][0] for x in res} + responsibles_to_remove_ids = set(self.ids) - {leave_manager.id for [leave_manager] in res} if responsibles_to_remove_ids: self.browse(responsibles_to_remove_ids).write({ 'groups_id': [Command.unlink(self.env.ref(approver_group).id)], diff --git a/addons/hr_holidays/models/resource.py b/addons/hr_holidays/models/resource.py index 40c5ee3b7df..5338fba46de 100644 --- a/addons/hr_holidays/models/resource.py +++ b/addons/hr_holidays/models/resource.py @@ -108,12 +108,12 @@ class ResourceCalendar(models.Model): associated_leaves_count = fields.Integer("Time Off Count", compute='_compute_associated_leaves_count') def _compute_associated_leaves_count(self): - leaves_read_group = self.env['resource.calendar.leaves'].read_group( - [('resource_id', '=', False)], + leaves_read_group = self.env['resource.calendar.leaves']._read_group( + [('resource_id', '=', False), ('calendar_id', 'in', [False, *self.ids])], ['calendar_id'], - ['calendar_id'] + ['__count'], ) - result = dict((data['calendar_id'][0] if data['calendar_id'] else 'global', data['calendar_id_count']) for data in leaves_read_group) + result = {calendar.id if calendar else 'global': count for calendar, count in leaves_read_group} global_leave_count = result.get('global', 0) for calendar in self: calendar.associated_leaves_count = result.get(calendar.id, 0) + global_leave_count diff --git a/addons/hr_recruitment/models/hr_applicant.py b/addons/hr_recruitment/models/hr_applicant.py index fd2f1478593..04c437e5ca8 100644 --- a/addons/hr_recruitment/models/hr_applicant.py +++ b/addons/hr_recruitment/models/hr_applicant.py @@ -205,8 +205,8 @@ class Applicant(models.Model): def _get_attachment_number(self): read_group_res = self.env['ir.attachment']._read_group( [('res_model', '=', 'hr.applicant'), ('res_id', 'in', self.ids)], - ['res_id'], ['res_id']) - attach_data = dict((res['res_id'], res['res_id_count']) for res in read_group_res) + ['res_id'], ['__count']) + attach_data = dict(read_group_res) for record in self: record.attachment_number = attach_data.get(record.id, 0) diff --git a/addons/hr_recruitment/models/hr_department.py b/addons/hr_recruitment/models/hr_department.py index 2114b12d696..29722ab6175 100644 --- a/addons/hr_recruitment/models/hr_department.py +++ b/addons/hr_recruitment/models/hr_department.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from odoo import api, fields, models +from odoo import fields, models class HrDepartment(models.Model): @@ -17,8 +17,8 @@ class HrDepartment(models.Model): if self.env.user.has_group('hr_recruitment.group_hr_recruitment_interviewer'): applicant_data = self.env['hr.applicant']._read_group( [('department_id', 'in', self.ids), ('stage_id.sequence', '<=', '1')], - ['department_id'], ['department_id']) - result = dict((data['department_id'][0], data['department_id_count']) for data in applicant_data) + ['department_id'], ['__count']) + result = {department.id: count for department, count in applicant_data} for department in self: department.new_applicant_count = result.get(department.id, 0) else: @@ -27,9 +27,9 @@ class HrDepartment(models.Model): def _compute_recruitment_stats(self): job_data = self.env['hr.job']._read_group( [('department_id', 'in', self.ids)], - ['no_of_hired_employee', 'no_of_recruitment', 'department_id'], ['department_id']) - new_emp = dict((data['department_id'][0], data['no_of_hired_employee']) for data in job_data) - expected_emp = dict((data['department_id'][0], data['no_of_recruitment']) for data in job_data) + ['department_id'], ['no_of_hired_employee:sum', 'no_of_recruitment:sum']) + new_emp = {department.id: nb_employee for department, nb_employee, __ in job_data} + expected_emp = {department.id: nb_recruitment for department, __, nb_recruitment in job_data} for department in self: department.new_hired_employee = new_emp.get(department.id, 0) department.expected_employee = expected_emp.get(department.id, 0) diff --git a/addons/hr_recruitment/models/hr_job.py b/addons/hr_recruitment/models/hr_job.py index 4d39bef26b4..219640df429 100644 --- a/addons/hr_recruitment/models/hr_job.py +++ b/addons/hr_recruitment/models/hr_job.py @@ -134,14 +134,14 @@ class Job(models.Model): ('active', '=', True), '&', ('active', '=', False), ('refuse_reason_id', '!=', False), - ], ['job_id'], ['job_id']) - result = dict((data['job_id'][0], data['job_id_count']) for data in read_group_result) + ], ['job_id'], ['__count']) + result = {job.id: count for job, count in read_group_result} for job in self: job.all_application_count = result.get(job.id, 0) def _compute_application_count(self): - read_group_result = self.env['hr.applicant']._read_group([('job_id', 'in', self.ids)], ['job_id'], ['job_id']) - result = dict((data['job_id'][0], data['job_id_count']) for data in read_group_result) + read_group_result = self.env['hr.applicant']._read_group([('job_id', 'in', self.ids)], ['job_id'], ['__count']) + result = {job.id: count for job, count in read_group_result} for job in self: job.application_count = result.get(job.id, 0) @@ -189,8 +189,8 @@ class Job(models.Model): hired_data = self.env['hr.applicant']._read_group([ ('job_id', 'in', self.ids), ('stage_id', 'in', hired_stages.ids), - ], ['job_id'], ['job_id']) - job_hires = {data['job_id'][0]: data['job_id_count'] for data in hired_data} + ], ['job_id'], ['__count']) + job_hires = {job.id: count for job, count in hired_data} for job in self: job.applicant_hired = job_hires.get(job.id, 0) diff --git a/addons/hr_recruitment/models/hr_recruitment_stage.py b/addons/hr_recruitment/models/hr_recruitment_stage.py index 10e89061fa5..af3cf604344 100644 --- a/addons/hr_recruitment/models/hr_recruitment_stage.py +++ b/addons/hr_recruitment/models/hr_recruitment_stage.py @@ -42,8 +42,8 @@ class RecruitmentStage(models.Model): @api.depends('hired_stage') def _compute_is_warning_visible(self): - applicant_data = self.env['hr.applicant']._read_group([('stage_id', 'in', self.ids)], ['stage_id'], 'stage_id') - applicants = dict((data['stage_id'][0], data['stage_id_count']) for data in applicant_data) + applicant_data = self.env['hr.applicant']._read_group([('stage_id', 'in', self.ids)], ['stage_id'], ['__count']) + applicants = {stage.id: count for stage, count in applicant_data} for stage in self: if stage._origin.hired_stage and not stage.hired_stage and applicants.get(stage._origin.id): stage.is_warning_visible = True diff --git a/addons/hr_recruitment/models/res_users.py b/addons/hr_recruitment/models/res_users.py index 996530feae4..77b7002aacc 100644 --- a/addons/hr_recruitment/models/res_users.py +++ b/addons/hr_recruitment/models/res_users.py @@ -23,11 +23,11 @@ class ResUsers(models.Model): interviewer_group = self.env.ref('hr_recruitment.group_hr_recruitment_interviewer') recruitment_group = self.env.ref('hr_recruitment.group_hr_recruitment_user') - job_interviewers = self.env['hr.job']._read_group([('interviewer_ids', 'in', self.ids)], ['interviewer_ids'], ['interviewer_ids']) - user_ids = {j['interviewer_ids'][0] for j in job_interviewers} + job_interviewers = self.env['hr.job']._read_group([('interviewer_ids', 'in', self.ids)], ['interviewer_ids']) + user_ids = {interviewer.id for [interviewer] in job_interviewers} - application_interviewers = self.env['hr.applicant']._read_group([('interviewer_ids', 'in', self.ids)], ['interviewer_ids'], ['interviewer_ids']) - user_ids |= {a['interviewer_ids'][0] for a in application_interviewers} + application_interviewers = self.env['hr.applicant']._read_group([('interviewer_ids', 'in', self.ids)], ['interviewer_ids']) + user_ids |= {interviewer.id for [interviewer] in application_interviewers} # Remove users that are no longer interviewers on at least a job or an application users_to_remove = set(self.ids) - (user_ids | set(recruitment_group.users.ids)) diff --git a/addons/hr_timesheet/controllers/portal.py b/addons/hr_timesheet/controllers/portal.py index 93d54d12492..8cc54671206 100644 --- a/addons/hr_timesheet/controllers/portal.py +++ b/addons/hr_timesheet/controllers/portal.py @@ -137,14 +137,14 @@ class TimesheetCustomerPortal(CustomerPortal): timesheets = Timesheet_sudo.search(domain, order=orderby, limit=_items_per_page, offset=pager['offset']) if field: if groupby == 'date': - raw_timesheets_group = Timesheet_sudo.read_group( - domain, ["unit_amount:sum", "ids:array_agg(id)"], ["date:day"] + raw_timesheets_group = Timesheet_sudo._read_group( + domain, ['date:day'], ['unit_amount:sum', 'id:array_agg'] ) - grouped_timesheets = [(Timesheet_sudo.browse(group["ids"]), group["unit_amount"]) for group in raw_timesheets_group] + grouped_timesheets = [(records, unit_amount) for __, unit_amount, records in raw_timesheets_group] else: - time_data = Timesheet_sudo.read_group(domain, [field, 'unit_amount:sum'], [field]) - mapped_time = dict([(m[field][0] if m[field] else False, m['unit_amount']) for m in time_data]) + time_data = Timesheet_sudo._read_group(domain, [field], ['unit_amount:sum']) + mapped_time = {field.id: unit_amount for field, unit_amount in time_data} grouped_timesheets = [(Timesheet_sudo.concat(*g), mapped_time[k.id]) for k, g in groupbyelem(timesheets, itemgetter(field))] return timesheets, grouped_timesheets diff --git a/addons/hr_timesheet/models/hr_employee.py b/addons/hr_timesheet/models/hr_employee.py index 30d58ce0f87..640e297f908 100644 --- a/addons/hr_timesheet/models/hr_employee.py +++ b/addons/hr_timesheet/models/hr_employee.py @@ -31,11 +31,11 @@ class HrEmployee(models.Model): name_mapping = dict(res) employees_count_per_user = { - res['user_id'][0]: res['user_id_count'] - for res in self.env['hr.employee'].sudo()._read_group( + user.id: count + for user, count in self.env['hr.employee'].sudo()._read_group( [('user_id', 'in', self.user_id.ids), ('company_id', 'in', allowed_company_ids)], ['user_id'], - ['user_id'], + ['__count'], ) } for employee in self: diff --git a/addons/hr_timesheet/models/project_project.py b/addons/hr_timesheet/models/project_project.py index 4881dac58a8..33ec9633f51 100644 --- a/addons/hr_timesheet/models/project_project.py +++ b/addons/hr_timesheet/models/project_project.py @@ -79,10 +79,10 @@ class Project(models.Model): def _compute_remaining_hours(self): timesheets_read_group = self.env['account.analytic.line']._read_group( [('project_id', 'in', self.ids)], - ['project_id', 'unit_amount'], ['project_id'], - lazy=False) - timesheet_time_dict = {res['project_id'][0]: res['unit_amount'] for res in timesheets_read_group} + ['unit_amount:sum'], + ) + timesheet_time_dict = {project.id: unit_amount_sum for project, unit_amount_sum in timesheets_read_group} for project in self: project.remaining_hours = project.allocated_hours - timesheet_time_dict.get(project.id, 0) project.is_project_overtime = project.remaining_hours < 0 @@ -120,28 +120,23 @@ class Project(models.Model): @api.depends('timesheet_ids') def _compute_total_timesheet_time(self): - timesheets_read_group = self.env['account.analytic.line'].read_group( + timesheets_read_group = self.env['account.analytic.line']._read_group( [('project_id', 'in', self.ids)], - ['project_id', 'unit_amount', 'product_uom_id'], ['project_id', 'product_uom_id'], - lazy=False) + ['unit_amount:sum'], + ) + timesheet_time_dict = defaultdict(list) - uom_ids = set(self.timesheet_encode_uom_id.ids) + for project, product_uom, unit_amount_sum in timesheets_read_group: + timesheet_time_dict[project.id].append((product_uom, unit_amount_sum)) - for result in timesheets_read_group: - uom_id = result['product_uom_id'] and result['product_uom_id'][0] - if uom_id: - uom_ids.add(uom_id) - timesheet_time_dict[result['project_id'][0]].append((uom_id, result['unit_amount'])) - - uoms_dict = {uom.id: uom for uom in self.env['uom.uom'].browse(uom_ids)} for project in self: # Timesheets may be stored in a different unit of measure, so first # we convert all of them to the reference unit # if the timesheet has no product_uom_id then we take the one of the project total_time = 0.0 - for product_uom_id, unit_amount in timesheet_time_dict[project.id]: - factor = uoms_dict.get(product_uom_id, project.timesheet_encode_uom_id).factor_inv + for product_uom, unit_amount in timesheet_time_dict[project.id]: + factor = (product_uom or project.timesheet_encode_uom_id).factor_inv total_time += unit_amount * (1.0 if project.encode_uom_in_days else factor) # Now convert to the proper unit of measure set in the settings total_time *= project.timesheet_encode_uom_id.factor diff --git a/addons/hr_timesheet/models/project_task.py b/addons/hr_timesheet/models/project_task.py index 603e37f8061..b91dc125052 100644 --- a/addons/hr_timesheet/models/project_task.py +++ b/addons/hr_timesheet/models/project_task.py @@ -66,8 +66,8 @@ class Task(models.Model): for task in self: task.effective_hours = round(sum(task.timesheet_ids.mapped('unit_amount')), 2) return - timesheet_read_group = self.env['account.analytic.line'].read_group([('task_id', 'in', self.ids)], ['unit_amount', 'task_id'], ['task_id']) - timesheets_per_task = {res['task_id'][0]: res['unit_amount'] for res in timesheet_read_group} + timesheet_read_group = self.env['account.analytic.line']._read_group([('task_id', 'in', self.ids)], ['task_id'], ['unit_amount:sum']) + timesheets_per_task = {task.id: amount for task, amount in timesheet_read_group} for task in self: task.effective_hours = round(timesheets_per_task.get(task.id, 0.0), 2) @@ -223,9 +223,8 @@ class Task(models.Model): timesheet_data = self.env['account.analytic.line'].sudo()._read_group( [('task_id', 'in', self.ids)], ['task_id'], - ['task_id'], ) - task_with_timesheets_ids = [res['task_id'][0] for res in timesheet_data] + task_with_timesheets_ids = [task.id for task in timesheet_data] if task_with_timesheets_ids: if len(task_with_timesheets_ids) > 1: warning_msg = _("These tasks have some timesheet entries referencing them. Before removing these tasks, you have to remove these timesheet entries.") diff --git a/addons/hr_timesheet/populate/hr_timesheet.py b/addons/hr_timesheet/populate/hr_timesheet.py index eb96f0b4f4e..fdb89914aaa 100644 --- a/addons/hr_timesheet/populate/hr_timesheet.py +++ b/addons/hr_timesheet/populate/hr_timesheet.py @@ -15,32 +15,32 @@ class AccountAnalyticLine(models.Model): def _populate_factories(self): projects_groups = self.env['project.project']._read_group( domain=[('id', 'in', self.env.registry.populated_models["project.project"])], - fields=['company_id', 'ids:array_agg(id)'], groupby=['company_id'], + aggregates=['id:array_agg'], ) project_ids = [] projects_per_company = defaultdict(list) - for group in projects_groups: - project_ids += group['ids'] - projects_per_company[group['company_id'][0]] = group['ids'] + for company, ids in projects_groups: + project_ids += ids + projects_per_company[company.id] = ids tasks_per_project = { - group['project_id'][0]: group['ids'] - for group in self.env['project.task']._read_group( + project.id: ids + for project, ids in self.env['project.task']._read_group( domain=[ ('id', 'in', self.env.registry.populated_models["project.task"]), ('project_id', 'in', project_ids), ], - fields=['project_id', 'ids:array_agg(id)'], groupby=['project_id'], + aggregates=['id:array_agg'], ) } employees_per_company = { - group['company_id'][0]: group['ids'] - for group in self.env['hr.employee']._read_group( + company.id: ids + for company, ids in self.env['hr.employee']._read_group( domain=[('id', 'in', self.env.registry.populated_models["hr.employee"])], - fields=['company_id', 'ids:array_agg(id)'], groupby=['company_id'], + aggregates=['id:array_agg'], ) } # Companies with projects and employees only diff --git a/addons/hr_timesheet/wizard/hr_employee_delete_wizard.py b/addons/hr_timesheet/wizard/hr_employee_delete_wizard.py index a978af598d4..e784d5c93a0 100644 --- a/addons/hr_timesheet/wizard/hr_employee_delete_wizard.py +++ b/addons/hr_timesheet/wizard/hr_employee_delete_wizard.py @@ -16,9 +16,9 @@ class HrEmployeDeleteWizard(models.TransientModel): def _compute_has_timesheet(self): timesheet_read_group = self.env['account.analytic.line']._read_group([ ('employee_id', 'in', self.employee_ids.ids)], - ['employee_id'], ['employee_id'], + ['employee_id'], ) - timesheet_employee_map = set(employee_info['employee_id'][0] for employee_info in timesheet_read_group) + timesheet_employee_map = {employee.id for [employee] in timesheet_read_group} for wizard in self: wizard.has_timesheet = timesheet_employee_map & set(wizard.employee_ids.ids) diff --git a/addons/hr_timesheet_attendance/tests/test_timesheet_attendance.py b/addons/hr_timesheet_attendance/tests/test_timesheet_attendance.py index 699e7445bc5..a70de68c7bc 100644 --- a/addons/hr_timesheet_attendance/tests/test_timesheet_attendance.py +++ b/addons/hr_timesheet_attendance/tests/test_timesheet_attendance.py @@ -24,12 +24,11 @@ class TestTimesheetAttendance(TestCommonTimesheet): 'unit_amount': 6.0, 'date': datetime(2022, 2, 9), }) - report_data = self.env['hr.timesheet.attendance.report']._read_group( + total_timesheet, total_attendance = self.env['hr.timesheet.attendance.report']._read_group( [('user_id', '=', self.user_employee.id), ('date', '>=', datetime(2022, 2, 9, 8, 0)), ('date', '<=', datetime(2022, 2, 9, 16, 0))], - ['total_timesheet', 'total_attendance', 'total_difference'], - ['user_id'], + aggregates=['total_timesheet:sum', 'total_attendance:sum'], )[0] - self.assertEqual(report_data['total_timesheet'], 6.0, "Total timesheet in report should be 4.0") - self.assertEqual(report_data['total_attendance'], 7.0, "Total attendance in report should be 8.0") - self.assertEqual(report_data['total_attendance'] - report_data['total_timesheet'], 1) + self.assertEqual(total_timesheet, 6.0, "Total timesheet in report should be 4.0") + self.assertEqual(total_attendance, 7.0, "Total attendance in report should be 8.0") + self.assertEqual(total_attendance - total_timesheet, 1) diff --git a/addons/hr_work_entry_holidays/models/hr_leave.py b/addons/hr_work_entry_holidays/models/hr_leave.py index ce02ba4cacc..5485b00a728 100644 --- a/addons/hr_work_entry_holidays/models/hr_leave.py +++ b/addons/hr_work_entry_holidays/models/hr_leave.py @@ -135,11 +135,11 @@ Contracts: ('date_start', '<', stop), ('date_stop', '>', start), ('employee_id', 'in', self.employee_id.ids), - ], ['work_entry_ids:array_agg(id)', 'employee_id'], ['employee_id', 'date_start', 'date_stop'], lazy=False) - work_entries_by_employee = defaultdict(lambda: self.env['hr.work.entry']) - for group in work_entry_groups: - employee_id = group.get('employee_id')[0] - work_entries_by_employee[employee_id] |= self.env['hr.work.entry'].browse(group.get('work_entry_ids')) + ], ['employee_id'], ['id:recordset']) + work_entries_by_employee = { + employee.id: work_entries + for employee, work_entries in work_entry_groups + } # 3. Archive work entries included in leaves included = self.env['hr.work.entry'] diff --git a/addons/im_livechat/models/chatbot_script.py b/addons/im_livechat/models/chatbot_script.py index 1907378c450..3bb69c7c8b2 100644 --- a/addons/im_livechat/models/chatbot_script.py +++ b/addons/im_livechat/models/chatbot_script.py @@ -28,9 +28,9 @@ class ChatbotScript(models.Model): ], compute="_compute_first_step_warning") def _compute_livechat_channel_count(self): - channels_data = self.env['im_livechat.channel.rule'].read_group( - [('chatbot_script_id', 'in', self.ids)], ['channel_id:count_distinct'], ['chatbot_script_id']) - mapped_channels = {channel['chatbot_script_id'][0]: channel['channel_id'] for channel in channels_data} + channels_data = self.env['im_livechat.channel.rule']._read_group( + [('chatbot_script_id', 'in', self.ids)], ['chatbot_script_id'], ['channel_id:count_distinct']) + mapped_channels = {chatbot_script.id: count_distinct for chatbot_script, count_distinct in channels_data} for script in self: script.livechat_channel_count = mapped_channels.get(script.id, 0) diff --git a/addons/im_livechat/models/chatbot_script_step.py b/addons/im_livechat/models/chatbot_script_step.py index 91f30ee456b..4684c4efe63 100644 --- a/addons/im_livechat/models/chatbot_script_step.py +++ b/addons/im_livechat/models/chatbot_script_step.py @@ -92,18 +92,15 @@ class ChatbotScriptStep(models.Model): step_values.append(vals) vals_by_chatbot_id[chatbot_id] = step_values - max_sequence_by_chatbot = {} - if vals_by_chatbot_id: - read_group_results = self.env['chatbot.script.step'].read_group( - [('chatbot_script_id', 'in', list(vals_by_chatbot_id.keys()))], - ['sequence:max'], - ['chatbot_script_id'] - ) - - max_sequence_by_chatbot = { - read_group_result['chatbot_script_id'][0]: read_group_result['sequence'] - for read_group_result in read_group_results - } + read_group_results = self.env['chatbot.script.step']._read_group( + [('chatbot_script_id', 'in', list(vals_by_chatbot_id))], + ['chatbot_script_id'], + ['sequence:max'], + ) + max_sequence_by_chatbot = { + chatbot_script.id: sequence + for chatbot_script, sequence in read_group_results + } for chatbot_id, step_vals in vals_by_chatbot_id.items(): current_sequence = 0 diff --git a/addons/im_livechat/models/digest.py b/addons/im_livechat/models/digest.py index d89dbb6d81f..045c2c6fc55 100644 --- a/addons/im_livechat/models/digest.py +++ b/addons/im_livechat/models/digest.py @@ -39,8 +39,8 @@ class Digest(models.Model): response_time = self.env['im_livechat.report.channel'].sudo()._read_group([ ('start_date', '>=', start), ('start_date', '<', end), - ], ['time_to_answer:avg'], []) - self.kpi_livechat_response_value = response_time[0]['time_to_answer'] + ], [], ['time_to_answer:avg']) + self.kpi_livechat_response_value = response_time[0][0] def _compute_kpis_actions(self, company, user): res = super(Digest, self)._compute_kpis_actions(company, user) diff --git a/addons/im_livechat/models/im_livechat_channel.py b/addons/im_livechat/models/im_livechat_channel.py index 14ac166b396..ee50c074f77 100644 --- a/addons/im_livechat/models/im_livechat_channel.py +++ b/addons/im_livechat/models/im_livechat_channel.py @@ -66,9 +66,9 @@ class ImLivechatChannel(models.Model): @api.depends('rule_ids.chatbot_script_id') def _compute_chatbot_script_count(self): - data = self.env['im_livechat.channel.rule'].read_group( - [('channel_id', 'in', self.ids)], ['chatbot_script_id:count_distinct'], ['channel_id']) - mapped_data = {rule['channel_id'][0]: rule['chatbot_script_id'] for rule in data} + data = self.env['im_livechat.channel.rule']._read_group( + [('channel_id', 'in', self.ids)], ['channel_id'], ['chatbot_script_id:count_distinct']) + mapped_data = {channel.id: count_distinct for channel, count_distinct in data} for channel in self: channel.chatbot_script_count = mapped_data.get(channel.id, 0) @@ -89,8 +89,8 @@ class ImLivechatChannel(models.Model): def _compute_nbr_channel(self): data = self.env['mail.channel']._read_group([ ('livechat_channel_id', 'in', self._ids), - ('has_message', '=', True)], ['__count'], ['livechat_channel_id'], lazy=False) - channel_count = {x['livechat_channel_id'][0]: x['__count'] for x in data} + ('has_message', '=', True)], ['livechat_channel_id'], ['__count']) + channel_count = {livechat_channel.id: count for livechat_channel, count in data} for record in self: record.nbr_channel = channel_count.get(record.id, 0) diff --git a/addons/link_tracker/models/link_tracker.py b/addons/link_tracker/models/link_tracker.py index 6b7cf27e88c..9938a8ee7da 100644 --- a/addons/link_tracker/models/link_tracker.py +++ b/addons/link_tracker/models/link_tracker.py @@ -58,15 +58,12 @@ class LinkTracker(models.Model): @api.depends('link_click_ids.link_id') def _compute_count(self): - if self.ids: - clicks_data = self.env['link.tracker.click']._read_group( - [('link_id', 'in', self.ids)], - ['link_id'], - ['link_id'] - ) - mapped_data = {m['link_id'][0]: m['link_id_count'] for m in clicks_data} - else: - mapped_data = dict() + clicks_data = self.env['link.tracker.click']._read_group( + [('link_id', 'in', self.ids)], + ['link_id'], + ['__count'], + ) + mapped_data = {link.id: count for link, count in clicks_data} for tracker in self: tracker.count = mapped_data.get(tracker.id, 0) diff --git a/addons/link_tracker/models/utm.py b/addons/link_tracker/models/utm.py index 1e7ccb50e02..05ed46c784c 100644 --- a/addons/link_tracker/models/utm.py +++ b/addons/link_tracker/models/utm.py @@ -13,9 +13,9 @@ class UtmCampaign(models.Model): def _compute_clicks_count(self): click_data = self.env['link.tracker.click']._read_group( [('campaign_id', 'in', self.ids)], - ['campaign_id'], ['campaign_id']) + ['campaign_id'], ['__count']) - mapped_data = {datum['campaign_id'][0]: datum['campaign_id_count'] for datum in click_data} + mapped_data = {campaign.id: count for campaign, count in click_data} for campaign in self: campaign.click_count = mapped_data.get(campaign.id, 0) diff --git a/addons/loyalty/models/loyalty_program.py b/addons/loyalty/models/loyalty_program.py index 71f94258604..d17f9057854 100644 --- a/addons/loyalty/models/loyalty_program.py +++ b/addons/loyalty/models/loyalty_program.py @@ -139,8 +139,8 @@ class LoyaltyProgram(models.Model): @api.depends('coupon_ids') def _compute_coupon_count(self): - read_group_data = self.env['loyalty.card']._read_group([('program_id', 'in', self.ids)], ['program_id'], ['program_id']) - count_per_program = {r['program_id'][0]: r['program_id_count'] for r in read_group_data} + read_group_data = self.env['loyalty.card']._read_group([('program_id', 'in', self.ids)], ['program_id'], ['__count']) + count_per_program = {program.id: count for program, count in read_group_data} for program in self: program.coupon_count = count_per_program.get(program.id, 0) diff --git a/addons/loyalty/models/res_partner.py b/addons/loyalty/models/res_partner.py index 0f22505e13f..c2c56f256cf 100644 --- a/addons/loyalty/models/res_partner.py +++ b/addons/loyalty/models/res_partner.py @@ -19,18 +19,15 @@ class ResPartner(models.Model): ('expiration_date', '>=', fields.Date().context_today(self)), ('expiration_date', '=', False), ], - fields=['partner_id'], groupby=['partner_id'], + aggregates=['__count'], ) - partners = self.browse() - for group in loyalty_groups: - partner = self.browse(group['partner_id'][0]) + self.loyalty_card_count = 0 + for partner, count in loyalty_groups: while partner: if partner in self: - partner.loyalty_card_count += group['partner_id_count'] - partners |= partner + partner.loyalty_card_count += count partner = partner.parent_id - (self - partners).loyalty_card_count = 0 def action_view_loyalty_cards(self): action = self.env['ir.actions.act_window']._for_xml_id('loyalty.loyalty_card_action') diff --git a/addons/lunch/models/lunch_product_category.py b/addons/lunch/models/lunch_product_category.py index bf204ece3c8..e53b8a89448 100644 --- a/addons/lunch/models/lunch_product_category.py +++ b/addons/lunch/models/lunch_product_category.py @@ -27,8 +27,8 @@ class LunchProductCategory(models.Model): image_1920 = fields.Image(default=_default_image) def _compute_product_count(self): - product_data = self.env['lunch.product']._read_group([('category_id', 'in', self.ids)], ['category_id'], ['category_id']) - data = {product['category_id'][0]: product['category_id_count'] for product in product_data} + product_data = self.env['lunch.product']._read_group([('category_id', 'in', self.ids)], ['category_id'], ['__count']) + data = {category.id: count for category, count in product_data} for category in self: category.product_count = data.get(category.id, 0) diff --git a/addons/mail/models/mail_activity.py b/addons/mail/models/mail_activity.py index 57749642846..54c26e87d8d 100644 --- a/addons/mail/models/mail_activity.py +++ b/addons/mail/models/mail_activity.py @@ -405,7 +405,7 @@ class MailActivity(models.Model): return activities._as_query(order) @api.model - def _read_group_raw(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True): + def _read_group(self, domain, groupby=(), aggregates=(), having=(), offset=0, limit=None, order=None): """ The base _read_group_raw method implementation computes a where based on a given domain (_where_calc) and manually applies ir rules (_apply_ir_rules). @@ -425,9 +425,9 @@ class MailActivity(models.Model): # force void result if no allowed ids found domain = expression.AND([domain, [(0, '=', 1)]]) - return super(MailActivity, self)._read_group_raw( - domain=domain, fields=fields, groupby=groupby, offset=offset, - limit=limit, orderby=orderby, lazy=lazy, + return super()._read_group( + domain=domain, groupby=groupby, aggregates=aggregates, + having=having, offset=offset, limit=limit, order=order, ) def name_get(self): @@ -608,28 +608,26 @@ class MailActivity(models.Model): if domain: res = self.env[res_model].search(domain) activity_domain.append(('res_id', 'in', res.ids)) - grouped_activities = self.env['mail.activity'].read_group( + grouped_activities = self.env['mail.activity']._read_group( activity_domain, - ['res_id', 'activity_type_id', 'ids:array_agg(id)', 'date_deadline:min(date_deadline)'], ['res_id', 'activity_type_id'], - lazy=False) + ['id:array_agg', 'date_deadline:min', '__count']) # filter out unreadable records if not domain: - res_ids = tuple(a['res_id'] for a in grouped_activities) - res = self.env[res_model].search([('id', 'in', res_ids)]) - grouped_activities = [a for a in grouped_activities if a['res_id'] in res.ids] + res_ids = tuple(res_id for res_id, *_ in grouped_activities) + res_ids_set = set(self.env[res_model].search([('id', 'in', res_ids)])._ids) + grouped_activities = [a for a in grouped_activities if a[0] in res_ids_set] res_id_to_deadline = {} activity_data = defaultdict(dict) - for group in grouped_activities: - res_id = group['res_id'] - activity_type_id = (group.get('activity_type_id') or (False, False))[0] - res_id_to_deadline[res_id] = group['date_deadline'] if (res_id not in res_id_to_deadline or group['date_deadline'] < res_id_to_deadline[res_id]) else res_id_to_deadline[res_id] - state = self._compute_state_from_date(group['date_deadline'], self.user_id.sudo().tz) + for res_id, activity_type, ids, date_deadline, count in grouped_activities: + activity_type_id = activity_type.id + res_id_to_deadline[res_id] = date_deadline if (res_id not in res_id_to_deadline or date_deadline < res_id_to_deadline[res_id]) else res_id_to_deadline[res_id] + state = self._compute_state_from_date(date_deadline, self.user_id.sudo().tz) activity_data[res_id][activity_type_id] = { - 'count': group['__count'], - 'ids': group['ids'], + 'count': count, + 'ids': ids, 'state': state, - 'o_closest_deadline': group['date_deadline'], + 'o_closest_deadline': date_deadline, } activity_type_infos = [] activity_type_ids = self.env['mail.activity.type'].search( diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index f93efd63670..fd0fbf158aa 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -191,8 +191,8 @@ class Channel(models.Model): @api.depends('channel_member_ids') def _compute_member_count(self): - read_group_res = self.env['mail.channel.member']._read_group(domain=[('channel_id', 'in', self.ids)], fields=['channel_id'], groupby=['channel_id']) - member_count_by_channel_id = {item['channel_id'][0]: item['channel_id_count'] for item in read_group_res} + read_group_res = self.env['mail.channel.member']._read_group(domain=[('channel_id', 'in', self.ids)], groupby=['channel_id'], aggregates=['__count']) + member_count_by_channel_id = {channel.id: count for channel, count in read_group_res} for channel in self: channel.member_count = member_count_by_channel_id.get(channel.id, 0) diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py index e58e830e25a..5ec2bfd7cb5 100644 --- a/addons/mail/models/mail_message.py +++ b/addons/mail/models/mail_message.py @@ -719,13 +719,13 @@ class Message(models.Model): return super(Message, self).unlink() @api.model - def _read_group_raw(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True): + def _read_group(self, domain, groupby=(), aggregates=(), having=(), offset=0, limit=None, order=None): if not self.env.is_admin(): raise AccessError(_("Only administrators are allowed to use grouped read on message model")) - return super(Message, self)._read_group_raw( - domain=domain, fields=fields, groupby=groupby, offset=offset, - limit=limit, orderby=orderby, lazy=lazy, + return super()._read_group( + domain=domain, groupby=groupby, aggregates=aggregates, + having=having, offset=offset, limit=limit, order=order, ) def export_data(self, fields_to_export): diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index f2fc53cb384..05c46009b41 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -228,10 +228,10 @@ class MailThread(models.AbstractModel): def _compute_message_attachment_count(self): read_group_var = self.env['ir.attachment']._read_group([('res_id', 'in', self.ids), ('res_model', '=', self._name)], - fields=['res_id'], - groupby=['res_id']) + groupby=['res_id'], + aggregates=['__count']) - attachment_count_dict = dict((d['res_id'], d['res_id_count']) for d in read_group_var) + attachment_count_dict = dict(read_group_var) for record in self: record.message_attachment_count = attachment_count_dict.get(record.id, 0) diff --git a/addons/mail_group/models/mail_group.py b/addons/mail_group/models/mail_group.py index d6bd7bd6345..aae6335e9e1 100644 --- a/addons/mail_group/models/mail_group.py +++ b/addons/mail_group/models/mail_group.py @@ -100,12 +100,12 @@ class MailGroup(models.Model): ('mail_group_id', 'in', self.ids), ('create_date', '>=', fields.Datetime.to_string(month_date)), ('moderation_status', '=', 'accepted'), - ], ['mail_group_id'], ['mail_group_id']) + ], ['mail_group_id'], ['__count']) # { mail_discusison_id: number_of_mail_group_message_last_month_count } messages_data = { - message['mail_group_id'][0]: message['mail_group_id_count'] - for message in messages_data + mail_group.id: count + for mail_group, count in messages_data } for group in self: @@ -120,11 +120,11 @@ class MailGroup(models.Model): results = self.env['mail.group.message']._read_group( [('mail_group_id', 'in', self.ids)], ['mail_group_id'], - ['mail_group_id'], + ['__count'], ) result_per_group = { - result['mail_group_id'][0]: result['mail_group_id_count'] - for result in results + mail_group.id: count + for mail_group, count in results } for group in self: group.mail_group_message_count = result_per_group.get(group.id, 0) @@ -134,11 +134,11 @@ class MailGroup(models.Model): results = self.env['mail.group.message']._read_group( [('mail_group_id', 'in', self.ids), ('moderation_status', '=', 'pending_moderation')], ['mail_group_id'], - ['mail_group_id'], + ['__count'], ) result_per_group = { - result['mail_group_id'][0]: result['mail_group_id_count'] - for result in results + mail_group.id: count + for mail_group, count in results } for group in self: @@ -486,12 +486,11 @@ class MailGroup(models.Model): _logger.warning('Template "mail_group.mail_group_notify_moderation" was not found. Cannot send reminder notifications.') return - results = self.env['mail.group.message'].read_group( + results = self.env['mail.group.message']._read_group( [('mail_group_id', 'in', self.ids), ('moderation_status', '=', 'pending_moderation')], ['mail_group_id'], - ['mail_group_id'], ) - groups = self.browse([result['mail_group_id'][0] for result in results]) + groups = self.browse([mail_group.id for [mail_group] in results]) for group in groups: moderators_to_notify = group.moderator_ids diff --git a/addons/maintenance/models/maintenance.py b/addons/maintenance/models/maintenance.py index 2d5cce39fb5..269a864cd33 100644 --- a/addons/maintenance/models/maintenance.py +++ b/addons/maintenance/models/maintenance.py @@ -54,14 +54,14 @@ class MaintenanceEquipmentCategory(models.Model): fold = fields.Boolean(string='Folded in Maintenance Pipe', compute='_compute_fold', store=True) def _compute_equipment_count(self): - equipment_data = self.env['maintenance.equipment']._read_group([('category_id', 'in', self.ids)], ['category_id'], ['category_id']) - mapped_data = dict([(m['category_id'][0], m['category_id_count']) for m in equipment_data]) + equipment_data = self.env['maintenance.equipment']._read_group([('category_id', 'in', self.ids)], ['category_id'], ['__count']) + mapped_data = {category.id: count for category, count in equipment_data} for category in self: category.equipment_count = mapped_data.get(category.id, 0) def _compute_maintenance_count(self): - maintenance_data = self.env['maintenance.request']._read_group([('category_id', 'in', self.ids)], ['category_id'], ['category_id']) - mapped_data = dict([(m['category_id'][0], m['category_id_count']) for m in maintenance_data]) + maintenance_data = self.env['maintenance.request']._read_group([('category_id', 'in', self.ids)], ['category_id'], ['__count']) + mapped_data = {category.id: count for category, count in maintenance_data} for category in self: category.maintenance_count = mapped_data.get(category.id, 0) diff --git a/addons/mass_mailing/models/mailing.py b/addons/mass_mailing/models/mailing.py index 2ac07c3c815..13b8725b795 100644 --- a/addons/mass_mailing/models/mailing.py +++ b/addons/mass_mailing/models/mailing.py @@ -385,8 +385,8 @@ class MassMailing(models.Model): def _compute_mailing_filter_count(self): filter_data = self.env['mailing.filter']._read_group([ ('mailing_model_id', 'in', self.mailing_model_id.ids) - ], ['mailing_model_id'], ['mailing_model_id']) - mapped_data = {data['mailing_model_id'][0]: data['mailing_model_id_count'] for data in filter_data} + ], ['mailing_model_id'], ['__count']) + mapped_data = {mailing_model.id: count for mailing_model, count in filter_data} for mailing in self: mailing.mailing_filter_count = mapped_data.get(mailing.mailing_model_id.id, 0) diff --git a/addons/mass_mailing/models/utm_campaign.py b/addons/mass_mailing/models/utm_campaign.py index 63165ef7f5d..9183b4f36b0 100644 --- a/addons/mass_mailing/models/utm_campaign.py +++ b/addons/mass_mailing/models/utm_campaign.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from collections import defaultdict from dateutil.relativedelta import relativedelta from odoo import api, fields, models, _ @@ -48,25 +49,20 @@ class UtmCampaign(models.Model): @api.depends('mailing_mail_ids') def _compute_mailing_mail_count(self): - if self.ids: - mailing_data = self.env['mailing.mailing']._read_group( - [('campaign_id', 'in', self.ids), ('mailing_type', '=', 'mail')], - ['campaign_id', 'ab_testing_enabled'], - ['campaign_id', 'ab_testing_enabled'], - lazy=False, - ) - ab_testing_mapped_data = {} - mapped_data = {} - for data in mailing_data: - if data['ab_testing_enabled']: - ab_testing_mapped_data.setdefault(data['campaign_id'][0], []).append(data['__count']) - mapped_data.setdefault(data['campaign_id'][0], []).append(data['__count']) - else: - mapped_data = dict() - ab_testing_mapped_data = dict() + mailing_data = self.env['mailing.mailing']._read_group( + [('campaign_id', 'in', self.ids), ('mailing_type', '=', 'mail')], + ['campaign_id', 'ab_testing_enabled'], + ['__count'], + ) + ab_testing_mapped_data = defaultdict(list) + mapped_data = defaultdict(list) + for campaign, ab_testing_enabled, count in mailing_data: + if ab_testing_enabled: + ab_testing_mapped_data[campaign.id].append(count) + mapped_data[campaign.id].append(count) for campaign in self: - campaign.mailing_mail_count = sum(mapped_data.get(campaign._origin.id or campaign.id, [])) - campaign.ab_testing_mailings_count = sum(ab_testing_mapped_data.get(campaign._origin.id or campaign.id, [])) + campaign.mailing_mail_count = sum(mapped_data[campaign._origin.id or campaign.id]) + campaign.ab_testing_mailings_count = sum(ab_testing_mapped_data[campaign._origin.id or campaign.id]) def _compute_statistics(self): """ Compute statistics of the mass mailing campaign """ diff --git a/addons/mass_mailing_crm/models/mailing_mailing.py b/addons/mass_mailing_crm/models/mailing_mailing.py index d9d316f56e3..65f2317467a 100644 --- a/addons/mass_mailing_crm/models/mailing_mailing.py +++ b/addons/mass_mailing_crm/models/mailing_mailing.py @@ -18,9 +18,9 @@ class MassMailing(models.Model): def _compute_crm_lead_count(self): lead_data = self.env['crm.lead'].with_context(active_test=False).sudo()._read_group( [('source_id', 'in', self.source_id.ids)], - ['source_id'], ['source_id'], + ['source_id'], ['__count'], ) - mapped_data = {datum['source_id'][0]: datum['source_id_count'] for datum in lead_data} + mapped_data = {source.id: count for source, count in lead_data} for mass_mailing in self: mass_mailing.crm_lead_count = mapped_data.get(mass_mailing.source_id.id, 0) diff --git a/addons/mass_mailing_sale/models/mailing_mailing.py b/addons/mass_mailing_sale/models/mailing_mailing.py index 09ee25e4626..7f8a92ef6b2 100644 --- a/addons/mass_mailing_sale/models/mailing_mailing.py +++ b/addons/mass_mailing_sale/models/mailing_mailing.py @@ -17,9 +17,9 @@ class MassMailing(models.Model): def _compute_sale_quotation_count(self): quotation_data = self.env['sale.order'].sudo()._read_group( [('source_id', 'in', self.source_id.ids)], - ['source_id'], ['source_id'], + ['source_id'], ['__count'], ) - mapped_data = {datum['source_id'][0]: datum['source_id_count'] for datum in quotation_data} + mapped_data = {source.id: count for source, count in quotation_data} for mass_mailing in self: mass_mailing.sale_quotation_count = mapped_data.get(mass_mailing.source_id.id, 0) @@ -30,9 +30,9 @@ class MassMailing(models.Model): [('state', 'not in', ['draft', 'cancel'])] ]) moves_data = self.env['account.move'].sudo()._read_group( - domain, ['source_id', 'amount_untaxed_signed'], ['source_id'], + domain, ['source_id'], ['amount_untaxed_signed:sum'], ) - mapped_data = {datum['source_id'][0]: datum['amount_untaxed_signed'] for datum in moves_data} + mapped_data = {source.id: amount_untaxed_signed for source, amount_untaxed_signed in moves_data} for mass_mailing in self: mass_mailing.sale_invoiced_amount = mapped_data.get(mass_mailing.source_id.id, 0) diff --git a/addons/mass_mailing_sms/models/mailing_mailing.py b/addons/mass_mailing_sms/models/mailing_mailing.py index 083ba141859..14ae27c23e8 100644 --- a/addons/mass_mailing_sms/models/mailing_mailing.py +++ b/addons/mass_mailing_sms/models/mailing_mailing.py @@ -70,23 +70,18 @@ class Mailing(models.Model): @api.depends('mailing_trace_ids.failure_type') def _compute_sms_has_iap_failure(self): - failures = ['sms_acc', 'sms_credit'] - if not self.ids: - self.sms_has_insufficient_credit = self.sms_has_unregistered_account = False - else: - traces = self.env['mailing.trace'].sudo().read_group([ - ('mass_mailing_id', 'in', self.ids), - ('trace_type', '=', 'sms'), - ('failure_type', 'in', failures) - ], ['mass_mailing_id', 'failure_type'], ['mass_mailing_id', 'failure_type'], lazy=False) + self.sms_has_insufficient_credit = self.sms_has_unregistered_account = False + traces = self.env['mailing.trace'].sudo()._read_group([ + ('mass_mailing_id', 'in', self.ids), + ('trace_type', '=', 'sms'), + ('failure_type', 'in', ['sms_acc', 'sms_credit']) + ], ['mass_mailing_id', 'failure_type']) - trace_dict = dict.fromkeys(self.ids, {key: False for key in failures}) - for t in traces: - trace_dict[t['mass_mailing_id'][0]][t['failure_type']] = bool(t['__count']) - - for mail in self: - mail.sms_has_insufficient_credit = trace_dict[mail.id]['sms_credit'] - mail.sms_has_unregistered_account = trace_dict[mail.id]['sms_acc'] + for mass_mailing, failure_type in traces: + if failure_type == 'sms_credit': + mass_mailing.sms_has_insufficient_credit = True + elif failure_type == 'sms_acc': + mass_mailing.sms_has_unregistered_account = True # -------------------------------------------------- # ORM OVERRIDES diff --git a/addons/mass_mailing_sms/models/utm.py b/addons/mass_mailing_sms/models/utm.py index 698aaeeb6ef..5e37d59f647 100644 --- a/addons/mass_mailing_sms/models/utm.py +++ b/addons/mass_mailing_sms/models/utm.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from collections import defaultdict from odoo import _, api, fields, models from odoo.exceptions import UserError @@ -26,25 +27,21 @@ class UtmCampaign(models.Model): @api.depends('mailing_sms_ids') def _compute_mailing_sms_count(self): - if self.ids: - mailing_sms_data = self.env['mailing.mailing'].read_group( - [('campaign_id', 'in', self.ids), ('mailing_type', '=', 'sms')], - ['campaign_id', 'ab_testing_enabled'], - ['campaign_id', 'ab_testing_enabled'], - lazy=False, - ) - ab_testing_mapped_sms_data = {} - mapped_sms_data = {} - for data in mailing_sms_data: - if data['ab_testing_enabled']: - ab_testing_mapped_sms_data.setdefault(data['campaign_id'][0], []).append(data['__count']) - mapped_sms_data.setdefault(data['campaign_id'][0], []).append(data['__count']) - else: - mapped_sms_data = dict() - ab_testing_mapped_sms_data = dict() + mailing_sms_data = self.env['mailing.mailing']._read_group( + [('campaign_id', 'in', self.ids), ('mailing_type', '=', 'sms')], + ['campaign_id', 'ab_testing_enabled'], + ['__count'], + ) + ab_testing_mapped_sms_data = defaultdict(list) + mapped_sms_data = defaultdict(list) + for campaign, ab_testing_enabled, count in mailing_sms_data: + if ab_testing_enabled: + ab_testing_mapped_sms_data[campaign.id].append(count) + mapped_sms_data[campaign.id].append(count) + for campaign in self: - campaign.mailing_sms_count = sum(mapped_sms_data.get(campaign._origin.id or campaign.id, [])) - campaign.ab_testing_mailings_sms_count = sum(ab_testing_mapped_sms_data.get(campaign._origin.id or campaign.id, [])) + campaign.mailing_sms_count = sum(mapped_sms_data[campaign.id]) + campaign.ab_testing_mailings_sms_count = sum(ab_testing_mapped_sms_data[campaign.id]) def action_create_mass_sms(self): action = self.env["ir.actions.actions"]._for_xml_id("mass_mailing.action_create_mass_mailings_from_campaign") diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index 3e487e7636c..febc47e5f72 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -411,8 +411,8 @@ class MrpProduction(models.Model): @api.depends('move_raw_ids.delay_alert_date') def _compute_delay_alert_date(self): - delay_alert_date_data = self.env['stock.move']._read_group([('id', 'in', self.move_raw_ids.ids), ('delay_alert_date', '!=', False)], ['delay_alert_date:max'], 'raw_material_production_id') - delay_alert_date_data = {data['raw_material_production_id'][0]: data['delay_alert_date'] for data in delay_alert_date_data} + delay_alert_date_data = self.env['stock.move']._read_group([('id', 'in', self.move_raw_ids.ids), ('delay_alert_date', '!=', False)], ['raw_material_production_id'], ['delay_alert_date:max']) + delay_alert_date_data = {raw_material_production.id: delay_alert_date_max for raw_material_production, delay_alert_date_max in delay_alert_date_data} for production in self: production.delay_alert_date = delay_alert_date_data.get(production.id, False) @@ -442,14 +442,11 @@ class MrpProduction(models.Model): ('production_id', 'in', self.ids), ('raw_material_production_id', 'in', self.ids) ] - res = self.env['stock.move'].read_group(domain, ['state', 'production_id', 'raw_material_production_id'], ['production_id', 'raw_material_production_id'], lazy=False) - productions_with_done_move = {} - for rec in res: - production_record = rec['production_id'] or rec['raw_material_production_id'] - if production_record: - productions_with_done_move[production_record[0]] = True - for production in self: - production.confirm_cancel = productions_with_done_move.get(production.id, False) + res = self.env['stock.move']._read_group(domain, ['production_id', 'raw_material_production_id']) + self.confirm_cancel = False + for production, raw_material_production in res: + production_record = production or raw_material_production + production_record.confirm_cancel = True @api.depends('procurement_group_id', 'procurement_group_id.stock_move_ids.group_id') def _compute_picking_ids(self): @@ -474,8 +471,8 @@ class MrpProduction(models.Model): location_by_company = self.env['stock.location']._read_group([ ('company_id', 'in', self.company_id.ids), ('usage', '=', 'production') - ], ['company_id', 'ids:array_agg(id)'], ['company_id']) - location_by_company = {lbc['company_id'][0]: lbc['ids'] for lbc in location_by_company} + ], ['company_id'], ['id:array_agg']) + location_by_company = {company.id: ids for company, ids in location_by_company} for production in self: if production.product_id: production.production_location_id = production.product_id.with_company(production.company_id).property_stock_production @@ -608,8 +605,8 @@ class MrpProduction(models.Model): return True def _compute_scrap_move_count(self): - data = self.env['stock.scrap']._read_group([('production_id', 'in', self.ids)], ['production_id'], ['production_id']) - count_data = dict((item['production_id'][0], item['production_id_count']) for item in data) + data = self.env['stock.scrap']._read_group([('production_id', 'in', self.ids)], ['production_id'], ['__count']) + count_data = {production.id: count for production, count in data} for production in self: production.scrap_count = count_data.get(production.id, 0) diff --git a/addons/mrp/models/mrp_routing.py b/addons/mrp/models/mrp_routing.py index bc00d505a8e..309ef14f6a7 100644 --- a/addons/mrp/models/mrp_routing.py +++ b/addons/mrp/models/mrp_routing.py @@ -95,8 +95,8 @@ class MrpRoutingWorkcenter(models.Model): def _compute_workorder_count(self): data = self.env['mrp.workorder']._read_group([ ('operation_id', 'in', self.ids), - ('state', '=', 'done')], ['operation_id'], ['operation_id']) - count_data = dict((item['operation_id'][0], item['operation_id_count']) for item in data) + ('state', '=', 'done')], ['operation_id'], ['__count']) + count_data = {operation.id: count for operation, count in data} for operation in self: operation.workorder_count = count_data.get(operation.id, 0) diff --git a/addons/mrp/models/mrp_workcenter.py b/addons/mrp/models/mrp_workcenter.py index 9b9b3e8c649..8444985e490 100644 --- a/addons/mrp/models/mrp_workcenter.py +++ b/addons/mrp/models/mrp_workcenter.py @@ -90,17 +90,16 @@ class MrpWorkcenter(models.Model): # Count Late Workorder data = MrpWorkorder._read_group( [('workcenter_id', 'in', self.ids), ('state', 'in', ('pending', 'waiting', 'ready')), ('date_start', '<', datetime.now().strftime('%Y-%m-%d'))], - ['workcenter_id'], ['workcenter_id']) - count_data = dict((item['workcenter_id'][0], item['workcenter_id_count']) for item in data) + ['workcenter_id'], ['__count']) + count_data = {workcenter.id: count for workcenter, count in data} # Count All, Pending, Ready, Progress Workorder res = MrpWorkorder._read_group( [('workcenter_id', 'in', self.ids)], - ['workcenter_id', 'state', 'duration_expected'], ['workcenter_id', 'state'], - lazy=False) - for res_group in res: - result[res_group['workcenter_id'][0]][res_group['state']] = res_group['__count'] - if res_group['state'] in ('pending', 'waiting', 'ready', 'progress'): - result_duration_expected[res_group['workcenter_id'][0]] += res_group['duration_expected'] + ['workcenter_id', 'state'], ['duration_expected:sum', '__count']) + for workcenter, state, duration_sum, count in res: + result[workcenter.id][state] = count + if state in ('pending', 'waiting', 'ready', 'progress'): + result_duration_expected[workcenter.id] += duration_sum for workcenter in self: workcenter.workorder_count = sum(count for state, count in result[workcenter.id].items() if state not in ('done', 'cancel')) workcenter.workorder_pending_count = result[workcenter.id].get('pending', 0) @@ -136,8 +135,8 @@ class MrpWorkcenter(models.Model): ('workcenter_id', 'in', self.ids), ('date_end', '!=', False), ('loss_type', '!=', 'productive')], - ['duration', 'workcenter_id'], ['workcenter_id'], lazy=False) - count_data = dict((item['workcenter_id'][0], item['duration']) for item in data) + ['workcenter_id'], ['duration:sum']) + count_data = {workcenter.id: duration for workcenter, duration in data} for workcenter in self: workcenter.blocked_time = count_data.get(workcenter.id, 0.0) / 60.0 @@ -148,8 +147,8 @@ class MrpWorkcenter(models.Model): ('workcenter_id', 'in', self.ids), ('date_end', '!=', False), ('loss_type', '=', 'productive')], - ['duration', 'workcenter_id'], ['workcenter_id'], lazy=False) - count_data = dict((item['workcenter_id'][0], item['duration']) for item in data) + ['workcenter_id'], ['duration:sum']) + count_data = {workcenter.id: duration for workcenter, duration in data} for workcenter in self: workcenter.productive_time = count_data.get(workcenter.id, 0.0) / 60.0 @@ -165,9 +164,9 @@ class MrpWorkcenter(models.Model): wo_data = self.env['mrp.workorder']._read_group([ ('date_start', '>=', fields.Datetime.to_string(datetime.now() - relativedelta.relativedelta(months=1))), ('workcenter_id', 'in', self.ids), - ('state', '=', 'done')], ['duration_expected', 'workcenter_id', 'duration'], ['workcenter_id'], lazy=False) - duration_expected = dict((data['workcenter_id'][0], data['duration_expected']) for data in wo_data) - duration = dict((data['workcenter_id'][0], data['duration']) for data in wo_data) + ('state', '=', 'done')], ['workcenter_id'], ['duration_expected:sum', 'duration:sum']) + duration_expected = {workcenter.id: expected for workcenter, expected, __ in wo_data} + duration = {workcenter.id: duration for workcenter, __, duration in wo_data} for workcenter in self: if duration.get(workcenter.id): workcenter.performance = 100 * duration_expected.get(workcenter.id, 0.0) / duration[workcenter.id] @@ -437,8 +436,11 @@ class MrpWorkcenterProductivity(models.Model): @api.constrains('workorder_id') def _check_open_time_ids(self): for workorder in self.workorder_id: - open_time_ids_by_user = self.env["mrp.workcenter.productivity"].read_group([("id", "in", workorder.time_ids.ids), ("date_end", "=", False)], ["user_id", "open_time_ids_count:count(id)"], ["user_id"]) - if any(data["open_time_ids_count"] > 1 for data in open_time_ids_by_user): + open_time_ids_by_user = self.env["mrp.workcenter.productivity"]._read_group( + [("id", "in", workorder.time_ids.ids), ("date_end", "=", False)], + ["user_id"], having=[("__count", ">", 1)], + ) + if open_time_ids_by_user: raise ValidationError(_('The Workorder (%s) cannot be started twice!', workorder.display_name)) def button_block(self): diff --git a/addons/mrp/models/mrp_workorder.py b/addons/mrp/models/mrp_workorder.py index 2f671309c66..d5d40384a59 100644 --- a/addons/mrp/models/mrp_workorder.py +++ b/addons/mrp/models/mrp_workorder.py @@ -397,8 +397,8 @@ class MrpWorkorder(models.Model): order.is_user_working = False def _compute_scrap_move_count(self): - data = self.env['stock.scrap']._read_group([('workorder_id', 'in', self.ids)], ['workorder_id'], ['workorder_id']) - count_data = dict((item['workorder_id'][0], item['workorder_id_count']) for item in data) + data = self.env['stock.scrap']._read_group([('workorder_id', 'in', self.ids)], ['workorder_id'], ['__count']) + count_data = {workorder.id: count for workorder, count in data} for workorder in self: workorder.scrap_count = count_data.get(workorder.id, 0) diff --git a/addons/mrp/models/product.py b/addons/mrp/models/product.py index 9517f976fa8..e1e699286ab 100644 --- a/addons/mrp/models/product.py +++ b/addons/mrp/models/product.py @@ -169,8 +169,8 @@ class ProductProduct(models.Model): date_from = fields.Datetime.to_string(fields.datetime.now() - timedelta(days=365)) #TODO: state = done? domain = [('state', '=', 'done'), ('product_id', 'in', self.ids), ('date_start', '>', date_from)] - read_group_res = self.env['mrp.production']._read_group(domain, ['product_id', 'product_uom_qty'], ['product_id']) - mapped_data = dict([(data['product_id'][0], data['product_uom_qty']) for data in read_group_res]) + read_group_res = self.env['mrp.production']._read_group(domain, ['product_id'], ['product_uom_qty:sum']) + mapped_data = {product.id: qty for product, qty in read_group_res} for product in self: if not product.id: product.mrp_product_qty = 0.0 diff --git a/addons/mrp/models/stock_orderpoint.py b/addons/mrp/models/stock_orderpoint.py index 6ce564a1e33..fa1cb4a07c6 100644 --- a/addons/mrp/models/stock_orderpoint.py +++ b/addons/mrp/models/stock_orderpoint.py @@ -103,15 +103,13 @@ class StockWarehouseOrderpoint(models.Model): bom_manufacture = self.env['mrp.bom']._bom_find(orderpoints_without_kit.product_id, bom_type='normal') bom_manufacture = self.env['mrp.bom'].concat(*bom_manufacture.values()) - productions_group = self.env['mrp.production'].read_group( + productions_group = self.env['mrp.production']._read_group( [('bom_id', 'in', bom_manufacture.ids), ('state', '=', 'draft'), ('orderpoint_id', 'in', orderpoints_without_kit.ids)], - ['orderpoint_id', 'product_qty', 'product_uom_id'], - ['orderpoint_id', 'product_uom_id'], lazy=False) - for p in productions_group: - uom = self.env['uom.uom'].browse(p['product_uom_id'][0]) - orderpoint = self.env['stock.warehouse.orderpoint'].browse(p['orderpoint_id'][0]) + ['orderpoint_id', 'product_uom_id'], + ['product_qty:sum']) + for orderpoint, uom, product_qty_sum in productions_group: res[orderpoint.id] += uom._compute_quantity( - p['product_qty'], orderpoint.product_uom, round=False) + product_qty_sum, orderpoint.product_uom, round=False) return res def _get_qty_multiple_to_order(self): diff --git a/addons/mrp/models/stock_picking.py b/addons/mrp/models/stock_picking.py index 359752fa773..e05de6dd31a 100644 --- a/addons/mrp/models/stock_picking.py +++ b/addons/mrp/models/stock_picking.py @@ -28,28 +28,20 @@ class StockPickingType(models.Model): def _get_mo_count(self): mrp_picking_types = self.filtered(lambda picking: picking.code == 'mrp_operation') - if not mrp_picking_types: - self.count_mo_waiting = False - self.count_mo_todo = False - self.count_mo_late = False - return + remaining = (self - mrp_picking_types) + remaining.count_mo_waiting = remaining.count_mo_todo = remaining.count_mo_late = False domains = { 'count_mo_waiting': [('reservation_state', '=', 'waiting')], 'count_mo_todo': ['|', ('state', 'in', ('confirmed', 'draft', 'progress', 'to_close')), ('is_planned', '=', True)], 'count_mo_late': [('date_start', '<', fields.Date.today()), ('state', '=', 'confirmed')], } - for field in domains: - data = self.env['mrp.production']._read_group(domains[field] + - [('state', 'not in', ('done', 'cancel')), ('picking_type_id', 'in', self.ids)], - ['picking_type_id'], ['picking_type_id']) - count = {x['picking_type_id'] and x['picking_type_id'][0]: x['picking_type_id_count'] for x in data} + for key, domain in domains.items(): + data = self.env['mrp.production']._read_group(domain + + [('state', 'not in', ('done', 'cancel')), ('picking_type_id', 'in', mrp_picking_types.ids)], + ['picking_type_id'], ['__count']) + count = {picking_type.id: count for picking_type, count in data} for record in mrp_picking_types: - record[field] = count.get(record.id, 0) - remaining = (self - mrp_picking_types) - if remaining: - remaining.count_mo_waiting = False - remaining.count_mo_todo = False - remaining.count_mo_late = False + record[key] = count.get(record.id, 0) def get_mrp_stock_picking_action_picking_type(self): action = self.env["ir.actions.actions"]._for_xml_id('mrp.mrp_production_action_picking_deshboard') diff --git a/addons/mrp/report/mrp_report_bom_structure.py b/addons/mrp/report/mrp_report_bom_structure.py index 518c8175ec2..662c6848869 100644 --- a/addons/mrp/report/mrp_report_bom_structure.py +++ b/addons/mrp/report/mrp_report_bom_structure.py @@ -555,9 +555,9 @@ class ReportBomStructure(models.AbstractModel): domain.append(('warehouse_id', '=', self.env.context.get('warehouse'))) # Seek the closest date in the forecast report where consummed quantity >= forecasted quantity - closest_forecasted = self.env['report.stock.quantity']._read_group(domain, ['min_date:min(date)', 'product_id'], ['product_id']) - if closest_forecasted: - days_to_forecast = (closest_forecasted[0]['min_date'] - date_today).days + [min_date] = self.env['report.stock.quantity']._read_group(domain, aggregates=['date:min'])[0] + if min_date: + days_to_forecast = (min_date - date_today).days return ('expected', days_to_forecast) return ('unavailable', False) diff --git a/addons/mrp/report/stock_forecasted.py b/addons/mrp/report/stock_forecasted.py index 2826558087f..b2cadba8b54 100644 --- a/addons/mrp/report/stock_forecasted.py +++ b/addons/mrp/report/stock_forecasted.py @@ -44,16 +44,16 @@ class StockForecasted(models.AbstractModel): # Pending incoming quantity. mo_domain = domain + [('location_dest_id', 'in', wh_location_ids)] - grouped_mo = self.env['mrp.production'].read_group(mo_domain, ['product_qty:sum'], 'product_id') - res['draft_production_qty']['in'] = sum(mo['product_qty'] for mo in grouped_mo) + [product_qty] = self.env['mrp.production']._read_group(mo_domain, aggregates=['product_qty:sum'])[0] + res['draft_production_qty']['in'] = product_qty or 0.0 # Pending outgoing quantity. move_domain = domain + [ ('raw_material_production_id', '!=', False), ('location_id', 'in', wh_location_ids), ] - grouped_moves = self.env['stock.move'].read_group(move_domain, ['product_qty:sum'], 'product_id') - res['draft_production_qty']['out'] = sum(move['product_qty'] for move in grouped_moves) + [product_qty] = self.env['stock.move']._read_group(move_domain, aggregates=['product_qty:sum'])[0] + res['draft_production_qty']['out'] = product_qty or 0.0 res['qty']['in'] += res['draft_production_qty']['in'] res['qty']['out'] += res['draft_production_qty']['out'] diff --git a/addons/mrp_subcontracting/models/res_partner.py b/addons/mrp_subcontracting/models/res_partner.py index e2aa3f1278f..ee3ee135e2f 100644 --- a/addons/mrp_subcontracting/models/res_partner.py +++ b/addons/mrp_subcontracting/models/res_partner.py @@ -18,30 +18,30 @@ class ResPartner(models.Model): picking_ids = fields.Many2many('stock.picking', compute='_compute_picking_ids', string="Stock Pickings for which the Partner is the subcontractor") def _compute_bom_ids(self): - results = self.env['mrp.bom'].read_group([('subcontractor_ids.commercial_partner_id', 'in', self.commercial_partner_id.ids)], ['ids:array_agg(id)', 'subcontractor_ids'], ['subcontractor_ids']) + results = self.env['mrp.bom']._read_group([('subcontractor_ids.commercial_partner_id', 'in', self.commercial_partner_id.ids)], ['subcontractor_ids'], ['id:array_agg']) for partner in self: bom_ids = [] - for res in results: - if partner.id == res['subcontractor_ids'][0] or res['subcontractor_ids'][0] in partner.child_ids.ids: - bom_ids += res['ids'] + for subcontractor, ids in results: + if partner.id == subcontractor.id or subcontractor.id in partner.child_ids.ids: + bom_ids += ids partner.bom_ids = bom_ids def _compute_production_ids(self): - results = self.env['mrp.production'].read_group([('subcontractor_id.commercial_partner_id', 'in', self.commercial_partner_id.ids)], ['ids:array_agg(id)'], ['subcontractor_id']) + results = self.env['mrp.production']._read_group([('subcontractor_id.commercial_partner_id', 'in', self.commercial_partner_id.ids)], ['subcontractor_id'], ['id:array_agg']) for partner in self: production_ids = [] - for res in results: - if partner.id == res['subcontractor_id'][0] or res['subcontractor_id'][0] in partner.child_ids.ids: - production_ids += res['ids'] + for subcontractor, ids in results: + if partner.id == subcontractor.id or subcontractor.id in partner.child_ids.ids: + production_ids += ids partner.production_ids = production_ids def _compute_picking_ids(self): - results = self.env['stock.picking'].read_group([('partner_id.commercial_partner_id', 'in', self.commercial_partner_id.ids)], ['ids:array_agg(id)'], ['partner_id']) + results = self.env['stock.picking']._read_group([('partner_id.commercial_partner_id', 'in', self.commercial_partner_id.ids)], ['partner_id'], ['id:array_agg']) for partner in self: picking_ids = [] - for res in results: - if partner.id == res['partner_id'][0] or res['partner_id'][0] in partner.child_ids.ids: - picking_ids += res['ids'] + for partner_rg, ids in results: + if partner_rg.id == partner.id or partner_rg.id in partner.child_ids.ids: + picking_ids += ids partner.picking_ids = picking_ids def _search_is_subcontractor(self, operator, value): diff --git a/addons/onboarding/models/onboarding_progress.py b/addons/onboarding/models/onboarding_progress.py index 654ab87ae4c..11539d2d143 100644 --- a/addons/onboarding/models/onboarding_progress.py +++ b/addons/onboarding/models/onboarding_progress.py @@ -33,11 +33,11 @@ class OnboardingProgress(models.Model): @api.depends('onboarding_id.step_ids', 'progress_step_ids', 'progress_step_ids.step_state') def _compute_onboarding_state(self): - progress_steps_data = self.env['onboarding.progress.step'].read_group( + progress_steps_data = self.env['onboarding.progress.step']._read_group( [('progress_id', 'in', self.ids), ('step_state', 'in', ['just_done', 'done'])], - ['progress_id'], ['progress_id'] + ['progress_id'], ['__count'] ) - result = dict((data['progress_id'][0], data['progress_id_count']) for data in progress_steps_data) + result = {progress.id: count for progress, count in progress_steps_data} for progress in self: progress.onboarding_state = ( 'not_done' if result.get(progress.id, 0) != len(progress.onboarding_id.step_ids) diff --git a/addons/payment/models/payment_transaction.py b/addons/payment/models/payment_transaction.py index ec18267b583..2a1835151f1 100644 --- a/addons/payment/models/payment_transaction.py +++ b/addons/payment/models/payment_transaction.py @@ -133,10 +133,10 @@ class PaymentTransaction(models.Model): def _compute_refunds_count(self): rg_data = self.env['payment.transaction']._read_group( domain=[('source_transaction_id', 'in', self.ids), ('operation', '=', 'refund')], - fields=['source_transaction_id'], groupby=['source_transaction_id'], + aggregates=['__count'], ) - data = {x['source_transaction_id'][0]: x['source_transaction_id_count'] for x in rg_data} + data = {source_transaction.id: count for source_transaction, count in rg_data} for record in self: record.refunds_count = data.get(record.id, 0) diff --git a/addons/payment/models/res_partner.py b/addons/payment/models/res_partner.py index f58d74d618a..afb594a70d8 100644 --- a/addons/payment/models/res_partner.py +++ b/addons/payment/models/res_partner.py @@ -14,9 +14,8 @@ class ResPartner(models.Model): @api.depends('payment_token_ids') def _compute_payment_token_count(self): payments_data = self.env['payment.token']._read_group( - [('partner_id', 'in', self.ids)], ['partner_id'], ['partner_id'] + [('partner_id', 'in', self.ids)], ['partner_id'], ['__count'], ) - partners_data = {payment_data['partner_id'][0]: payment_data['partner_id_count'] - for payment_data in payments_data} + partners_data = {partner.id: count for partner, count in payments_data} for partner in self: partner.payment_token_count = partners_data.get(partner.id, 0) diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index 746f4843834..cf5dcbed779 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -389,13 +389,13 @@ class PosOrder(models.Model): Practical to be used for migrations """ amounts = {order_id: {'paid': 0, 'return': 0, 'taxed': 0, 'taxes': 0} for order_id in self.ids} - for order in self.env['pos.payment'].read_group([('pos_order_id', 'in', self.ids)], ['pos_order_id', 'amount'], ['pos_order_id']): - amounts[order['pos_order_id'][0]]['paid'] = order['amount'] - for order in self.env['pos.payment'].read_group(['&', ('pos_order_id', 'in', self.ids), ('amount', '<', 0)], ['pos_order_id', 'amount'], ['pos_order_id']): - amounts[order['pos_order_id'][0]]['return'] = order['amount'] - for order in self.env['pos.order.line'].read_group([('order_id', 'in', self.ids)], ['order_id', 'price_subtotal', 'price_subtotal_incl'], ['order_id']): - amounts[order['order_id'][0]]['taxed'] = order['price_subtotal_incl'] - amounts[order['order_id'][0]]['taxes'] = order['price_subtotal_incl'] - order['price_subtotal'] + for pos_order, amount in self.env['pos.payment']._read_group([('pos_order_id', 'in', self.ids)], ['pos_order_id'], ['amount:sum']): + amounts[pos_order.id]['paid'] = amount + for pos_order, amount in self.env['pos.payment']._read_group(['&', ('pos_order_id', 'in', self.ids), ('amount', '<', 0)], ['pos_order_id'], ['amount:sum']): + amounts[pos_order.id]['return'] = amount + for order, price_subtotal, price_subtotal_incl in self.env['pos.order.line']._read_group([('order_id', 'in', self.ids)], ['order_id'], ['price_subtotal:sum', 'price_subtotal_incl:sum']): + amounts[order.id]['taxed'] = price_subtotal_incl + amounts[order.id]['taxes'] = price_subtotal_incl - price_subtotal for order in self: order.write({ diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py index fef7720f3f9..1a54af83cc7 100644 --- a/addons/point_of_sale/models/pos_session.py +++ b/addons/point_of_sale/models/pos_session.py @@ -111,9 +111,8 @@ class PosSession(models.Model): if cash_payment_method: total_cash_payment = 0.0 last_session = session.search([('config_id', '=', session.config_id.id), ('id', '!=', session.id)], limit=1) - result = self.env['pos.payment']._read_group([('session_id', '=', session.id), ('payment_method_id', '=', cash_payment_method.id)], ['amount'], ['session_id']) - if result: - total_cash_payment = result[0]['amount'] + result = self.env['pos.payment']._read_group([('payment_method_id', '=', cash_payment_method.id)], aggregates=['amount:sum']) + total_cash_payment = result[0][0] or 0.0 session.cash_register_total_entry_encoding = sum(session.statement_line_ids.mapped('amount')) + ( 0.0 if session.state == 'closed' else total_cash_payment ) @@ -126,14 +125,14 @@ class PosSession(models.Model): @api.depends('order_ids.payment_ids.amount') def _compute_total_payments_amount(self): - result = self.env['pos.payment']._read_group([('session_id', 'in', self.ids)], ['amount'], ['session_id']) - session_amount_map = dict((data['session_id'][0], data['amount']) for data in result) + result = self.env['pos.payment']._read_group([('session_id', 'in', self.ids)], ['session_id'], ['amount:sum']) + session_amount_map = {session.id: amount for session, amount in result} for session in self: session.total_payments_amount = session_amount_map.get(session.id) or 0 def _compute_order_count(self): - orders_data = self.env['pos.order']._read_group([('session_id', 'in', self.ids)], ['session_id'], ['session_id']) - sessions_data = {order_data['session_id'][0]: order_data['session_id_count'] for order_data in orders_data} + orders_data = self.env['pos.order']._read_group([('session_id', 'in', self.ids)], ['session_id'], ['__count']) + sessions_data = {session.id: count for session, count in orders_data} for session in self: session.order_count = sessions_data.get(session.id, 0) @@ -1741,16 +1740,16 @@ class PosSession(models.Model): # Add the 'sum_repartition_factor' as needed in the compute_all # Note that the factor = factor_percent/100 - groups = self.env['account.tax.repartition.line'].read_group( + groups = self.env['account.tax.repartition.line']._read_group( domain=[ ('tax_id', 'in', tuple([t['id'] for t in taxes])), ('document_type', '=', 'invoice'), ('repartition_type', '=', 'tax'), ], - fields=["factor_percent:sum"], groupby=["tax_id"], + aggregates=["factor_percent:sum"], ) - tax_id_to_factor_sum = {g['tax_id'][0]: g['factor_percent']/100 for g in groups} + tax_id_to_factor_sum = {tax.id: factor_sum / 100 for tax, factor_sum in groups} for tax in taxes: tax['sum_repartition_factor'] = tax_id_to_factor_sum[tax['id']] diff --git a/addons/point_of_sale/models/res_partner.py b/addons/point_of_sale/models/res_partner.py index 5ba9d4040fd..2fd88331e44 100644 --- a/addons/point_of_sale/models/res_partner.py +++ b/addons/point_of_sale/models/res_partner.py @@ -22,15 +22,15 @@ class ResPartner(models.Model): ) pos_order_data = self.env['pos.order']._read_group( domain=[('partner_id', 'in', all_partners.ids)], - fields=['partner_id'], groupby=['partner_id'] + groupby=['partner_id'], aggregates=['__count'] ) + self_ids = set(self._ids) self.pos_order_count = 0 - for group in pos_order_data: - partner = self.browse(group['partner_id'][0]) + for partner, count in pos_order_data: while partner: - if partner in self: - partner.pos_order_count += group['partner_id_count'] + if partner.id in self_ids: + partner.pos_order_count += count partner = partner.parent_id def action_view_pos_order(self): diff --git a/addons/pos_loyalty/models/loyalty_card.py b/addons/pos_loyalty/models/loyalty_card.py index 47cea55cb4c..db3a6a08a14 100644 --- a/addons/pos_loyalty/models/loyalty_card.py +++ b/addons/pos_loyalty/models/loyalty_card.py @@ -27,7 +27,7 @@ class LoyaltyCard(models.Model): def _compute_use_count(self): super()._compute_use_count() read_group_res = self.env['pos.order.line']._read_group( - [('coupon_id', 'in', self.ids)], ['id'], ['coupon_id']) - count_per_coupon = {r['coupon_id'][0]: r['coupon_id_count'] for r in read_group_res} + [('coupon_id', 'in', self.ids)], ['coupon_id'], ['__count']) + count_per_coupon = {coupon.id: count for coupon, count in read_group_res} for card in self: card.use_count += count_per_coupon.get(card.id, 0) diff --git a/addons/pos_loyalty/models/loyalty_program.py b/addons/pos_loyalty/models/loyalty_program.py index 4aa748b4d0a..508f5a4b555 100644 --- a/addons/pos_loyalty/models/loyalty_program.py +++ b/addons/pos_loyalty/models/loyalty_program.py @@ -53,10 +53,10 @@ class LoyaltyProgram(models.Model): def _compute_pos_order_count(self): read_group_res = self.env['pos.order.line']._read_group( - [('reward_id', 'in', self.reward_ids.ids)], ['reward_id:array_agg'], ['order_id']) + [('reward_id', 'in', self.reward_ids.ids)], ['order_id'], ['reward_id:array_agg']) for program in self: program_reward_ids = program.reward_ids.ids - program.pos_order_count = sum(1 if any(id in group['reward_id'] for id in program_reward_ids) else 0 for group in read_group_res) + program.pos_order_count = sum(1 if any(id in reward_ids for id in program_reward_ids) else 0 for __, reward_ids in read_group_res) def _compute_total_order_count(self): super()._compute_total_order_count() diff --git a/addons/pos_loyalty/models/pos_session.py b/addons/pos_loyalty/models/pos_session.py index a5b7bc705ea..270411b560b 100644 --- a/addons/pos_loyalty/models/pos_session.py +++ b/addons/pos_loyalty/models/pos_session.py @@ -80,15 +80,17 @@ class PosSession(models.Model): loyalty_programs = self.config_id._get_program_ids().filtered(lambda p: p.program_type == 'loyalty') loyalty_card_fields = ['points', 'code', 'program_id'] partner_id_to_loyalty_card = {} - for group in self.env['loyalty.card'].read_group( + for partner, *field_values in self.env['loyalty.card']._read_group( domain=[('partner_id', 'in', [p['id'] for p in partners]), ('program_id', 'in', loyalty_programs.ids)], - fields=[f"{field_name}:array_agg" for field_name in loyalty_card_fields] + ["ids:array_agg(id)"], - groupby=['partner_id'] + groupby=['partner_id'], + aggregates=[f'{field_name}:array_agg' for field_name in loyalty_card_fields] + ['id:array_agg'], ): + # field_values = [(a1, a2, ...), (b1, b2, ...), ..., (id1, id2, ...)] loyalty_cards = {} - for i in range(group['partner_id_count']): - loyalty_cards[group['ids'][i]] = {field_name: group[field_name][i] for field_name in loyalty_card_fields} - partner_id_to_loyalty_card[group['partner_id'][0]] = loyalty_cards + for *values, id_ in zip(*field_values): + # values = [ak, bk, ...], id_ = idk + loyalty_cards[id_] = dict(zip(loyalty_card_fields, values)) + partner_id_to_loyalty_card[partner.id] = loyalty_cards # Assign loyalty cards to each partner to load. for partner in partners: diff --git a/addons/pos_restaurant/models/pos_config.py b/addons/pos_restaurant/models/pos_config.py index e21a4568414..f3a408dc758 100644 --- a/addons/pos_restaurant/models/pos_config.py +++ b/addons/pos_restaurant/models/pos_config.py @@ -29,8 +29,8 @@ class PosConfig(models.Model): tables = self.env['restaurant.table'].search([('floor_id.pos_config_ids', '=', self.id)]) domain = [('state', '=', 'draft'), ('table_id', 'in', tables.ids)] - order_stats = self.env['pos.order'].read_group(domain, ['table_id'], 'table_id') - orders_map = dict((s['table_id'][0], s['table_id_count']) for s in order_stats) + order_stats = self.env['pos.order']._read_group(domain, ['table_id'], ['__count']) + orders_map = {table.id: count for table, count in order_stats} result = [] for table in tables: diff --git a/addons/pos_sale/models/crm_team.py b/addons/pos_sale/models/crm_team.py index 8ffd32d0b2e..bdde081f745 100644 --- a/addons/pos_sale/models/crm_team.py +++ b/addons/pos_sale/models/crm_team.py @@ -22,8 +22,8 @@ class CrmTeam(models.Model): data = self.env['report.pos.order']._read_group([ ('session_id.state', '=', 'opened'), ('config_id.crm_team_id', 'in', self.ids), - ], ['price_total:sum', 'config_id'], ['config_id']) - rg_results = dict((d['config_id'][0], d['price_total']) for d in data) + ], ['config_id'], ['price_total:sum']) + rg_results = {config.id: price_total_sum for config, price_total_sum in data} for team in self: team.pos_order_amount_total = sum([ rg_results.get(config.id, 0.0) diff --git a/addons/product/models/product_category.py b/addons/product/models/product_category.py index 52d5b35f7e5..c29bbb54b8a 100644 --- a/addons/product/models/product_category.py +++ b/addons/product/models/product_category.py @@ -33,8 +33,8 @@ class ProductCategory(models.Model): category.complete_name = category.name def _compute_product_count(self): - read_group_res = self.env['product.template'].read_group([('categ_id', 'child_of', self.ids)], ['categ_id'], ['categ_id']) - group_data = dict((data['categ_id'][0], data['categ_id_count']) for data in read_group_res) + read_group_res = self.env['product.template']._read_group([('categ_id', 'child_of', self.ids)], ['categ_id'], ['__count']) + group_data = {categ.id: count for categ, count in read_group_res} for categ in self: product_count = 0 for sub_categ_id in categ.search([('id', 'child_of', categ.ids)]).ids: diff --git a/addons/product/models/product_pricelist.py b/addons/product/models/product_pricelist.py index 987e18f1013..a552836d081 100644 --- a/addons/product/models/product_pricelist.py +++ b/addons/product/models/product_pricelist.py @@ -293,13 +293,12 @@ class Pricelist(models.Model): Pricelist.search(pl_domain, limit=1) ) # group partners by country, and find a pricelist for each country - domain = [('id', 'in', remaining_partner_ids)] - groups = Partner.read_group(domain, ['country_id'], ['country_id']) - for group in groups: - country_id = group['country_id'] and group['country_id'][0] - pl = Pricelist.search(pl_domain + [('country_group_ids.country_ids', '=', country_id)], limit=1) + remaining_partners = self.env['res.partner'].browse(remaining_partner_ids) + partners_by_country = remaining_partners.grouped('country_id') + for country, partners in partners_by_country.items(): + pl = Pricelist.search(pl_domain + [('country_group_ids.country_ids', '=', country.id if country else False)], limit=1) pl = pl or pl_fallback - for pid in Partner.search(group['__domain']).ids: + for pid in partners.ids: result[pid] = pl return result diff --git a/addons/project/controllers/portal.py b/addons/project/controllers/portal.py index 40b3785e036..7f14127abbd 100644 --- a/addons/project/controllers/portal.py +++ b/addons/project/controllers/portal.py @@ -473,13 +473,12 @@ class ProjectCustomerPortal(CustomerPortal): # extends filterby criteria with project (criteria name is the project id) # Note: portal users can't view projects they don't follow - project_groups = request.env['project.task'].read_group(AND([[('project_id', 'not in', projects.ids)], task_domain or []]), - ['project_id'], ['project_id']) - for group in project_groups: - proj_id = group['project_id'][0] if group['project_id'] else False - proj_name = group['project_id'][1] if group['project_id'] else _('Others') + project_groups = request.env['project.task']._read_group(AND([[('project_id', 'not in', projects.ids)], task_domain or []]), + ['project_id']) + for [project] in project_groups: + proj_name = project.sudo().display_name if project else _('Others') searchbar_filters.update({ - str(proj_id): {'label': proj_name, 'domain': [('project_id', '=', proj_id)]} + str(project.id): {'label': proj_name, 'domain': [('project_id', '=', project.id)]} }) return searchbar_filters diff --git a/addons/project/models/account_analytic_account.py b/addons/project/models/account_analytic_account.py index ea17b0b7757..d0ed490cdc9 100644 --- a/addons/project/models/account_analytic_account.py +++ b/addons/project/models/account_analytic_account.py @@ -14,8 +14,8 @@ class AccountAnalyticAccount(models.Model): @api.depends('project_ids') def _compute_project_count(self): - project_data = self.env['project.project']._read_group([('analytic_account_id', 'in', self.ids)], ['analytic_account_id'], ['analytic_account_id']) - mapping = {m['analytic_account_id'][0]: m['analytic_account_id_count'] for m in project_data} + project_data = self.env['project.project']._read_group([('analytic_account_id', 'in', self.ids)], ['analytic_account_id'], ['__count']) + mapping = {analytic_account.id: count for analytic_account, count in project_data} for account in self: account.project_count = mapping.get(account.id, 0) diff --git a/addons/project/models/project_milestone.py b/addons/project/models/project_milestone.py index 8c6e4391e53..b60f5e435ff 100644 --- a/addons/project/models/project_milestone.py +++ b/addons/project/models/project_milestone.py @@ -47,10 +47,10 @@ class ProjectMilestone(models.Model): @api.depends('task_ids.milestone_id') def _compute_task_count(self): all_and_done_task_count_per_milestone = { - group['milestone_id'][0]: (group['milestone_id_count'], group['is_closed'].count(True)) - for group in self.env['project.task']._read_group( + milestone.id: (count, is_closed_list.count(True)) + for milestone, count, is_closed_list in self.env['project.task']._read_group( [('milestone_id', 'in', self.ids), ('allow_milestones', '=', True)], - ['milestone_id', 'is_closed:array_agg'], ['milestone_id'] + ['milestone_id'], ['__count', 'is_closed:array_agg'], ) } for milestone in self: @@ -63,24 +63,22 @@ class ProjectMilestone(models.Model): return unreached_milestones = self.filtered(lambda milestone: not milestone.is_reached) (self - unreached_milestones).can_be_marked_as_done = False - if unreached_milestones: - task_read_group = self.env['project.task']._read_group( - [('milestone_id', 'in', unreached_milestones.ids)], - ['milestone_id', 'is_closed', 'task_count:count(id)'], - ['milestone_id', 'is_closed'], - lazy=False, - ) - task_count_per_milestones = defaultdict(lambda: (0, 0)) - for res in task_read_group: - opened_task_count, closed_task_count = task_count_per_milestones[res['milestone_id'][0]] - if res['is_closed']: - closed_task_count += res['task_count'] - else: - opened_task_count += res['task_count'] - task_count_per_milestones[res['milestone_id'][0]] = opened_task_count, closed_task_count - for milestone in unreached_milestones: - opened_task_count, closed_task_count = task_count_per_milestones[milestone.id] - milestone.can_be_marked_as_done = closed_task_count > 0 and not opened_task_count + task_read_group = self.env['project.task']._read_group( + [('milestone_id', 'in', unreached_milestones.ids)], + ['milestone_id', 'is_closed'], + ['__count'], + ) + task_count_per_milestones = defaultdict(lambda: (0, 0)) + for milestone, is_closed, count in task_read_group: + opened_task_count, closed_task_count = task_count_per_milestones[milestone.id] + if is_closed: + closed_task_count += count + else: + opened_task_count += count + task_count_per_milestones[milestone.id] = opened_task_count, closed_task_count + for milestone in unreached_milestones: + opened_task_count, closed_task_count = task_count_per_milestones[milestone.id] + milestone.can_be_marked_as_done = closed_task_count > 0 and not opened_task_count def toggle_is_reached(self, is_reached): self.ensure_one() diff --git a/addons/project/models/project_project.py b/addons/project/models/project_project.py index 7e07f9599e7..aa5d0b7db8f 100644 --- a/addons/project/models/project_project.py +++ b/addons/project/models/project_project.py @@ -51,13 +51,13 @@ class Project(models.Model): def _compute_task_count(self): task_count_per_project = { - group['project_id'][0]: group['project_id_count'] - for group in self.env['project.task'].with_context( + project.id: count + for project, count in self.env['project.task'].with_context( active_test=any(project.active for project in self) )._read_group( [('is_closed', '=', False), ('project_id', 'in', self.ids)], ['project_id'], - ['project_id'], + ['__count'], ) } for project in self: @@ -255,8 +255,8 @@ class Project(models.Model): @api.depends('milestone_ids') def _compute_milestone_count(self): - read_group = self.env['project.milestone']._read_group([('project_id', 'in', self.ids)], ['project_id'], ['project_id']) - mapped_count = {group['project_id'][0]: group['project_id_count'] for group in read_group} + read_group = self.env['project.milestone']._read_group([('project_id', 'in', self.ids)], ['project_id'], ['__count']) + mapped_count = {project.id: count for project, count in read_group} for project in self: project.milestone_count = mapped_count.get(project.id, 0) @@ -265,9 +265,9 @@ class Project(models.Model): read_group = self.env['project.milestone']._read_group( [('project_id', 'in', self.ids), ('is_reached', '=', True)], ['project_id'], - ['project_id'], + ['__count'], ) - mapped_count = {group['project_id'][0]: group['project_id_count'] for group in read_group} + mapped_count = {project.id: count for project, count in read_group} for project in self: project.milestone_count_reached = mapped_count.get(project.id, 0) @@ -277,8 +277,8 @@ class Project(models.Model): read_group = self.env['project.milestone']._read_group([ ('project_id', 'in', self.filtered('allow_milestones').ids), ('is_reached', '=', False), - ('deadline', '<=', today)], ['project_id'], ['project_id']) - mapped_count = {group['project_id'][0]: group['project_id_count'] for group in read_group} + ('deadline', '<=', today)], ['project_id'], ['__count']) + mapped_count = {project.id: count for project, count in read_group} for project in self: project.is_milestone_exceeded = bool(mapped_count.get(project.id, 0)) @@ -317,9 +317,9 @@ class Project(models.Model): collaborator_read_group = self.env['project.collaborator']._read_group( [('project_id', 'in', project_sharings.ids)], ['project_id'], - ['project_id'], + ['__count'], ) - collaborator_count_by_project = {res['project_id'][0]: res['project_id_count'] for res in collaborator_read_group} + collaborator_count_by_project = {project.id: count for project, count in collaborator_read_group} for project in self: project.collaborator_count = collaborator_count_by_project.get(project.id, 0) @@ -447,12 +447,10 @@ class Project(models.Model): projects_read_group = self.env['project.project']._read_group( [('analytic_account_id', 'in', self.analytic_account_id.ids)], ['analytic_account_id'], - ['analytic_account_id'] + having=[('__count', '=', 1)], ) analytic_account_to_update = self.env['account.analytic.account'].browse([ - res['analytic_account_id'][0] - for res in projects_read_group - if res['analytic_account_id'] and res['analytic_account_id_count'] == 1 + analytic_account.id for [analytic_account] in projects_read_group ]) analytic_account_to_update.write({'name': self.name}) return res diff --git a/addons/project/models/project_task.py b/addons/project/models/project_task.py index 758fdab1334..6f0bcce7a37 100644 --- a/addons/project/models/project_task.py +++ b/addons/project/models/project_task.py @@ -439,8 +439,8 @@ class Task(models.Model): def _compute_recurring_count(self): self.recurring_count = 0 recurring_tasks = self.filtered(lambda l: l.recurrence_id) - count = self.env['project.task']._read_group([('recurrence_id', 'in', recurring_tasks.recurrence_id.ids)], ['id'], 'recurrence_id') - tasks_count = {c.get('recurrence_id')[0]: c.get('recurrence_id_count') for c in count} + count = self.env['project.task']._read_group([('recurrence_id', 'in', recurring_tasks.recurrence_id.ids)], ['recurrence_id'], ['__count']) + tasks_count = {recurrence.id: count for recurrence, count in count} for task in recurring_tasks: task.recurring_count = tasks_count.get(task.recurrence_id.id, 0) @@ -451,10 +451,10 @@ class Task(models.Model): if tasks_with_dependency: group_dependent = self.env['project.task']._read_group([ ('depend_on_ids', 'in', tasks_with_dependency.ids), - ], ['depend_on_ids'], ['depend_on_ids']) + ], ['depend_on_ids'], ['__count']) dependent_tasks_count_dict = { - group['depend_on_ids'][0]: group['depend_on_ids_count'] - for group in group_dependent + depend_on.id: count + for depend_on, count in group_dependent } for task in tasks_with_dependency: task.dependent_tasks_count = dependent_tasks_count_dict.get(task.id, 0) @@ -522,13 +522,11 @@ class Task(models.Model): @api.depends('child_ids') def _compute_subtask_count(self): total_and_closed_subtask_count_per_parent_id = { - group['parent_id'][0]: ( - group['parent_id_count'], - sum(s in CLOSED_STATES for s in group['state']), - ) for group in self.env['project.task']._read_group( + parent.id: (count, sum(s in CLOSED_STATES for s in states)) + for parent, states, count in self.env['project.task']._read_group( [('parent_id', 'in', self.ids)], - ['state:array_agg', 'parent_id'], ['parent_id'], + ['state:array_agg', '__count'], ) } for task in self: diff --git a/addons/project/models/project_task_recurrence.py b/addons/project/models/project_task_recurrence.py index c78ac396003..89f74460c3c 100644 --- a/addons/project/models/project_task_recurrence.py +++ b/addons/project/models/project_task_recurrence.py @@ -66,11 +66,11 @@ class ProjectTaskRecurrence(models.Model): def _get_last_task_id_per_recurrence_id(self): return {} if not self else { - record['recurrence_id'][0]: record['max_id'] - for record in self.env['project.task'].sudo()._read_group( + recurrence.id: max_task_id + for recurrence, max_task_id in self.env['project.task'].sudo()._read_group( [('recurrence_id', 'in', self.ids)], - ['max_id:max(id)'], ['recurrence_id'], + ['id:max'], ) } diff --git a/addons/project/models/project_task_type.py b/addons/project/models/project_task_type.py index 0a6c0ddf7db..c5d9cc68c95 100644 --- a/addons/project/models/project_task_type.py +++ b/addons/project/models/project_task_type.py @@ -48,8 +48,8 @@ class ProjectTaskType(models.Model): self = self.with_context(active_test=False) # retrieves all the projects with a least 1 task in that stage # a task can be in a stage even if the project is not assigned to the stage - readgroup = self.with_context(active_test=False).env['project.task']._read_group([('stage_id', 'in', self.ids)], ['project_id'], ['project_id']) - project_ids = list(set([project['project_id'][0] for project in readgroup] + self.project_ids.ids)) + readgroup = self.with_context(active_test=False).env['project.task']._read_group([('stage_id', 'in', self.ids)], ['project_id']) + project_ids = list(set([project.id for [project] in readgroup] + self.project_ids.ids)) wizard = self.with_context(project_ids=project_ids).env['project.task.type.delete.wizard'].create({ 'project_ids': project_ids, diff --git a/addons/project/models/res_partner.py b/addons/project/models/res_partner.py index 44a510dd9fd..90c13136bd4 100644 --- a/addons/project/models/res_partner.py +++ b/addons/project/models/res_partner.py @@ -22,15 +22,15 @@ class ResPartner(models.Model): ) task_data = self.env['project.task']._read_group( domain=[('partner_id', 'in', all_partners.ids)], - fields=['partner_id'], groupby=['partner_id'] + groupby=['partner_id'], aggregates=['__count'] ) + self_ids = set(self._ids) self.task_count = 0 - for group in task_data: - partner = self.browse(group['partner_id'][0]) + for partner, count in task_data: while partner: - if partner in self: - partner.task_count += group['partner_id_count'] + if partner.id in self_ids: + partner.task_count += count partner = partner.parent_id # Deprecated: remove me in MASTER diff --git a/addons/project/populate/project.py b/addons/project/populate/project.py index 3e145ffcd71..7acf9059088 100644 --- a/addons/project/populate/project.py +++ b/addons/project/populate/project.py @@ -64,11 +64,11 @@ class ProjectTask(models.Model): stage_ids = self.env.registry.populated_models["project.task.type"] user_ids = self.env.registry.populated_models['res.users'] partner_ids_per_company_id = { - group['company_id'][0]: group['ids'] - for group in self.env['res.partner']._read_group( + company.id: ids + for company, ids in self.env['res.partner']._read_group( [('company_id', '!=', False), ('id', 'in', self.env.registry.populated_models["res.partner"])], - ['company_id', 'ids:array_agg(id)'], ['company_id'], + ['id:array_agg'], ) } company_id_per_project_id = { diff --git a/addons/project/report/project_task_burndown_chart_report.py b/addons/project/report/project_task_burndown_chart_report.py index a5d7bb3c13a..08cc8045a7e 100644 --- a/addons/project/report/project_task_burndown_chart_report.py +++ b/addons/project/report/project_task_burndown_chart_report.py @@ -264,7 +264,7 @@ class ReportProjectTaskBurndownChart(models.AbstractModel): context of the Burndown Chart. Indeed, series are generated so no empty are returned by the `SQL`, except if explicitly specified in the domain through the `date` field, which is then expected. """ - + # TODO # --- Below code is custom self._validate_group_by(groupby) diff --git a/addons/project_mrp/models/project_project.py b/addons/project_mrp/models/project_project.py index d9084fd07d9..cf53dbfe70c 100644 --- a/addons/project_mrp/models/project_project.py +++ b/addons/project_mrp/models/project_project.py @@ -65,17 +65,16 @@ class Project(models.Model): def _get_profitability_items(self, with_action=True): profitability_items = super()._get_profitability_items(with_action) mrp_category = 'manufacturing_order' - mrp_aal_read_group = self.env['account.analytic.line'].sudo()._read_group( + count, amount_sum = self.env['account.analytic.line'].sudo()._read_group( [('account_id', 'in', self.analytic_account_id.ids), ('category', '=', mrp_category)], - ['amount'], - ['account_id'], - ) - if mrp_aal_read_group: + aggregates=['__count', 'amount:sum'], + )[0] + if count: can_see_manufactoring_order = with_action and len(self) == 1 and self.user_has_groups('mrp.group_mrp_user') mrp_costs = { 'id': mrp_category, 'sequence': self._get_profitability_sequence_per_invoice_type()[mrp_category], - 'billed': sum([res['amount'] for res in mrp_aal_read_group]), + 'billed': amount_sum, 'to_bill': 0.0, } if can_see_manufactoring_order: diff --git a/addons/project_sale_expense/models/project_project.py b/addons/project_sale_expense/models/project_project.py index 5d65c436d04..cb44926484b 100644 --- a/addons/project_sale_expense/models/project_project.py +++ b/addons/project_sale_expense/models/project_project.py @@ -37,18 +37,16 @@ class Project(models.Model): ('is_expense', '=', True), ('state', 'in', ['sale', 'done']), ], - ['order_id', 'product_id', 'untaxed_amount_to_invoice', 'untaxed_amount_invoiced'], ['order_id', 'product_id'], - lazy=False) + ['untaxed_amount_to_invoice:sum', 'untaxed_amount_invoiced:sum']) total_amount_expense_invoiced = total_amount_expense_to_invoice = 0.0 reinvoice_expense_ids = [] - for res in sol_read_group: - expense_data_per_product_id = expenses_per_so_id[res['order_id'][0]] - product_id = res['product_id'][0] - if product_id in expense_data_per_product_id: - total_amount_expense_to_invoice += res['untaxed_amount_to_invoice'] - total_amount_expense_invoiced += res['untaxed_amount_invoiced'] - reinvoice_expense_ids += expense_data_per_product_id[product_id] + for order, product, untaxed_amount_to_invoice_sum, untaxed_amount_invoiced_sum in sol_read_group: + expense_data_per_product_id = expenses_per_so_id[order.id] + if product.id in expense_data_per_product_id: + total_amount_expense_to_invoice += untaxed_amount_to_invoice_sum + total_amount_expense_invoiced += untaxed_amount_invoiced_sum + reinvoice_expense_ids += expense_data_per_product_id[product.id] section_id = 'expenses' sequence = self._get_profitability_sequence_per_invoice_type()[section_id] expense_data = { diff --git a/addons/project_timesheet_holidays/models/project_task.py b/addons/project_timesheet_holidays/models/project_task.py index 91825a8dd48..2af333f1a2a 100644 --- a/addons/project_timesheet_holidays/models/project_task.py +++ b/addons/project_timesheet_holidays/models/project_task.py @@ -13,9 +13,9 @@ class Task(models.Model): time_off_type_read_group = self.env['hr.leave.type']._read_group( [('timesheet_task_id', 'in', self.ids)], ['timesheet_task_id'], - ['timesheet_task_id'], + ['__count'], ) - time_off_type_count_per_task = {res['timesheet_task_id'][0]: res['timesheet_task_id_count'] for res in time_off_type_read_group} + time_off_type_count_per_task = {timesheet_task.id: count for timesheet_task, count in time_off_type_read_group} for task in self: task.leave_types_count = time_off_type_count_per_task.get(task.id, 0) @@ -29,10 +29,10 @@ class Task(models.Model): raise NotImplementedError(_('Operation not supported')) leave_type_read_group = self.env['hr.leave.type']._read_group( [('timesheet_task_id', '!=', False)], - ['timesheet_task_ids:array_agg(timesheet_task_id)'], - [], limit=1 + [], + ['timesheet_task_id:array_agg'], ) - timeoff_task_ids = leave_type_read_group[0]['timesheet_task_ids'] or [] + [timeoff_task_ids] = leave_type_read_group[0] if self.env.company.leave_timesheet_task_id: timeoff_task_ids.append(self.env.company.leave_timesheet_task_id.id) if operator == '!=': diff --git a/addons/project_timesheet_holidays/models/resource_calendar_leaves.py b/addons/project_timesheet_holidays/models/resource_calendar_leaves.py index a9d378d9af1..7b320cfe4d4 100644 --- a/addons/project_timesheet_holidays/models/resource_calendar_leaves.py +++ b/addons/project_timesheet_holidays/models/resource_calendar_leaves.py @@ -43,24 +43,24 @@ class ResourceCalendarLeaves(models.Model): resource_calendars = resource_calendars or self._get_resource_calendars() leaves_read_group = self.env['resource.calendar.leaves']._read_group( [('id', 'in', self.ids)], - ['calendar_id', 'ids:array_agg(id)', 'resource_ids:array_agg(resource_id)', 'min_date_from:min(date_from)', 'max_date_to:max(date_to)'], ['calendar_id'], + ['id:recordset', 'resource_id:recordset', 'date_from:min', 'date_to:max'], ) # dict of keys: calendar_id # and values : { 'date_from': datetime, 'date_to': datetime, resources: self.env['resource.resource'] } cal_attendance_intervals_dict = {} - for res in leaves_read_group: + for calendar, leaves, resources, date_from_min, date_to_max in leaves_read_group: calendar_data = { - 'date_from': utc.localize(res['min_date_from']), - 'date_to': utc.localize(res['max_date_to']), - 'resources': self.env['resource.resource'].browse(res['resource_ids'] if res['resource_ids'] and res['resource_ids'][0] else []), - 'leaves': self.env['resource.calendar.leaves'].browse(res['ids']), + 'date_from': utc.localize(date_from_min), + 'date_to': utc.localize(date_to_max), + 'resources': resources, + 'leaves': leaves, } - if not res.get('calendar_id', False): + if not calendar: for calendar_id in resource_calendars.ids: cal_attendance_intervals_dict[calendar_id] = calendar_data else: - cal_attendance_intervals_dict[res['calendar_id'][0]] = calendar_data + cal_attendance_intervals_dict[calendar.id] = calendar_data # to easily find the calendar with its id. calendars_dict = {calendar.id: calendar for calendar in resource_calendars} @@ -97,14 +97,13 @@ class ResourceCalendarLeaves(models.Model): work_hours_data = self._work_time_per_day(resource_calendars) employees_groups = self.env['hr.employee']._read_group( [('resource_calendar_id', 'in', resource_calendars.ids)], - ['resource_calendar_id', 'ids:array_agg(id)'], - ['resource_calendar_id']) + ['resource_calendar_id'], + ['id:recordset']) mapped_employee = { - employee['resource_calendar_id'][0]: self.env['hr.employee'].browse(employee['ids']) - for employee in employees_groups + resource_calendar.id: employees + for resource_calendar, employees in employees_groups } - employee_ids_set = set() - employee_ids_set.update(*[line['ids'] for line in employees_groups]) + employee_ids_all = [_id for __, employees in employees_groups for _id in employees._ids] min_date = max_date = None for values in work_hours_data.values(): for vals in values.values(): @@ -117,15 +116,15 @@ class ResourceCalendarLeaves(models.Model): max_date = d holidays_read_group = self.env['hr.leave']._read_group([ - ('employee_id', 'in', list(employee_ids_set)), + ('employee_id', 'in', employee_ids_all), ('date_from', '<=', max_date), ('date_to', '>=', min_date), ('state', 'not in', ('cancel', 'refuse')), - ], ['date_from_list:array_agg(date_from)', 'date_to_list:array_agg(date_to)', 'employee_id'], ['employee_id']) + ], ['employee_id'], ['date_from:array_agg', 'date_to:array_agg']) holidays_by_employee = { - line['employee_id'][0]: [ - (date_from.date(), date_to.date()) for date_from, date_to in zip(line['date_from_list'], line['date_to_list']) - ] for line in holidays_read_group + employee.id: [ + (date_from.date(), date_to.date()) for date_from, date_to in zip(date_from_list, date_to_list) + ] for employee, date_from_list, date_to_list in holidays_read_group } vals_list = [] diff --git a/addons/project_timesheet_holidays/tests/test_timesheet_global_time_off.py b/addons/project_timesheet_holidays/tests/test_timesheet_global_time_off.py index d19d3c8a8ff..2ad21429a8d 100644 --- a/addons/project_timesheet_holidays/tests/test_timesheet_global_time_off.py +++ b/addons/project_timesheet_holidays/tests/test_timesheet_global_time_off.py @@ -69,11 +69,11 @@ class TestTimesheetGlobalTimeOff(common.TransactionCase): ]) def _get_timesheets_by_employee(self, leave_task): - timesheets_by_read_dict = self.env['account.analytic.line']._read_group([('task_id', '=', leave_task.id)], ['employee_id'], ['employee_id']) - timesheets_by_employee = {} - for timesheet in timesheets_by_read_dict: - timesheets_by_employee[timesheet['employee_id'][0]] = timesheet['employee_id_count'] - return timesheets_by_employee + timesheets_by_read_dict = self.env['account.analytic.line']._read_group([('task_id', '=', leave_task.id)], ['employee_id'], ['__count']) + return { + timesheet.id: count + for timesheet, count in timesheets_by_read_dict + } # This tests that timesheets are created for every employee with the same calendar # when a global time off is created. diff --git a/addons/purchase/models/product.py b/addons/purchase/models/product.py index 6dafd1f0ba7..f8e9b54c4bb 100644 --- a/addons/purchase/models/product.py +++ b/addons/purchase/models/product.py @@ -61,8 +61,8 @@ class ProductProduct(models.Model): ('product_id', 'in', self.ids), ('order_id.date_approve', '>=', date_from) ] - order_lines = self.env['purchase.order.line']._read_group(domain, ['product_id', 'product_uom_qty'], ['product_id']) - purchased_data = dict([(data['product_id'][0], data['product_uom_qty']) for data in order_lines]) + order_lines = self.env['purchase.order.line']._read_group(domain, ['product_id'], ['product_uom_qty:sum']) + purchased_data = {product.id: qty for product, qty in order_lines} for product in self: if not product.id: product.purchased_product_qty = 0.0 diff --git a/addons/purchase/models/res_partner.py b/addons/purchase/models/res_partner.py index 3f47adbefc9..76d4e12914b 100644 --- a/addons/purchase/models/res_partner.py +++ b/addons/purchase/models/res_partner.py @@ -17,17 +17,16 @@ class res_partner(models.Model): ) purchase_order_groups = self.env['purchase.order']._read_group( domain=[('partner_id', 'in', all_partners.ids)], - fields=['partner_id'], groupby=['partner_id'] + groupby=['partner_id'], aggregates=['__count'], ) - partners = self.browse() - for group in purchase_order_groups: - partner = self.browse(group['partner_id'][0]) + self_ids = set(self._ids) + + self.purchase_order_count = 0 + for partner, count in purchase_order_groups: while partner: - if partner in self: - partner.purchase_order_count += group['partner_id_count'] - partners |= partner + if partner.id in self_ids: + partner.purchase_order_count += count partner = partner.parent_id - (self - partners).purchase_order_count = 0 def _compute_supplier_invoice_count(self): # retrieve all children partners and prefetch 'parent_id' on them @@ -38,17 +37,16 @@ class res_partner(models.Model): supplier_invoice_groups = self.env['account.move']._read_group( domain=[('partner_id', 'in', all_partners.ids), ('move_type', 'in', ('in_invoice', 'in_refund'))], - fields=['partner_id'], groupby=['partner_id'] + groupby=['partner_id'], aggregates=['__count'] ) - partners = self.browse() - for group in supplier_invoice_groups: - partner = self.browse(group['partner_id'][0]) + self_ids = set(self._ids) + + self.supplier_invoice_count = 0 + for partner, count in supplier_invoice_groups: while partner: - if partner in self: - partner.supplier_invoice_count += group['partner_id_count'] - partners |= partner + if partner.id in self_ids: + partner.supplier_invoice_count += count partner = partner.parent_id - (self - partners).supplier_invoice_count = 0 @api.model def _commercial_fields(self): diff --git a/addons/purchase_stock/models/product.py b/addons/purchase_stock/models/product.py index bb54de04479..4eacaac340e 100644 --- a/addons/purchase_stock/models/product.py +++ b/addons/purchase_stock/models/product.py @@ -48,17 +48,14 @@ class ProductProduct(models.Model): qty_by_product_location, qty_by_product_wh = super()._get_quantity_in_progress(location_ids, warehouse_ids) domain = self._get_lines_domain(location_ids, warehouse_ids) groups = self.env['purchase.order.line']._read_group(domain, - ['product_id', 'product_qty', 'order_id', 'product_uom', 'orderpoint_id'], - ['order_id', 'product_id', 'product_uom', 'orderpoint_id'], lazy=False) - for group in groups: - if group.get('orderpoint_id'): - location = self.env['stock.warehouse.orderpoint'].browse(group['orderpoint_id'][:1]).location_id + ['order_id', 'product_id', 'product_uom', 'orderpoint_id'], + ['product_qty:sum']) + for order, product, uom, orderpoint, product_qty_sum in groups: + if orderpoint: + location = orderpoint.location_id else: - order = self.env['purchase.order'].browse(group['order_id'][0]) location = order.picking_type_id.default_location_dest_id - product = self.env['product.product'].browse(group['product_id'][0]) - uom = self.env['uom.uom'].browse(group['product_uom'][0]) - product_qty = uom._compute_quantity(group['product_qty'], product.uom_id, round=False) + product_qty = uom._compute_quantity(product_qty_sum, product.uom_id, round=False) qty_by_product_location[(product.id, location.id)] += product_qty qty_by_product_wh[(product.id, location.warehouse_id.id)] += product_qty return qty_by_product_location, qty_by_product_wh diff --git a/addons/rating/models/rating_mixin.py b/addons/rating/models/rating_mixin.py index a638375026e..327219222ab 100644 --- a/addons/rating/models/rating_mixin.py +++ b/addons/rating/models/rating_mixin.py @@ -51,8 +51,8 @@ class RatingMixin(models.AbstractModel): def _compute_rating_stats(self): """ Compute avg and count in one query, as thoses fields will be used together most of the time. """ domain = expression.AND([self._rating_domain(), [('rating', '>=', rating_data.RATING_LIMIT_MIN)]]) - read_group_res = self.env['rating.rating'].read_group(domain, ['rating:avg'], groupby=['res_id'], lazy=False) # force average on rating column - mapping = {item['res_id']: {'rating_count': item['__count'], 'rating_avg': item['rating']} for item in read_group_res} + read_group_res = self.env['rating.rating']._read_group(domain, ['res_id'], aggregates=['__count', 'rating:avg']) # force average on rating column + mapping = {res_id: {'rating_count': count, 'rating_avg': rating_avg} for res_id, count, rating_avg in read_group_res} for record in self: record.rating_count = mapping.get(record.id, {}).get('rating_count', 0) record.rating_avg = mapping.get(record.id, {}).get('rating_avg', 0) @@ -60,13 +60,13 @@ class RatingMixin(models.AbstractModel): def _search_rating_avg(self, operator, value): if operator not in rating_data.OPERATOR_MAPPING: raise NotImplementedError('This operator %s is not supported in this search method.' % operator) - rating_read_group = self.env['rating.rating'].sudo().read_group( + rating_read_group = self.env['rating.rating'].sudo()._read_group( [('res_model', '=', self._name), ('consumed', '=', True), ('rating', '>=', rating_data.RATING_LIMIT_MIN)], - ['res_id', 'rating_avg:avg(rating)'], ['res_id']) + ['res_id'], ['rating:avg']) res_ids = [ - res['res_id'] - for res in rating_read_group - if rating_data.OPERATOR_MAPPING[operator](float_compare(res['rating_avg'], value, 2), 0) + res_id + for res_id, rating_avg in rating_read_group + if rating_data.OPERATOR_MAPPING[operator](float_compare(rating_avg, value, 2), 0) ] return [('id', 'in', res_ids)] @@ -81,14 +81,13 @@ class RatingMixin(models.AbstractModel): since the query is different, to avoid computing if it is not necessary""" domain = expression.AND([self._rating_domain(), [('rating', '>=', rating_data.RATING_LIMIT_MIN)]]) # See `_compute_rating_percentage_satisfaction` above - read_group_res = self.env['rating.rating']._read_group(domain, ['res_id', 'rating'], groupby=['res_id', 'rating'], lazy=False) + read_group_res = self.env['rating.rating']._read_group(domain, ['res_id', 'rating'], aggregates=['__count']) default_grades = {'great': 0, 'okay': 0, 'bad': 0} grades_per_record = {record_id: default_grades.copy() for record_id in self.ids} - for group in read_group_res: - record_id = group['res_id'] - grade = rating_data._rating_to_grade(group['rating']) - grades_per_record[record_id][grade] += group['__count'] + for record_id, rating, count in read_group_res: + grade = rating_data._rating_to_grade(rating) + grades_per_record[record_id][grade] += count for record in self: grade_repartition = grades_per_record.get(record.id, default_grades) @@ -133,21 +132,20 @@ class RatingMixin(models.AbstractModel): base_domain = expression.AND([self._rating_domain(), [('rating', '>=', 1)]]) if domain: base_domain += domain - rg_data = self.env['rating.rating'].read_group(base_domain, ['rating'], ['rating', 'res_id']) + rg_data = self.env['rating.rating']._read_group(base_domain, ['rating'], ['__count']) # init dict with all possible rate value, except 0 (no value for the rating) values = dict.fromkeys(range(1, 6), 0) - for rating_rg in rg_data: - rating_val_round = float_round(rating_rg['rating'], precision_digits=1) - values[rating_val_round] = values.get(rating_val_round, 0) + rating_rg['rating_count'] + for rating, count in rg_data: + rating_val_round = float_round(rating, precision_digits=1) + values[rating_val_round] = values.get(rating_val_round, 0) + count # add other stats if add_stats: rating_number = sum(values.values()) - result = { + return { 'repartition': values, 'avg': sum(float(key * values[key]) for key in values) / rating_number if rating_number > 0 else 0, - 'total': sum(it['rating_count'] for it in rg_data), + 'total': sum(count for __, count in rg_data), } - return result return values def rating_get_grades(self, domain=None): diff --git a/addons/rating/models/rating_parent_mixin.py b/addons/rating/models/rating_parent_mixin.py index 2ba78e3b45f..89d083735a5 100644 --- a/addons/rating/models/rating_parent_mixin.py +++ b/addons/rating/models/rating_parent_mixin.py @@ -35,17 +35,16 @@ class RatingParentMixin(models.AbstractModel): domain = [('parent_res_model', '=', self._name), ('parent_res_id', 'in', self.ids), ('rating', '>=', rating_data.RATING_LIMIT_MIN), ('consumed', '=', True)] if self._rating_satisfaction_days: domain += [('write_date', '>=', fields.Datetime.to_string(fields.datetime.now() - timedelta(days=self._rating_satisfaction_days)))] - data = self.env['rating.rating'].read_group(domain, ['parent_res_id', 'rating'], ['parent_res_id', 'rating'], lazy=False) + data = self.env['rating.rating']._read_group(domain, ['parent_res_id', 'rating'], ['__count']) # get repartition of grades per parent id default_grades = {'great': 0, 'okay': 0, 'bad': 0} grades_per_parent = dict((parent_id, dict(default_grades)) for parent_id in self.ids) # map: {parent_id: {'great': 0, 'bad': 0, 'ok': 0}} rating_scores_per_parent = defaultdict(int) # contains the total of the rating values per record - for item in data: - parent_id = item['parent_res_id'] - grade = rating_data._rating_to_grade(item['rating']) - grades_per_parent[parent_id][grade] += item['__count'] - rating_scores_per_parent[parent_id] += item['rating'] * item['__count'] + for parent_id, rating, count in data: + grade = rating_data._rating_to_grade(rating) + grades_per_parent[parent_id][grade] += count + rating_scores_per_parent[parent_id] += rating * count # compute percentage per parent for record in self: @@ -63,10 +62,10 @@ class RatingParentMixin(models.AbstractModel): if self._rating_satisfaction_days: min_date = fields.datetime.now() - timedelta(days=self._rating_satisfaction_days) domain = expression.AND([domain, [('write_date', '>=', fields.Datetime.to_string(min_date))]]) - rating_read_group = self.env['rating.rating'].sudo().read_group(domain, ['parent_res_id', 'rating_avg:avg(rating)'], ['parent_res_id']) + rating_read_group = self.env['rating.rating'].sudo()._read_group(domain, ['parent_res_id'], ['rating:avg']) parent_res_ids = [ - res['parent_res_id'] - for res in rating_read_group - if rating_data.OPERATOR_MAPPING[operator](float_compare(res['rating_avg'], value, 2), 0) + parent_res_id + for parent_res_id, rating_avg in rating_read_group + if rating_data.OPERATOR_MAPPING[operator](float_compare(rating_avg, value, 2), 0) ] return [('id', 'in', parent_res_ids)] diff --git a/addons/sale/models/crm_team.py b/addons/sale/models/crm_team.py index 6013800f718..9073d4ac03d 100644 --- a/addons/sale/models/crm_team.py +++ b/addons/sale/models/crm_team.py @@ -62,8 +62,8 @@ class CrmTeam(models.Model): sale_order_data = self.env['sale.order']._read_group([ ('team_id', 'in', self.ids), ('invoice_status','=','to invoice'), - ], ['team_id'], ['team_id']) - data_map = {datum['team_id'][0]: datum['team_id_count'] for datum in sale_order_data} + ], ['team_id'], ['__count']) + data_map = {team.id: count for team, count in sale_order_data} for team in self: team.sales_to_invoice_count = data_map.get(team.id,0.0) @@ -92,13 +92,11 @@ class CrmTeam(models.Model): team.invoiced = data_map.get(team.id, 0.0) def _compute_sale_order_count(self): - data_map = {} - if self.ids: - sale_order_data = self.env['sale.order']._read_group([ - ('team_id', 'in', self.ids), - ('state', '!=', 'cancel'), - ], ['team_id'], ['team_id']) - data_map = {datum['team_id'][0]: datum['team_id_count'] for datum in sale_order_data} + sale_order_data = self.env['sale.order']._read_group([ + ('team_id', 'in', self.ids), + ('state', '!=', 'cancel'), + ], ['team_id'], ['__count']) + data_map = {team.id: count for team, count in sale_order_data} for team in self: team.sale_order_count = data_map.get(team.id, 0) diff --git a/addons/sale/models/product_product.py b/addons/sale/models/product_product.py index 4a519537146..917e230fc54 100644 --- a/addons/sale/models/product_product.py +++ b/addons/sale/models/product_product.py @@ -25,8 +25,8 @@ class ProductProduct(models.Model): ('product_id', 'in', self.ids), ('date', '>=', date_from), ] - for group in self.env['sale.report']._read_group(domain, ['product_id', 'product_uom_qty'], ['product_id']): - r[group['product_id'][0]] = group['product_uom_qty'] + for product, product_uom_qty in self.env['sale.report']._read_group(domain, ['product_id'], ['product_uom_qty:sum']): + r[product.id] = product_uom_qty for product in self: if not product.id: product.sales_count = 0.0 @@ -66,8 +66,8 @@ class ProductProduct(models.Model): def _filter_to_unlink(self): domain = [('product_id', 'in', self.ids)] - lines = self.env['sale.order.line']._read_group(domain, ['product_id'], ['product_id']) - linked_product_ids = [group['product_id'][0] for group in lines] + lines = self.env['sale.order.line']._read_group(domain, ['product_id']) + linked_product_ids = [product.id for [product] in lines] return super(ProductProduct, self - self.browse(linked_product_ids))._filter_to_unlink() diff --git a/addons/sale/models/res_partner.py b/addons/sale/models/res_partner.py index 5fd492b5168..de1fc21f665 100644 --- a/addons/sale/models/res_partner.py +++ b/addons/sale/models/res_partner.py @@ -25,17 +25,16 @@ class ResPartner(models.Model): ) sale_order_groups = self.env['sale.order']._read_group( domain=expression.AND([self._get_sale_order_domain_count(), [('partner_id', 'in', all_partners.ids)]]), - fields=['partner_id'], groupby=['partner_id'] + groupby=['partner_id'], aggregates=['__count'] ) - partners = self.browse() - for group in sale_order_groups: - partner = self.browse(group['partner_id'][0]) + self_ids = set(self._ids) + + self.sale_order_count = 0 + for partner, count in sale_order_groups: while partner: - if partner in self: - partner.sale_order_count += group['partner_id_count'] - partners |= partner + if partner.id in self_ids: + partner.sale_order_count += count partner = partner.parent_id - (self - partners).sale_order_count = 0 def can_edit_vat(self): ''' Can't edit `vat` if there is (non draft) issued SO. ''' @@ -58,10 +57,9 @@ class ResPartner(models.Model): def _credit_debit_get(self): super()._credit_debit_get() domain = [('partner_id', 'in', self.ids), ('state', 'in', ['sale', 'done'])] - group = self.env['sale.order'].read_group(domain, ['amount_to_invoice'], ['partner_id']) - for res in group: - partner = self.browse(res['partner_id'][0]) - partner.credit += res['amount_to_invoice'] + group = self.env['sale.order']._read_group(domain, ['partner_id'], ['amount_to_invoice:sum']) + for partner, amount_to_invoice_sum in group: + partner.credit += amount_to_invoice_sum def unlink(self): # Unlink draft/cancelled SO so that the partner can be removed from database diff --git a/addons/sale/models/sale_order.py b/addons/sale/models/sale_order.py index 7f1619c0f0b..e4cb337eb28 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -516,14 +516,13 @@ class SaleOrder(models.Model): if not confirmed_orders: return line_invoice_status_all = [ - (d['order_id'][0], d['invoice_status']) - for d in self.env['sale.order.line'].read_group([ + (order.id, invoice_status) + for order, invoice_status in self.env['sale.order.line']._read_group([ ('order_id', 'in', confirmed_orders.ids), ('is_downpayment', '=', False), ('display_type', '=', False), ], - ['order_id', 'invoice_status'], - ['order_id', 'invoice_status'], lazy=False)] + ['order_id', 'invoice_status'])] for order in confirmed_orders: line_invoice_status = [d[1] for d in line_invoice_status_all if d[0] == order.id] if order.state not in ('sale', 'done'): diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index 9eb17666c28..676660ae9b3 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -705,29 +705,20 @@ class SaleOrderLine(models.Model): # group analytic lines by product uom and so line domain = expression.AND([[('so_line', 'in', self.ids)], additional_domain]) - data = self.env['account.analytic.line'].read_group( + data = self.env['account.analytic.line']._read_group( domain, - ['so_line', 'unit_amount', 'product_uom_id'], ['product_uom_id', 'so_line'], lazy=False + ['product_uom_id', 'so_line'], ['unit_amount:sum'], ) - # convert uom and sum all unit_amount of analytic lines to get the delivered qty of SO lines - # browse so lines and product uoms here to make them share the same prefetch - lines = self.browse([item['so_line'][0] for item in data]) - lines_map = {line.id: line for line in lines} - product_uom_ids = [item['product_uom_id'][0] for item in data if item['product_uom_id']] - product_uom_map = {uom.id: uom for uom in self.env['uom.uom'].browse(product_uom_ids)} - for item in data: - if not item['product_uom_id']: + for uom, so_line, unit_amount_sum in data: + if not uom: continue - so_line_id = item['so_line'][0] - so_line = lines_map[so_line_id] - result.setdefault(so_line_id, 0.0) - uom = product_uom_map.get(item['product_uom_id'][0]) + result.setdefault(so_line.id, 0.0) if so_line.product_uom.category_id == uom.category_id: - qty = uom._compute_quantity(item['unit_amount'], so_line.product_uom, rounding_method='HALF-UP') + qty = uom._compute_quantity(unit_amount_sum, so_line.product_uom, rounding_method='HALF-UP') else: - qty = item['unit_amount'] - result[so_line_id] += qty + qty = unit_amount_sum + result[so_line.id] += qty return result diff --git a/addons/sale/models/utm_campaign.py b/addons/sale/models/utm_campaign.py index 8e1d63dceaa..cc7de72b68f 100644 --- a/addons/sale/models/utm_campaign.py +++ b/addons/sale/models/utm_campaign.py @@ -17,8 +17,8 @@ class UtmCampaign(models.Model): def _compute_quotation_count(self): quotation_data = self.env['sale.order']._read_group([ ('campaign_id', 'in', self.ids)], - ['campaign_id'], ['campaign_id']) - data_map = {datum['campaign_id'][0]: datum['campaign_id_count'] for datum in quotation_data} + ['campaign_id'], ['__count']) + data_map = {campaign.id: count for campaign, count in quotation_data} for campaign in self: campaign.quotation_count = data_map.get(campaign.id, 0) diff --git a/addons/sale_expense/models/sale_order.py b/addons/sale_expense/models/sale_order.py index 9cc5116b8b0..14b4fd957c8 100644 --- a/addons/sale_expense/models/sale_order.py +++ b/addons/sale_expense/models/sale_order.py @@ -25,7 +25,7 @@ class SaleOrder(models.Model): @api.depends('expense_ids') def _compute_expense_count(self): - expense_data = self.env['hr.expense']._read_group([('sale_order_id', 'in', self.ids)], ['sale_order_id'], ['sale_order_id']) - mapped_data = dict([(item['sale_order_id'][0], item['sale_order_id_count']) for item in expense_data]) + expense_data = self.env['hr.expense']._read_group([('sale_order_id', 'in', self.ids)], ['sale_order_id'], ['__count']) + mapped_data = {sale_order.id: count for sale_order, count in expense_data} for sale_order in self: sale_order.expense_count = mapped_data.get(sale_order.id, 0) diff --git a/addons/sale_loyalty/models/loyalty_card.py b/addons/sale_loyalty/models/loyalty_card.py index f4d612c8faf..85a56df5e80 100644 --- a/addons/sale_loyalty/models/loyalty_card.py +++ b/addons/sale_loyalty/models/loyalty_card.py @@ -25,8 +25,8 @@ class LoyaltyCard(models.Model): def _compute_use_count(self): super()._compute_use_count() read_group_res = self.env['sale.order.line']._read_group( - [('coupon_id', 'in', self.ids)], ['id'], ['coupon_id']) - count_per_coupon = {r['coupon_id'][0]: r['coupon_id_count'] for r in read_group_res} + [('coupon_id', 'in', self.ids)], ['coupon_id'], ['__count']) + count_per_coupon = {coupon.id: count for coupon, count in read_group_res} for card in self: card.use_count += count_per_coupon.get(card.id, 0) diff --git a/addons/sale_loyalty/models/loyalty_program.py b/addons/sale_loyalty/models/loyalty_program.py index f0b3bafe6b9..85ab9fb72da 100644 --- a/addons/sale_loyalty/models/loyalty_program.py +++ b/addons/sale_loyalty/models/loyalty_program.py @@ -12,10 +12,13 @@ class LoyaltyProgram(models.Model): def _compute_order_count(self): # An order should count only once PER program but may appear in multiple programs read_group_res = self.env['sale.order.line']._read_group( - [('reward_id', 'in', self.reward_ids.ids)], ['reward_id:array_agg'], ['order_id']) + [('reward_id', 'in', self.reward_ids.ids)], ['order_id'], ['reward_id:array_agg']) for program in self: program_reward_ids = program.reward_ids.ids - program.order_count = sum(1 if any(id in group['reward_id'] for id in program_reward_ids) else 0 for group in read_group_res) + program.order_count = sum( + any(id_ in reward_ids for id_ in program_reward_ids) + for __, reward_ids in read_group_res + ) def _compute_total_order_count(self): super()._compute_total_order_count() diff --git a/addons/sale_margin/models/sale_order.py b/addons/sale_margin/models/sale_order.py index 047b8fb6fca..45b18a6b90f 100644 --- a/addons/sale_margin/models/sale_order.py +++ b/addons/sale_margin/models/sale_order.py @@ -21,11 +21,11 @@ class SaleOrder(models.Model): # with a single read_group query for better performance. # This isn't done in an onchange environment because (part of) the data # may not be stored in database (new records or unsaved modifications). - grouped_order_lines_data = self.env['sale.order.line'].read_group( + grouped_order_lines_data = self.env['sale.order.line']._read_group( [ ('order_id', 'in', self.ids), - ], ['margin', 'order_id'], ['order_id']) - mapped_data = {m['order_id'][0]: m['margin'] for m in grouped_order_lines_data} + ], ['order_id'], ['margin:sum']) + mapped_data = {order.id: margin for order, margin in grouped_order_lines_data} for order in self: order.margin = mapped_data.get(order.id, 0.0) order.margin_percent = order.amount_untaxed and order.margin/order.amount_untaxed diff --git a/addons/sale_mrp/models/sale_order.py b/addons/sale_mrp/models/sale_order.py index 92e83af6779..d253933655d 100644 --- a/addons/sale_mrp/models/sale_order.py +++ b/addons/sale_mrp/models/sale_order.py @@ -19,11 +19,10 @@ class SaleOrder(models.Model): @api.depends('procurement_group_id.stock_move_ids.created_production_id.procurement_group_id.mrp_production_ids') def _compute_mrp_production_ids(self): - data = self.env['procurement.group'].read_group([('sale_id', 'in', self.ids)], ['ids:array_agg(id)'], ['sale_id']) - mrp_productions = dict() - for item in data: - procurement_groups = self.env['procurement.group'].browse(item['ids']) - mrp_productions[item['sale_id'][0]] = procurement_groups.stock_move_ids.created_production_id.procurement_group_id.mrp_production_ids | procurement_groups.mrp_production_ids + data = self.env['procurement.group']._read_group([('sale_id', 'in', self.ids)], ['sale_id'], ['id:recordset']) + mrp_productions = {} + for sale, procurement_groups in data: + mrp_productions[sale.id] = procurement_groups.stock_move_ids.created_production_id.procurement_group_id.mrp_production_ids | procurement_groups.mrp_production_ids for sale in self: mrp_production_ids = mrp_productions.get(sale.id, self.env['mrp.production']) sale.mrp_production_count = len(mrp_production_ids) diff --git a/addons/sale_project/models/project.py b/addons/sale_project/models/project.py index 20e91b1afa9..09912eecf50 100755 --- a/addons/sale_project/models/project.py +++ b/addons/sale_project/models/project.py @@ -355,34 +355,32 @@ class Project(models.Model): def _get_revenues_items_from_sol(self, domain=None, with_action=True): sale_line_read_group = self.env['sale.order.line'].sudo()._read_group( self._get_profitability_sale_order_items_domain(domain), - ['product_id', 'ids:array_agg(id)', 'untaxed_amount_to_invoice', 'untaxed_amount_invoiced'], ['product_id'], + ['id:array_agg', 'untaxed_amount_to_invoice:sum', 'untaxed_amount_invoiced:sum'], ) display_sol_action = with_action and len(self) == 1 and self.user_has_groups('sales_team.group_sale_salesman') revenues_dict = {} total_to_invoice = total_invoiced = 0.0 if sale_line_read_group: sols_per_product = { - res['product_id'][0]: ( - res['untaxed_amount_to_invoice'], - res['untaxed_amount_invoiced'], - res['ids'], - ) for res in sale_line_read_group + product.id: ( + untaxed_amount_to_invoice, + untaxed_amount_invoiced, + ids, + ) for product, ids, untaxed_amount_to_invoice, untaxed_amount_invoiced in sale_line_read_group } product_read_group = self.env['product.product'].sudo()._read_group( [('id', 'in', list(sols_per_product)), ('expense_policy', '=', 'no')], - ['invoice_policy', 'service_type', 'type', 'ids:array_agg(id)'], ['invoice_policy', 'service_type', 'type'], - lazy=False, + ['id:array_agg'], ) service_policy_to_invoice_type = self._get_service_policy_to_invoice_type() general_to_service_map = self.env['product.template']._get_general_to_service_map() - for res in product_read_group: - product_ids = res['ids'] + for invoice_policy, service_type, type_, product_ids in product_read_group: service_policy = None - if res['type'] == 'service': + if type_ == 'service': service_policy = general_to_service_map.get( - (res['invoice_policy'], res['service_type']), + (invoice_policy, service_type), 'ordered_prepaid') for product_id, (amount_to_invoice, amount_invoiced, sol_ids) in sols_per_product.items(): if product_id in product_ids: @@ -497,15 +495,13 @@ class Project(models.Model): return {'data': [], 'total': {'invoiced': 0.0, 'to_invoice': 0.0}} def _add_invoice_items(self, domain, profitability_items, with_action=True): - sale_line_read_group = self.env['sale.order.line'].sudo()._read_group( + sale_lines = self.env['sale.order.line'].sudo()._read_group( self._get_profitability_sale_order_items_domain(domain), - ['ids:array_agg(id)'], - ['product_id'], - ) + [], + ['id:recordset'], + )[0][0] revenue_items_from_invoices = self._get_revenues_items_from_invoices( - excluded_move_line_ids=self.env['sale.order.line'].browse( - [sol_id for sol_read in sale_line_read_group for sol_id in sol_read['ids']] - ).invoice_lines.ids, + excluded_move_line_ids=sale_lines.invoice_lines.ids, with_action=with_action ) profitability_items['revenues']['data'] += revenue_items_from_invoices['data'] diff --git a/addons/sale_project/models/res_config_settings.py b/addons/sale_project/models/res_config_settings.py index de4d32da918..e132c888760 100644 --- a/addons/sale_project/models/res_config_settings.py +++ b/addons/sale_project/models/res_config_settings.py @@ -12,13 +12,8 @@ class ResConfigSettings(models.TransientModel): if self.group_project_milestone: # Search the milestones containing a SOL and change the qty_delivered_method field of the SOL and the # service_policy field set on the product to convert from manual to milestones. - milestone_read_group = self.env['project.milestone'].read_group( - [('sale_line_id', '!=', False)], - ['sale_line_ids:array_agg(sale_line_id)'], - [], - ) - sale_line_ids = milestone_read_group[0]['sale_line_ids'] if milestone_read_group else [] - sale_lines = self.env['sale.order.line'].sudo().browse(sale_line_ids) + milestones = self.env['project.milestone'].search_fetch([('sale_line_id', '!=', False)], ['sale_line_id']) + sale_lines = milestones.sale_line_id.sudo() sale_lines.product_id.service_policy = 'delivered_milestones' else: product_domain = [('type', '=', 'service'), ('service_type', '=', 'milestones')] diff --git a/addons/sale_project/models/sale_order.py b/addons/sale_project/models/sale_order.py index e282b376fef..78043d67079 100644 --- a/addons/sale_project/models/sale_order.py +++ b/addons/sale_project/models/sale_order.py @@ -26,9 +26,9 @@ class SaleOrder(models.Model): read_group = self.env['project.milestone']._read_group( [('sale_line_id', 'in', self.order_line.ids)], ['sale_line_id'], - ['sale_line_id'], + ['__count'], ) - line_data = {res['sale_line_id'][0]: res['sale_line_id_count'] for res in read_group} + line_data = {sale_line.id: count for sale_line, count in read_group} for order in self: order.milestone_count = sum(line_data.get(line.id, 0) for line in order.order_line) diff --git a/addons/sale_project/models/sale_order_line.py b/addons/sale_project/models/sale_order_line.py index bbaac554216..11a5e3133e3 100644 --- a/addons/sale_project/models/sale_order_line.py +++ b/addons/sale_project/models/sale_order_line.py @@ -47,12 +47,12 @@ class SaleOrderLine(models.Model): if not lines_by_milestones: return - project_milestone_read_group = self.env['project.milestone'].read_group( + project_milestone_read_group = self.env['project.milestone']._read_group( [('sale_line_id', 'in', lines_by_milestones.ids), ('is_reached', '=', True)], - ['sale_line_id', 'quantity_percentage'], ['sale_line_id'], + ['quantity_percentage:sum'], ) - reached_milestones_per_sol = {res['sale_line_id'][0]: res['quantity_percentage'] for res in project_milestone_read_group} + reached_milestones_per_sol = {sale_line.id: percentage_sum for sale_line, percentage_sum in project_milestone_read_group} for line in lines_by_milestones: sol_id = line.id or line._origin.id line.qty_delivered = reached_milestones_per_sol.get(sol_id, 0.0) * line.product_uom_qty @@ -292,21 +292,21 @@ class SaleOrderLine(models.Model): elif self.project_id.analytic_account_id: values['analytic_distribution'] = {self.project_id.analytic_account_id.id: 100} elif self.is_service and not self.is_expense: - task_analytic_account_id = self.env['project.task'].read_group([ + [task_analytic_accounts] = self.env['project.task']._read_group([ ('sale_line_id', '=', self.id), ('analytic_account_id', '!=', False), - ], ['analytic_account_id'], ['analytic_account_id']) - project_analytic_account_id = self.env['project.project'].read_group([ + ], aggregates=['analytic_account_id:recordset'])[0] + [project_analytic_accounts] = self.env['project.project']._read_group([ ('analytic_account_id', '!=', False), '|', ('sale_line_id', '=', self.id), '&', ('tasks.sale_line_id', '=', self.id), ('tasks.analytic_account_id', '=', False) - ], ['analytic_account_id'], ['analytic_account_id']) - analytic_account_ids = {rec['analytic_account_id'][0] for rec in (task_analytic_account_id + project_analytic_account_id)} - if len(analytic_account_ids) == 1: - values['analytic_distribution'] = {analytic_account_ids.pop(): 100} + ], aggregates=['analytic_account_id:recordset'])[0] + analytic_accounts = task_analytic_accounts | project_analytic_accounts + if len(analytic_accounts) == 1: + values['analytic_distribution'] = {analytic_accounts.id: 100} return values def _get_action_per_item(self): diff --git a/addons/sale_project_stock/models/sale_order_line.py b/addons/sale_project_stock/models/sale_order_line.py index a94b4a39192..c4e351c7e87 100644 --- a/addons/sale_project_stock/models/sale_order_line.py +++ b/addons/sale_project_stock/models/sale_order_line.py @@ -15,8 +15,8 @@ class SaleOrderLine(models.Model): stock_move_action = self.env.ref('sale_project_stock.stock_move_per_sale_order_line_action').id stock_move_ids_per_sol = {} if self.user_has_groups('stock.group_stock_user'): - stock_move_read_group = self.env['stock.move']._read_group([('sale_line_id', 'in', self.ids)], ['sale_line_id', 'ids:array_agg(id)'], ['sale_line_id']) - stock_move_ids_per_sol = {res['sale_line_id'][0]: res['ids'] for res in stock_move_read_group} + stock_move_read_group = self.env['stock.move']._read_group([('sale_line_id', 'in', self.ids)], ['sale_line_id'], ['id:array_agg']) + stock_move_ids_per_sol = {sale_line.id: ids for sale_line, ids in stock_move_read_group} for sol in self: stock_move_ids = stock_move_ids_per_sol.get(sol.id, []) if not sol.is_service and stock_move_ids: diff --git a/addons/sale_purchase/models/sale_order_line.py b/addons/sale_purchase/models/sale_order_line.py index e566200d5ca..51f47925974 100644 --- a/addons/sale_purchase/models/sale_order_line.py +++ b/addons/sale_purchase/models/sale_order_line.py @@ -17,8 +17,8 @@ class SaleOrderLine(models.Model): @api.depends('purchase_line_ids') def _compute_purchase_count(self): - database_data = self.env['purchase.order.line'].sudo().read_group([('sale_line_id', 'in', self.ids)], ['sale_line_id'], ['sale_line_id']) - mapped_data = dict([(db['sale_line_id'][0], db['sale_line_id_count']) for db in database_data]) + database_data = self.env['purchase.order.line'].sudo()._read_group([('sale_line_id', 'in', self.ids)], ['sale_line_id'], ['__count']) + mapped_data = {sale_line.id: count for sale_line, count in database_data} for line in self: line.purchase_line_count = mapped_data.get(line.id, 0) diff --git a/addons/sale_timesheet/models/account_move.py b/addons/sale_timesheet/models/account_move.py index dacc6d1f99c..3061ee003db 100644 --- a/addons/sale_timesheet/models/account_move.py +++ b/addons/sale_timesheet/models/account_move.py @@ -24,17 +24,17 @@ class AccountMove(models.Model): return group_data = self.env['account.analytic.line']._read_group([ ('timesheet_invoice_id', 'in', self.ids) - ], ['timesheet_invoice_id', 'unit_amount'], ['timesheet_invoice_id']) + ], ['timesheet_invoice_id'], ['unit_amount:sum']) timesheet_unit_amount_dict = defaultdict(float) - timesheet_unit_amount_dict.update({data['timesheet_invoice_id'][0]: data['unit_amount'] for data in group_data}) + timesheet_unit_amount_dict.update({timesheet_invoice.id: amount for timesheet_invoice, amount in group_data}) for invoice in self: total_time = invoice.company_id.project_time_mode_id._compute_quantity(timesheet_unit_amount_dict[invoice.id], invoice.timesheet_encode_uom_id) invoice.timesheet_total_duration = round(total_time) @api.depends('timesheet_ids') def _compute_timesheet_count(self): - timesheet_data = self.env['account.analytic.line']._read_group([('timesheet_invoice_id', 'in', self.ids)], ['timesheet_invoice_id'], ['timesheet_invoice_id']) - mapped_data = dict([(t['timesheet_invoice_id'][0], t['timesheet_invoice_id_count']) for t in timesheet_data]) + timesheet_data = self.env['account.analytic.line']._read_group([('timesheet_invoice_id', 'in', self.ids)], ['timesheet_invoice_id'], ['__count']) + mapped_data = {timesheet_invoice.id: count for timesheet_invoice, count in timesheet_data} for invoice in self: invoice.timesheet_count = mapped_data.get(invoice.id, 0) @@ -119,15 +119,13 @@ class AccountMoveLine(models.Model): ('timesheet_invoice_id.move_type', '=', 'out_invoice'), ('timesheet_invoice_id.state', '=', 'draft'), ('timesheet_invoice_id', 'in', self.move_id.ids)], - ['timesheet_invoice_id', 'so_line', 'ids:array_agg(id)'], ['timesheet_invoice_id', 'so_line'], - lazy=False) + ['id:array_agg']) timesheet_ids = [] - for timesheet in timesheet_read_group: - move_id = timesheet['timesheet_invoice_id'][0] - if timesheet['so_line'][0] in sale_line_ids_per_move[move_id].ids: - timesheet_ids += timesheet['ids'] + for timesheet_invoice, so_line, ids in timesheet_read_group: + if so_line.id in sale_line_ids_per_move[timesheet_invoice.id].ids: + timesheet_ids += ids self.sudo().env['account.analytic.line'].browse(timesheet_ids).write({'timesheet_invoice_id': False}) return super().unlink() diff --git a/addons/sale_timesheet/models/project.py b/addons/sale_timesheet/models/project.py index ec89b48eb78..a403c38f62d 100644 --- a/addons/sale_timesheet/models/project.py +++ b/addons/sale_timesheet/models/project.py @@ -122,10 +122,10 @@ class Project(models.Model): @api.depends('analytic_account_id', 'timesheet_ids') def _compute_billable_percentage(self): - timesheets_read_group = self.env['account.analytic.line']._read_group([('project_id', 'in', self.ids)], ['project_id', 'so_line', 'unit_amount'], ['project_id', 'so_line'], lazy=False) + timesheets_read_group = self.env['account.analytic.line']._read_group([('project_id', 'in', self.ids)], ['project_id', 'so_line'], ['unit_amount:sum']) timesheets_by_project = defaultdict(list) - for res in timesheets_read_group: - timesheets_by_project[res['project_id'][0]].append((res['unit_amount'], bool(res['so_line']))) + for project, so_line, unit_amount_sum in timesheets_read_group: + timesheets_by_project[project.id].append((unit_amount_sum, bool(so_line))) for project in self: timesheet_total = timesheet_billable = 0.0 for unit_amount, is_billable_timesheet in timesheets_by_project[project.id]: @@ -152,12 +152,17 @@ class Project(models.Model): @api.depends('pricing_type', 'allow_timesheets', 'allow_billable', 'sale_line_employee_ids', 'sale_line_employee_ids.employee_id') def _compute_warning_employee_rate(self): projects = self.filtered(lambda p: p.allow_billable and p.allow_timesheets and p.pricing_type == 'employee_rate') - employees = self.env['account.analytic.line']._read_group([('task_id', 'in', projects.task_ids.ids)], ['employee_id', 'project_id'], ['employee_id', 'project_id'], ['employee_id', 'project_id'], lazy=False) - dict_project_employee = defaultdict(list) - for line in employees: - dict_project_employee[line['project_id'][0]] += [line['employee_id'][0]] if line['employee_id'] else [] + employees = self.env['account.analytic.line']._read_group( + [('task_id', 'in', projects.task_ids.ids), ('employee_id', '!=', False)], + ['project_id'], + ['employee_id:array_agg'], + ) + dict_project_employee = {project.id: employee_ids for project, employee_ids in employees} for project in projects: - project.warning_employee_rate = any(x not in project.sale_line_employee_ids.employee_id.ids for x in dict_project_employee[project.id]) + project.warning_employee_rate = any( + x not in project.sale_line_employee_ids.employee_id.ids + for x in dict_project_employee.get(project.id, ()) + ) (self - projects).warning_employee_rate = False @@ -398,17 +403,14 @@ class Project(models.Model): return profitability_items aa_line_read_group = self.env['account.analytic.line'].sudo()._read_group( self.sudo()._get_profitability_aal_domain(), - ['timesheet_invoice_type', 'timesheet_invoice_id', 'unit_amount', 'amount', 'ids:array_agg(id)'], - ['timesheet_invoice_type', 'timesheet_invoice_id'], - lazy=False) + ['timesheet_invoice_type'], + ['amount:sum', 'id:array_agg']) can_see_timesheets = with_action and len(self) == 1 and self.user_has_groups('hr_timesheet.group_hr_timesheet_approver') revenues_dict = {} costs_dict = {} total_revenues = {'invoiced': 0.0, 'to_invoice': 0.0} total_costs = {'billed': 0.0, 'to_bill': 0.0} - for res in aa_line_read_group: - amount = res['amount'] - invoice_type = res['timesheet_invoice_type'] + for invoice_type, amount, ids in aa_line_read_group: cost = costs_dict.setdefault(invoice_type, {'billed': 0.0, 'to_bill': 0.0}) revenue = revenues_dict.setdefault(invoice_type, {'invoiced': 0.0, 'to_invoice': 0.0}) if amount < 0: # cost @@ -418,8 +420,8 @@ class Project(models.Model): revenue['invoiced'] += amount total_revenues['invoiced'] += amount if can_see_timesheets and invoice_type not in ['other_costs', 'other_revenues']: - cost.setdefault('record_ids', []).extend(res['ids']) - revenue.setdefault('record_ids', []).extend(res['ids']) + cost.setdefault('record_ids', []).extend(ids) + revenue.setdefault('record_ids', []).extend(ids) action_name = None if can_see_timesheets: diff --git a/addons/sale_timesheet/models/project_sale_line_employee_map.py b/addons/sale_timesheet/models/project_sale_line_employee_map.py index 7a0bee7c5f0..27b0afe2826 100644 --- a/addons/sale_timesheet/models/project_sale_line_employee_map.py +++ b/addons/sale_timesheet/models/project_sale_line_employee_map.py @@ -71,12 +71,12 @@ class ProjectProductEmployeeMap(models.Model): read_group_data = self.env['resource.calendar']._read_group( [('id', 'in', self.employee_id.resource_calendar_id.ids)], - ['ids:array_agg(id)', 'hours_per_day'], ['hours_per_day'], + ['id:array_agg'], ) - for res in read_group_data: - for calendar_id in res.get('ids', []): - resource_calendar_per_hours[calendar_id] = res.get('hours_per_day') + for hours_per_day, ids in read_group_data: + for calendar_id in ids: + resource_calendar_per_hours[calendar_id] = hours_per_day return resource_calendar_per_hours diff --git a/addons/sale_timesheet/models/sale_order.py b/addons/sale_timesheet/models/sale_order.py index 558253a43e3..0b4aa7ded2b 100644 --- a/addons/sale_timesheet/models/sale_order.py +++ b/addons/sale_timesheet/models/sale_order.py @@ -23,11 +23,11 @@ class SaleOrder(models.Model): def _compute_timesheet_count(self): timesheets_per_so = { - group['order_id'][0]: group['order_id_count'] - for group in self.env['account.analytic.line']._read_group( + order.id: count + for order, count in self.env['account.analytic.line']._read_group( [('order_id', 'in', self.ids), ('project_id', '!=', False)], ['order_id'], - ['order_id'] + ['__count'], ) } @@ -38,9 +38,9 @@ class SaleOrder(models.Model): def _compute_timesheet_total_duration(self): group_data = self.env['account.analytic.line']._read_group([ ('order_id', 'in', self.ids) - ], ['order_id', 'unit_amount'], ['order_id']) + ], ['order_id'], ['unit_amount:sum']) timesheet_unit_amount_dict = defaultdict(float) - timesheet_unit_amount_dict.update({data['order_id'][0]: data['unit_amount'] for data in group_data}) + timesheet_unit_amount_dict.update({order.id: unit_amount for order, unit_amount in group_data}) for sale_order in self: total_time = sale_order.company_id.project_time_mode_id._compute_quantity(timesheet_unit_amount_dict[sale_order.id], sale_order.timesheet_encode_uom_id) sale_order.timesheet_total_duration = round(total_time) @@ -310,8 +310,8 @@ class SaleOrderLine(models.Model): timesheet_action = self.env.ref('sale_timesheet.timesheet_action_from_sales_order_item').id timesheet_ids_per_sol = {} if self.user_has_groups('hr_timesheet.group_hr_timesheet_user'): - timesheet_read_group = self.env['account.analytic.line']._read_group([('so_line', 'in', self.ids), ('project_id', '!=', False)], ['so_line', 'ids:array_agg(id)'], ['so_line']) - timesheet_ids_per_sol = {res['so_line'][0]: res['ids'] for res in timesheet_read_group} + timesheet_read_group = self.env['account.analytic.line']._read_group([('so_line', 'in', self.ids), ('project_id', '!=', False)], ['so_line'], ['id:array_agg']) + timesheet_ids_per_sol = {so_line.id: ids for so_line, ids in timesheet_read_group} for sol in self: timesheet_ids = timesheet_ids_per_sol.get(sol.id, []) if sol.is_service and len(timesheet_ids) > 0: diff --git a/addons/sale_timesheet/wizard/project_create_sale_order.py b/addons/sale_timesheet/wizard/project_create_sale_order.py index c13668b22d6..7a057b634a6 100644 --- a/addons/sale_timesheet/wizard/project_create_sale_order.py +++ b/addons/sale_timesheet/wizard/project_create_sale_order.py @@ -59,8 +59,8 @@ class ProjectCreateSalesOrder(models.TransientModel): def _compute_info_invoice(self): for line in self: domain = self.env['sale.order.line']._timesheet_compute_delivered_quantity_domain() - timesheet = self.env['account.analytic.line'].read_group(domain + [('task_id', 'in', line.project_id.tasks.ids), ('so_line', '=', False), ('timesheet_invoice_id', '=', False)], ['unit_amount'], ['task_id']) - unit_amount = round(sum(t.get('unit_amount', 0) for t in timesheet), 2) if timesheet else 0 + timesheet = self.env['account.analytic.line']._read_group(domain + [('task_id', 'in', line.project_id.tasks.ids), ('so_line', '=', False), ('timesheet_invoice_id', '=', False)], aggregates=['unit_amount:sum']) + [unit_amount] = timesheet[0] if not unit_amount: line.info_invoice = False continue @@ -68,7 +68,7 @@ class ProjectCreateSalesOrder(models.TransientModel): label = _("hours") if company_uom == self.env.ref('uom.product_uom_day'): label = _("days") - line.info_invoice = _("%(amount)s %(label)s will be added to the new Sales Order.", amount=unit_amount, label=label) + line.info_invoice = _("%(amount)s %(label)s will be added to the new Sales Order.", amount=round(unit_amount, 2), label=label) @api.onchange('partner_id') def _onchange_partner_id(self): diff --git a/addons/sale_timesheet_margin/models/sale_order_line.py b/addons/sale_timesheet_margin/models/sale_order_line.py index 09012c7b538..6ccfed4f2d2 100644 --- a/addons/sale_timesheet_margin/models/sale_order_line.py +++ b/addons/sale_timesheet_margin/models/sale_order_line.py @@ -12,13 +12,13 @@ class SaleOrderLine(models.Model): ) super(SaleOrderLine, self - timesheet_sols)._compute_purchase_price() if timesheet_sols: - group_amount = self.env['account.analytic.line'].read_group( + group_amount = self.env['account.analytic.line']._read_group( [('so_line', 'in', timesheet_sols.ids), ('project_id', '!=', False)], - ['so_line', 'amount:sum', 'unit_amount:sum'], - ['so_line']) + ['so_line'], + ['amount:sum', 'unit_amount:sum']) mapped_sol_timesheet_amount = { - amount['so_line'][0]: -amount['amount'] / amount['unit_amount'] if amount['unit_amount'] else 0.0 - for amount in group_amount + so_line.id: - amount_sum / unit_amount_sum if unit_amount_sum else 0.0 + for so_line, amount_sum, unit_amount_sum in group_amount } for line in timesheet_sols: line = line.with_company(line.company_id) diff --git a/addons/spreadsheet_account/models/account.py b/addons/spreadsheet_account/models/account.py index f9b8769d5af..43b6e52639e 100644 --- a/addons/spreadsheet_account/models/account.py +++ b/addons/spreadsheet_account/models/account.py @@ -136,8 +136,8 @@ class AccountMove(models.Model): ("account_type", "in", account_types), ("company_id", "=", self.env.company.id), ], - ["code:array_agg"], - ["account_type"], + ['account_type'], + ['code:array_agg'], ) - mapped = {group["account_type"]: group["code"] for group in data} + mapped = dict(data) return [mapped.get(account_type, []) for account_type in account_types] diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py index da82cebea50..b15eeba01fe 100644 --- a/addons/stock/models/product.py +++ b/addons/stock/models/product.py @@ -169,15 +169,15 @@ class Product(models.Model): Quant = self.env['stock.quant'].with_context(active_test=False) domain_move_in_todo = [('state', 'in', ('waiting', 'confirmed', 'assigned', 'partially_available'))] + domain_move_in domain_move_out_todo = [('state', 'in', ('waiting', 'confirmed', 'assigned', 'partially_available'))] + domain_move_out - moves_in_res = dict((item['product_id'][0], item['product_qty']) for item in Move._read_group(domain_move_in_todo, ['product_id', 'product_qty'], ['product_id'], orderby='id')) - moves_out_res = dict((item['product_id'][0], item['product_qty']) for item in Move._read_group(domain_move_out_todo, ['product_id', 'product_qty'], ['product_id'], orderby='id')) - quants_res = dict((item['product_id'][0], (item['quantity'], item['reserved_quantity'])) for item in Quant._read_group(domain_quant, ['product_id', 'quantity', 'reserved_quantity'], ['product_id'], orderby='id')) + moves_in_res = {product.id: product_qty for product, product_qty in Move._read_group(domain_move_in_todo, ['product_id'], ['product_qty:sum'])} + moves_out_res = {product.id: product_qty for product, product_qty in Move._read_group(domain_move_out_todo, ['product_id'], ['product_qty:sum'])} + quants_res = {product.id: (quantity, reserved_quantity) for product, quantity, reserved_quantity in Quant._read_group(domain_quant, ['product_id'], ['quantity:sum', 'reserved_quantity:sum'])} if dates_in_the_past: # Calculate the moves that were done before now to calculate back in time (as most questions will be recent ones) domain_move_in_done = [('state', '=', 'done'), ('date', '>', to_date)] + domain_move_in_done domain_move_out_done = [('state', '=', 'done'), ('date', '>', to_date)] + domain_move_out_done - moves_in_res_past = dict((item['product_id'][0], item['product_qty']) for item in Move._read_group(domain_move_in_done, ['product_id', 'product_qty'], ['product_id'], orderby='id')) - moves_out_res_past = dict((item['product_id'][0], item['product_qty']) for item in Move._read_group(domain_move_out_done, ['product_id', 'product_qty'], ['product_id'], orderby='id')) + moves_in_res_past = {product.id: product_qty for product, product_qty in Move._read_group(domain_move_in_done, ['product_id'], ['product_qty:sum'])} + moves_out_res_past = {product.id: product_qty for product, product_qty in Move._read_group(domain_move_out_done, ['product_id'], ['product_qty:sum'])} res = dict() for product in self.with_context(prefetch_fields=False): @@ -207,27 +207,23 @@ class Product(models.Model): return res def _compute_nbr_moves(self): - res = defaultdict(dict) - incoming_moves = self.env['stock.move.line'].read_group([ + incoming_moves = self.env['stock.move.line']._read_group([ ('product_id', 'in', self.ids), ('state', '=', 'done'), ('picking_code', '=', 'incoming'), ('date', '>=', fields.Datetime.now() - relativedelta(years=1)) - ], ['product_id'], ['product_id']) - outgoing_moves = self.env['stock.move.line'].read_group([ + ], ['product_id'], ['__count']) + outgoing_moves = self.env['stock.move.line']._read_group([ ('product_id', 'in', self.ids), ('state', '=', 'done'), ('picking_code', '=', 'outgoing'), ('date', '>=', fields.Datetime.now() - relativedelta(years=1)) - ], ['product_id'], ['product_id']) - for move in incoming_moves: - res[move['product_id'][0]]['moves_in'] = int(move['product_id_count']) - for move in outgoing_moves: - res[move['product_id'][0]]['moves_out'] = int(move['product_id_count']) + ], ['product_id'], ['__count']) + res_incoming = {product.id: count for product, count in incoming_moves} + res_outgoing = {product.id: count for product, count in outgoing_moves} for product in self: - product_res = res.get(product.id) or {} - product.nbr_moves_in = product_res.get('moves_in', 0) - product.nbr_moves_out = product_res.get('moves_out', 0) + product.nbr_moves_in = res_incoming.get(product.id, 0) + product.nbr_moves_out = res_outgoing.get(product.id, 0) def get_components(self): self.ensure_one() @@ -369,7 +365,7 @@ class Product(models.Model): domain_quant.append(('owner_id', '=', owner_id)) if package_id: domain_quant.append(('package_id', '=', package_id)) - quants_groupby = self.env['stock.quant']._read_group(domain_quant, ['product_id', 'quantity'], ['product_id'], orderby='id') + quants_groupby = self.env['stock.quant']._read_group(domain_quant, ['product_id'], ['quantity:sum']) # check if we need include zero values in result include_zero = ( @@ -379,11 +375,11 @@ class Product(models.Model): ) processed_product_ids = set() - for quant in quants_groupby: - product_id = quant['product_id'][0] + for product, quantity_sum in quants_groupby: + product_id = product.id if include_zero: processed_product_ids.add(product_id) - if OPERATORS[operator](quant['quantity'], value): + if OPERATORS[operator](quantity_sum, value): product_ids.add(product_id) if include_zero: @@ -396,20 +392,15 @@ class Product(models.Model): return list(product_ids) def _compute_nbr_reordering_rules(self): + self.nbr_reordering_rules = self.reordering_min_qty = self.reordering_max_qty = 0 read_group_res = self.env['stock.warehouse.orderpoint']._read_group( [('product_id', 'in', self.ids)], - ['product_id', 'product_min_qty', 'product_max_qty'], - ['product_id']) - res = {i: {} for i in self.ids} - for data in read_group_res: - res[data['product_id'][0]]['nbr_reordering_rules'] = int(data['product_id_count']) - res[data['product_id'][0]]['reordering_min_qty'] = data['product_min_qty'] - res[data['product_id'][0]]['reordering_max_qty'] = data['product_max_qty'] - for product in self: - product_res = res.get(product.id) or {} - product.nbr_reordering_rules = product_res.get('nbr_reordering_rules', 0) - product.reordering_min_qty = product_res.get('reordering_min_qty', 0) - product.reordering_max_qty = product_res.get('reordering_max_qty', 0) + ['product_id'], + ['__count', 'product_min_qty:sum', 'product_max_qty:sum']) + for product, count, product_min_qty_sum, product_max_qty_sum in read_group_res: + product.nbr_reordering_rules = count + product.reordering_min_qty = product_min_qty_sum + product.reordering_max_qty = product_max_qty_sum @api.onchange('tracking') def _onchange_tracking(self): @@ -602,16 +593,15 @@ class Product(models.Model): :rtype: defaultdict(float) """ domain_quant = expression.AND([self._get_domain_locations()[0], [('product_id', 'in', self.ids)]]) - quants_groupby = self.env['stock.quant']._read_group(domain_quant, ['product_id', 'quantity'], ['product_id'], orderby='id') + quants_groupby = self.env['stock.quant']._read_group(domain_quant, ['product_id'], ['quantity:sum']) currents = defaultdict(float) - for c in quants_groupby: - currents[c['product_id'][0]] = c['quantity'] + currents.update({product.id: quantity for product, quantity in quants_groupby}) return currents def _filter_to_unlink(self): domain = [('product_id', 'in', self.ids)] - lines = self.env['stock.lot']._read_group(domain, ['product_id'], ['product_id']) - linked_product_ids = [group['product_id'][0] for group in lines] + lines = self.env['stock.lot']._read_group(domain, ['product_id']) + linked_product_ids = [product.id for [product] in lines] return super(Product, self - self.browse(linked_product_ids))._filter_to_unlink() @api.model @@ -745,24 +735,22 @@ class ProductTemplate(models.Model): ('state', '=', 'done'), ('picking_code', '=', 'incoming'), ('date', '>=', fields.Datetime.now() - relativedelta(years=1)) - ], ['product_id'], ['product_id']) + ], ['product_id'], ['__count']) outgoing_moves = self.env['stock.move.line']._read_group([ ('product_id.product_tmpl_id', 'in', self.ids), ('state', '=', 'done'), ('picking_code', '=', 'outgoing'), ('date', '>=', fields.Datetime.now() - relativedelta(years=1)) - ], ['product_id'], ['product_id']) - for move in incoming_moves: - product = self.env['product.product'].browse([move['product_id'][0]]) + ], ['product_id'], ['__count']) + for product, count in incoming_moves: product_tmpl_id = product.product_tmpl_id.id - res[product_tmpl_id]['moves_in'] += int(move['product_id_count']) - for move in outgoing_moves: - product = self.env['product.product'].browse([move['product_id'][0]]) + res[product_tmpl_id]['moves_in'] += count + for product, count in outgoing_moves: product_tmpl_id = product.product_tmpl_id.id - res[product_tmpl_id]['moves_out'] += int(move['product_id_count']) + res[product_tmpl_id]['moves_out'] += count for template in self: - template.nbr_moves_in = int(res[template.id]['moves_in']) - template.nbr_moves_out = int(res[template.id]['moves_out']) + template.nbr_moves_in = res[template.id]['moves_in'] + template.nbr_moves_out = res[template.id]['moves_out'] @api.model def _get_action_view_related_putaway_rules(self, domain): @@ -796,13 +784,12 @@ class ProductTemplate(models.Model): def _compute_nbr_reordering_rules(self): res = {k: {'nbr_reordering_rules': 0, 'reordering_min_qty': 0, 'reordering_max_qty': 0} for k in self.ids} - product_data = self.env['stock.warehouse.orderpoint']._read_group([('product_id.product_tmpl_id', 'in', self.ids)], ['product_id', 'product_min_qty', 'product_max_qty'], ['product_id']) - for data in product_data: - product = self.env['product.product'].browse([data['product_id'][0]]) + product_data = self.env['stock.warehouse.orderpoint']._read_group([('product_id.product_tmpl_id', 'in', self.ids)], ['product_id'], ['__count', 'product_min_qty:sum', 'product_max_qty:sum']) + for product, count, product_min_qty, product_max_qty in product_data: product_tmpl_id = product.product_tmpl_id.id - res[product_tmpl_id]['nbr_reordering_rules'] += int(data['product_id_count']) - res[product_tmpl_id]['reordering_min_qty'] = data['product_min_qty'] - res[product_tmpl_id]['reordering_max_qty'] = data['product_max_qty'] + res[product_tmpl_id]['nbr_reordering_rules'] += count + res[product_tmpl_id]['reordering_min_qty'] = product_min_qty + res[product_tmpl_id]['reordering_max_qty'] = product_max_qty for template in self: if not template.id: template.nbr_reordering_rules = 0 diff --git a/addons/stock/models/stock_location.py b/addons/stock/models/stock_location.py index 860bd6584d0..93b7e4aa2ee 100644 --- a/addons/stock/models/stock_location.py +++ b/addons/stock/models/stock_location.py @@ -295,34 +295,32 @@ class Location(models.Model): ('id', 'not in', self._context.get('exclude_sml_ids', [])), ('result_package_id.package_type_id', '=', package_type.id), ('state', 'not in', ['draft', 'cancel', 'done']), - ], ['result_package_id:count_distinct'], ['location_dest_id']) + ], ['location_dest_id'], ['result_package_id:count_distinct']) quant_data = self.env['stock.quant']._read_group([ ('package_id.package_type_id', '=', package_type.id), ('location_id', 'in', locations.ids), - ], ['package_id:count_distinct'], ['location_id']) - for values in move_line_data: - qty_by_location[values['location_dest_id'][0]] = values['result_package_id'] - for values in quant_data: - qty_by_location[values['location_id'][0]] += values['package_id'] + ], ['location_id'], ['package_id:count_distinct']) + qty_by_location.update({location_dest.id: count for location_dest, count in move_line_data}) + for location, count in quant_data: + qty_by_location[location.id] += count else: move_line_data = self.env['stock.move.line']._read_group([ ('id', 'not in', self._context.get('exclude_sml_ids', [])), ('product_id', '=', product.id), ('location_dest_id', 'in', locations.ids), ('state', 'not in', ['draft', 'done', 'cancel']) - ], ['location_dest_id', 'product_id', 'reserved_qty:array_agg', 'qty_done:array_agg', 'product_uom_id:array_agg'], ['location_dest_id']) + ], ['location_dest_id'], ['reserved_qty:array_agg', 'qty_done:array_agg', 'product_uom_id:recordset']) quant_data = self.env['stock.quant']._read_group([ ('product_id', '=', product.id), ('location_id', 'in', locations.ids), - ], ['location_id', 'product_id', 'quantity:sum'], ['location_id']) + ], ['location_id'], ['quantity:sum']) - for values in move_line_data: - uoms = self.env['uom.uom'].browse(values['product_uom_id']) + qty_by_location.update({location.id: quantity_sum for location, quantity_sum in quant_data}) + for location_dest, reserved_qty_list, qty_done_list, uoms in move_line_data: qty_done = sum(max(ml_uom._compute_quantity(float(qty), product.uom_id), float(qty_reserved)) - for qty_reserved, qty, ml_uom in zip(values['reserved_qty'], values['qty_done'], list(uoms))) - qty_by_location[values['location_dest_id'][0]] = qty_done - for values in quant_data: - qty_by_location[values['location_id'][0]] += values['quantity'] + for qty_reserved, qty, ml_uom in zip(reserved_qty_list, qty_done_list, uoms)) + qty_by_location[location_dest.id] += qty_done + if additional_qty: for location_id, qty in additional_qty.items(): qty_by_location[location_id] += qty diff --git a/addons/stock/models/stock_lot.py b/addons/stock/models/stock_lot.py index 7edcb7e15cd..cc1e2ea98b0 100644 --- a/addons/stock/models/stock_lot.py +++ b/addons/stock/models/stock_lot.py @@ -76,14 +76,11 @@ class StockLot(models.Model): domain = [('product_id', 'in', self.product_id.ids), ('company_id', 'in', self.company_id.ids), ('name', 'in', self.mapped('name'))] - fields = ['company_id', 'product_id', 'name'] groupby = ['company_id', 'product_id', 'name'] - records = self._read_group(domain, fields, groupby, lazy=False) + records = self._read_group(domain, groupby, having=[('__count', '>', 1)]) error_message_lines = [] - for rec in records: - if rec['__count'] != 1: - product_name = self.env['product.product'].browse(rec['product_id'][0]).display_name - error_message_lines.append(_(" - Product: %s, Serial Number: %s", product_name, rec['name'])) + for __, product, name in records: + error_message_lines.append(_(" - Product: %s, Serial Number: %s", product.display_name, name)) if error_message_lines: raise ValidationError(_('The combination of serial number and product must be unique across a company.\nFollowing combination contains duplicates:\n') + '\n'.join(error_message_lines)) diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 62ffd9e54ff..ae1f58ff8e9 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -354,20 +354,15 @@ class StockMove(models.Model): data = self.env['stock.move.line']._read_group( [('id', 'in', list(move_lines_ids))], - ['move_id', 'product_uom_id', 'qty_done'], ['move_id', 'product_uom_id'], - lazy=False + ['move_id', 'product_uom_id'], ['qty_done:sum'] ) - - rec = defaultdict(list) - for d in data: - rec[d['move_id'][0]] += [(d['product_uom_id'][0], d['qty_done'])] + sum_qty_done = defaultdict(float) + for move, product_uom, qty_done_sum in data: + uom = move.product_uom + sum_qty_done[move.id] += product_uom._compute_quantity(qty_done_sum, uom, round=False) for move in self: - uom = move.product_uom - move.quantity_done = sum( - self.env['uom.uom'].browse(line_uom_id)._compute_quantity(qty, uom, round=False) - for line_uom_id, qty in rec.get(move.ids[0] if move.ids else move.id, []) - ) + move.quantity_done = sum_qty_done[move.id] def _quantity_done_set(self): def _process_decrease(move, quantity): @@ -454,8 +449,8 @@ Please change the quantity done or the rounding precision of your unit of measur reserved_availability, move.product_uom, rounding_method='HALF-UP') else: # compute - result = {data['move_id'][0]: data['reserved_qty'] for data in - self.env['stock.move.line']._read_group([('move_id', 'in', self.ids)], ['move_id', 'reserved_qty'], ['move_id'])} + result = {move.id: reserved_qty for move, reserved_qty in + self.env['stock.move.line']._read_group([('move_id', 'in', self.ids)], ['move_id'], ['reserved_qty:sum'])} for move in self: move.reserved_availability = move.product_id.uom_id._compute_quantity( result.get(move.id, 0.0), move.product_uom, rounding_method='HALF-UP') @@ -559,9 +554,9 @@ Please change the quantity done or the rounding precision of your unit of measur for domain in [domain_nosuggest, domain_suggest]: lots_by_move_id = self.env['stock.move.line']._read_group( domain, - ['move_id', 'lot_ids:array_agg(lot_id)'], ['move_id'], + ['move_id'], ['lot_id:array_agg'], ) - lots_by_move_id_list.append({by_move['move_id'][0]: by_move['lot_ids'] for by_move in lots_by_move_id}) + lots_by_move_id_list.append({move.id: lot_ids for move, lot_ids in lots_by_move_id}) for move in self: move.lot_ids = lots_by_move_id_list[0 if move.picking_type_id.show_reserved else 1].get(move._origin.id, []) diff --git a/addons/stock/models/stock_orderpoint.py b/addons/stock/models/stock_orderpoint.py index 8c19a285991..322da9211ec 100644 --- a/addons/stock/models/stock_orderpoint.py +++ b/addons/stock/models/stock_orderpoint.py @@ -293,12 +293,10 @@ class StockWarehouseOrderpoint(models.Model): ('location_dest_id', 'in', self.location_id.ids), ('action', 'in', ['pull_push', 'pull']), ('route_id.active', '!=', False) - ], ['location_dest_id', 'route_id'], ['location_dest_id', 'route_id'], lazy=False) - for g in rules_groups: - if not g.get('route_id'): - continue - orderpoints = self.filtered(lambda o: o.location_id.id == g['location_dest_id'][0]) - orderpoints.route_id = g['route_id'] + ], ['location_dest_id', 'route_id']) + for location_dest, route in rules_groups: + orderpoints = self.filtered(lambda o: o.location_id.id == location_dest.id) + orderpoints.route_id = route def _get_lead_days_values(self): self.ensure_one() @@ -374,11 +372,11 @@ class StockWarehouseOrderpoint(models.Model): # Group orderpoint by product-location orderpoint_by_product_location = self.env['stock.warehouse.orderpoint']._read_group( [('id', 'in', orderpoints.ids)], - ['product_id', 'location_id', 'qty_to_order:sum'], - ['product_id', 'location_id'], lazy=False) + ['product_id', 'location_id'], + ['qty_to_order:sum']) orderpoint_by_product_location = { - (record.get('product_id')[0], record.get('location_id')[0]): record.get('qty_to_order') - for record in orderpoint_by_product_location + (product.id, location.id): qty_to_order_sum + for product, location, qty_to_order_sum in orderpoint_by_product_location } for (product, location), product_qty in to_refill.items(): qty_in_progress = qty_by_product_loc.get((product, location)) or 0.0 @@ -393,18 +391,18 @@ class StockWarehouseOrderpoint(models.Model): # With archived ones to avoid `product_location_check` SQL constraints orderpoint_by_product_location = self.env['stock.warehouse.orderpoint'].with_context(active_test=False)._read_group( [('id', 'in', orderpoints.ids)], - ['product_id', 'location_id', 'ids:array_agg(id)'], - ['product_id', 'location_id'], lazy=False) + ['product_id', 'location_id'], + ['id:recordset']) orderpoint_by_product_location = { - (record.get('product_id')[0], record.get('location_id')[0]): record.get('ids')[0] - for record in orderpoint_by_product_location + (product.id, location.id): orderpoint + for product, location, orderpoint in orderpoint_by_product_location } orderpoint_values_list = [] for (product, location_id), product_qty in to_refill.items(): - orderpoint_id = orderpoint_by_product_location.get((product, location_id)) - if orderpoint_id: - self.env['stock.warehouse.orderpoint'].browse(orderpoint_id).qty_forecast += product_qty + orderpoint = orderpoint_by_product_location.get((product, location_id)) + if orderpoint: + orderpoint.qty_forecast += product_qty else: orderpoint_values = self.env['stock.warehouse.orderpoint']._get_orderpoint_values(product, location_id) warehouse_id = self.env['stock.location'].browse(location_id).warehouse_id diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py index ab7ca0f379f..cbe9c28fece 100644 --- a/addons/stock/models/stock_picking.py +++ b/addons/stock/models/stock_picking.py @@ -152,16 +152,13 @@ class PickingType(models.Model): 'count_picking_late': [('scheduled_date', '<', time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)), ('state', 'in', ('assigned', 'waiting', 'confirmed'))], 'count_picking_backorders': [('backorder_id', '!=', False), ('state', 'in', ('confirmed', 'assigned', 'waiting'))], } - for field in domains: - data = self.env['stock.picking']._read_group(domains[field] + + for field_name, domain in domains.items(): + data = self.env['stock.picking']._read_group(domain + [('state', 'not in', ('done', 'cancel')), ('picking_type_id', 'in', self.ids)], - ['picking_type_id'], ['picking_type_id']) - count = { - x['picking_type_id'][0]: x['picking_type_id_count'] - for x in data if x['picking_type_id'] - } + ['picking_type_id'], ['__count']) + count = {picking_type.id: count for picking_type, count in data} for record in self: - record[field] = count.get(record.id, 0) + record[field_name] = count.get(record.id, 0) def name_get(self): """ Display 'Warehouse_name: PickingType_name' """ @@ -474,8 +471,8 @@ class Picking(models.Model): @api.depends('move_ids.delay_alert_date') def _compute_delay_alert_date(self): - delay_alert_date_data = self.env['stock.move']._read_group([('id', 'in', self.move_ids.ids), ('delay_alert_date', '!=', False)], ['delay_alert_date:max'], 'picking_id') - delay_alert_date_data = {data['picking_id'][0]: data['delay_alert_date'] for data in delay_alert_date_data} + delay_alert_date_data = self.env['stock.move']._read_group([('id', 'in', self.move_ids.ids), ('delay_alert_date', '!=', False)], ['picking_id'], ['delay_alert_date:max']) + delay_alert_date_data = {picking.id: delay_alert_date for picking, delay_alert_date in delay_alert_date_data} for picking in self: picking.delay_alert_date = delay_alert_date_data.get(picking.id, False) @@ -631,8 +628,8 @@ class Picking(models.Model): def _compute_has_packages(self): domain = [('picking_id', 'in', self.ids), ('result_package_id', '!=', False)] - cnt_by_picking = self.env['stock.move.line']._read_group(domain, ['picking_id'], ['picking_id']) - cnt_by_picking = {d['picking_id'][0]: d['picking_id_count'] for d in cnt_by_picking} + cnt_by_picking = self.env['stock.move.line']._read_group(domain, ['picking_id'], ['__count']) + cnt_by_picking = {picking.id: count for picking, count in cnt_by_picking} for picking in self: picking.has_packages = bool(cnt_by_picking.get(picking.id, False)) diff --git a/addons/stock/models/stock_quant.py b/addons/stock/models/stock_quant.py index 74d694635d9..99f48e4feb7 100644 --- a/addons/stock/models/stock_quant.py +++ b/addons/stock/models/stock_quant.py @@ -161,9 +161,8 @@ class StockQuant(models.Model): ('package_id', 'in', self.package_id.ids), ('result_package_id', 'in', self.package_id.ids), ], - ['date:max', 'product_id', 'lot_id', 'package_id', 'owner_id', 'result_package_id', 'location_id', 'location_dest_id'], ['product_id', 'lot_id', 'package_id', 'owner_id', 'result_package_id', 'location_id', 'location_dest_id'], - lazy=False) + ['date:max']) def _update_dict(date_by_quant, key, value): current_date = date_by_quant.get(key) @@ -171,15 +170,14 @@ class StockQuant(models.Model): date_by_quant[key] = value date_by_quant = {} - for group in groups: - move_line_date = group['date'] - location_id = group['location_id'][0] - location_dest_id = group['location_dest_id'][0] - package_id = group['package_id'] and group['package_id'][0] - result_package_id = group['result_package_id'] and group['result_package_id'][0] - lot_id = group['lot_id'] and group['lot_id'][0] - owner_id = group['owner_id'] and group['owner_id'][0] - product_id = group['product_id'][0] + for product, lot, package, owner, result_package, location, location_dest, move_line_date in groups: + location_id = location.id + location_dest_id = location_dest.id + package_id = package.id + result_package_id = result_package.id + lot_id = lot.id + owner_id = owner.id + product_id = product.id _update_dict(date_by_quant, (location_id, package_id, product_id, lot_id, owner_id), move_line_date) _update_dict(date_by_quant, (location_dest_id, package_id, product_id, lot_id, owner_id), move_line_date) _update_dict(date_by_quant, (location_id, result_package_id, product_id, lot_id, owner_id), move_line_date) @@ -212,8 +210,8 @@ class StockQuant(models.Model): def _compute_sn_duplicated(self): self.sn_duplicated = False domain = [('tracking', '=', 'serial'), ('lot_id', 'in', self.lot_id.ids), ('location_id.usage', 'in', ['internal', 'transit'])] - results = self._read_group(domain, ['lot_id'], ['lot_id']) - duplicated_sn_ids = [x['lot_id'][0] for x in results if x['lot_id_count'] > 1] + results = self._read_group(domain, ['lot_id'], having=[('__count', '>', 1)]) + duplicated_sn_ids = [lot.id for [lot] in results] quants_with_duplicated_sn = self.env['stock.quant'].search([('lot_id', 'in', duplicated_sn_ids)]) quants_with_duplicated_sn.sn_duplicated = True diff --git a/addons/stock/models/stock_scrap.py b/addons/stock/models/stock_scrap.py index 9d00bf2c510..c2719d63f14 100644 --- a/addons/stock/models/stock_scrap.py +++ b/addons/stock/models/stock_scrap.py @@ -61,10 +61,10 @@ class StockScrap(models.Model): @api.depends('company_id', 'picking_id') def _compute_location_id(self): groups = self.env['stock.warehouse']._read_group( - [('company_id', 'in', self.company_id.ids)], ['min_id:min(id)'], ['company_id']) + [('company_id', 'in', self.company_id.ids)], ['company_id'], ['lot_stock_id:array_agg']) locations_per_company = { - group['company_id'][0]: self.env['stock.warehouse'].browse(group['min_id']).lot_stock_id - for group in groups + company.id: lot_stock_ids[0] if lot_stock_ids else False + for company, lot_stock_ids in groups } for scrap in self: if scrap.picking_id: @@ -75,10 +75,10 @@ class StockScrap(models.Model): @api.depends('company_id') def _compute_scrap_location_id(self): groups = self.env['stock.location']._read_group( - [('company_id', 'in', self.company_id.ids), ('scrap_location', '=', True)], ['min_id:min(id)'], ['company_id']) + [('company_id', 'in', self.company_id.ids), ('scrap_location', '=', True)], ['company_id'], ['id:min']) locations_per_company = { - group['company_id'][0]: self.env['stock.location'].browse(group['min_id']) - for group in groups + company.id: stock_warehouse_id + for company, stock_warehouse_id in groups } for scrap in self: scrap.scrap_location_id = locations_per_company[scrap.company_id.id] diff --git a/addons/stock/models/stock_warehouse.py b/addons/stock/models/stock_warehouse.py index 1a3c89b2a1b..2f94eb59b1a 100644 --- a/addons/stock/models/stock_warehouse.py +++ b/addons/stock/models/stock_warehouse.py @@ -285,16 +285,16 @@ class Warehouse(models.Model): return res def _check_multiwarehouse_group(self): - cnt_by_company = self.env['stock.warehouse'].sudo()._read_group([('active', '=', True)], ['company_id'], groupby=['company_id']) + cnt_by_company = self.env['stock.warehouse'].sudo()._read_group([('active', '=', True)], ['company_id'], aggregates=['__count']) if cnt_by_company: - max_cnt = max(cnt_by_company, key=lambda k: k['company_id_count']) + max_count = max(count for company, count in cnt_by_company) group_user = self.env.ref('base.group_user') group_stock_multi_warehouses = self.env.ref('stock.group_stock_multi_warehouses') group_stock_multi_locations = self.env.ref('stock.group_stock_multi_locations') - if max_cnt['company_id_count'] <= 1 and group_stock_multi_warehouses in group_user.implied_ids: + if max_count <= 1 and group_stock_multi_warehouses in group_user.implied_ids: group_user.write({'implied_ids': [(3, group_stock_multi_warehouses.id)]}) group_stock_multi_warehouses.write({'users': [(3, user.id) for user in group_user.users]}) - if max_cnt['company_id_count'] > 1 and group_stock_multi_warehouses not in group_user.implied_ids: + if max_count > 1 and group_stock_multi_warehouses not in group_user.implied_ids: if group_stock_multi_locations not in group_user.implied_ids: self.env['res.config.settings'].create({ 'group_stock_multi_locations': True, diff --git a/addons/stock/report/stock_forecasted.py b/addons/stock/report/stock_forecasted.py index dab2705aa29..e8da19730a5 100644 --- a/addons/stock/report/stock_forecasted.py +++ b/addons/stock/report/stock_forecasted.py @@ -84,10 +84,8 @@ class StockForecasted(models.AbstractModel): res['outgoing_qty'] = sum(products.mapped('outgoing_qty')) in_domain, out_domain = self._move_draft_domain(product_template_ids, product_ids, wh_location_ids) - incoming_moves = self.env['stock.move']._read_group(in_domain, ['product_qty:sum'], 'product_id') - outgoing_moves = self.env['stock.move']._read_group(out_domain, ['product_qty:sum'], 'product_id') - in_sum = sum(move['product_qty'] for move in incoming_moves) - out_sum = sum(move['product_qty'] for move in outgoing_moves) + [in_sum] = self.env['stock.move']._read_group(in_domain, aggregates=['product_qty:sum'])[0] + [out_sum] = self.env['stock.move']._read_group(out_domain, aggregates=['product_qty:sum'])[0] res.update({ 'draft_picking_qty': { @@ -285,15 +283,15 @@ class StockForecasted(models.AbstractModel): }) qties = self.env['stock.quant']._read_group([('location_id', 'in', wh_location_ids), ('quantity', '>', 0), ('product_id', 'in', outs.product_id.ids)], - ['product_id', 'location_id', 'quantity:sum', 'reserved_quantity:sum'], ['product_id', 'location_id'], lazy=False) + ['product_id', 'location_id'], ['quantity:sum']) wh_stock_sub_location_ids = wh_stock_location.search([('id', 'child_of', wh_stock_location.id)]).ids currents = defaultdict(float) - for qty in qties: - location_id = qty['location_id'][0] + for product, location, quantity in qties: + location_id = location.id # any sublocation qties will be added to the main stock location qty if location_id in wh_stock_sub_location_ids: location_id = wh_stock_location.id - currents[(qty['product_id'][0], location_id)] += qty['quantity'] + currents[(product.id, location_id)] += quantity moves_data = {} for _, out_moves in outs_per_product.items(): # to handle multiple out wtih same in (ex: same pick/pack for 2 outs) diff --git a/addons/stock/tests/test_report.py b/addons/stock/tests/test_report.py index 01cca185b15..aa9d5dcb541 100644 --- a/addons/stock/tests/test_report.py +++ b/addons/stock/tests/test_report.py @@ -79,18 +79,18 @@ class TestReports(TestReportsCommon): 'inventory_quantity': 50 }).action_apply_inventory() self.env.flush_all() - report_records_today = self.env['report.stock.quantity'].read_group( + report_records_today = self.env['report.stock.quantity']._read_group( [('product_id', '=', product.id), ('date', '=', date.today())], - ['product_qty'], [], lazy=False) - report_records_tomorrow = self.env['report.stock.quantity'].read_group( + [], ['product_qty:sum']) + report_records_tomorrow = self.env['report.stock.quantity']._read_group( [('product_id', '=', product.id), ('date', '=', date.today() + timedelta(days=1))], - ['product_qty'], []) - report_records_yesterday = self.env['report.stock.quantity'].read_group( + [], ['product_qty:sum']) + report_records_yesterday = self.env['report.stock.quantity']._read_group( [('product_id', '=', product.id), ('date', '=', date.today() - timedelta(days=1))], - ['product_qty'], []) - self.assertEqual(sum([r['product_qty'] for r in report_records_today]), 50.0) - self.assertEqual(sum([r['product_qty'] for r in report_records_tomorrow]), 50.0) - self.assertEqual(sum([r['product_qty'] for r in report_records_yesterday]), 0.0) + [], ['product_qty:sum']) + self.assertEqual(report_records_today[0][0], 50.0) + self.assertEqual(report_records_tomorrow[0][0], 50.0) + self.assertEqual(report_records_yesterday[0][0], 0.0) # Delivery of 20.0 units tomorrow move_out = self.env['stock.move'].create({ @@ -103,21 +103,21 @@ class TestReports(TestReportsCommon): 'product_uom_qty': 20.0, }) self.env.flush_all() - report_records_tomorrow = self.env['report.stock.quantity'].read_group( + report_records_tomorrow = self.env['report.stock.quantity']._read_group( [('product_id', '=', product.id), ('date', '=', date.today() + timedelta(days=1))], - ['product_qty'], []) - self.assertEqual(sum([r['product_qty'] for r in report_records_tomorrow]), 50.0) + [], ['product_qty:sum']) + self.assertEqual(report_records_tomorrow[0][0], 50.0) move_out._action_confirm() self.env.flush_all() - report_records_tomorrow = self.env['report.stock.quantity'].read_group( + report_records_tomorrow = self.env['report.stock.quantity']._read_group( [('product_id', '=', product.id), ('date', '=', date.today() + timedelta(days=1))], - ['product_qty', 'state'], ['state'], lazy=False) - self.assertEqual(sum([r['product_qty'] for r in report_records_tomorrow if r['state'] == 'forecast']), 30.0) - self.assertEqual(sum([r['product_qty'] for r in report_records_tomorrow if r['state'] == 'out']), -20.0) - report_records_today = self.env['report.stock.quantity'].read_group( + ['state'], ['product_qty:sum']) + self.assertEqual(sum(product_qty for state, product_qty in report_records_tomorrow if state == 'forecast'), 30.0) + self.assertEqual(sum(product_qty for state, product_qty in report_records_tomorrow if state == 'out'), -20.0) + report_records_today = self.env['report.stock.quantity']._read_group( [('product_id', '=', product.id), ('date', '=', date.today())], - ['product_qty', 'state'], ['state'], lazy=False) - self.assertEqual(sum([r['product_qty'] for r in report_records_today if r['state'] == 'forecast']), 50.0) + ['state'], ['product_qty:sum']) + self.assertEqual(sum(product_qty for state, product_qty in report_records_today if state == 'forecast'), 50.0) # Receipt of 10.0 units tomorrow move_in = self.env['stock.move'].create({ @@ -131,16 +131,16 @@ class TestReports(TestReportsCommon): }) move_in._action_confirm() self.env.flush_all() - report_records_tomorrow = self.env['report.stock.quantity'].read_group( + report_records_tomorrow = self.env['report.stock.quantity']._read_group( [('product_id', '=', product.id), ('date', '=', date.today() + timedelta(days=1))], - ['product_qty', 'state'], ['state'], lazy=False) - self.assertEqual(sum([r['product_qty'] for r in report_records_tomorrow if r['state'] == 'forecast']), 40.0) - self.assertEqual(sum([r['product_qty'] for r in report_records_tomorrow if r['state'] == 'out']), -20.0) - self.assertEqual(sum([r['product_qty'] for r in report_records_tomorrow if r['state'] == 'in']), 10.0) - report_records_today = self.env['report.stock.quantity'].read_group( + ['state'], ['product_qty:sum']) + self.assertEqual(sum(product_qty for state, product_qty in report_records_tomorrow if state == 'forecast'), 40.0) + self.assertEqual(sum(product_qty for state, product_qty in report_records_tomorrow if state == 'out'), -20.0) + self.assertEqual(sum(product_qty for state, product_qty in report_records_tomorrow if state == 'in'), 10.0) + report_records_today = self.env['report.stock.quantity']._read_group( [('product_id', '=', product.id), ('date', '=', date.today())], - ['product_qty', 'state'], ['state'], lazy=False) - self.assertEqual(sum([r['product_qty'] for r in report_records_today if r['state'] == 'forecast']), 50.0) + ['state'], ['product_qty:sum']) + self.assertEqual(sum(product_qty for state, product_qty in report_records_today if state == 'forecast'), 50.0) # Delivery of 20.0 units tomorrow move_out = self.env['stock.move'].create({ @@ -154,27 +154,27 @@ class TestReports(TestReportsCommon): }) move_out._action_confirm() self.env.flush_all() - report_records_today = self.env['report.stock.quantity'].read_group( + report_records_today = self.env['report.stock.quantity']._read_group( [('product_id', '=', product.id), ('date', '=', date.today())], - ['product_qty', 'state'], ['state'], lazy=False) - report_records_tomorrow = self.env['report.stock.quantity'].read_group( + ['state'], ['product_qty:sum']) + report_records_tomorrow = self.env['report.stock.quantity']._read_group( [('product_id', '=', product.id), ('date', '=', date.today() + timedelta(days=1))], - ['product_qty', 'state'], ['state'], lazy=False) - report_records_yesterday = self.env['report.stock.quantity'].read_group( + ['state'], ['product_qty:sum']) + report_records_yesterday = self.env['report.stock.quantity']._read_group( [('product_id', '=', product.id), ('date', '=', date.today() - timedelta(days=1))], - ['product_qty', 'state'], ['state'], lazy=False) + ['state'], ['product_qty:sum']) - self.assertEqual(sum([r['product_qty'] for r in report_records_yesterday if r['state'] == 'forecast']), -30.0) - self.assertEqual(sum([r['product_qty'] for r in report_records_yesterday if r['state'] == 'out']), -30.0) - self.assertEqual(sum([r['product_qty'] for r in report_records_yesterday if r['state'] == 'in']), 0.0) + self.assertEqual(sum(product_qty for state, product_qty in report_records_yesterday if state == 'forecast'), -30.0) + self.assertEqual(sum(product_qty for state, product_qty in report_records_yesterday if state == 'out'), -30.0) + self.assertEqual(sum(product_qty for state, product_qty in report_records_yesterday if state == 'in'), 0.0) - self.assertEqual(sum([r['product_qty'] for r in report_records_today if r['state'] == 'forecast']), 20.0) - self.assertEqual(sum([r['product_qty'] for r in report_records_today if r['state'] == 'out']), 0.0) - self.assertEqual(sum([r['product_qty'] for r in report_records_today if r['state'] == 'in']), 0.0) + self.assertEqual(sum(product_qty for state, product_qty in report_records_today if state == 'forecast'), 20.0) + self.assertEqual(sum(product_qty for state, product_qty in report_records_today if state == 'out'), 0.0) + self.assertEqual(sum(product_qty for state, product_qty in report_records_today if state == 'in'), 0.0) - self.assertEqual(sum([r['product_qty'] for r in report_records_tomorrow if r['state'] == 'forecast']), 10.0) - self.assertEqual(sum([r['product_qty'] for r in report_records_tomorrow if r['state'] == 'out']), -20.0) - self.assertEqual(sum([r['product_qty'] for r in report_records_tomorrow if r['state'] == 'in']), 10.0) + self.assertEqual(sum(product_qty for state, product_qty in report_records_tomorrow if state == 'forecast'), 10.0) + self.assertEqual(sum(product_qty for state, product_qty in report_records_tomorrow if state == 'out'), -20.0) + self.assertEqual(sum(product_qty for state, product_qty in report_records_tomorrow if state == 'in'), 10.0) def test_report_quantity_2(self): """ Not supported case. @@ -215,14 +215,14 @@ class TestReports(TestReportsCommon): }) move._action_confirm() self.env.flush_all() - report_records = self.env['report.stock.quantity'].read_group( + report_records = self.env['report.stock.quantity']._read_group( [('product_id', '=', product.id), ('date', '=', date.today()), ('warehouse_id', '!=', False)], - ['product_qty', 'state'], ['state'], lazy=False) - self.assertEqual(sum([r['product_qty'] for r in report_records if r['state'] == 'forecast']), 40.0) - report_records = self.env['report.stock.quantity'].read_group( + ['state'], ['product_qty:sum']) + self.assertEqual(sum(product_qty for state, product_qty in report_records if state == 'forecast'), 40.0) + report_records = self.env['report.stock.quantity']._read_group( [('product_id', '=', product.id), ('date', '=', date.today())], - ['product_qty', 'state'], ['state'], lazy=False) - self.assertEqual(sum([r['product_qty'] for r in report_records if r['state'] == 'forecast']), 40.0) + ['state'], ['product_qty:sum']) + self.assertEqual(sum(product_qty for state, product_qty in report_records if state == 'forecast'), 40.0) move = self.env['stock.move'].create({ 'name': 'Move outside warehouse', 'location_id': stock_without_wh.id, @@ -233,10 +233,10 @@ class TestReports(TestReportsCommon): }) move._action_confirm() self.env.flush_all() - report_records = self.env['report.stock.quantity'].read_group( + report_records = self.env['report.stock.quantity']._read_group( [('product_id', '=', product.id), ('date', '=', date.today())], - ['product_qty', 'state'], ['state'], lazy=False) - self.assertEqual(sum([r['product_qty'] for r in report_records if r['state'] == 'forecast']), 40.0) + ['state'], ['product_qty:sum']) + self.assertEqual(sum(product_qty for state, product_qty in report_records if state == 'forecast'), 40.0) def test_report_quantity_3(self): product_form = Form(self.env['product.product']) @@ -257,10 +257,10 @@ class TestReports(TestReportsCommon): }) self.env.flush_all() - report_records = self.env['report.stock.quantity'].read_group( + report_records = self.env['report.stock.quantity']._read_group( [('product_id', '=', product.id), ('date', '=', date.today())], - ['product_qty'], [], lazy=False) - self.assertEqual(sum([r['product_qty'] for r in report_records if r['product_qty']]), 0.0) + [], ['product_qty:sum']) + self.assertEqual(report_records[0][0], 0.0) # Receipt of 20.0 units tomorrow move_in = self.env['stock.move'].create({ @@ -276,10 +276,10 @@ class TestReports(TestReportsCommon): move_in.move_line_ids.qty_done = 20.0 move_in._action_done() self.env.flush_all() - report_records = self.env['report.stock.quantity'].read_group( + report_records = self.env['report.stock.quantity']._read_group( [('product_id', '=', product.id), ('date', '=', date.today())], - ['product_qty'], [], lazy=False) - self.assertEqual(sum([r['product_qty'] for r in report_records]), 20.0) + [], ['product_qty:sum']) + self.assertEqual(report_records[0][0], 20.0) # Delivery of 10.0 units tomorrow move_out = self.env['stock.move'].create({ @@ -295,10 +295,10 @@ class TestReports(TestReportsCommon): move_out.move_line_ids.qty_done = 10.0 move_out._action_done() self.env.flush_all() - report_records = self.env['report.stock.quantity'].read_group( + report_records = self.env['report.stock.quantity']._read_group( [('product_id', '=', product.id), ('date', '=', date.today())], - ['product_qty'], [], lazy=False) - self.assertEqual(sum([r['product_qty'] for r in report_records]), 10.0) + [], ['product_qty:sum']) + self.assertEqual(report_records[0][0], 10.0) def test_report_forecast_1(self): """ Checks report data for product is empty. Then creates and process diff --git a/addons/stock/tests/test_report_stock_quantity.py b/addons/stock/tests/test_report_stock_quantity.py index 14881e4db69..5b7513e7f46 100644 --- a/addons/stock/tests/test_report_stock_quantity.py +++ b/addons/stock/tests/test_report_stock_quantity.py @@ -60,12 +60,11 @@ class TestReportStockQuantity(tests.TransactionCase): def test_report_stock_quantity(self): from_date = fields.Date.to_string(fields.Date.add(fields.Date.today(), days=-1)) to_date = fields.Date.to_string(fields.Date.add(fields.Date.today(), days=4)) - report = self.env['report.stock.quantity'].read_group( + report = self.env['report.stock.quantity']._read_group( [('date', '>=', from_date), ('date', '<=', to_date), ('product_id', '=', self.product1.id)], - ['product_qty', 'date', 'product_id', 'state'], ['date:day', 'product_id', 'state'], - lazy=False) - forecast_report = [x['product_qty'] for x in report if x['state'] == 'forecast'] + ['product_qty:sum']) + forecast_report = [qty for __, __, state, qty in report if state == 'forecast'] self.assertEqual(forecast_report, [0, 100, 100, 100, -20, -20]) def test_report_stock_quantity_stansit(self): @@ -96,26 +95,24 @@ class TestReportStockQuantity(tests.TransactionCase): }) self.env.flush_all() - report = self.env['report.stock.quantity'].read_group( + report = self.env['report.stock.quantity']._read_group( [('date', '>=', fields.Date.today()), ('date', '<=', fields.Date.today()), ('product_id', '=', self.product1.id)], - ['product_qty', 'date', 'product_id', 'state'], ['date:day', 'product_id', 'state'], - lazy=False) + ['product_qty:sum']) - forecast_in_report = [x['product_qty'] for x in report if x['state'] == 'in'] + forecast_in_report = [qty for __, __, state, qty in report if state == 'in'] self.assertEqual(forecast_in_report, [25]) - forecast_out_report = [x['product_qty'] for x in report if x['state'] == 'out'] + forecast_out_report = [qty for __, __, state, qty in report if state == 'out'] self.assertEqual(forecast_out_report, [-25]) def test_report_stock_quantity_with_product_qty_filter(self): from_date = fields.Date.to_string(fields.Date.add(fields.Date.today(), days=-1)) to_date = fields.Date.to_string(fields.Date.add(fields.Date.today(), days=4)) - report = self.env['report.stock.quantity'].read_group( + report = self.env['report.stock.quantity']._read_group( [('product_qty', '<', 0), ('date', '>=', from_date), ('date', '<=', to_date), ('product_id', '=', self.product1.id)], - ['product_qty', 'date', 'product_id', 'state'], ['date:day', 'product_id', 'state'], - lazy=False) - forecast_report = [x['product_qty'] for x in report if x['state'] == 'forecast'] + ['product_qty:sum']) + forecast_report = [qty for __, __, state, qty in report if state == 'forecast'] self.assertEqual(forecast_report, [-20, -20]) def test_replenishment_report_1(self): @@ -225,15 +222,13 @@ class TestReportStockQuantity(tests.TransactionCase): self.env.flush_all() - data = self.env['report.stock.quantity'].read_group( + data = self.env['report.stock.quantity']._read_group( [('state', '=', 'forecast'), ('product_id', '=', product.id), ('date', '>=', two_days_ago), ('date', '<=', in_two_days)], - ['product_qty', 'date', 'warehouse_id'], ['date:day', 'warehouse_id'], - orderby='date, warehouse_id', - lazy=False, + ['product_qty:sum'], ) - for row, qty in zip(data, [ + for (date_day, warehouse, qty_rd), qty in zip(data, [ # wh01_qty, wh02_qty 3.0, 0.0, # two days ago 3.0, 0.0, @@ -241,4 +236,4 @@ class TestReportStockQuantity(tests.TransactionCase): 2.0, 1.0, 1.0, 2.0, # in two days ]): - self.assertEqual(row['product_qty'], qty, "Incorrect qty for Date '%s' Warehouse '%s'" % (row['date:day'], row['warehouse_id'][1])) + self.assertEqual(qty_rd, qty, f"Incorrect qty for Date '{date_day}' Warehouse '{warehouse.display_name}'") diff --git a/addons/stock/wizard/stock_replenishment_info.py b/addons/stock/wizard/stock_replenishment_info.py index 01f48365691..eaf7ef63cd0 100644 --- a/addons/stock/wizard/stock_replenishment_info.py +++ b/addons/stock/wizard/stock_replenishment_info.py @@ -71,10 +71,10 @@ class StockReplenishmentInfo(models.TransientModel): ] quantity_by_month_out = self.env['stock.move'].read_group( AND([domain, [('location_dest_id.usage', '=', 'customer')]]), - ['date', 'product_qty'], ['date:month']) + ['product_qty'], ['date:month']) quantity_by_month_returned = self.env['stock.move'].read_group( AND([domain, [('location_id.usage', '=', 'customer')]]), - ['date', 'product_qty'], ['date:month']) + ['product_qty'], ['date:month']) quantity_by_month_returned = { g['date:month']: g['product_qty'] for g in quantity_by_month_returned} for group in quantity_by_month_out: diff --git a/addons/stock_account/models/product.py b/addons/stock_account/models/product.py index 0ffb438d65d..82f5adbe3a7 100644 --- a/addons/stock_account/models/product.py +++ b/addons/stock_account/models/product.py @@ -124,20 +124,18 @@ class ProductProduct(models.Model): if self.env.context.get('to_date'): to_date = fields.Datetime.to_datetime(self.env.context['to_date']) domain.append(('create_date', '<=', to_date)) - groups = self.env['stock.valuation.layer']._read_group(domain, ['value:sum', 'quantity:sum'], ['product_id']) - products = self.browse() + groups = self.env['stock.valuation.layer']._read_group(domain, ['product_id'], ['value:sum', 'quantity:sum']) + remaining = self # Browse all products and compute products' quantities_dict in batch. - self.env['product.product'].browse([group['product_id'][0] for group in groups]).sudo(False).mapped('qty_available') - for group in groups: - product = self.browse(group['product_id'][0]) - value_svl = company_id.currency_id.round(group['value']) - avg_cost = value_svl / group['quantity'] if group['quantity'] else 0 + for product, value_sum, quantity_sum in groups: + value_svl = company_id.currency_id.round(value_sum) + avg_cost = value_svl / quantity_sum if quantity_sum else 0 product.value_svl = value_svl - product.quantity_svl = group['quantity'] + product.quantity_svl = quantity_sum product.avg_cost = avg_cost product.total_value = avg_cost * product.sudo(False).qty_available - products |= product - remaining = (self - products) + remaining -= product + remaining.value_svl = 0 remaining.quantity_svl = 0 remaining.avg_cost = 0 diff --git a/addons/stock_delivery/models/stock_picking.py b/addons/stock_delivery/models/stock_picking.py index 56fe85c1ffa..7a95ffaa43d 100644 --- a/addons/stock_delivery/models/stock_picking.py +++ b/addons/stock_delivery/models/stock_picking.py @@ -45,27 +45,16 @@ class StockPicking(models.Model): @api.depends('move_line_ids', 'move_line_ids.result_package_id', 'move_line_ids.product_uom_id', 'move_line_ids.qty_done') def _compute_bulk_weight(self): picking_weights = defaultdict(float) - # Ordering by qty_done prevents the default ordering by groupby fields that can inject multiple Left Joins in the resulting query. - res_groups = self.env['stock.move.line'].read_group( + res_groups = self.env['stock.move.line']._read_group( [('picking_id', 'in', self.ids), ('product_id', '!=', False), ('result_package_id', '=', False)], - ['id:count'], ['picking_id', 'product_id', 'product_uom_id', 'qty_done'], - lazy=False, orderby='qty_done asc' + ['__count'], ) - products_by_id = { - product_res['id']: (product_res['uom_id'][0], product_res['weight']) - for product_res in - self.env['product.product'].with_context(active_test=False).search_read( - [('id', 'in', list(set(grp["product_id"][0] for grp in res_groups)))], ['uom_id', 'weight']) - } - for res_group in res_groups: - uom_id, weight = products_by_id[res_group['product_id'][0]] - uom = self.env['uom.uom'].browse(uom_id) - product_uom_id = self.env['uom.uom'].browse(res_group['product_uom_id'][0]) - picking_weights[res_group['picking_id'][0]] += ( - res_group['__count'] - * product_uom_id._compute_quantity(res_group['qty_done'], uom) - * weight + for picking, product, product_uom, qty_done, count in res_groups: + picking_weights[picking.id] += ( + count + * product_uom._compute_quantity(qty_done, product.uom_id) + * product.weight ) for picking in self: picking.weight_bulk = picking_weights[picking.id] diff --git a/addons/stock_delivery/models/stock_quant_package.py b/addons/stock_delivery/models/stock_quant_package.py index 7e61fab7114..bd7546e517a 100644 --- a/addons/stock_delivery/models/stock_quant_package.py +++ b/addons/stock_delivery/models/stock_quant_package.py @@ -12,20 +12,16 @@ class StockQuantPackage(models.Model): def _compute_weight(self): if self.env.context.get('picking_id'): package_weights = defaultdict(float) - # Ordering by qty_done prevents the default ordering by groupby fields that can inject multiple Left Joins in the resulting query. - res_groups = self.env['stock.move.line'].read_group( + res_groups = self.env['stock.move.line']._read_group( [('result_package_id', 'in', self.ids), ('product_id', '!=', False), ('picking_id', '=', self.env.context['picking_id'])], - ['id:count'], ['result_package_id', 'product_id', 'product_uom_id', 'qty_done'], - lazy=False, orderby='qty_done asc' + ['__count'], ) - for res_group in res_groups: - product_id = self.env['product.product'].browse(res_group['product_id'][0]) - product_uom_id = self.env['uom.uom'].browse(res_group['product_uom_id'][0]) - package_weights[res_group['result_package_id'][0]] += ( - res_group['__count'] - * product_uom_id._compute_quantity(res_group['qty_done'], product_id.uom_id) - * product_id.weight + for result_package, product, product_uom, qty_done, count in res_groups: + package_weights[result_package.id] += ( + count + * product_uom._compute_quantity(qty_done, product.uom_id) + * product.weight ) for package in self: weight = package.package_type_id.base_weight or 0.0 diff --git a/addons/stock_landed_costs/tests/test_stock_landed_costs_purchase.py b/addons/stock_landed_costs/tests/test_stock_landed_costs_purchase.py index ca69084dc05..2c54d567878 100644 --- a/addons/stock_landed_costs/tests/test_stock_landed_costs_purchase.py +++ b/addons/stock_landed_costs/tests/test_stock_landed_costs_purchase.py @@ -241,9 +241,9 @@ class TestLandedCosts(TestStockLandedCostsCommon): stock_negative_landed_cost.button_validate() self.assertEqual(stock_negative_landed_cost.state, 'done', 'Negative landed costs should be in done state') self.assertTrue(stock_negative_landed_cost.account_move_id, 'Landed costs should be available account move lines') - account_entry = self.env['account.move.line'].read_group( - [('move_id', '=', stock_negative_landed_cost.account_move_id.id)], ['balance', 'move_id'], ['move_id'])[0] - self.assertEqual(account_entry['balance'], 0, 'Move is not balanced') + [balance] = self.env['account.move.line']._read_group( + [('move_id', '=', stock_negative_landed_cost.account_move_id.id)], aggregates=['balance:sum'])[0] + self.assertEqual(balance, 0, 'Move is not balanced') move_lines = [ {'name': 'split by volume - Microwave Oven', 'debit': 3.75, 'credit': 0.0}, {'name': 'split by volume - Microwave Oven', 'debit': 0.0, 'credit': 3.75}, diff --git a/addons/stock_picking_batch/models/stock_picking.py b/addons/stock_picking_batch/models/stock_picking.py index 01d8d4f94c1..0ee96f4cb4a 100644 --- a/addons/stock_picking_batch/models/stock_picking.py +++ b/addons/stock_picking_batch/models/stock_picking.py @@ -31,20 +31,13 @@ class StockPickingType(models.Model): def _compute_picking_count(self): super()._compute_picking_count() - domains = { - 'count_picking_batch': [('is_wave', '=', False)], - 'count_picking_wave': [('is_wave', '=', True)], - } - for field in domains: - data = self.env['stock.picking.batch']._read_group(domains[field] + - [('state', 'not in', ('done', 'cancel')), ('picking_type_id', 'in', self.ids)], - ['picking_type_id'], ['picking_type_id']) - count = { - x['picking_type_id'][0]: x['picking_type_id_count'] - for x in data if x['picking_type_id'] - } - for record in self: - record[field] = count.get(record.id, 0) + data = self.env['stock.picking.batch']._read_group( + [('state', 'not in', ('done', 'cancel')), ('picking_type_id', 'in', self.ids)], + ['picking_type_id', 'is_wave'], ['__count']) + count = {(picking_type.id, is_wave): count for picking_type, is_wave, count in data} + for record in self: + record.count_picking_wave = count.get((record.id, True), 0) + record.count_picking_batch = count.get((record.id, False), 0) @api.model def _get_batch_group_by_keys(self): diff --git a/addons/survey/models/res_partner.py b/addons/survey/models/res_partner.py index d8c12f6c8f5..85b442bfa3b 100644 --- a/addons/survey/models/res_partner.py +++ b/addons/survey/models/res_partner.py @@ -14,9 +14,9 @@ class ResPartner(models.Model): def _compute_certifications_count(self): read_group_res = self.env['survey.user_input'].sudo()._read_group( [('partner_id', 'in', self.ids), ('scoring_success', '=', True)], - ['partner_id'], 'partner_id' + ['partner_id'], ['__count'] ) - data = dict((res['partner_id'][0], res['partner_id_count']) for res in read_group_res) + data = {partner.id: count for partner, count in read_group_res} for partner in self: partner.certifications_count = data.get(partner.id, 0) diff --git a/addons/survey/models/survey_survey.py b/addons/survey/models/survey_survey.py index da6375a2f60..01aea02c629 100644 --- a/addons/survey/models/survey_survey.py +++ b/addons/survey/models/survey_survey.py @@ -202,14 +202,14 @@ class Survey(models.Model): UserInput = self.env['survey.user_input'] base_domain = [('survey_id', 'in', self.ids)] - read_group_res = UserInput._read_group(base_domain, ['survey_id', 'state'], ['survey_id', 'state', 'scoring_percentage', 'scoring_success'], lazy=False) - for item in read_group_res: - stat[item['survey_id'][0]]['answer_count'] += item['__count'] - stat[item['survey_id'][0]]['answer_score_avg_total'] += item['scoring_percentage'] - if item['state'] == 'done': - stat[item['survey_id'][0]]['answer_done_count'] += item['__count'] - if item['scoring_success']: - stat[item['survey_id'][0]]['success_count'] += item['__count'] + read_group_res = UserInput._read_group(base_domain, ['survey_id', 'state', 'scoring_percentage', 'scoring_success'], ['__count']) + for survey, state, scoring_percentage, scoring_success, count in read_group_res: + stat[survey.id]['answer_count'] += count + stat[survey.id]['answer_score_avg_total'] += scoring_percentage + if state == 'done': + stat[survey.id]['answer_done_count'] += count + if scoring_success: + stat[survey.id]['success_count'] += count for survey_stats in stat.values(): avg_total = survey_stats.pop('answer_score_avg_total') @@ -262,18 +262,13 @@ class Survey(models.Model): context of sessions, so it should not matter too much. """ for survey in self: - answer_count = 0 - input_count = self.env['survey.user_input']._read_group( + [answer_count] = self.env['survey.user_input']._read_group( [('survey_id', '=', survey.id), ('is_session_answer', '=', True), ('state', '!=', 'done'), ('create_date', '>=', survey.session_start_time)], - ['create_uid:count'], - ['survey_id'], - ) - if input_count: - answer_count = input_count[0].get('create_uid', 0) - + aggregates=['create_uid:count'], + )[0] survey.session_answer_count = answer_count @api.depends('session_question_id', 'session_start_time', 'user_input_ids.user_input_line_ids') @@ -283,17 +278,12 @@ class Survey(models.Model): This field is currently used to display the count about a single survey, in the context of sessions, so it should not matter too much. """ for survey in self: - answer_count = 0 - input_line_count = self.env['survey.user_input.line']._read_group( + [answer_count] = self.env['survey.user_input.line']._read_group( [('question_id', '=', survey.session_question_id.id), ('survey_id', '=', survey.id), ('create_date', '>=', survey.session_start_time)], - ['user_input_id:count_distinct'], - ['question_id'], - ) - if input_line_count: - answer_count = input_line_count[0].get('user_input_id', 0) - + aggregates=['user_input_id:count_distinct'], + )[0] survey.session_question_answer_count = answer_count @api.depends('session_code') @@ -1076,16 +1066,16 @@ class Survey(models.Model): ('state', '=', 'done'), ('test_entry', '=', False) ] - count_data_success = self.env['survey.user_input'].sudo()._read_group(user_input_domain, ['scoring_success', 'id:count_distinct'], ['scoring_success']) + count_data_success = self.env['survey.user_input'].sudo()._read_group(user_input_domain, ['scoring_success'], ['__count']) completed_count = self.env['survey.user_input'].sudo().search_count(user_input_domain + [('state', "=", "done")]) scoring_success_count = 0 scoring_failed_count = 0 - for count_data_item in count_data_success: - if count_data_item['scoring_success']: - scoring_success_count += count_data_item['scoring_success_count'] + for scoring_success, count in count_data_success: + if scoring_success: + scoring_success_count += count else: - scoring_failed_count += count_data_item['scoring_success_count'] + scoring_failed_count += count success_graph = json.dumps([{ 'text': _('Passed'), diff --git a/addons/web/controllers/export.py b/addons/web/controllers/export.py index e944e898593..0d595596205 100644 --- a/addons/web/controllers/export.py +++ b/addons/web/controllers/export.py @@ -483,7 +483,7 @@ class ExportFormat(object): if not import_compat and groupby: groupby_type = [Model._fields[x.split(':')[0]].type for x in groupby] domain = [('id', 'in', ids)] if ids else domain - groups_data = Model.read_group(domain, [x if x != '.id' else 'id' for x in field_names], groupby, lazy=False) + groups_data = Model.read_group(domain, ['__count'], groupby, lazy=False) # read_group(lazy=False) returns a dict only for final groups (with actual data), # not for intermediary groups. The full group tree must be re-constructed. diff --git a/addons/web/models/models.py b/addons/web/models/models.py index fc5ebb10a5e..c7d1c1f061b 100644 --- a/addons/web/models/models.py +++ b/addons/web/models/models.py @@ -98,15 +98,13 @@ class Base(models.AbstractModel): if not groups: length = 0 elif limit and len(groups) == limit: - # We need to fetch all groups to know the total number - # this cannot be done all at once to avoid MemoryError - length = limit - chunk_size = 100000 - while True: - more = len(self.read_group(domain, ['display_name'], groupby, offset=length, limit=chunk_size, lazy=True)) - length += more - if more < chunk_size: - break + length = limit + len(self._read_group( + domain, + groupby=groupby if not lazy else groupby[0], + offset=limit, + order=orderby, + )) + else: length = len(groups) + offset return { diff --git a/addons/website/models/website_visitor.py b/addons/website/models/website_visitor.py index 5056f813c45..8ba9a3c5f42 100644 --- a/addons/website/models/website_visitor.py +++ b/addons/website/models/website_visitor.py @@ -121,15 +121,15 @@ class WebsiteVisitor(models.Model): @api.depends('website_track_ids') def _compute_page_statistics(self): results = self.env['website.track']._read_group( - [('visitor_id', 'in', self.ids), ('url', '!=', False)], ['visitor_id', 'page_id', 'url'], ['visitor_id', 'page_id', 'url'], lazy=False) + [('visitor_id', 'in', self.ids), ('url', '!=', False)], ['visitor_id', 'page_id'], ['__count']) mapped_data = {} - for result in results: - visitor_info = mapped_data.get(result['visitor_id'][0], {'page_count': 0, 'visitor_page_count': 0, 'page_ids': set()}) - visitor_info['visitor_page_count'] += result['__count'] + for visitor, page, count in results: + visitor_info = mapped_data.get(visitor.id, {'page_count': 0, 'visitor_page_count': 0, 'page_ids': set()}) + visitor_info['visitor_page_count'] += count visitor_info['page_count'] += 1 - if result['page_id']: - visitor_info['page_ids'].add(result['page_id'][0]) - mapped_data[result['visitor_id'][0]] = visitor_info + if page: + visitor_info['page_ids'].add(page.id) + mapped_data[visitor.id] = visitor_info for visitor in self: visitor_info = mapped_data.get(visitor.id, {'page_count': 0, 'visitor_page_count': 0, 'page_ids': set()}) @@ -145,10 +145,10 @@ class WebsiteVisitor(models.Model): @api.depends('website_track_ids.page_id') def _compute_last_visited_page_id(self): results = self.env['website.track']._read_group( - [('visitor_id', 'in', self.ids)], - ['visitor_id', 'page_id', 'visit_datetime:max'], - ['visitor_id', 'page_id'], lazy=False) - mapped_data = {result['visitor_id'][0]: result['page_id'][0] for result in results if result['page_id']} + [('visitor_id', 'in', self.ids), ('page_id', '!=', False)], + ['visitor_id', 'page_id'], + order='visit_datetime:max') + mapped_data = {visitor.id: page.id for visitor, page in results} for visitor in self: visitor.last_visited_page_id = mapped_data.get(visitor.id, False) diff --git a/addons/website_blog/controllers/main.py b/addons/website_blog/controllers/main.py index 63ad44c45f8..6dc04305d65 100644 --- a/addons/website_blog/controllers/main.py +++ b/addons/website_blog/controllers/main.py @@ -5,9 +5,9 @@ import werkzeug import itertools import pytz import babel.dates -from collections import OrderedDict +from collections import defaultdict -from odoo import http, fields, tools +from odoo import http, fields, tools, models from odoo.addons.http_routing.models.ir_http import slug, unslug from odoo.addons.website.controllers.main import QueryURL from odoo.addons.portal.controllers.portal import _build_url_w_params @@ -35,25 +35,23 @@ class WebsiteBlog(http.Controller): dom = blog and [('blog_id', '=', blog.id)] or [] if not request.env.user.has_group('website.group_website_designer'): dom += [('post_date', '<=', fields.Datetime.now())] - groups = request.env['blog.post']._read_group_raw( - dom, - ['name', 'post_date'], - groupby=["post_date"], orderby="post_date desc") - for group in groups: - (r, label) = group['post_date'] - start, end = r.split('/') - group['post_date'] = label - group['date_begin'] = start - group['date_end'] = end + groups = request.env['blog.post']._read_group( + dom, groupby=['post_date:month']) - locale = get_lang(request.env).code - start = pytz.UTC.localize(fields.Datetime.from_string(start)) - tzinfo = pytz.timezone(request.context.get('tz', 'utc') or 'utc') + locale = get_lang(request.env).code + tzinfo = pytz.timezone(request.context.get('tz', 'utc') or 'utc') + fmt = tools.DEFAULT_SERVER_DATETIME_FORMAT - group['month'] = babel.dates.format_datetime(start, format='MMMM', tzinfo=tzinfo, locale=locale) - group['year'] = babel.dates.format_datetime(start, format='yyyy', tzinfo=tzinfo, locale=locale) - - return OrderedDict((year, [m for m in months]) for year, months in itertools.groupby(groups, lambda g: g['year'])) + res = defaultdict(list) + for [start] in groups: + year = babel.dates.format_datetime(start, format='yyyy', tzinfo=tzinfo, locale=locale) + res[year].append({ + 'date_begin': start.strftime(fmt), + 'date_end': (start + models.READ_GROUP_TIME_GRANULARITY['month']).strftime(fmt), + 'month': babel.dates.format_datetime(start, format='MMMM', tzinfo=tzinfo, locale=locale), + 'year': year, + }) + return res def _get_blog_post_search_options(self, blog=None, active_tags=None, date_begin=None, date_end=None, state=None, **post): return { diff --git a/addons/website_crm_iap_reveal/models/crm_reveal_rule.py b/addons/website_crm_iap_reveal/models/crm_reveal_rule.py index 441d8922c10..732b56fbbbc 100644 --- a/addons/website_crm_iap_reveal/models/crm_reveal_rule.py +++ b/addons/website_crm_iap_reveal/models/crm_reveal_rule.py @@ -69,8 +69,8 @@ class CRMRevealRule(models.Model): def _compute_lead_count(self): leads = self.env['crm.lead']._read_group([ ('reveal_rule_id', 'in', self.ids) - ], fields=['reveal_rule_id', 'type'], groupby=['reveal_rule_id', 'type'], lazy=False) - mapping = {(lead['reveal_rule_id'][0], lead['type']): lead['__count'] for lead in leads} + ], groupby=['reveal_rule_id', 'type'], aggregates=['__count']) + mapping = {(reveal_rule.id, type_crm): count for reveal_rule, type_crm, count in leads} for rule in self: rule.lead_count = mapping.get((rule.id, 'lead'), 0) rule.opportunity_count = mapping.get((rule.id, 'opportunity'), 0) diff --git a/addons/website_crm_partner_assign/controllers/main.py b/addons/website_crm_partner_assign/controllers/main.py index 2893c047a2e..f032a90809a 100644 --- a/addons/website_crm_partner_assign/controllers/main.py +++ b/addons/website_crm_partner_assign/controllers/main.py @@ -190,9 +190,9 @@ class WebsiteCrmPartnerAssign(WebsitePartnerPage): partners_dom = [('is_company', '=', True), ('grade_id', '!=', False), ('website_published', '=', True), ('grade_id.website_published', '=', True), ('country_id', '!=', False)] dom += sitemap_qs2dom(qs=qs, route='/partners/country/') - countries = env['res.partner'].sudo().read_group(partners_dom, fields=['id', 'country_id'], groupby='country_id') - for country in countries: - loc = '/partners/country/%s' % slug(country['country_id']) + countries = env['res.partner'].sudo()._read_group(partners_dom, groupby=['country_id']) + for [country] in countries: + loc = '/partners/country/%s' % slug(country) if not qs or qs.lower() in loc: yield {'loc': loc} diff --git a/addons/website_crm_partner_assign/models/res_partner.py b/addons/website_crm_partner_assign/models/res_partner.py index 3b2498045a1..769bf4a36ba 100644 --- a/addons/website_crm_partner_assign/models/res_partner.py +++ b/addons/website_crm_partner_assign/models/res_partner.py @@ -42,16 +42,13 @@ class ResPartner(models.Model): @api.depends('implemented_partner_ids.is_published', 'implemented_partner_ids.active') def _compute_implemented_partner_count(self): - if not self.ids: - self.implemented_partner_count = 0 - return rg_result = self.env['res.partner']._read_group( [('assigned_partner_id', 'in', self.ids), ('is_published', '=', True)], ['assigned_partner_id'], - ['assigned_partner_id'] + ['__count'], ) - rg_data = {rg_item['assigned_partner_id'][0]: rg_item['assigned_partner_id_count'] for rg_item in rg_result} + rg_data = {assigned_partner.id: count for assigned_partner, count in rg_result} for partner in self: partner.implemented_partner_count = rg_data.get(partner.id, 0) @@ -62,13 +59,11 @@ class ResPartner(models.Model): def _compute_opportunity_count(self): super()._compute_opportunity_count() - assign_counts = {} - if self.ids: - opportunity_data = self.env['crm.lead'].with_context(active_test=False)._read_group( - [('partner_assigned_id', 'in', self.ids)], - ['partner_assigned_id'], ['partner_assigned_id'] - ) - assign_counts = {datum['partner_assigned_id'][0]: datum['partner_assigned_id_count'] for datum in opportunity_data} + opportunity_data = self.env['crm.lead'].with_context(active_test=False)._read_group( + [('partner_assigned_id', 'in', self.ids)], + ['partner_assigned_id'], ['__count'] + ) + assign_counts = {partner_assigned.id: count for partner_assigned, count in opportunity_data} for partner in self: partner.opportunity_count += assign_counts.get(partner.id, 0) diff --git a/addons/website_customer/controllers/main.py b/addons/website_customer/controllers/main.py index 9bc6960b603..0c92ca98f45 100644 --- a/addons/website_customer/controllers/main.py +++ b/addons/website_customer/controllers/main.py @@ -26,9 +26,9 @@ class WebsiteCustomer(http.Controller): dom = [('website_published', '=', True), ('assigned_partner_id', '!=', False), ('country_id', '!=', False)] dom += sitemap_qs2dom(qs, '/customers/country') - countries = env['res.partner'].sudo().read_group(dom, ['id', 'country_id'], groupby='country_id') - for country in countries: - loc = '/customers/country/%s' % slug(country['country_id']) + countries = env['res.partner'].sudo()._read_group(dom, ['country_id']) + for [country] in countries: + loc = '/customers/country/%s' % slug(country) if not qs or qs.lower() in loc: yield {'loc': loc} diff --git a/addons/website_event/models/website_visitor.py b/addons/website_event/models/website_visitor.py index c4dd71a0bfe..9b2a9d5c685 100644 --- a/addons/website_event/models/website_visitor.py +++ b/addons/website_event/models/website_visitor.py @@ -33,17 +33,12 @@ class WebsiteVisitor(models.Model): @api.depends('event_registration_ids') def _compute_event_registration_count(self): - if self.ids: - read_group_res = self.env['event.registration']._read_group( - [('visitor_id', 'in', self.ids)], - ['visitor_id'], ['visitor_id']) - visitor_mapping = dict( - (item['visitor_id'][0], item['visitor_id_count']) - for item in read_group_res) - else: - visitor_mapping = dict() + read_group_res = self.env['event.registration']._read_group( + [('visitor_id', 'in', self.ids)], + ['visitor_id'], ['__count']) + visitor_mapping = {visitor.id: count for visitor, count in read_group_res} for visitor in self: - visitor.event_registration_count = visitor_mapping.get(visitor.id) or 0 + visitor.event_registration_count = visitor_mapping.get(visitor.id, 0) @api.depends('event_registration_ids.email', 'event_registration_ids.mobile', 'event_registration_ids.phone') def _compute_email_phone(self): diff --git a/addons/website_event_exhibitor/models/event_event.py b/addons/website_event_exhibitor/models/event_event.py index 0ddf80ad004..e6e1a764399 100644 --- a/addons/website_event_exhibitor/models/event_event.py +++ b/addons/website_event_exhibitor/models/event_event.py @@ -20,8 +20,8 @@ class EventEvent(models.Model): domain=[('menu_type', '=', 'exhibitor')]) def _compute_sponsor_count(self): - data = self.env['event.sponsor']._read_group([], ['event_id'], ['event_id']) - result = dict((data['event_id'][0], data['event_id_count']) for data in data) + data = self.env['event.sponsor']._read_group([('event_id', 'in', self.ids)], ['event_id'], ['__count']) + result = {event.id: count for event, count in data} for event in self: event.sponsor_count = result.get(event.id, 0) diff --git a/addons/website_event_meet/models/event_event.py b/addons/website_event_meet/models/event_event.py index 7dfe6ff4fb5..ac343bdcb8a 100644 --- a/addons/website_event_meet/models/event_event.py +++ b/addons/website_event_meet/models/event_event.py @@ -29,13 +29,13 @@ class Event(models.Model): def _compute_meeting_room_count(self): meeting_room_count = self.env["event.meeting.room"].sudo()._read_group( domain=[("event_id", "in", self.ids)], - fields=["id:count"], - groupby=["event_id"], + groupby=['event_id'], + aggregates=['__count'], ) meeting_room_count = { - result["event_id"][0]: result["event_id_count"] - for result in meeting_room_count + event.id: count + for event, count in meeting_room_count } for event in self: diff --git a/addons/website_event_track/models/event_event.py b/addons/website_event_track/models/event_event.py index 4c78750cd1a..ce994b5ba96 100644 --- a/addons/website_event_track/models/event_event.py +++ b/addons/website_event_track/models/event_event.py @@ -24,8 +24,8 @@ class Event(models.Model): compute='_compute_tracks_tag_ids', store=True) def _compute_track_count(self): - data = self.env['event.track']._read_group([('stage_id.is_cancel', '!=', True)], ['event_id'], ['event_id']) - result = dict((data['event_id'][0], data['event_id_count']) for data in data) + data = self.env['event.track']._read_group([('stage_id.is_cancel', '!=', True)], ['event_id'], ['__count']) + result = {event.id: count for event, count in data} for event in self: event.track_count = result.get(event.id, 0) diff --git a/addons/website_event_track/models/event_track.py b/addons/website_event_track/models/event_track.py index c78ac6e58f3..df5e42ea6bb 100644 --- a/addons/website_event_track/models/event_track.py +++ b/addons/website_event_track/models/event_track.py @@ -329,10 +329,10 @@ class Track(models.Model): def _compute_wishlist_visitor_ids(self): results = self.env['event.track.visitor']._read_group( [('track_id', 'in', self.ids), ('is_wishlisted', '=', True)], - ['track_id', 'visitor_id:array_agg'], - ['track_id'] + ['track_id'], + ['visitor_id:array_agg'], ) - visitor_ids_map = {result['track_id'][0]: result['visitor_id'] for result in results} + visitor_ids_map = {track.id: visitor_ids for track, visitor_ids in results} for track in self: track.wishlist_visitor_ids = visitor_ids_map.get(track.id, []) track.wishlist_visitor_count = len(visitor_ids_map.get(track.id, [])) diff --git a/addons/website_event_track/models/website_visitor.py b/addons/website_event_track/models/website_visitor.py index 3ecfbcb6439..542b936cc5a 100644 --- a/addons/website_event_track/models/website_visitor.py +++ b/addons/website_event_track/models/website_visitor.py @@ -26,10 +26,10 @@ class WebsiteVisitor(models.Model): def _compute_event_track_wishlisted_ids(self): results = self.env['event.track.visitor']._read_group( [('visitor_id', 'in', self.ids), ('is_wishlisted', '=', True)], - ['visitor_id', 'track_id:array_agg'], - ['visitor_id'] + ['visitor_id'], + ['track_id:array_agg'], ) - track_ids_map = {result['visitor_id'][0]: result['track_id'] for result in results} + track_ids_map = {visitor.id: track_ids for visitor, track_ids in results} for visitor in self: visitor.event_track_wishlisted_ids = track_ids_map.get(visitor.id, []) visitor.event_track_wishlisted_count = len(visitor.event_track_wishlisted_ids) diff --git a/addons/website_event_track_quiz/controllers/community.py b/addons/website_event_track_quiz/controllers/community.py index 08c7ee14b49..8884398bb8d 100644 --- a/addons/website_event_track_quiz/controllers/community.py +++ b/addons/website_event_track_quiz/controllers/community.py @@ -56,13 +56,13 @@ class WebsiteEventTrackQuizCommunityController(EventCommunityController): def _get_leaderboard(self, event, searched_name=None): current_visitor = request.env['website.visitor']._get_visitor_from_request(force_create=False) - track_visitor_data = request.env['event.track.visitor'].sudo().read_group( + track_visitor_data = request.env['event.track.visitor'].sudo()._read_group( [('track_id', 'in', event.track_ids.ids), ('visitor_id', '!=', False), ('quiz_points', '>', 0)], - ['id', 'visitor_id', 'points:sum(quiz_points)'], - ['visitor_id'], orderby="points DESC") - data_map = {datum['visitor_id'][0]: datum['points'] for datum in track_visitor_data if datum.get('visitor_id')} + ['visitor_id'], + ['quiz_points:sum'], order='quiz_points:sum DESC, visitor_id ASC') + data_map = {visitor.id: points for visitor, points in track_visitor_data} leaderboard = [] position = 1 current_visitor_position = False diff --git a/addons/website_forum/controllers/website_forum.py b/addons/website_forum/controllers/website_forum.py index 572851ed15b..3273b2cfcf9 100644 --- a/addons/website_forum/controllers/website_forum.py +++ b/addons/website_forum/controllers/website_forum.py @@ -622,14 +622,14 @@ class WebsiteForum(WebsiteProfile): [('favourite_ids', '=', user.id), ('forum_id', 'in', forums.ids), ('parent_id', '=', False)]) # votes which given on users questions and answers. - data = Vote.read_group([('forum_id', 'in', forums.ids), ('recipient_id', '=', user.id)], ["vote"], - groupby=["vote"]) + data = Vote._read_group([('forum_id', 'in', forums.ids), ('recipient_id', '=', user.id)], ['vote'], + aggregates=['__count']) up_votes, down_votes = 0, 0 - for rec in data: - if rec['vote'] == '1': - up_votes = rec['vote_count'] - elif rec['vote'] == '-1': - down_votes = rec['vote_count'] + for vote, count in data: + if vote == '1': + up_votes = count + elif vote == '-1': + down_votes = count # Votes which given by users on others questions and answers. vote_ids = Vote.search([('user_id', '=', user.id)]) diff --git a/addons/website_forum/models/forum_forum.py b/addons/website_forum/models/forum_forum.py index c5c4e4ace56..81efe3335c0 100644 --- a/addons/website_forum/models/forum_forum.py +++ b/addons/website_forum/models/forum_forum.py @@ -155,15 +155,14 @@ class Forum(models.Model): result = {cid: dict(default_stats) for cid in self.ids} read_group_res = self.env['forum.post']._read_group( [('forum_id', 'in', self.ids), ('state', 'in', ('active', 'close')), ('parent_id', '=', False)], - ['forum_id', 'views', 'child_count', 'favourite_count'], - groupby=['forum_id'], - lazy=False) - for res_group in read_group_res: - cid = res_group['forum_id'][0] - result[cid]['total_posts'] += res_group.get('__count', 0) - result[cid]['total_views'] += res_group.get('views', 0) - result[cid]['total_answers'] += res_group.get('child_count', 0) - result[cid]['total_favorites'] += 1 if res_group.get('favourite_count', 0) else 0 + ['forum_id'], + ['__count', 'views:sum', 'child_count:sum', 'favourite_count:sum']) + for forum, count, views_sum, child_count_sum, favourite_count_sum in read_group_res: + stat_forum = result[forum.id] + stat_forum['total_posts'] += count + stat_forum['total_views'] += views_sum + stat_forum['total_answers'] += child_count_sum + stat_forum['total_favorites'] += 1 if favourite_count_sum else 0 for record in self: record.update(result[record.id]) diff --git a/addons/website_forum/models/forum_post.py b/addons/website_forum/models/forum_post.py index 88baea92c91..d9bf15487b0 100644 --- a/addons/website_forum/models/forum_post.py +++ b/addons/website_forum/models/forum_post.py @@ -195,10 +195,10 @@ class Post(models.Model): @api.depends('vote_ids.vote') def _compute_vote_count(self): - read_group_res = self.env['forum.post.vote']._read_group([('post_id', 'in', self._ids)], ['post_id', 'vote'], ['post_id', 'vote'], lazy=False) + read_group_res = self.env['forum.post.vote']._read_group([('post_id', 'in', self._ids)], ['post_id', 'vote'], ['__count']) result = dict.fromkeys(self._ids, 0) - for data in read_group_res: - result[data['post_id'][0]] += data['__count'] * int(data['vote']) + for post, vote, count in read_group_res: + result[post.id] += count * int(vote) for post in self: post.vote_count = result[post.id] diff --git a/addons/website_forum/models/res_users.py b/addons/website_forum/models/res_users.py index d9976feee76..287f3e45b6e 100644 --- a/addons/website_forum/models/res_users.py +++ b/addons/website_forum/models/res_users.py @@ -12,17 +12,14 @@ class Users(models.Model): 'Waiting post', compute="_compute_forum_waiting_posts_count") def _compute_forum_waiting_posts_count(self): - if not self.ids: - self.forum_waiting_posts_count = 0 - return read_group_res = self.env['forum.post']._read_group( [('create_uid', 'in', self.ids), ('state', '=', 'pending'), ('parent_id', '=', False)], ['create_uid'], - ['create_uid'], + ['__count'], ) mapping = { - res_group['create_uid'][0]: res_group['create_uid_count'] - for res_group in read_group_res + create_uid.id: count + for create_uid, count in read_group_res } for user in self: user.forum_waiting_posts_count = mapping.get(user.id, 0) diff --git a/addons/website_mail/controllers/main.py b/addons/website_mail/controllers/main.py index cd0bd09b091..79b2711a818 100644 --- a/addons/website_mail/controllers/main.py +++ b/addons/website_mail/controllers/main.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from collections import defaultdict from odoo import http from odoo.http import request @@ -61,17 +62,16 @@ class WebsiteMail(http.Controller): elif request.session.get('partner_id'): partner = request.env['res.partner'].sudo().browse(request.session.get('partner_id')) - res = {} + res = defaultdict(list) if partner: for model in records: - mail_followers_ids = request.env['mail.followers'].sudo().read_group([ + mail_followers_ids = request.env['mail.followers'].sudo()._read_group([ ('res_model', '=', model), ('res_id', 'in', records[model]), ('partner_id', '=', partner.id) - ], ['res_id', 'follow_count:count(id)'], ['res_id']) - # `read_group` will filter out the ones without count result - for m in mail_followers_ids: - res.setdefault(model, []).append(m['res_id']) + ], ['res_id']) + # `_read_group` will filter out the ones not matching the domain + res[model].extend(res_id for [res_id] in mail_followers_ids) return [{ 'is_user': user != public_user, diff --git a/addons/website_membership/controllers/main.py b/addons/website_membership/controllers/main.py index ad38a40abba..52b909f45df 100644 --- a/addons/website_membership/controllers/main.py +++ b/addons/website_membership/controllers/main.py @@ -63,7 +63,7 @@ class WebsiteMembership(http.Controller): if post_name: country_domain += ['|', ('name', 'ilike', post_name), ('website_description', 'ilike', post_name)] - countries = Partner.sudo().read_group(country_domain + [("website_published", "=", True)], ["id", "country_id"], groupby="country_id", orderby="country_id") + countries = Partner.sudo().read_group(country_domain + [("website_published", "=", True)], ["__count"], groupby="country_id") countries_total = sum(country_dict['country_id_count'] for country_dict in countries) line_domain = list(base_line_domain) diff --git a/addons/website_sale/controllers/backend.py b/addons/website_sale/controllers/backend.py index 96b6d935499..5465b0b7e5c 100644 --- a/addons/website_sale/controllers/backend.py +++ b/addons/website_sale/controllers/backend.py @@ -51,17 +51,16 @@ class WebsiteSaleBackend(WebsiteBackend): ('date', '>=', datetime_from), ('date', '<=', fields.Datetime.now()) ] - report_product_lines = request.env['sale.report'].read_group( + report_product_lines = request.env['sale.report']._read_group( domain=sale_report_domain, - fields=['product_tmpl_id', 'product_uom_qty', 'price_subtotal'], - groupby='product_tmpl_id', orderby='product_uom_qty desc', limit=5) - for product_line in report_product_lines: - product_tmpl_id = request.env['product.template'].browse(product_line['product_tmpl_id'][0]) + groupby=['product_tmpl_id'], + aggregates=['product_uom_qty:sum', 'price_subtotal:sum'], order='product_uom_qty:sum desc', limit=5) + for product_tmpl, product_uom_qty_sum, price_subtotal_sum in report_product_lines: sales_values['best_sellers'].append({ - 'id': product_tmpl_id.id, - 'name': product_tmpl_id.name, - 'qty': product_line['product_uom_qty'], - 'sales': product_line['price_subtotal'], + 'id': product_tmpl.id, + 'name': product_tmpl.name, + 'qty': product_uom_qty_sum, + 'sales': price_subtotal_sum, }) # Sale-based results computation @@ -69,23 +68,22 @@ class WebsiteSaleBackend(WebsiteBackend): ('website_id', '=', current_website.id), ('date_order', '>=', fields.Datetime.to_string(datetime_from)), ('date_order', '<=', fields.Datetime.to_string(datetime_to))] - so_group_data = request.env['sale.order'].read_group(sale_order_domain, fields=['state'], groupby='state') - for res in so_group_data: - if res.get('state') == 'sent': - sales_values['summary']['order_unpaid_count'] += res['state_count'] - elif res.get('state') in ['sale', 'done']: - sales_values['summary']['order_count'] += res['state_count'] - sales_values['summary']['order_carts_count'] += res['state_count'] + so_group_data = request.env['sale.order']._read_group(sale_order_domain, groupby=['state'], aggregates=['__count']) + for state, count in so_group_data: + if state == 'sent': + sales_values['summary']['order_unpaid_count'] += count + elif state in ['sale', 'done']: + sales_values['summary']['order_count'] += count + sales_values['summary']['order_carts_count'] += count - report_price_lines = request.env['sale.report'].read_group( + [price_subtotal] = request.env['sale.report']._read_group( domain=[ ('website_id', '=', current_website.id), ('state', 'in', ['sale', 'done']), ('date', '>=', datetime_from), ('date', '<=', datetime_to)], - fields=['team_id', 'price_subtotal'], - groupby=['team_id'], - ) + aggregates=['price_subtotal:sum'], + )[0] sales_values['summary'].update( order_to_invoice_count=request.env['sale.order'].search_count(sale_order_domain + [ ('state', 'in', ['sale', 'done']), @@ -102,7 +100,7 @@ class WebsiteSaleBackend(WebsiteBackend): # that part perform a search on sale.order in order to comply with access rights as tx do not have any ('sale_order_ids', 'in', request.env['sale.order'].search(sale_order_domain + [('state', '!=', 'cancel')]).ids), ]), - total_sold=sum(price_line['price_subtotal'] for price_line in report_price_lines) + total_sold=price_subtotal, ) # Ratio computation @@ -138,46 +136,46 @@ class WebsiteSaleBackend(WebsiteBackend): orders_data_groupby_campaign_id = request.env['sale.order']._read_group( domain=sale_utm_domain + [('campaign_id', '!=', False)], - fields=['amount_total', 'id', 'campaign_id'], - groupby='campaign_id') + groupby=['campaign_id'], + aggregates=['amount_total:sum']) orders_data_groupby_medium_id = request.env['sale.order']._read_group( domain=sale_utm_domain + [('medium_id', '!=', False)], - fields=['amount_total', 'id', 'medium_id'], - groupby='medium_id') + groupby=['medium_id'], + aggregates=['amount_total:sum']) orders_data_groupby_source_id = request.env['sale.order']._read_group( domain=sale_utm_domain + [('source_id', '!=', False)], - fields=['amount_total', 'id', 'source_id'], - groupby='source_id') + groupby=['source_id'], + aggregates=['amount_total:sum']) return { - 'campaign_id': self.compute_utm_graph_data('campaign_id', orders_data_groupby_campaign_id), - 'medium_id': self.compute_utm_graph_data('medium_id', orders_data_groupby_medium_id), - 'source_id': self.compute_utm_graph_data('source_id', orders_data_groupby_source_id), + 'campaign_id': self.compute_utm_graph_data(orders_data_groupby_campaign_id), + 'medium_id': self.compute_utm_graph_data(orders_data_groupby_medium_id), + 'source_id': self.compute_utm_graph_data(orders_data_groupby_source_id), } - def compute_utm_graph_data(self, utm_type, utm_graph_data): + def compute_utm_graph_data(self, utm_graph_data): return [{ - 'utm_type': data[utm_type][1], - 'amount_total': data['amount_total'] - } for data in utm_graph_data] + 'utm_type': utm_type.display_name, + 'amount_total': amount_total, + } for utm_type, amount_total in utm_graph_data] def _compute_sale_graph(self, date_from, date_to, sales_domain, previous=False): days_between = (date_to - date_from).days date_list = [(date_from + timedelta(days=x)) for x in range(0, days_between + 1)] - daily_sales = request.env['sale.report'].read_group( + daily_sales = request.env['sale.report']._read_group( domain=sales_domain, - fields=['date', 'price_subtotal'], - groupby='date:day') + groupby=['date:day'], + aggregates=['price_subtotal:sum']) - daily_sales_dict = {p['date:day']: p['price_subtotal'] for p in daily_sales} + daily_sales_dict = {date_day: price_subtotal_sum for date_day, price_subtotal_sum in daily_sales} sales_graph = [{ '0': fields.Date.to_string(d) if not previous else fields.Date.to_string(d + timedelta(days=days_between)), # Respect read_group format in models.py - '1': daily_sales_dict.get(babel.dates.format_date(d, format='dd MMM yyyy', locale=get_lang(request.env).code), 0) + '1': daily_sales_dict.get(d, 0), } for d in date_list] return sales_graph diff --git a/addons/website_sale/models/crm_team.py b/addons/website_sale/models/crm_team.py index 50a4a88fd16..a2582efed9c 100644 --- a/addons/website_sale/models/crm_team.py +++ b/addons/website_sale/models/crm_team.py @@ -23,17 +23,14 @@ class CrmTeam(models.Model): # abandoned carts to recover are draft sales orders that have no order lines, # a partner other than the public user, and created over an hour ago # and the recovery mail was not yet sent - counts = {} - amounts = {} website_teams = self.filtered(lambda team: team.website_ids) - if website_teams: - abandoned_carts_data = self.env['sale.order']._read_group([ - ('is_abandoned_cart', '=', True), - ('cart_recovery_email_sent', '=', False), - ('team_id', 'in', website_teams.ids), - ], ['amount_total', 'team_id'], ['team_id']) - counts = {data['team_id'][0]: data['team_id_count'] for data in abandoned_carts_data} - amounts = {data['team_id'][0]: data['amount_total'] for data in abandoned_carts_data} + abandoned_carts_data = self.env['sale.order']._read_group([ + ('is_abandoned_cart', '=', True), + ('cart_recovery_email_sent', '=', False), + ('team_id', 'in', website_teams.ids), + ], ['team_id'], ['amount_total:sum', '__count']) + counts = {team.id: count for team, __, count in abandoned_carts_data} + amounts = {team.id: amount_total_sum for team, amount_total_sum, __ in abandoned_carts_data} for team in self: team.abandoned_carts_count = counts.get(team.id, 0) team.abandoned_carts_amount = amounts.get(team.id, 0) diff --git a/addons/website_sale/models/website_snippet_filter.py b/addons/website_sale/models/website_snippet_filter.py index c2b58f7eab9..8b47a9e21d2 100644 --- a/addons/website_sale/models/website_snippet_filter.py +++ b/addons/website_sale/models/website_snippet_filter.py @@ -104,8 +104,8 @@ class WebsiteSnippetFilter(models.Model): excluded_products = website.sale_get_order().order_line.product_id.ids tracked_products = self.env['website.track'].sudo()._read_group( [('visitor_id', '=', visitor.id), ('product_id', '!=', False), ('product_id.website_published', '=', True), ('product_id', 'not in', excluded_products)], - ['product_id', 'visit_datetime:max'], ['product_id'], limit=limit, orderby='visit_datetime DESC') - products_ids = [product['product_id'][0] for product in tracked_products] + ['product_id'], limit=limit, order='visit_datetime:max DESC') + products_ids = [product.id for [product] in tracked_products] if products_ids: domain = expression.AND([ domain, diff --git a/addons/website_sale/models/website_visitor.py b/addons/website_sale/models/website_visitor.py index 2b44169e3b6..48dd536814a 100644 --- a/addons/website_sale/models/website_visitor.py +++ b/addons/website_sale/models/website_visitor.py @@ -23,14 +23,11 @@ class WebsiteVisitor(models.Model): results = self.env['website.track']._read_group( [('visitor_id', 'in', self.ids), ('product_id', '!=', False), '|', ('product_id.company_id', 'in', self.env.companies.ids), ('product_id.company_id', '=', False)], - ['visitor_id', 'product_id'], ['visitor_id', 'product_id'], - lazy=False) - mapped_data = {} - for result in results: - visitor_info = mapped_data.get(result['visitor_id'][0], {'product_count': 0, 'product_ids': set()}) - visitor_info['product_count'] += result['__count'] - visitor_info['product_ids'].add(result['product_id'][0]) - mapped_data[result['visitor_id'][0]] = visitor_info + ['visitor_id'], ['product_id:array_agg', '__count']) + mapped_data = { + visitor.id: {'product_count': count, 'product_ids': product_ids} + for visitor, product_ids, count in results + } for visitor in self: visitor_info = mapped_data.get(visitor.id, {'product_ids': [], 'product_count': 0}) diff --git a/addons/website_sale_digital/models/product_template.py b/addons/website_sale_digital/models/product_template.py index c2b8e4de819..81d31c2e38a 100644 --- a/addons/website_sale_digital/models/product_template.py +++ b/addons/website_sale_digital/models/product_template.py @@ -9,8 +9,8 @@ class ProductTemplate(models.Model): attachment_count = fields.Integer(compute='_compute_attachment_count', string="File") def _compute_attachment_count(self): - attachment_data = self.env['ir.attachment']._read_group([('res_model', '=', self._name), ('res_id', 'in', self.ids), ('product_downloadable', '=', True)], ['res_id'], ['res_id']) - mapped_data = {data['res_id']: data['res_id_count'] for data in attachment_data} + attachment_data = self.env['ir.attachment']._read_group([('res_model', '=', self._name), ('res_id', 'in', self.ids), ('product_downloadable', '=', True)], ['res_id'], ['__count']) + mapped_data = dict(attachment_data) for product_template in self: product_template.attachment_count = mapped_data.get(product_template.id, 0) diff --git a/addons/website_sale_slides/models/slide_channel.py b/addons/website_sale_slides/models/slide_channel.py index e03b567b1d0..cbf9b37266b 100644 --- a/addons/website_sale_slides/models/slide_channel.py +++ b/addons/website_sale_slides/models/slide_channel.py @@ -32,10 +32,10 @@ class Channel(models.Model): ('state', 'in', self.env['sale.report']._get_done_states()), ('product_id', 'in', self.product_id.ids), ] - rg_data = dict( - (item['product_id'][0], item['price_total']) - for item in self.env['sale.report']._read_group(domain, ['product_id', 'price_total'], ['product_id']) - ) + rg_data = { + product.id: price_total + for product, price_total in self.env['sale.report']._read_group(domain, ['product_id'], ['price_total:sum']) + } for channel in self: channel.product_sale_revenues = rg_data.get(channel.product_id.id, 0) @@ -61,12 +61,11 @@ class Channel(models.Model): self.filtered(lambda channel: channel.is_published and not channel.product_id.is_published).sudo().product_id.write({'is_published': True}) unpublished_channel_products = self.filtered(lambda channel: not channel.is_published).product_id - group_data = self.read_group( + group_data = self._read_group( [('is_published', '=', True), ('product_id', 'in', unpublished_channel_products.ids)], ['product_id'], - ['product_id'], ) - used_product_ids = [product['product_id'][0] for product in group_data if product['product_id_count'] > 0] + used_product_ids = {product.id for [product] in group_data} product_to_unpublish = unpublished_channel_products.filtered(lambda product: product.id not in used_product_ids) if product_to_unpublish: product_to_unpublish.sudo().write({'is_published': False}) diff --git a/addons/website_slides/models/res_partner.py b/addons/website_slides/models/res_partner.py index 069367ea95f..b6ffd1bfc30 100644 --- a/addons/website_slides/models/res_partner.py +++ b/addons/website_slides/models/res_partner.py @@ -40,9 +40,9 @@ class ResPartner(models.Model): def _compute_slide_channel_count(self): read_group_res = self.env['slide.channel.partner'].sudo()._read_group( [('partner_id', 'in', self.ids)], - ['partner_id'], 'partner_id' + ['partner_id'], ['__count'] ) - data = dict((res['partner_id'][0], res['partner_id_count']) for res in read_group_res) + data = {partner.id: count for partner, count in read_group_res} for partner in self: partner.slide_channel_count = data.get(partner.id, 0) diff --git a/addons/website_slides/models/slide_channel.py b/addons/website_slides/models/slide_channel.py index 96d85673011..3568c48e215 100644 --- a/addons/website_slides/models/slide_channel.py +++ b/addons/website_slides/models/slide_channel.py @@ -88,15 +88,16 @@ class ChannelUsersRelation(models.Model): ('slide_id.is_published', '=', True), ('slide_id.active', '=', True)], ['channel_id', 'partner_id'], - groupby=['channel_id', 'partner_id'], lazy=False) - mapped_data = defaultdict(dict) - for item in read_group_res: - mapped_data[item['channel_id'][0]][item['partner_id'][0]] = item['__count'] + aggregates=['__count']) + mapped_data = { + (channel.id, partner.id): count + for channel, partner, count in read_group_res + } completed_records = self.env['slide.channel.partner'] uncompleted_records = self.env['slide.channel.partner'] for record in self: - record.completed_slides_count = mapped_data.get(record.channel_id.id, dict()).get(record.partner_id.id, 0) + record.completed_slides_count = mapped_data.get((record.channel_id.id, record.partner_id.id), 0) record.completion = 100.0 if record.completed else round(100.0 * record.completed_slides_count / (record.channel_id.total_slides or 1)) if not record.channel_id.active: @@ -378,15 +379,15 @@ class Channel(models.Model): @api.depends('channel_partner_ids.channel_id') def _compute_members_count(self): - read_group_res = self.env['slide.channel.partner'].sudo()._read_group([('channel_id', 'in', self.ids)], ['channel_id'], 'channel_id') - data = dict((res['channel_id'][0], res['channel_id_count']) for res in read_group_res) + read_group_res = self.env['slide.channel.partner'].sudo()._read_group([('channel_id', 'in', self.ids)], ['channel_id'], ['__count']) + data = {channel.id: count for channel, count in read_group_res} for channel in self: channel.members_count = data.get(channel.id, 0) @api.depends('channel_partner_ids.channel_id', 'channel_partner_ids.completed') def _compute_members_done_count(self): - read_group_res = self.env['slide.channel.partner'].sudo()._read_group(['&', ('channel_id', 'in', self.ids), ('completed', '=', True)], ['channel_id'], 'channel_id') - data = dict((res['channel_id'][0], res['channel_id_count']) for res in read_group_res) + read_group_res = self.env['slide.channel.partner'].sudo()._read_group(['&', ('channel_id', 'in', self.ids), ('completed', '=', True)], ['channel_id'], ['__count']) + data = {channel.id: count for channel, count in read_group_res} for channel in self: channel.members_done_count = data.get(channel.id, 0) @@ -432,37 +433,21 @@ class Channel(models.Model): result = dict((cid, dict(default_vals)) for cid in self.ids) read_group_res = self.env['slide.slide']._read_group( [('active', '=', True), ('is_published', '=', True), ('channel_id', 'in', self.ids), ('is_category', '=', False)], - ['channel_id', 'slide_category', 'likes', 'dislikes', 'total_views', 'completion_time'], - groupby=['channel_id', 'slide_category'], - lazy=False) - for res_group in read_group_res: - cid = res_group['channel_id'][0] - result[cid]['total_views'] += res_group.get('total_views', 0) - result[cid]['total_votes'] += res_group.get('likes', 0) - result[cid]['total_votes'] -= res_group.get('dislikes', 0) - result[cid]['total_time'] += res_group.get('completion_time', 0) - - category_stats = self._compute_slides_statistics_category(read_group_res) - for cid, cdata in category_stats.items(): - result[cid].update(cdata) + ['channel_id', 'slide_category'], + aggregates=['__count', 'likes:sum', 'dislikes:sum', 'total_views:sum', 'completion_time:sum']) + for channel, slide_category, count, likes_sum, dislikes_sum, total_views_sum, completion_time_sum in read_group_res: + channel_dict = result[channel.id] + channel_dict['total_votes'] += likes_sum + channel_dict['total_votes'] -= dislikes_sum + channel_dict['total_views'] += total_views_sum + channel_dict['total_time'] += completion_time_sum + if slide_category: + channel_dict[f'nbr_{slide_category}'] = count + channel_dict['total_slides'] += count for record in self: record.update(result.get(record.id, default_vals)) - def _compute_slides_statistics_category(self, read_group_res): - """ Compute statistics based on all existing slide categories """ - slide_categories = self.env['slide.slide']._fields['slide_category'].get_values(self.env) - keys = ['nbr_%s' % slide_category for slide_category in slide_categories] - result = dict((cid, dict((key, 0) for key in keys + ['total_slides'])) for cid in self.ids) - for res_group in read_group_res: - cid = res_group['channel_id'][0] - slide_category = res_group.get('slide_category') - if slide_category: - slide_category_count = res_group.get('__count', 0) - result[cid]['nbr_%s' % slide_category] = slide_category_count - result[cid]['total_slides'] += slide_category_count - return result - def _compute_rating_stats(self): super(Channel, self)._compute_rating_stats() for record in self: diff --git a/addons/website_slides/models/slide_slide.py b/addons/website_slides/models/slide_slide.py index a5f87ca0671..e77c6af959f 100644 --- a/addons/website_slides/models/slide_slide.py +++ b/addons/website_slides/models/slide_slide.py @@ -323,30 +323,18 @@ class Slide(models.Model): @api.depends('slide_partner_ids.vote') def _compute_like_info(self): - if not self.ids: - self.update({'likes': 0, 'dislikes': 0}) - return - - rg_data_like = self.env['slide.slide.partner'].sudo().read_group( - [('slide_id', 'in', self.ids), ('vote', '=', 1)], - ['slide_id'], ['slide_id'] - ) - rg_data_dislike = self.env['slide.slide.partner'].sudo().read_group( - [('slide_id', 'in', self.ids), ('vote', '=', -1)], - ['slide_id'], ['slide_id'] - ) - mapped_data_like = dict( - (rg_data['slide_id'][0], rg_data['slide_id_count']) - for rg_data in rg_data_like - ) - mapped_data_dislike = dict( - (rg_data['slide_id'][0], rg_data['slide_id_count']) - for rg_data in rg_data_dislike + rg_data = self.env['slide.slide.partner'].sudo()._read_group( + [('slide_id', 'in', self.ids), ('vote', 'in', (-1, 1))], + ['slide_id', 'vote'], ['__count'], ) + mapped_data = { + (slide.id, vote): count + for slide, vote, count in rg_data + } for slide in self: - slide.likes = mapped_data_like.get(slide.id, 0) - slide.dislikes = mapped_data_dislike.get(slide.id, 0) + slide.likes = mapped_data.get((slide.id, 1), 0) + slide.dislikes = mapped_data.get((slide.id, -1), 0) @api.depends('slide_partner_ids.slide_id') def _compute_slide_views(self): @@ -354,26 +342,23 @@ class Slide(models.Model): read_group_res = self.env['slide.slide.partner'].sudo()._read_group( [('slide_id', 'in', self.ids)], ['slide_id'], - groupby=['slide_id'] + aggregates=['__count'], ) - mapped_data = dict((res['slide_id'][0], res['slide_id_count']) for res in read_group_res) + mapped_data = {slide.id: count for slide, count in read_group_res} for slide in self: slide.slide_views = mapped_data.get(slide.id, 0) @api.depends('embed_ids.slide_id') def _compute_embed_counts(self): - mapped_data = {} - - if self.ids: - read_group_res = self.env['slide.embed']._read_group( - [('slide_id', 'in', self.ids)], - ['count_views'], - ['slide_id'] - ) - mapped_data = { - res['slide_id'][0]: res.get('count_views', 0) - for res in read_group_res - } + read_group_res = self.env['slide.embed']._read_group( + [('slide_id', 'in', self.ids)], + ['slide_id'], + ['count_views:sum'], + ) + mapped_data = { + slide.id: count_views_sum + for slide, count_views_sum in read_group_res + } for slide in self: slide.embed_count = mapped_data.get(slide.id, 0) @@ -386,29 +371,16 @@ class Slide(models.Model): default_vals = dict((key, 0) for key in keys + ['total_slides']) res = self.env['slide.slide']._read_group( - [('is_published', '=', True), ('category_id', 'in', self.ids), ('is_category', '=', False)], - ['category_id', 'slide_category'], ['category_id', 'slide_category'], - lazy=False) + [('is_published', '=', True), ('category_id', 'in', self.filtered('is_category').ids), ('is_category', '=', False)], + ['category_id', 'slide_category'], ['__count']) - category_stats = self._compute_slides_statistics_category(res) + result = {category_id: dict(default_vals) for category_id in self.ids} + for category, slide_category, count in res: + result[category.id][f'nbr_{slide_category}'] = count + result[category.id]['total_slides'] += count - for record in self.filtered(lambda slide: slide.is_category): - record.update(category_stats.get(record._origin.id, default_vals)) - - def _compute_slides_statistics_category(self, read_group_res): - """ Compute statistics based on all existing slide categories """ - slide_categories = self.env['slide.slide']._fields['slide_category'].get_values(self.env) - keys = ['nbr_%s' % slide_category for slide_category in slide_categories] - result = dict((cid, dict((key, 0) for key in keys + ['total_slides'])) for cid in self.ids) - for res_group in read_group_res: - cid = res_group['category_id'][0] - slide_category = res_group.get('slide_category') - if slide_category: - slide_category_count = res_group.get('__count', 0) - result[cid]['nbr_%s' % slide_category] += slide_category_count - result[cid]['total_slides'] += slide_category_count - - return result + for record in self: + record.update(result.get(record._origin.id, default_vals)) @api.depends('slide_ids.sequence', 'slide_ids.active', 'slide_ids.completion_time', 'slide_ids.is_published', 'slide_ids.is_category') def _compute_category_completion_time(self): diff --git a/odoo/addons/base/models/ir_attachment.py b/odoo/addons/base/models/ir_attachment.py index ed3005a3f8b..fea5c05736b 100644 --- a/odoo/addons/base/models/ir_attachment.py +++ b/odoo/addons/base/models/ir_attachment.py @@ -512,20 +512,22 @@ class IrAttachment(models.Model): return ['type', 'company_id', 'res_id', 'create_date', 'create_uid', 'name', 'mimetype', 'id', 'url', 'res_field', 'res_model'] @api.model - def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True): + def _read_group(self, domain, groupby=(), aggregates=(), having=(), offset=0, limit=None, order=None) -> 'list[tuple]': """Override read_group to add res_field=False in domain if not present.""" - if not fields: - raise AccessError(_("Sorry, you must provide fields to read on attachments")) + # TODO: security check apply on the _search are not apply, _search used by _read_group ? + groupby = [groupby] if isinstance(groupby, str) else groupby - if any('(' in field for field in fields + groupby): - raise AccessError(_("Sorry, the syntax 'name:agg(field)' is not available for attachments")) + aggregates = [aggregates] if isinstance(aggregates, str) else aggregates + if not any(item[0] in ('id', 'res_field') for item in domain): domain.insert(0, ('res_field', '=', False)) + allowed_fields = self._read_group_allowed_fields() - fields_set = set(field.split(':')[0] for field in fields + groupby) - if not self.env.is_system() and (not fields or fields_set.difference(allowed_fields)): + fields_set = {field.split(':')[0] for field in aggregates + groupby if field != '__count'} + if not self.env.is_system() and (fields_set.difference(allowed_fields) or having): # Having is not allowed raise AccessError(_("Sorry, you are not allowed to access these fields on attachments.")) - return super().read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy) + + return super()._read_group(domain, groupby, aggregates, having, offset=offset, limit=limit, order=order) @api.model def _search(self, domain, offset=0, limit=None, order=None, access_rights_uid=None): diff --git a/odoo/addons/test_new_api/tests/test_new_fields.py b/odoo/addons/test_new_api/tests/test_new_fields.py index 1f847228d69..9a5ea4f3e79 100644 --- a/odoo/addons/test_new_api/tests/test_new_fields.py +++ b/odoo/addons/test_new_api/tests/test_new_fields.py @@ -97,7 +97,7 @@ class TestFields(TransactionCaseWithUserDemo): cat.read_group([], fields=['zzz:sum'], groupby=['parent']) with self.assertRaisesRegex(ValueError, 'Invalid field'): cat.read_group([], fields=['color'], groupby=['zzz']) - with self.assertRaisesRegex(ValueError, 'Invalid field'): + with self.assertRaisesRegex(ValueError, 'is not a valid aggregate'): cat.read_group([], fields=['color'], groupby=['parent'], orderby='zzz') # exception: accept '__count' as field to aggregate cat.read_group([], fields=['__count'], groupby=['parent']) diff --git a/odoo/models.py b/odoo/models.py index 17070615fea..b3e3c96dbf4 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -1799,21 +1799,21 @@ class BaseModel(metaclass=MetaModel): list to match all records. :param list groupby: list of groupby descriptions by which the records will be grouped. A groupby description is either a field (then it will be grouped by that field) - or a string 'field:granularity'. Right now, the only supported granularities - are 'day', 'week', 'month', 'quarter' or 'year', and they only make sense for + or a string `'field:granularity'`. Right now, the only supported granularities + are `'day'`, `'week'`, `'month'`, `'quarter'` or `'year'`, and they only make sense for date/datetime fields. :param list aggregates: list of aggregates specification. - Each element is 'field:agg' (aggregate field with aggregation function 'agg'). + Each element is `'field:agg'` (aggregate field with aggregation function `'agg'`). The possible aggregation functions are the ones provided by `PostgreSQL `_, - 'count_distinct' with the expected meaning and 'recordset' to act like 'array_agg' + `'count_distinct'` with the expected meaning and `'recordset'` to act like `'array_agg'` converted into a recordset. :param list having: A domain where the valid "fields" are the aggregates. :param int offset: optional number of groups to skip :param int limit: optional max number of groups to return :param str order: optional ``order by`` specification, for overriding the natural sort ordering of the groups, - see also :py:meth:`~osv.osv.osv.search`. + see also :meth:`~.search`. :return: list of tuple containing in the order the groups values and aggregates values (flatten): `[(groupby_1_value, ... , aggregate_1_value_aggregate, ...), ...]`. If group is related field, the value of it will be a recordset (with a correct prefetch set).