From 0479b2b59466ae1d6d74165345aa3a7dc5de24ed Mon Sep 17 00:00:00 2001 From: william-andre Date: Thu, 15 Jun 2023 13:01:58 +0000 Subject: [PATCH] [IMP] account,*: manage subsidiary companies Allow sharing records between company * accounts * taxes * fiscal positions * products * ...and some related models These records can be read and used in children companies. This can be used to * have different branding for different businesses * allow more complex security rules * consolidate branches differently * manage different tax reports with different tax ids in the same country task-3371677 closes odoo/odoo#125642 Related: odoo/enterprise#43215 Signed-off-by: Quentin De Paoli --- addons/account/demo/account_demo.py | 18 +- addons/account/models/account_account.py | 113 +++++----- .../account_analytic_distribution_model.py | 1 + .../account/models/account_analytic_line.py | 3 +- .../models/account_bank_statement_line.py | 4 +- .../account/models/account_cash_rounding.py | 15 +- addons/account/models/account_journal.py | 58 +++-- .../models/account_journal_dashboard.py | 16 +- addons/account/models/account_move.py | 92 ++++---- addons/account/models/account_move_line.py | 22 +- .../models/account_partial_reconcile.py | 11 +- addons/account/models/account_payment.py | 11 +- .../account/models/account_payment_method.py | 7 +- .../account/models/account_reconcile_model.py | 10 +- addons/account/models/account_report.py | 2 +- addons/account/models/account_tax.py | 69 +++--- addons/account/models/chart_template.py | 53 +++-- addons/account/models/company.py | 119 +++++++--- addons/account/models/ir_module.py | 12 +- .../models/onboarding_onboarding_step.py | 5 +- addons/account/models/partner.py | 50 +++-- addons/account/models/product.py | 10 +- addons/account/models/res_config_settings.py | 55 +++-- addons/account/models/res_partner_bank.py | 1 + addons/account/models/template_generic_coa.py | 7 +- .../populate/account_bank_statement.py | 4 +- addons/account/populate/account_move.py | 11 +- addons/account/populate/account_payment.py | 4 +- .../populate/account_reconcile_model.py | 2 +- addons/account/security/account_security.xml | 22 +- addons/account/tests/__init__.py | 1 + addons/account/tests/test_account_journal.py | 3 +- .../test_account_move_line_tax_details.py | 2 +- .../tests/test_account_move_reconcile.py | 2 +- addons/account/tests/test_company_branch.py | 208 ++++++++++++++++++ .../account/views/account_account_views.xml | 4 +- addons/account/views/account_move_views.xml | 12 +- addons/account/views/account_payment_view.xml | 1 + .../views/account_reconcile_model_views.xml | 1 + .../views/res_config_settings_views.xml | 2 +- .../wizard/account_automatic_entry_wizard.py | 18 +- .../account/wizard/account_move_reversal.py | 8 +- .../wizard/account_payment_register.py | 37 +++- .../wizard/account_tour_upload_bill.py | 6 +- addons/account/wizard/accrued_orders.py | 15 +- addons/account/wizard/setup_wizards.py | 3 +- .../models/account_edi_common.py | 6 +- .../models/account_edi_xml_ubl_20.py | 6 +- .../account_payment/models/account_journal.py | 4 +- .../account_payment/models/account_payment.py | 4 +- .../models/account_payment_method_line.py | 9 +- .../models/payment_provider.py | 8 +- .../wizards/account_payment_register.py | 4 +- .../models/account_edi_proxy_user.py | 8 +- addons/account_peppol/models/res_company.py | 8 +- addons/analytic/models/analytic_account.py | 25 +-- addons/analytic/models/analytic_plan.py | 5 +- .../analytic_distribution_model_views.xml | 2 + .../base_setup/models/res_config_settings.py | 6 + .../views/res_config_settings_views.xml | 1 + .../models/account_fiscal_position.py | 2 +- addons/crm/models/crm_lead.py | 4 +- .../crm/tests/test_crm_lead_multicompany.py | 1 + addons/delivery/models/sale_order.py | 2 +- .../wizard/choose_delivery_carrier.py | 2 +- addons/event/models/event_event.py | 6 +- addons/event_crm/models/event_lead_rule.py | 2 +- addons/fleet/models/fleet_vehicle.py | 2 +- addons/hr/models/hr_department.py | 4 +- addons/hr/models/hr_employee.py | 2 +- addons/hr/models/hr_employee_base.py | 15 +- addons/hr/models/hr_job.py | 2 +- addons/hr/models/hr_plan.py | 2 +- addons/hr/models/hr_work_location.py | 2 +- addons/hr/models/res_users.py | 5 +- addons/hr/models/resource.py | 2 +- addons/hr/wizard/hr_plan_wizard.py | 3 +- addons/hr_expense/models/hr_expense.py | 59 ++++- addons/hr_expense/models/res_company.py | 4 +- .../hr_expense/models/res_config_settings.py | 2 +- addons/hr_expense/wizard/hr_expense_split.py | 14 +- .../data/template/account.tax-ar_ex.csv | 2 +- addons/l10n_ar/demo/exento_demo.xml | 1 - addons/l10n_ar/demo/mono_demo.xml | 1 - addons/l10n_ar/demo/respinsc_demo.xml | 1 - addons/l10n_ar/models/account_move.py | 6 +- addons/l10n_ar/tests/common.py | 2 - addons/l10n_cl/demo/account_demo.py | 2 +- addons/l10n_de/models/datev.py | 20 +- .../l10n_do/data/template/account.tax-do.csv | 2 +- addons/l10n_es_edi_facturae/__init__.py | 2 +- addons/l10n_eu_oss/models/res_company.py | 11 +- addons/l10n_fr/__init__.py | 2 +- addons/l10n_fr/models/res_company.py | 4 +- addons/l10n_fr_fec/wizard/account_fr_fec.py | 9 +- .../models/account_closing.py | 3 +- .../demo/chart_template.py | 7 +- .../l10n_it_edi/models/account_edi_format.py | 26 ++- addons/l10n_it_edi/tests/common.py | 2 +- .../l10n_latam_payment_mass_transfer.py | 6 +- addons/l10n_pe/demo/account_demo.py | 4 +- addons/l10n_sa_edi/models/res_company.py | 8 +- addons/mrp/models/mrp_bom.py | 6 +- addons/mrp/models/mrp_production.py | 16 +- addons/mrp/models/mrp_unbuild.py | 10 +- addons/mrp_account/models/product.py | 2 +- addons/payment/models/payment_provider.py | 6 +- addons/payment/models/payment_token.py | 1 + addons/payment/models/res_company.py | 14 +- addons/payment/security/payment_security.xml | 4 +- .../wizards/payment_onboarding_wizard.py | 28 ++- .../payment_custom/models/payment_provider.py | 3 +- addons/point_of_sale/models/chart_template.py | 4 +- addons/point_of_sale/models/pos_config.py | 110 ++++----- addons/point_of_sale/models/pos_order.py | 4 +- addons/point_of_sale/models/pos_session.py | 16 +- addons/point_of_sale/models/res_company.py | 8 +- .../models/res_config_settings.py | 5 +- addons/pos_restaurant/models/pos_config.py | 19 +- .../models/product.py | 5 +- addons/product/security/product_security.xml | 10 +- addons/purchase/models/account_invoice.py | 2 +- addons/purchase/models/purchase.py | 2 +- addons/purchase/views/purchase_views.xml | 2 + .../wizard/product_replenish.py | 9 +- addons/repair/models/repair.py | 18 +- addons/sale/models/product_template.py | 2 +- addons/sale/models/sale_order.py | 6 +- addons/sale/models/sale_order_line.py | 7 +- .../tests/test_sale_order_down_payment.py | 4 +- addons/sale/tests/test_sale_to_invoice.py | 5 +- .../sale/wizard/sale_make_invoice_advance.py | 2 + .../models/sale_order_template_line.py | 2 +- .../models/sale_order_template_option.py | 2 +- addons/sale_project/models/product.py | 6 +- addons/sale_timesheet/models/project.py | 3 +- addons/spreadsheet_account/models/account.py | 2 +- addons/stock/models/product_strategy.py | 8 +- addons/stock/models/stock_lot.py | 3 - addons/stock/models/stock_move.py | 2 +- addons/stock/models/stock_move_line.py | 4 +- addons/stock/models/stock_orderpoint.py | 2 +- addons/stock/models/stock_quant.py | 6 +- addons/stock/models/stock_scrap.py | 12 +- addons/stock/models/stock_storage_category.py | 2 +- addons/stock/wizard/product_replenish.py | 7 +- addons/stock/wizard/stock_rules_report.py | 2 +- .../wizard/stock_warn_insufficient_qty.py | 3 +- addons/stock_account/models/product.py | 15 +- .../models/payment_provider.py | 2 +- .../models/res_config_settings.py | 7 +- .../website_sale/models/product_template.py | 2 +- addons/website_sale/models/sale_order.py | 11 +- addons/website_sale/models/website_visitor.py | 8 +- 154 files changed, 1225 insertions(+), 700 deletions(-) create mode 100644 addons/account/tests/test_company_branch.py diff --git a/addons/account/demo/account_demo.py b/addons/account/demo/account_demo.py index 54253b4b12a..b639cf8f768 100644 --- a/addons/account/demo/account_demo.py +++ b/addons/account/demo/account_demo.py @@ -50,7 +50,7 @@ class AccountChartTemplate(models.AbstractModel): @api.model def _get_demo_data_products(self, company=False): - prod_templates = self.env['product.product'].search(['|', ('company_id', '=', self.env.company.id), ('company_id', '=', False)]) + prod_templates = self.env['product.product'].search(self.env['product.product']._check_company_domain(company)) if self.env.company.account_sale_tax_id: prod_templates.write({'taxes_id': [Command.link(self.env.company.account_sale_tax_id.id)]}) if self.env.company.account_purchase_tax_id: @@ -138,7 +138,10 @@ class AccountChartTemplate(models.AbstractModel): def _get_demo_data_statement(self, company=False): cid = company.id or self.env.company.id bnk_journal = self.env['account.journal'].search( - domain=[('type', '=', 'bank'), ('company_id', '=', cid)], + domain=[ + *self.env['account.journal']._check_company_domain(cid), + ('type', '=', 'bank'), + ], limit=1, ) return { @@ -168,7 +171,10 @@ class AccountChartTemplate(models.AbstractModel): def _get_demo_data_transactions(self, company=False): cid = company.id or self.env.company.id bnk_journal = self.env['account.journal'].search( - domain=[('type', '=', 'bank'), ('company_id', '=', cid)], + domain=[ + *self.env['account.journal']._check_company_domain(cid), + ('type', '=', 'bank'), + ], limit=1, ) return { @@ -350,8 +356,10 @@ class AccountChartTemplate(models.AbstractModel): ('module', '=like', 'l10n%') ], limit=1).res_id) or self.env['account.account'].search([ + *self.env['account.account']._check_company_domain(company), ('account_type', '=', account_type), - ('company_id', '=', company.id) ], limit=1) - or self.env['account.account'].search([('company_id', '=', company.id)], limit=1) + or self.env['account.account'].search([ + *self.env['account.account']._check_company_domain(company), + ], limit=1) ) diff --git a/addons/account/models/account_account.py b/addons/account/models/account_account.py index e832c21e6a9..18facde6872 100644 --- a/addons/account/models/account_account.py +++ b/addons/account/models/account_account.py @@ -16,6 +16,7 @@ class AccountAccount(models.Model): _description = "Account" _order = "code, company_id" _check_company_auto = True + _check_company_domain = models.check_company_domain_parent_of @api.constrains('account_type', 'reconcile') def _check_reconcile(self): @@ -92,13 +93,18 @@ class AccountAccount(models.Model): check_company=True, context={'append_type_to_tax_name': True}) note = fields.Text('Internal Notes', tracking=True) - company_id = fields.Many2one('res.company', string='Company', required=True, readonly=True, + company_id = fields.Many2one('res.company', string='Company', required=True, readonly=False, default=lambda self: self.env.company) tag_ids = fields.Many2many('account.account.tag', 'account_account_account_tag', string='Tags', help="Optional tags you may want to assign for custom reporting", ondelete='restrict') group_id = fields.Many2one('account.group', compute='_compute_account_group', store=True, readonly=True, help="Account prefixes can determine account groups.") root_id = fields.Many2one('account.root', compute='_compute_account_root', store=True, precompute=True) - allowed_journal_ids = fields.Many2many('account.journal', string="Allowed Journals", help="Define in which journals this account can be used. If empty, can be used in all journals.") + allowed_journal_ids = fields.Many2many( + 'account.journal', + string="Allowed Journals", + help="Define in which journals this account can be used. If empty, can be used in all journals.", + check_company=True, + ) opening_debit = fields.Monetary(string="Opening Debit", compute='_compute_opening_debit_credit', inverse='_set_opening_debit') opening_credit = fields.Monetary(string="Opening Credit", compute='_compute_opening_debit_credit', inverse='_set_opening_credit') opening_balance = fields.Monetary(string="Opening Balance", compute='_compute_opening_debit_credit', inverse='_set_opening_balance') @@ -106,14 +112,25 @@ class AccountAccount(models.Model): current_balance = fields.Float(compute='_compute_current_balance') related_taxes_amount = fields.Integer(compute='_compute_related_taxes_amount') - _sql_constraints = [ - ('code_company_uniq', 'unique (code,company_id)', 'The code of the account must be unique per company!') - ] - non_trade = fields.Boolean(default=False, help="If set, this account will belong to Non Trade Receivable/Payable in reports and filters.\n" "If not, this account will belong to Trade Receivable/Payable in reports and filters.") + @api.constrains('company_id', 'code') + def _constrains_code(self): + domains = [] + for record in self: + domains.append([ + ('company_id', 'child_of', record.company_id.root_id.id), + ('code', '=', record.code), + ('id', '!=', record.id), + ]) + if duplicates := self.search(expression.OR(domains)): + raise ValidationError( + _("The code of the account must be unique per company!") + + "\n" + "\n".join(f"- {duplicate.code} in {duplicate.company_id.name}" for duplicate in duplicates) + ) + @api.constrains('reconcile', 'internal_group', 'tax_ids') def _constrains_reconcile(self): for record in self: @@ -213,20 +230,12 @@ class AccountAccount(models.Model): @api.constrains('company_id') def _check_company_consistency(self): - if not self: - return - - self.env['account.move.line'].flush_model(['account_id', 'company_id']) - self.flush_recordset(['company_id']) - self._cr.execute(''' - SELECT line.id - FROM account_move_line line - JOIN account_account account ON account.id = line.account_id - WHERE line.account_id IN %s - AND line.company_id != account.company_id - ''', [tuple(self.ids)]) - if self._cr.fetchone(): - raise UserError(_("You can't change the company of your account since there are some journal items linked to it.")) + for company, accounts in tools.groupby(self, lambda account: account.company_id): + if self.env['account.move.line'].search([ + ('account_id', 'in', [account.id for account in accounts]), + '!', ('company_id', 'child_of', company.id) + ], limit=1): + raise UserError(_("You can't change the company of your account since there are some journal items linked to it.")) @api.constrains('account_type') def _check_account_type_sales_purchase_journal(self): @@ -347,7 +356,7 @@ class AccountAccount(models.Model): new_code = str(prefix.ljust(digits - 1, '0')) + str(num) if new_code in (cache or []): continue - rec = self.search([('code', '=', new_code), ('company_id', '=', company.id)], limit=1) + rec = self.search([('code', '=', new_code), ('company_id', 'child_of', company.root_id.id)], limit=1) if not rec: return new_code raise UserError(_('Cannot generate an unused account code.')) @@ -531,28 +540,31 @@ class AccountAccount(models.Model): :param limit: the maximum number of accounts to retrieve :returns: List of account ids, ordered by frequency (from most to least frequent) """ - join = "INNER JOIN" if filter_never_user_accounts else "LEFT JOIN" - limit = f"LIMIT {limit:d}" if limit else "" - where_internal_group = "" + domain = [ + *self.env['account.move.line']._check_company_domain(company_id), + ('partner_id', '=', partner_id), + ('account_id.deprecated', '=', False), + ('date', '>=', fields.Date.add(fields.Date.today(), days=-365 * 2)), + ] if move_type in self.env['account.move'].get_inbound_types(include_receipts=True): - where_internal_group = "AND account.internal_group = 'income'" + domain.append(('account_id.internal_group', '=', 'income')) elif move_type in self.env['account.move'].get_outbound_types(include_receipts=True): - where_internal_group = "AND account.internal_group = 'expense'" + domain.append(('account_id.internal_group', '=', 'expense')) + + query = self.env['account.move.line']._where_calc(domain) + if not filter_never_user_accounts: + _kind, rhs_table, condition, condition_params = query._joins['account_move_line__account_id'] + query._joins['account_move_line__account_id'] = ("RIGHT JOIN", rhs_table, condition, condition_params) + + from_clause, where_clause, params = query.get_sql() self._cr.execute(f""" - SELECT account.id - FROM account_account account - {join} account_move_line aml - ON aml.account_id = account.id - AND aml.partner_id = %s - AND account.deprecated = FALSE - AND account.company_id = aml.company_id - AND aml.date >= now() - interval '2 years' - WHERE account.company_id = %s - {where_internal_group} - GROUP BY account.id - ORDER BY COUNT(aml.id) DESC, account.code - {limit} - """, [partner_id, company_id]) + SELECT account_move_line__account_id.id + FROM {from_clause} + WHERE {where_clause} + GROUP BY account_move_line__account_id.id + ORDER BY COUNT(account_move_line.id) DESC, account_move_line__account_id.code + {f"LIMIT {limit:d}" if limit else ""} + """, params) return [r[0] for r in self._cr.fetchall()] @api.model @@ -610,8 +622,10 @@ class AccountAccount(models.Model): try: default['code'] = (str(int(self.code) + 10) or '').zfill(len(self.code)) default.setdefault('name', _("%s (copy)") % (self.name or '')) - while self.env['account.account'].search([('code', '=', default['code']), - ('company_id', '=', default.get('company_id', False) or self.company_id.id)], limit=1): + while self.env['account.account'].search([ + *self.env['account.account']._check_company_domain(default.get('company_id', False) or self.company_id), + ('code', '=', default['code']), + ], limit=1): default['code'] = (str(int(default['code']) + 10) or '') default['name'] = _("%s (copy)") % (self.name or '') except ValueError: @@ -715,12 +729,6 @@ class AccountAccount(models.Model): return super().create(vals_list) def write(self, vals): - # Do not allow changing the company_id when account_move_line already exist - if vals.get('company_id', False): - move_lines = self.env['account.move.line'].search([('account_id', 'in', self.ids)], limit=1) - for account in self: - if (account.company_id.id != vals['company_id']) and move_lines: - raise UserError(_('You cannot change the owner company of an account that already contains journal items.')) if 'reconcile' in vals: if vals['reconcile']: self.filtered(lambda r: not r.reconcile)._toggle_reconcile_to_true() @@ -799,8 +807,10 @@ class AccountGroup(models.Model): _description = 'Account Group' _parent_store = True _order = 'code_prefix_start' + _check_company_auto = True + _check_company_domain = models.check_company_domain_parent_of - parent_id = fields.Many2one('account.group', index=True, ondelete='cascade', readonly=True) + parent_id = fields.Many2one('account.group', index=True, ondelete='cascade', readonly=True, check_company=True) parent_path = fields.Char(index=True, unaccent=False) name = fields.Char(required=True, translate=True) code_prefix_start = fields.Char(compute='_compute_code_prefix_start', readonly=False, store=True, precompute=True) @@ -906,7 +916,7 @@ class AccountGroup(models.Model): """ if self.env.context.get('delay_account_group_sync'): return - company_ids = account_ids.company_id.ids if account_ids else self.company_id.ids + company_ids = account_ids.company_id.root_id.ids if account_ids else self.company_id.ids account_ids = account_ids.ids if account_ids else [] if not company_ids and not account_ids: return @@ -925,10 +935,11 @@ class AccountGroup(models.Model): account.id AS account_id, agroup.id AS group_id FROM account_account account + JOIN res_company account_company ON account_company.id = account.company_id LEFT JOIN account_group agroup ON agroup.code_prefix_start <= LEFT(account.code, char_length(agroup.code_prefix_start)) AND agroup.code_prefix_end >= LEFT(account.code, char_length(agroup.code_prefix_end)) - AND agroup.company_id = account.company_id + AND agroup.company_id = split_part(account_company.parent_path, '/', 1)::int WHERE account.company_id IN %s {account_where_clause} ORDER BY account.id, char_length(agroup.code_prefix_start) DESC, agroup.id ) diff --git a/addons/account/models/account_analytic_distribution_model.py b/addons/account/models/account_analytic_distribution_model.py index 757fc861dc5..cdd752cebd5 100644 --- a/addons/account/models/account_analytic_distribution_model.py +++ b/addons/account/models/account_analytic_distribution_model.py @@ -14,6 +14,7 @@ class AccountAnalyticDistributionModel(models.Model): 'product.product', string='Product', ondelete='cascade', + check_company=True, help="Select a product for which the analytic distribution will be used (e.g. create new customer invoice or Sales order if we select this product, it will automatically take this as an analytic account)", ) product_categ_id = fields.Many2one( diff --git a/addons/account/models/account_analytic_line.py b/addons/account/models/account_analytic_line.py index fd504a55cc7..9180f812080 100644 --- a/addons/account/models/account_analytic_line.py +++ b/addons/account/models/account_analytic_line.py @@ -16,7 +16,8 @@ class AccountAnalyticLine(models.Model): 'account.account', string='Financial Account', ondelete='restrict', - domain="[('deprecated', '=', False), ('company_id', '=', company_id)]", + domain="[('deprecated', '=', False)]", + check_company=True, compute='_compute_general_account_id', store=True, readonly=False ) journal_id = fields.Many2one( diff --git a/addons/account/models/account_bank_statement_line.py b/addons/account/models/account_bank_statement_line.py index a1b6b08ffba..57080973671 100644 --- a/addons/account/models/account_bank_statement_line.py +++ b/addons/account/models/account_bank_statement_line.py @@ -453,7 +453,7 @@ class AccountBankStatementLine(models.Model): # Base domain. ('display_type', 'not in', ('line_section', 'line_note')), ('parent_state', '=', 'posted'), - ('company_id', '=', self.company_id.id), + ('company_id', 'child_of', self.company_id.root_id.id), # Reconciliation domain. ('reconciled', '=', False), ('account_id.reconcile', '=', True), @@ -469,8 +469,8 @@ class AccountBankStatementLine(models.Model): def _get_default_journal(self): journal_type = self.env.context.get('journal_type', 'bank') return self.env['account.journal'].search([ + *self.env['account.journal']._check_company_domain(self.env.company), ('type', '=', journal_type), - ('company_id', '=', self.env.company.id) ], limit=1) @api.model diff --git a/addons/account/models/account_cash_rounding.py b/addons/account/models/account_cash_rounding.py index b4cb8a23560..d73ada9c3cd 100644 --- a/addons/account/models/account_cash_rounding.py +++ b/addons/account/models/account_cash_rounding.py @@ -14,6 +14,7 @@ class AccountCashRounding(models.Model): """ _name = 'account.cash.rounding' _description = 'Account Cash Rounding' + _check_company_auto = True name = fields.Char(string='Name', translate=True, required=True) rounding = fields.Float(string='Rounding Precision', required=True, default=0.01, @@ -21,8 +22,18 @@ class AccountCashRounding(models.Model): strategy = fields.Selection([('biggest_tax', 'Modify tax amount'), ('add_invoice_line', 'Add a rounding line')], string='Rounding Strategy', default='add_invoice_line', required=True, help='Specify which way will be used to round the invoice amount to the rounding precision') - profit_account_id = fields.Many2one('account.account', string='Profit Account', company_dependent=True, domain="[('deprecated', '=', False), ('company_id', '=', current_company_id)]") - loss_account_id = fields.Many2one('account.account', string='Loss Account', company_dependent=True, domain="[('deprecated', '=', False), ('company_id', '=', current_company_id)]") + profit_account_id = fields.Many2one( + 'account.account', + string='Profit Account', + company_dependent=True, + domain="[('deprecated', '=', False)]", + ) + loss_account_id = fields.Many2one( + 'account.account', + string='Loss Account', + company_dependent=True, + domain="[('deprecated', '=', False)]", + ) rounding_method = fields.Selection(string='Rounding Method', required=True, selection=[('UP', 'UP'), ('DOWN', 'DOWN'), ('HALF-UP', 'HALF-UP')], default='HALF-UP', help='The tie-breaking rule used for float rounding operations') diff --git a/addons/account/models/account_journal.py b/addons/account/models/account_journal.py index 485b707b85f..459b4dc0778 100644 --- a/addons/account/models/account_journal.py +++ b/addons/account/models/account_journal.py @@ -2,7 +2,7 @@ from odoo import api, Command, fields, models, _ from odoo.exceptions import UserError, ValidationError from odoo.addons.base.models.res_bank import sanitize_account_number -from odoo.tools import remove_accents +from odoo.tools import remove_accents, groupby from collections import defaultdict import logging import re @@ -21,10 +21,11 @@ class AccountJournalGroup(models.Model): _name = 'account.journal.group' _description = "Account Journal Group" _check_company_auto = True + _check_company_domain = models.check_company_domain_parent_of name = fields.Char("Journal Group", required=True, translate=True) company_id = fields.Many2one('res.company', required=True, default=lambda self: self.env.company) - excluded_journal_ids = fields.Many2many('account.journal', string="Excluded Journals", domain="[('company_id', '=', company_id)]", + excluded_journal_ids = fields.Many2many('account.journal', string="Excluded Journals", check_company=True) sequence = fields.Integer(default=10) @@ -38,6 +39,7 @@ class AccountJournal(models.Model): _order = 'sequence, type, code' _inherit = ['mail.thread', 'mail.activity.mixin', 'portal.mixin'] _check_company_auto = True + _check_company_domain = models.check_company_domain_parent_of _rec_names_search = ['name', 'code'] def _default_inbound_payment_methods(self): @@ -86,20 +88,20 @@ class AccountJournal(models.Model): "Select 'General' for miscellaneous operations journals.") account_control_ids = fields.Many2many('account.account', 'journal_account_control_rel', 'journal_id', 'account_id', string='Allowed accounts', check_company=True, - domain="[('deprecated', '=', False), ('company_id', '=', company_id), ('account_type', '!=', 'off_balance')]") + domain="[('deprecated', '=', False), ('account_type', '!=', 'off_balance')]") default_account_type = fields.Char(string='Default Account Type', compute="_compute_default_account_type") default_account_id = fields.Many2one( comodel_name='account.account', check_company=True, copy=False, ondelete='restrict', string='Default Account', - domain="[('deprecated', '=', False), ('company_id', '=', company_id)," + domain="[('deprecated', '=', False)," "('account_type', '=', default_account_type), ('account_type', 'not in', ('asset_receivable', 'liability_payable'))]") suspense_account_id = fields.Many2one( comodel_name='account.account', check_company=True, ondelete='restrict', readonly=False, store=True, compute='_compute_suspense_account_id', help="Bank statements transactions will be posted on the suspense account until the final reconciliation " "allowing finding the right account.", string='Suspense Account', - domain="[('deprecated', '=', False), ('company_id', '=', company_id), \ - ('account_type', '=', 'asset_current')]") + domain="[('deprecated', '=', False), ('account_type', '=', 'asset_current')]", + ) restrict_mode_hash_table = fields.Boolean(string="Lock Posted Entries with Hash", help="If ticked, the accounting entry or invoice receives a hash as soon as it is posted and cannot be modified anymore.") sequence = fields.Integer(help='Used to order Journals in the dashboard view', default=10) @@ -158,13 +160,13 @@ class AccountJournal(models.Model): comodel_name='account.account', check_company=True, help="Used to register a profit when the ending balance of a cash register differs from what the system computes", string='Profit Account', - domain="[('deprecated', '=', False), ('company_id', '=', company_id), \ + domain="[('deprecated', '=', False), \ ('account_type', 'in', ('income', 'income_other'))]") loss_account_id = fields.Many2one( comodel_name='account.account', check_company=True, help="Used to register a loss when the ending balance of a cash register differs from what the system computes", string='Loss Account', - domain="[('deprecated', '=', False), ('company_id', '=', company_id), \ + domain="[('deprecated', '=', False), \ ('account_type', '=', 'expense')]") # Bank journals fields @@ -173,7 +175,7 @@ class AccountJournal(models.Model): string="Bank Account", ondelete='restrict', copy=False, check_company=True, - domain="[('partner_id','=', company_partner_id), '|', ('company_id', '=', False), ('company_id', '=', company_id)]") + domain="[('partner_id','=', company_partner_id)]") bank_statements_source = fields.Selection(selection=_get_bank_statements_available_sources, string='Bank Feeds', default='undefined', help="Defines how the bank statements will be registered") bank_acc_number = fields.Char(related='bank_account_id.acc_number', readonly=False) bank_id = fields.Many2one('res.bank', related='bank_account_id.bank_id', readonly=False) @@ -191,7 +193,6 @@ class AccountJournal(models.Model): alias_name = fields.Char('Alias Name', copy=False, compute='_compute_alias_name', inverse='_inverse_type', help="It creates draft invoices and bills by sending an email.") journal_group_ids = fields.Many2many('account.journal.group', - domain="[('company_id', '=', company_id)]", check_company=True, string="Journal Groups") @@ -432,20 +433,12 @@ class AccountJournal(models.Model): @api.constrains('company_id') def _check_company_consistency(self): - if not self: - return - - self.env['account.move'].flush_model(['company_id', 'journal_id']) - self.flush_recordset(['company_id']) - self._cr.execute(''' - SELECT move.id - FROM account_move move - JOIN account_journal journal ON journal.id = move.journal_id - WHERE move.journal_id IN %s - AND move.company_id != journal.company_id - ''', [tuple(self.ids)]) - if self._cr.fetchone(): - raise UserError(_("You can't change the company of your journal since there are some journal entries linked to it.")) + for company, journals in groupby(self, lambda journal: journal.company_id): + if self.env['account.move'].search([ + ('journal_id', 'in', [journal.id for journal in journals]), + '!', ('company_id', 'child_of', company.id) + ], limit=1): + raise UserError(_("You can't change the company of your journal since there are some journal entries linked to it.")) @api.constrains('type', 'default_account_id') def _check_type_default_account_id_type(self): @@ -529,7 +522,10 @@ class AccountJournal(models.Model): default = dict(default or {}) # Find a unique code for the copied journal - read_codes = self.env['account.journal'].with_context(active_test=False).search_read([('company_id', '=', self.company_id.id)], ['code']) + read_codes = self.env['account.journal'].with_context(active_test=False).search_read( + self.env['account.journal']._check_company_domain(self.company_id), + ['code'], + ) all_journal_codes = {code_data['code'] for code_data in read_codes} copy_code = self.code @@ -556,8 +552,6 @@ class AccountJournal(models.Model): for journal in self: company = journal.company_id if ('company_id' in vals and journal.company_id.id != vals['company_id']): - if self.env['account.move'].search([('journal_id', '=', journal.id)], limit=1): - raise UserError(_('This journal already contains items, therefore you cannot modify its company.')) company = self.env['res.company'].browse(vals['company_id']) if journal.bank_account_id.company_id and journal.bank_account_id.company_id != company: journal.bank_account_id.write({ @@ -598,8 +592,8 @@ class AccountJournal(models.Model): def get_next_bank_cash_default_code(self, journal_type, company, cache=None): journal_code_base = (journal_type == 'cash' and 'CSH' or 'BNK') existing_codes = set(self.env['account.journal'].with_context(active_test=False).search([ + *self.env['account.journal']._check_company_domain(company), ('code', 'like', journal_code_base + '%'), - ('company_id', '=', company.id), ]).mapped('code') + (cache or [])) for num in range(1, 100): # journal_code has a maximal size of 5, hence we can enforce the boundary num < 100 @@ -634,7 +628,10 @@ class AccountJournal(models.Model): vals['company_id'] = company.id # Don't get the digits on 'chart_template' since the chart template could be a custom one. - random_account = self.env['account.account'].search([('company_id', '=', company.id)], limit=1) + random_account = self.env['account.account'].search( + self.env['account.account']._check_company_domain(company), + limit=1, + ) digits = len(random_account.code) if random_account else 6 if journal_type in ('bank', 'cash'): @@ -729,7 +726,8 @@ class AccountJournal(models.Model): else: raise UserError(_("The journal in which to upload the invoice is not specified. ")) self = self.env['account.journal'].search([ - ('company_id', '=', self.env.company.id), ('type', '=', journal_type) + *self.env['account.journal']._check_company_domain(self.env.company), + ('type', '=', journal_type), ], limit=1) attachments = self.env['ir.attachment'].browse(attachment_ids) diff --git a/addons/account/models/account_journal_dashboard.py b/addons/account/models/account_journal_dashboard.py index 9f491afe40a..4a151b87e4a 100644 --- a/addons/account/models/account_journal_dashboard.py +++ b/addons/account/models/account_journal_dashboard.py @@ -78,8 +78,14 @@ class account_journal(models.Model): JOIN mail_activity activity ON activity.res_id = move.id AND activity.res_model = 'account.move' LEFT JOIN mail_activity_type act_type ON activity.activity_type_id = act_type.id WHERE move.journal_id = ANY(%(ids)s) + AND move.company_id = ANY(%(company_ids)s) """ - self.env.cr.execute(sql_query, {'ids': self.ids, 'today': today, 'lang': lang}) + self.env.cr.execute(sql_query, { + 'ids': self.ids, + 'company_ids': self.env.companies.ids, + 'today': today, + 'lang': lang, + }) for activity in self.env.cr.dictfetchall(): act = { 'id': activity['id'], @@ -335,14 +341,15 @@ class account_journal(models.Model): SELECT journal.id, statement.id FROM account_journal journal LEFT JOIN LATERAL ( - SELECT id + SELECT id, company_id FROM account_bank_statement WHERE journal_id = journal.id ORDER BY first_line_index DESC LIMIT 1 ) statement ON TRUE WHERE journal.id = ANY(%s) - """, [self.ids]) + AND statement.company_id = ANY(%s) + """, [self.ids, self.env.companies.ids]) last_statements = {journal_id: statement_id for journal_id, statement_id in self.env.cr.fetchall()} self.env['account.bank.statement'].browse(i for i in last_statements.values() if i).mapped('balance_end_real') # prefetch @@ -463,6 +470,7 @@ class account_journal(models.Model): def _get_open_bills_to_pay_query(self): return self.env['account.move']._where_calc([ + *self.env['account.move']._check_company_domain(self.env.companies), ('journal_id', 'in', self.ids), ('state', '=', 'posted'), ('payment_state', 'in', ('not_paid', 'partial')), @@ -471,6 +479,7 @@ class account_journal(models.Model): def _get_draft_bills_query(self): return self.env['account.move']._where_calc([ + *self.env['account.move']._check_company_domain(self.env.companies), ('journal_id', 'in', self.ids), ('state', '=', 'draft'), ('payment_state', 'in', ('not_paid', 'partial')), @@ -479,6 +488,7 @@ class account_journal(models.Model): def _get_late_bills_query(self): return self.env['account.move']._where_calc([ + *self.env['account.move']._check_company_domain(self.env.companies), ('journal_id', 'in', self.ids), ('invoice_date_due', '<', fields.Date.context_today(self)), ('state', '=', 'posted'), diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 5272de36776..07fb4fbc6b0 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -329,7 +329,7 @@ class AccountMove(models.Model): comodel_name='res.partner', string='Delivery Address', compute='_compute_partner_shipping_id', store=True, readonly=False, precompute=True, - domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", + check_company=True, help="The delivery address will be used in the computation of the fiscal position.", ) partner_bank_id = fields.Many2one( @@ -349,7 +349,6 @@ class AccountMove(models.Model): check_company=True, compute='_compute_fiscal_position_id', store=True, readonly=False, precompute=True, states={'posted': [('readonly', True)], 'cancel': [('readonly', True)]}, - domain="[('company_id', '=', company_id)]", ondelete="restrict", help="Fiscal positions are used to adapt taxes and accounts for particular " "customers or sales orders/invoices. The default value comes from the customer.", @@ -678,9 +677,8 @@ class AccountMove(models.Model): @api.depends('journal_id') def _compute_company_id(self): for move in self: - company_id = move.journal_id.company_id or self.env.company - if company_id != move.company_id: - move.company_id = company_id + if move.journal_id.company_id not in move.company_id.parent_ids: + move.company_id = (move.journal_id.company_id or self.env.company)._accessible_branches()[:1] @api.depends('move_type') def _compute_journal_id(self): @@ -705,15 +703,18 @@ class AccountMove(models.Model): return self.statement_line_ids.statement_id.journal_id[:1] journal_types = self._get_valid_journal_types() - company_id = (self.company_id or self.env.company).id - domain = [('company_id', '=', company_id), ('type', 'in', journal_types)] + company = self.company_id or self.env.company + domain = [ + *self.env['account.journal']._check_company_domain(company), + ('type', 'in', journal_types), + ] journal = None # the currency is not a hard dependence, it triggers via manual add_to_compute # avoid computing the currency before all it's dependences are set (like the journal...) if self.env.cache.contains(self, self._fields['currency_id']): currency_id = self.currency_id.id or self._context.get('default_currency_id') - if currency_id and currency_id != self.company_id.currency_id.id: + if currency_id and currency_id != company.currency_id.id: currency_domain = domain + [('currency_id', '=', currency_id)] journal = self.env['account.journal'].search(currency_domain, limit=1) @@ -721,8 +722,6 @@ class AccountMove(models.Model): journal = self.env['account.journal'].search(domain, limit=1) if not journal: - company = self.env['res.company'].browse(company_id) - error_msg = _( "No journal could be found in company %(company_name)s for any of those types: %(journal_types)s", company_name=company.display_name, @@ -741,9 +740,11 @@ class AccountMove(models.Model): def _compute_suitable_journal_ids(self): for m in self: journal_type = m.invoice_filter_type_domain or 'general' - company_id = m.company_id.id or self.env.company.id - domain = [('company_id', '=', company_id), ('type', '=', journal_type)] - m.suitable_journal_ids = self.env['account.journal'].search(domain) + company = m.company_id or self.env.company + m.suitable_journal_ids = self.env['account.journal'].search([ + *self.env['account.journal']._check_company_domain(company), + ('type', '=', journal_type), + ]) @api.depends('posted_before', 'state', 'journal_id', 'date', 'move_type', 'payment_id') def _compute_name(self): @@ -1176,7 +1177,7 @@ class AccountMove(models.Model): if move.state == 'posted' and move.is_invoice(include_receipts=True): reconciled_vals = [] - reconciled_partials = move._get_all_reconciled_invoice_partials() + reconciled_partials = move.sudo()._get_all_reconciled_invoice_partials() for reconciled_partial in reconciled_partials: counterpart_line = reconciled_partial['aml'] if counterpart_line.move_id.ref: @@ -1654,7 +1655,7 @@ class AccountMove(models.Model): @api.onchange('company_id') def _inverse_company_id(self): self._conditional_add_to_compute('journal_id', lambda m: ( - m.journal_id.company_id != m.company_id + not m.journal_id.filtered_domain(self.env['account.journal']._check_company_domain(m.company_id)) )) @api.onchange('currency_id') @@ -2621,9 +2622,9 @@ class AccountMove(models.Model): domain += [('move_type', 'in' if self.move_type in refund_types else 'not in', refund_types)] if self.journal_id.payment_sequence: domain += [('payment_id', '!=' if self.payment_id else '=', False)] - reference_move_name = self.search(domain + [('date', '<=', self.date)], order='date desc', limit=1).name + reference_move_name = self.sudo().search(domain + [('date', '<=', self.date)], order='date desc', limit=1).name if not reference_move_name: - reference_move_name = self.search(domain, order='date asc', limit=1).name + reference_move_name = self.sudo().search(domain, order='date asc', limit=1).name sequence_number_reset = self._deduce_sequence_number_reset(reference_move_name) if sequence_number_reset == 'year': where_string += " AND date_trunc('year', date::timestamp without time zone) = date_trunc('year', %(date)s) " @@ -2732,38 +2733,37 @@ class AccountMove(models.Model): """ if not partner_id: return 0, False, False - where_internal_group = "" + domain = [ + *self.env['account.move.line']._check_company_domain(company_id), + ('partner_id', '=', partner_id), + ('account_id.deprecated', '=', False), + ('date', '>=', date.today() - timedelta(days=365 * 2)), + ] if move_type in self.env['account.move'].get_inbound_types(include_receipts=True): - where_internal_group = "AND account.internal_group = 'income'" + domain.append(('account_id.internal_group', '=', 'income')) elif move_type in self.env['account.move'].get_outbound_types(include_receipts=True): - where_internal_group = "AND account.internal_group = 'expense'" + domain.append(('account_id.internal_group', '=', 'expense')) + + query = self.env['account.move.line']._where_calc(domain) + from_clause, where_clause, params = query.get_sql() self._cr.execute(f""" - SELECT - COUNT(foo.id), foo.account_id, foo.taxes - FROM - ( - SELECT - account.id AS account_id, - account.code, - aml.id, - ARRAY_AGG(tax_rel.account_tax_id) AS taxes - FROM account_account account - LEFT JOIN account_move_line aml - ON (account.id = aml.account_id - AND aml.partner_id = %s - AND aml.date >= now() - interval '2 years') - LEFT JOIN account_move_line_account_tax_rel tax_rel ON (aml.id = tax_rel.account_move_line_id) - WHERE - account.company_id = %s - AND account.deprecated = FALSE - {where_internal_group} - GROUP BY account.id, account.code, aml.id - ) AS foo - GROUP BY foo.account_id, foo.code, foo.taxes - ORDER BY COUNT(foo.id) DESC, foo.code - LIMIT 1 - """, [partner_id, company_id]) - return self._cr.fetchone() + SELECT COUNT(foo.id), foo.account_id, foo.taxes + FROM ( + SELECT account_move_line__account_id.id AS account_id, + account_move_line__account_id.code, + account_move_line.id, + ARRAY_AGG(tax_rel.account_tax_id) AS taxes + FROM {from_clause} + LEFT JOIN account_move_line_account_tax_rel tax_rel ON account_move_line.id = tax_rel.account_move_line_id + WHERE {where_clause} + GROUP BY account_move_line__account_id.id, + account_move_line.id + ) AS foo + GROUP BY foo.account_id, foo.code, foo.taxes + ORDER BY COUNT(foo.id) DESC, foo.code + LIMIT 1 + """, params) + return self._cr.fetchone() or (0, False, False) def _get_quick_edit_suggestions(self): """ diff --git a/addons/account/models/account_move_line.py b/addons/account/models/account_move_line.py index 5dbfaa9d27c..81deecbb2ce 100644 --- a/addons/account/models/account_move_line.py +++ b/addons/account/models/account_move_line.py @@ -84,7 +84,7 @@ class AccountMoveLine(models.Model): index=True, auto_join=True, ondelete="cascade", - domain="[('deprecated', '=', False), ('company_id', '=', company_id), ('account_type', '!=', 'off_balance')]", + domain="[('deprecated', '=', False), ('account_type', '!=', 'off_balance')]", check_company=True, tracking=True, ) @@ -178,6 +178,7 @@ class AccountMoveLine(models.Model): comodel_name='account.tax', string="Originator Group of Taxes", index='btree_not_null', + check_company=True, ) tax_line_id = fields.Many2one( comodel_name='account.tax', @@ -295,6 +296,7 @@ class AccountMoveLine(models.Model): string='Product', inverse='_inverse_product_id', ondelete='restrict', + check_company=True, ) product_uom_id = fields.Many2one( comodel_name='uom.uom', @@ -850,17 +852,18 @@ class AccountMoveLine(models.Model): def _get_computed_taxes(self): self.ensure_one() + company_domain = self.env['account.tax']._check_company_domain(self.move_id.company_id) if self.move_id.is_sale_document(include_receipts=True): # Out invoice. if self.product_id.taxes_id: - tax_ids = self.product_id.taxes_id.filtered(lambda tax: tax.company_id == self.move_id.company_id) + tax_ids = self.product_id.taxes_id.filtered_domain(company_domain) else: tax_ids = self.account_id.tax_ids.filtered(lambda tax: tax.type_tax_use == 'sale') elif self.move_id.is_purchase_document(include_receipts=True): # In invoice. if self.product_id.supplier_taxes_id: - tax_ids = self.product_id.supplier_taxes_id.filtered(lambda tax: tax.company_id == self.move_id.company_id) + tax_ids = self.product_id.supplier_taxes_id.filtered_domain(company_domain) else: tax_ids = self.account_id.tax_ids.filtered(lambda tax: tax.type_tax_use == 'purchase') @@ -868,7 +871,7 @@ class AccountMoveLine(models.Model): tax_ids = self.account_id.tax_ids if self.company_id and tax_ids: - tax_ids = tax_ids.filtered(lambda tax: tax.company_id == self.company_id) + tax_ids = tax_ids.filtered_domain(company_domain) if tax_ids and self.move_id.fiscal_position_id: tax_ids = self.move_id.fiscal_position_id.map_tax(tax_ids) @@ -1192,10 +1195,10 @@ class AccountMoveLine(models.Model): def _check_tax_lock_date(self): for line in self.filtered(lambda l: l.move_id.state == 'posted'): move = line.move_id - if move.company_id.tax_lock_date and move.date <= move.company_id.tax_lock_date and line._affect_tax_report(): + if move.company_id.max_tax_lock_date and move.date <= move.company_id.max_tax_lock_date and line._affect_tax_report(): raise UserError(_("The operation is refused as it would impact an already issued tax statement. " "Please change the journal entry date or the tax lock date set in the settings (%s) to proceed.") - % format_date(self.env, move.company_id.tax_lock_date)) + % format_date(self.env, move.company_id.max_tax_lock_date)) def _check_reconciliation(self): for line in self: @@ -2026,7 +2029,7 @@ class AccountMoveLine(models.Model): "Entries are not from the same account: %s", ", ".join(accounts.mapped('display_name')), )) - if len(self.company_id) > 1: + if len(self.company_id.root_id) > 1: raise UserError(_( "Entries don't belong to the same company: %s", ", ".join(self.company_id.mapped('display_name')), @@ -2417,7 +2420,10 @@ class AccountMoveLine(models.Model): * to_reconcile: A list of tuple in order to perform the reconciliation after the move creation. """ - company = self.company_id or company + company = ( + (self.move_id.filtered(lambda m: m.is_invoice(True)) or self.move_id).company_id + or company + )[:1] if not company: return diff --git a/addons/account/models/account_partial_reconcile.py b/addons/account/models/account_partial_reconcile.py index 2ed70e1dfd9..8a871028f04 100644 --- a/addons/account/models/account_partial_reconcile.py +++ b/addons/account/models/account_partial_reconcile.py @@ -54,7 +54,7 @@ class AccountPartialReconcile(models.Model): company_id = fields.Many2one( comodel_name='res.company', string="Company", store=True, readonly=False, - related='debit_move_id.company_id') + compute='_compute_company_id') max_date = fields.Date( string="Max Date of Matched Lines", store=True, compute='_compute_max_date') @@ -82,6 +82,15 @@ class AccountPartialReconcile(models.Model): partial.credit_move_id.date ) + @api.depends('debit_move_id', 'credit_move_id') + def _compute_company_id(self): + for partial in self: + # Potential exchange diff and caba entries should be created on the invoice side if any + if partial.debit_move_id.move_id.is_invoice(True): + partial.company_id = partial.debit_move_id.company_id + else: + partial.company_id = partial.credit_move_id.company_id + # ------------------------------------------------------------------------- # LOW-LEVEL METHODS # ------------------------------------------------------------------------- diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py index 4306cdca2c6..85e239c4984 100644 --- a/addons/account/models/account_payment.py +++ b/addons/account/models/account_payment.py @@ -105,12 +105,12 @@ class AccountPayment(models.Model): string='Destination Account', store=True, readonly=False, compute='_compute_destination_account_id', - domain="[('account_type', 'in', ('asset_receivable', 'liability_payable')), ('company_id', '=', company_id)]", + domain="[('account_type', 'in', ('asset_receivable', 'liability_payable'))]", check_company=True) destination_journal_id = fields.Many2one( comodel_name='account.journal', string='Destination Journal', - domain="[('type', 'in', ('bank','cash')), ('company_id', '=', company_id), ('id', '!=', journal_id)]", + domain="[('type', 'in', ('bank','cash')), ('id', '!=', journal_id)]", check_company=True, ) @@ -458,7 +458,8 @@ class AccountPayment(models.Model): Get all journals having at least one payment method for inbound/outbound depending on the payment_type. """ journals = self.env['account.journal'].search([ - ('company_id', 'in', self.company_id.ids), ('type', 'in', ('bank', 'cash')) + *self.env['account.journal']._check_company_domain(self.company_id), + ('type', 'in', ('bank', 'cash')), ]) for pay in self: if pay.payment_type == 'inbound': @@ -514,7 +515,7 @@ class AccountPayment(models.Model): pay.destination_account_id = pay.partner_id.with_company(pay.company_id).property_account_receivable_id else: pay.destination_account_id = self.env['account.account'].search([ - ('company_id', '=', pay.company_id.id), + *self.env['account.account']._check_company_domain(pay.company_id), ('account_type', '=', 'asset_receivable'), ('deprecated', '=', False), ], limit=1) @@ -524,7 +525,7 @@ class AccountPayment(models.Model): pay.destination_account_id = pay.partner_id.with_company(pay.company_id).property_account_payable_id else: pay.destination_account_id = self.env['account.account'].search([ - ('company_id', '=', pay.company_id.id), + *self.env['account.account']._check_company_domain(pay.company_id), ('account_type', '=', 'liability_payable'), ('deprecated', '=', False), ], limit=1) diff --git a/addons/account/models/account_payment_method.py b/addons/account/models/account_payment_method.py index 371b1bf6321..ed5cb399208 100644 --- a/addons/account/models/account_payment_method.py +++ b/addons/account/models/account_payment_method.py @@ -108,10 +108,13 @@ class AccountPaymentMethodLine(models.Model): copy=False, ondelete='restrict', domain="[('deprecated', '=', False), " - "('company_id', '=', company_id), " "'|', ('account_type', 'in', ('asset_current', 'liability_current')), ('id', '=', parent.default_account_id)]" ) - journal_id = fields.Many2one(comodel_name='account.journal', ondelete="cascade") + journal_id = fields.Many2one( + comodel_name='account.journal', + ondelete="cascade", + check_company=True, + ) # == Display purpose fields == code = fields.Char(related='payment_method_id.code') diff --git a/addons/account/models/account_reconcile_model.py b/addons/account/models/account_reconcile_model.py index f62bb3a40df..3cd8fdb0e6f 100644 --- a/addons/account/models/account_reconcile_model.py +++ b/addons/account/models/account_reconcile_model.py @@ -11,9 +11,11 @@ from dateutil.relativedelta import relativedelta class AccountReconcileModelPartnerMapping(models.Model): _name = 'account.reconcile.model.partner.mapping' _description = 'Partner mapping for reconciliation models' + _check_company_auto = True model_id = fields.Many2one(comodel_name='account.reconcile.model', readonly=True, required=True, ondelete='cascade') - partner_id = fields.Many2one(comodel_name='res.partner', string="Partner", required=True, ondelete='cascade') + company_id = fields.Many2one(related='model_id.company_id') + partner_id = fields.Many2one(comodel_name='res.partner', string="Partner", required=True, ondelete='cascade', check_company=True) payment_ref_regex = fields.Char(string="Find Text in Label") narration_regex = fields.Char(string="Find Text in Notes") @@ -48,12 +50,12 @@ class AccountReconcileModelLine(models.Model): company_id = fields.Many2one(related='model_id.company_id', store=True) sequence = fields.Integer(required=True, default=10) account_id = fields.Many2one('account.account', string='Account', ondelete='cascade', - domain="[('deprecated', '=', False), ('company_id', '=', company_id), ('account_type', '!=', 'off_balance')]", + domain="[('deprecated', '=', False), ('account_type', '!=', 'off_balance')]", required=True, check_company=True) # This field is ignored in a bank statement reconciliation. journal_id = fields.Many2one('account.journal', string='Journal', ondelete='cascade', - domain="[('type', '=', 'general'), ('company_id', '=', company_id)]", check_company=True) + domain="[('type', '=', 'general')]", check_company=True) label = fields.Char(string='Journal Item Label') amount_type = fields.Selection([ ('fixed', 'Fixed'), @@ -169,7 +171,7 @@ class AccountReconcileModel(models.Model): tracking=True, ) match_journal_ids = fields.Many2many('account.journal', string='Journals Availability', - domain="[('type', 'in', ('bank', 'cash')), ('company_id', '=', company_id)]", + domain="[('type', 'in', ('bank', 'cash'))]", check_company=True, help='The reconciliation model will only be available from the selected journals.') match_nature = fields.Selection(selection=[ diff --git a/addons/account/models/account_report.py b/addons/account/models/account_report.py index 28a02c57a65..1a8a916b030 100644 --- a/addons/account/models/account_report.py +++ b/addons/account/models/account_report.py @@ -773,7 +773,7 @@ class AccountReportExternalValue(models.Model): foreign_vat_fiscal_position_id = fields.Many2one( string="Fiscal position", comodel_name='account.fiscal.position', - domain="[('company_id', '=', company_id), ('country_id', '=', report_country_id), ('foreign_vat', '!=', False)]", + domain="[('country_id', '=', report_country_id), ('foreign_vat', '!=', False)]", check_company=True, help="The foreign fiscal position for which this external value is made.", ) diff --git a/addons/account/models/account_tax.py b/addons/account/models/account_tax.py index 0424a147781..33ec630a9d1 100644 --- a/addons/account/models/account_tax.py +++ b/addons/account/models/account_tax.py @@ -4,7 +4,7 @@ from odoo.osv import expression from odoo.tools.float_utils import float_round from odoo.exceptions import UserError, ValidationError from odoo.tools.misc import formatLang -from odoo.tools import frozendict +from odoo.tools import frozendict, groupby from collections import defaultdict import math @@ -22,6 +22,8 @@ class AccountTaxGroup(models.Model): _name = 'account.tax.group' _description = 'Tax Group' _order = 'sequence asc' + _check_company_auto = True + _check_company_domain = models.check_company_domain_parent_of name = fields.Char(required=True, translate=True) sequence = fields.Integer(default=10) @@ -68,7 +70,7 @@ class AccountTaxGroup(models.Model): of these countries, in this company """ return bool(self.env['account.tax'].search([ - ('company_id', '=', company.id), + *self.env['account.tax']._check_company_domain(company), ('country_id', 'in', countries.ids), '|', ('tax_group_id.tax_payable_account_id', '=', False), @@ -82,6 +84,7 @@ class AccountTax(models.Model): _order = 'sequence,id' _check_company_auto = True _rec_names_search = ['name', 'description', 'invoice_label'] + _check_company_domain = models.check_company_domain_parent_of name = fields.Char(string='Tax Name', required=True, translate=True) name_searchable = fields.Char(store=False, search='_search_name', @@ -139,7 +142,7 @@ class AccountTax(models.Model): "Based on Payment: the tax is due as soon as the payment of the invoice is received.") cash_basis_transition_account_id = fields.Many2one(string="Cash Basis Transition Account", check_company=True, - domain="[('deprecated', '=', False), ('company_id', '=', company_id)]", + domain="[('deprecated', '=', False)]", comodel_name='account.account', help="Account used to transition the tax amount for cash basis taxes. It will contain the tax amount as long as the original invoice has not been reconciled ; at reconciliation, this amount cancelled on this account and put on the regular tax account.") invoice_repartition_line_ids = fields.One2many( @@ -172,9 +175,23 @@ class AccountTax(models.Model): ) country_code = fields.Char(related='country_id.code', readonly=True) - _sql_constraints = [ - ('name_company_uniq', 'unique(name, company_id, type_tax_use, tax_scope)', 'Tax names must be unique!'), - ] + @api.constrains('company_id', 'name', 'type_tax_use', 'tax_scope') + def _constrains_name(self): + domains = [] + for record in self: + if record.type_tax_use != 'none': + domains.append([ + ('company_id', 'child_of', record.company_id.root_id.id), + ('name', '=', record.name), + ('type_tax_use', '=', record.type_tax_use), + ('tax_scope', '=', record.tax_scope), + ('id', '!=', record.id), + ]) + if duplicates := self.search(expression.OR(domains)): + raise ValidationError( + _("Tax names must be unique!") + + "\n" + "\n".join(f"- {duplicate.name} in {duplicate.company_id.name}" for duplicate in duplicates) + ) @api.constrains('tax_group_id') def validate_tax_group_id(self): @@ -199,10 +216,10 @@ class AccountTax(models.Model): by_country_company[(tax.country_id, tax.company_id)] += tax for (country, company), taxes in by_country_company.items(): taxes.tax_group_id = self.env['account.tax.group'].search([ - ('company_id', '=', company.id), + *self.env['account.tax.group']._check_company_domain(company), ('country_id', '=', country.id), ], limit=1) or self.env['account.tax.group'].search([ - ('company_id', '=', company.id), + *self.env['account.tax.group']._check_company_domain(company), ('country_id', '=', False), ], limit=1) @@ -308,29 +325,14 @@ class AccountTax(models.Model): @api.constrains('company_id') def _check_company_consistency(self): - if not self: - return - - self.env['account.move.line'].flush_model(['company_id', 'tax_line_id']) - self.flush_recordset(['company_id']) - self._cr.execute(''' - SELECT line.id - FROM account_move_line line - JOIN account_tax tax ON tax.id = line.tax_line_id - WHERE line.tax_line_id IN %s - AND line.company_id != tax.company_id - - UNION ALL - - SELECT line.id - FROM account_move_line_account_tax_rel tax_rel - JOIN account_tax tax ON tax.id = tax_rel.account_tax_id - JOIN account_move_line line ON line.id = tax_rel.account_move_line_id - WHERE tax_rel.account_tax_id IN %s - AND line.company_id != tax.company_id - ''', [tuple(self.ids)] * 2) - if self._cr.fetchone(): - raise UserError(_("You can't change the company of your tax since there are some journal items linked to it.")) + for company, taxes in groupby(self, lambda tax: tax.company_id): + if self.env['account.move.line'].search([ + '|', + ('tax_line_id', 'in', [tax.id for tax in taxes]), + ('tax_ids', 'in', [tax.id for tax in taxes]), + '!', ('company_id', 'child_of', company.id) + ], limit=1): + raise UserError(_("You can't change the company of your tax since there are some journal items linked to it.")) def _sanitize_vals(self, vals): """Normalize the create/write values.""" @@ -522,7 +524,7 @@ class AccountTax(models.Model): if not self: company = self.env.company else: - company = self[0].company_id + company = self[0].company_id._accessible_branches()[:1] # 1) Flatten the taxes. taxes, groups_map = self.flatten_taxes_hierarchy(create_map=True) @@ -1341,6 +1343,7 @@ class AccountTaxRepartitionLine(models.Model): _description = "Tax Repartition Line" _order = 'sequence, repartition_type, id' _check_company_auto = True + _check_company_domain = models.check_company_domain_parent_of factor_percent = fields.Float( string="%", @@ -1353,7 +1356,7 @@ class AccountTaxRepartitionLine(models.Model): document_type = fields.Selection(string="Related to", selection=[('invoice', 'Invoice'), ('refund', 'Refund')], required=True) account_id = fields.Many2one(string="Account", comodel_name='account.account', - domain="[('deprecated', '=', False), ('company_id', '=', company_id), ('account_type', 'not in', ('asset_receivable', 'liability_payable'))]", + domain="[('deprecated', '=', False), ('account_type', 'not in', ('asset_receivable', 'liability_payable'))]", check_company=True, help="Account on which to post the tax amount") tag_ids = fields.Many2many(string="Tax Grids", comodel_name='account.account.tag', domain=[('applicability', '=', 'taxes')], copy=True, ondelete='restrict') diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py index a315a8aaf00..099f474f0d4 100644 --- a/addons/account/models/chart_template.py +++ b/addons/account/models/chart_template.py @@ -174,10 +174,15 @@ class AccountChartTemplate(models.AbstractModel): if not reload_template: for model in ('account.move',) + TEMPLATE_MODELS[::-1]: - self.env[model].sudo().search([('company_id', '=', company.id)]).with_context({MODULE_UNINSTALL_FLAG: True}).unlink() + if not company.parent_id: + self.env[model].sudo().search([('company_id', 'child_of', company.id)]).with_context({MODULE_UNINSTALL_FLAG: True}).unlink() data = self._get_chart_template_data(template_code) template_data = data.pop('template_data') + if company.parent_id: + data = { + 'res.company': data['res.company'], + } if reload_template: self._pre_reload_data(company, template_data, data) @@ -201,6 +206,8 @@ class AccountChartTemplate(models.AbstractModel): except Exception: # Do not rollback installation of CoA if demo data failed _logger.exception('Error while loading accounting demo data') + for subsidiary in company.child_ids: + self._load(template_code, subsidiary, install_demo) def _pre_reload_data(self, company, template_data, data): """Pre-process the data in case of reloading the chart of accounts. @@ -221,15 +228,15 @@ class AccountChartTemplate(models.AbstractModel): else: if 'code' in journal_data: journal = self.env['account.journal'].with_context(active_test=False).search([ + *self.env['account.journal']._check_company_domain(company), ('code', '=', journal_data['code']), - ('company_id', '=', company.id), ]) # Try to match by journal name to avoid conflict in the unique constraint on the mail alias if not journal and 'name' in journal_data and 'type' in journal_data: journal = self.env['account.journal'].with_context(active_test=False).search([ + *self.env['account.journal']._check_company_domain(company), ('type', '=', journal_data['type']), ('name', '=', journal_data['name']), - ('company_id', '=', company.id), ], limit=1) if journal: del data['account.journal'][xmlid] @@ -239,11 +246,13 @@ class AccountChartTemplate(models.AbstractModel): 'noupdate': True, }]) - account_group_count = self.env['account.group'].search_count([('company_id', '=', company.id)]) + account_group_count = self.env['account.group'].search_count([]) if account_group_count: data.pop('account.group', None) - current_taxes = self.env['account.tax'].search([('company_id', '=', company.id)]) + current_taxes = self.env['account.tax'].search([ + *self.env['account.tax']._check_company_domain(company), + ]) unique_tax_name_key = lambda t: (t.name, t.type_tax_use, t.tax_scope, t.company_id) unique_tax_name_keys = set(current_taxes.mapped(unique_tax_name_key)) xmlid2tax = { @@ -314,8 +323,8 @@ class AccountChartTemplate(models.AbstractModel): account = self.ref(xmlid, raise_if_not_found=False) if not account or (account and account.code != values['code']): existing_account = self.env['account.account'].search([ + *self.env['account.account']._check_company_domain(company), ('code', '=', values['code']), - ('company_id', '=', company.id), ]) if existing_account: self.env['ir.model.data']._update_xmlids([{ @@ -525,12 +534,14 @@ class AccountChartTemplate(models.AbstractModel): # Set default Purchase and Sale taxes on the company if not company.account_sale_tax_id: company.account_sale_tax_id = self.env['account.tax'].search([ - ('type_tax_use', 'in', ('sale', 'all')), ('company_id', '=', company.id)], limit=1).id + *self.env['account.tax']._check_company_domain(company), + ('type_tax_use', 'in', ('sale', 'all'))], limit=1).id if not company.account_purchase_tax_id: company.account_purchase_tax_id = self.env['account.tax'].search([ - ('type_tax_use', 'in', ('purchase', 'all')), ('company_id', '=', company.id)], limit=1).id + *self.env['account.tax']._check_company_domain(company), + ('type_tax_use', 'in', ('purchase', 'all'))], limit=1).id # Display caba fields if there are caba taxes - if self.env['account.tax'].search([('tax_exigibility', '=', 'on_payment')]): + if not company.parent_id and self.env['account.tax'].search([('tax_exigibility', '=', 'on_payment')]): company.tax_exigibility = True for field, model in { @@ -633,10 +644,13 @@ class AccountChartTemplate(models.AbstractModel): for fname in list(accounts_data): if company[fname]: del accounts_data[fname] - - accounts = self.env['account.account'].create(accounts_data.values()) - for company_attr_name, account in zip(accounts_data.keys(), accounts): - company[company_attr_name] = account + if company.parent_id: + for company_attr_name in accounts_data: + company[company_attr_name] = company.parent_ids[0][company_attr_name] + else: + accounts = self.env['account.account'].create(accounts_data.values()) + for company_attr_name, account in zip(accounts_data.keys(), accounts): + company[company_attr_name] = account @api.model def _instantiate_foreign_taxes(self, country, company): @@ -655,8 +669,8 @@ class AccountChartTemplate(models.AbstractModel): # - Creates tax group and taxes with their ir.model.data taxes_in_country = self.env['account.tax'].search([ + *self.env['account.tax']._check_company_domain(company), ('country_id', '=', country.id), - ('company_id', '=', company.id) ]) if taxes_in_country: return @@ -693,6 +707,7 @@ class AccountChartTemplate(models.AbstractModel): if account_template_xml_id in existing_accounts: continue local_tax_group = self.env["account.tax.group"].search([ + *self.env['account.tax.group']._check_company_domain(company), ('country_id', '=', company.account_fiscal_country_id.id), (field, '!=', False), ], limit=1) @@ -710,7 +725,7 @@ class AccountChartTemplate(models.AbstractModel): sign_comparator = '<' if float(foreign_tax_rep_line.get('factor_percent', 100)) < 0 else '>' minimal_domain = [ - ('company_id', '=', company.id), + *self.env['account.tax.repartition.line']._check_company_domain(company), ('account_id', '!=', False), ('factor_percent', sign_comparator, 0), ] @@ -735,6 +750,7 @@ class AccountChartTemplate(models.AbstractModel): # Try to create cash basis account if not mapped local_cash_basis_tax = self.env["account.tax"].search([ + *self.env['account.tax']._check_company_domain(company), ('country_id', '=', company.account_fiscal_country_id.id), ('cash_basis_transition_account_id', '!=', False) ], limit=1) @@ -900,7 +916,12 @@ class AccountChartTemplate(models.AbstractModel): # -------------------------------------------------------------------------------- def ref(self, xmlid, raise_if_not_found=True): - return self.env.ref(f"account.{self.env.company.id}_{xmlid}" if xmlid and '.' not in xmlid else xmlid, raise_if_not_found) + if '.' in xmlid: + return self.env.ref(xmlid, raise_if_not_found) + return ( + self.env.ref(f"account.{self.env.company.id}_{xmlid}", raise_if_not_found=False) + or self.env.ref(f"account.{self.env.company.parent_ids[0].id}_{xmlid}", raise_if_not_found) + ) def _get_parent_template(self, code): parents = [] diff --git a/addons/account/models/company.py b/addons/account/models/company.py index c705106109c..471bcdb1a6b 100644 --- a/addons/account/models/company.py +++ b/addons/account/models/company.py @@ -47,22 +47,24 @@ class ResCompany(models.Model): string="Tax Return Lock Date", tracking=True, help="No users can edit journal entries related to a tax prior and inclusive of this date.") + max_tax_lock_date = fields.Date(compute='_compute_max_tax_lock_date', recursive=True) # TODO maybe store transfer_account_id = fields.Many2one('account.account', + check_company=True, domain="[('reconcile', '=', True), ('account_type', '=', 'asset_current'), ('deprecated', '=', False)]", string="Inter-Banks Transfer Account", help="Intermediary account used when moving money from a liqity account to another") expects_chart_of_accounts = fields.Boolean(string='Expects a Chart of Accounts', default=True) chart_template = fields.Selection(selection='_chart_template_selection') bank_account_code_prefix = fields.Char(string='Prefix of the bank accounts') cash_account_code_prefix = fields.Char(string='Prefix of the cash accounts') - default_cash_difference_income_account_id = fields.Many2one('account.account', string="Cash Difference Income") - default_cash_difference_expense_account_id = fields.Many2one('account.account', string="Cash Difference Expense") - account_journal_suspense_account_id = fields.Many2one('account.account', string='Journal Suspense Account') - account_journal_payment_debit_account_id = fields.Many2one('account.account', string='Journal Outstanding Receipts') - account_journal_payment_credit_account_id = fields.Many2one('account.account', string='Journal Outstanding Payments') - account_journal_early_pay_discount_gain_account_id = fields.Many2one(comodel_name='account.account', string='Cash Discount Write-Off Gain Account') - account_journal_early_pay_discount_loss_account_id = fields.Many2one(comodel_name='account.account', string='Cash Discount Write-Off Loss Account') + default_cash_difference_income_account_id = fields.Many2one('account.account', string="Cash Difference Income", check_company=True) + default_cash_difference_expense_account_id = fields.Many2one('account.account', string="Cash Difference Expense", check_company=True) + account_journal_suspense_account_id = fields.Many2one('account.account', string='Journal Suspense Account', check_company=True) + account_journal_payment_debit_account_id = fields.Many2one('account.account', string='Journal Outstanding Receipts', check_company=True) + account_journal_payment_credit_account_id = fields.Many2one('account.account', string='Journal Outstanding Payments', check_company=True) + account_journal_early_pay_discount_gain_account_id = fields.Many2one(comodel_name='account.account', string='Cash Discount Write-Off Gain Account', check_company=True) + account_journal_early_pay_discount_loss_account_id = fields.Many2one(comodel_name='account.account', string='Cash Discount Write-Off Loss Account', check_company=True) transfer_account_code_prefix = fields.Char(string='Prefix of the transfer accounts') - account_sale_tax_id = fields.Many2one('account.tax', string="Default Sale Tax") - account_purchase_tax_id = fields.Many2one('account.tax', string="Default Purchase Tax") + account_sale_tax_id = fields.Many2one('account.tax', string="Default Sale Tax", check_company=True) + account_purchase_tax_id = fields.Many2one('account.tax', string="Default Purchase Tax", check_company=True) tax_calculation_rounding_method = fields.Selection([ ('round_per_line', 'Round per Line'), ('round_globally', 'Round Globally'), @@ -71,12 +73,14 @@ class ResCompany(models.Model): income_currency_exchange_account_id = fields.Many2one( comodel_name='account.account', string="Gain Exchange Rate Account", - domain="[('deprecated', '=', False), ('company_id', '=', id), \ + check_company=True, + domain="[('deprecated', '=', False),\ ('account_type', 'in', ('income', 'income_other'))]") expense_currency_exchange_account_id = fields.Many2one( comodel_name='account.account', string="Loss Exchange Rate Account", - domain="[('deprecated', '=', False), ('company_id', '=', id), \ + check_company=True, + domain="[('deprecated', '=', False), \ ('account_type', '=', 'expense')]") anglo_saxon_accounting = fields.Boolean(string="Use anglo-saxon accounting") bank_journal_ids = fields.One2many('account.journal', 'company_id', domain=[('type', '=', 'bank')], string='Bank Journals') @@ -104,16 +108,23 @@ class ResCompany(models.Model): compute='_compute_invoice_terms_html', store=True, readonly=False) # Needed in the Point of Sale - account_default_pos_receivable_account_id = fields.Many2one('account.account', string="Default PoS Receivable Account") + account_default_pos_receivable_account_id = fields.Many2one('account.account', string="Default PoS Receivable Account", check_company=True) # Accrual Accounting expense_accrual_account_id = fields.Many2one('account.account', help="Account used to move the period of an expense", - domain="[('internal_group', '=', 'liability'), ('account_type', 'not in', ('asset_receivable', 'liability_payable')), ('company_id', '=', id)]") + check_company=True, + domain="[('internal_group', '=', 'liability'), ('account_type', 'not in', ('asset_receivable', 'liability_payable'))]") revenue_accrual_account_id = fields.Many2one('account.account', help="Account used to move the period of a revenue", - domain="[('internal_group', '=', 'asset'), ('account_type', 'not in', ('asset_receivable', 'liability_payable')), ('company_id', '=', id)]") - automatic_entry_default_journal_id = fields.Many2one('account.journal', help="Journal used by default for moving the period of an entry", domain="[('type', '=', 'general')]") + check_company=True, + domain="[('internal_group', '=', 'asset'), ('account_type', 'not in', ('asset_receivable', 'liability_payable'))]") + automatic_entry_default_journal_id = fields.Many2one( + 'account.journal', + domain="[('type', '=', 'general')]", + check_company=True, + help="Journal used by default for moving the period of an entry", + ) # Technical field to hide country specific fields in company form view country_code = fields.Char(related='country_id.code', depends=['country_id']) @@ -138,9 +149,11 @@ class ResCompany(models.Model): tax_exigibility = fields.Boolean(string='Use Cash Basis') tax_cash_basis_journal_id = fields.Many2one( comodel_name='account.journal', + check_company=True, string="Cash Basis Journal") account_cash_basis_base_account_id = fields.Many2one( comodel_name='account.account', + check_company=True, domain=[('deprecated', '=', False)], string="Base Tax Received Account", help="Account that will be set on lines created in cash basis journal entry and used to keep track of the " @@ -166,6 +179,14 @@ class ResCompany(models.Model): ('out_and_in_invoices', 'Customer Invoices and Vendor Bills')], string="Quick encoding") + def _get_company_root_delegated_field_names(self): + return super()._get_company_root_delegated_field_names() + [ + 'fiscalyear_last_day', + 'fiscalyear_last_month', + 'account_storno', + 'tax_exigibility', + ] + @api.constrains('account_opening_move_id', 'fiscalyear_last_day', 'fiscalyear_last_month') def _check_fiscalyear_last_day(self): # if the user explicitly chooses the 29th of February we allow it: @@ -188,7 +209,10 @@ class ResCompany(models.Model): company_to_foreign_vat_country = { 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)], + domain=[ + *self.env['account.fiscal.position']._check_company_domain(self), + ('foreign_vat', '!=', False), + ], groupby=['company_id'], aggregates=['country_id:array_agg'], ) @@ -205,7 +229,14 @@ class ResCompany(models.Model): @api.depends('account_fiscal_country_id') def _compute_account_enabled_tax_country_ids(self): for record in self: - foreign_vat_fpos = self.env['account.fiscal.position'].search([('company_id', '=', record.id), ('foreign_vat', '!=', False)]) + if record not in self.env.user.company_ids: + # can have access to the company form without having access to its content (see base.res_company_rule_erp_manager) + record.account_enabled_tax_country_ids = False + continue + foreign_vat_fpos = self.env['account.fiscal.position'].search([ + *self.env['account.fiscal.position']._check_company_domain(record), + ('foreign_vat', '!=', False) + ]) record.account_enabled_tax_country_ids = foreign_vat_fpos.country_id + record.account_fiscal_country_id @api.depends('terms_type') @@ -217,6 +248,25 @@ class ResCompany(models.Model): if html: company.invoice_terms_html = html + @api.depends('parent_id.max_tax_lock_date') + def _compute_max_tax_lock_date(self): + for company in self: + company.max_tax_lock_date = max(company.tax_lock_date or date.min, company.parent_id.max_tax_lock_date or date.min) + + @api.model_create_multi + def create(self, vals_list): + companies = super().create(vals_list) + for company in companies: + if root_template := company.parent_ids[0].chart_template: + def try_loading(company=company): + self.env['account.chart.template']._load( + root_template, + company, + install_demo=False, + ) + self.env.cr.precommit.add(try_loading) + return companies + def get_new_account_code(self, current_code, old_prefix, new_prefix): digits = len(current_code) return new_prefix + current_code.replace(old_prefix, '', 1).lstrip('0').rjust(digits-len(new_prefix), '0') @@ -225,9 +275,9 @@ class ResCompany(models.Model): if not old_code: return accounts = self.env['account.account'].search([ + *self.env['account.account']._check_company_domain(self), ('code', '=like', old_code + '%'), ('account_type', 'in', ('asset_cash', 'liability_credit_card')), - ('company_id', '=', self.id) ], order='code asc') for account in accounts: account.write({'code': self.get_new_account_code(account.code, old_code, new_code)}) @@ -262,7 +312,7 @@ class ResCompany(models.Model): if values.get('fiscalyear_lock_date'): draft_entries = self.env['account.move'].search([ - ('company_id', 'in', self.ids), + ('company_id', 'child_of', self.ids), ('state', '=', 'draft'), ('date', '<=', values['fiscalyear_lock_date'])]) if draft_entries: @@ -279,7 +329,7 @@ class ResCompany(models.Model): raise RedirectWarning(error_msg, action_error, _('Show unposted entries')) unreconciled_statement_lines = self.env['account.bank.statement.line'].search([ - ('company_id', 'in', self.ids), + ('company_id', 'child_of', self.ids), ('is_reconciled', '=', False), ('date', '<=', values['fiscalyear_lock_date']), ('move_id.state', 'in', ('draft', 'posted')), @@ -292,12 +342,11 @@ class ResCompany(models.Model): def _get_user_fiscal_lock_date(self): """Get the fiscal lock date for this company depending on the user""" - if not self: - return date.min - self.ensure_one() lock_date = max(self.period_lock_date or date.min, self.fiscalyear_lock_date or date.min) if self.user_has_groups('account.group_account_manager'): lock_date = self.fiscalyear_lock_date or date.min + if self.parent_id: + lock_date = max(lock_date, self.parent_id._get_user_fiscal_lock_date()) return lock_date def _get_violated_lock_dates(self, accounting_date, has_tax): @@ -311,7 +360,7 @@ class ResCompany(models.Model): user_lock_date = self._get_user_fiscal_lock_date() if accounting_date and user_lock_date and accounting_date <= user_lock_date: locks.append((user_lock_date, _('user'))) - tax_lock_date = self.tax_lock_date + tax_lock_date = self.max_tax_lock_date if accounting_date and tax_lock_date and has_tax and accounting_date <= tax_lock_date: locks.append((tax_lock_date, _('tax'))) locks.sort() @@ -359,7 +408,10 @@ class ResCompany(models.Model): """ self.ensure_one() if not self.account_opening_move_id: - default_journal = self.env['account.journal'].search([('type', '=', 'general'), ('company_id', '=', self.id)], limit=1) + default_journal = self.env['account.journal'].search([ + *self.env['account.journal']._check_company_domain(self), + ('type', '=', 'general') + ], limit=1) if not default_journal: raise UserError(_("Please install a chart of accounts or create a miscellaneous journal before proceeding.")) @@ -382,14 +434,19 @@ class ResCompany(models.Model): if none has yet been defined. """ unaffected_earnings_type = "equity_unaffected" - account = self.env['account.account'].search([('company_id', '=', self.id), - ('account_type', '=', unaffected_earnings_type)]) + account = self.env['account.account'].search([ + *self.env['account.account']._check_company_domain(self), + ('account_type', '=', unaffected_earnings_type), + ]) if account: return account[0] # Do not assume '999999' doesn't exist since the user might have created such an account # manually. code = 999999 - while self.env['account.account'].search([('code', '=', str(code)), ('company_id', '=', self.id)]): + while self.env['account.account'].search([ + *self.env['account.account']._check_company_domain(self), + ('code', '=', str(code)), + ]): code -= 1 return self.env['account.account'].create({ 'code': str(code), @@ -454,7 +511,7 @@ class ResCompany(models.Model): self.env['onboarding.onboarding.step'].action_validate_step('account.onboarding_onboarding_step_sales_tax') def get_chart_of_accounts_or_fail(self): - account = self.env['account.account'].search([('company_id', '=', self.id)], limit=1) + account = self.env['account.account'].search(self.env['account.account']._check_company_domain(self), limit=1) if len(account) == 0: action = self.env.ref('account.action_account_config') msg = _( @@ -466,7 +523,7 @@ class ResCompany(models.Model): def _existing_accounting(self) -> bool: """Return True iff some accounting entries have already been made for the current company.""" self.ensure_one() - return bool(self.env['account.move.line'].search([('company_id', '=', self.id)], order="id", limit=1)) + return bool(self.env['account.move.line'].search([('company_id', 'child_of', self.root_id.id)], limit=1)) def _chart_template_selection(self): return self.env['account.chart.template']._select_chart_template(self.country_id) @@ -485,7 +542,7 @@ class ResCompany(models.Model): def build_move_info(move): return(move.name, move.inalterable_hash, fields.Date.to_string(move.date)) - journals = self.env['account.journal'].search([('company_id', '=', self.id)]) + journals = self.env['account.journal'].search(self.env['account.journal']._check_company_domain(self)) results_by_journal = { 'results': [], 'printing_date': format_date(self.env, fields.Date.to_string(fields.Date.context_today(self))) diff --git a/addons/account/models/ir_module.py b/addons/account/models/ir_module.py index 48adb7bd55d..8592b7e0bbc 100644 --- a/addons/account/models/ir_module.py +++ b/addons/account/models/ir_module.py @@ -63,7 +63,12 @@ class IrModule(models.Model): res = super().write(vals) is_installed = len(self) == 1 and self.state == 'installed' if not was_installed and is_installed and not self.env.company.chart_template and self.account_templates: - self.env.registry._auto_install_template = next(iter(self.account_templates)) + def try_loading(env): + env['account.chart.template'].try_loading( + next(iter(self.account_templates)), + env.company, + ) + self.env.registry._auto_install_template = try_loading return res def _load_module_terms(self, modules, langs, overwrite=False): @@ -82,10 +87,7 @@ class IrModule(models.Model): self.env.registry._delayed_account_translator(self.env) del self.env.registry._delayed_account_translator if hasattr(self.env.registry, '_auto_install_template'): - self.env['account.chart.template'].try_loading( - self.env.registry._auto_install_template, - self.env.company, - ) + self.env.registry._auto_install_template(self.env) del self.env.registry._auto_install_template def module_uninstall(self): diff --git a/addons/account/models/onboarding_onboarding_step.py b/addons/account/models/onboarding_onboarding_step.py index df74c659eb0..e0f4fe11229 100644 --- a/addons/account/models/onboarding_onboarding_step.py +++ b/addons/account/models/onboarding_onboarding_step.py @@ -105,7 +105,10 @@ class OnboardingStep(models.Model): # Then, we open will open a custom tree view allowing to edit opening balances of the account view_id = self.env.ref('account.init_accounts_tree').id # Hide the current year earnings account as it is automatically computed - domain = [('account_type', '!=', 'equity_unaffected'), ('company_id', '=', company.id)] + domain = [ + *self.env['account.account']._check_company_domain(company), + ('account_type', '!=', 'equity_unaffected'), + ] return { 'type': 'ir.actions.act_window', 'name': _('Chart of Accounts'), diff --git a/addons/account/models/partner.py b/addons/account/models/partner.py index e9a5a47e11c..62c64ee6be1 100644 --- a/addons/account/models/partner.py +++ b/addons/account/models/partner.py @@ -19,6 +19,8 @@ class AccountFiscalPosition(models.Model): _name = 'account.fiscal.position' _description = 'Fiscal Position' _order = 'sequence' + _check_company_auto = True + _check_company_domain = models.check_company_domain_parent_of sequence = fields.Integer() name = fields.Char(string='Fiscal Position', required=True, translate=True) @@ -92,9 +94,9 @@ class AccountFiscalPosition(models.Model): raise ValidationError(_("You cannot create a fiscal position with a foreign VAT within your fiscal country.")) similar_fpos_domain = [ + *self.env['account.fiscal.position']._check_company_domain(record.company_id), ('foreign_vat', '!=', False), ('country_id', '=', record.country_id.id), - ('company_id', '=', record.company_id.id), ('id', '!=', record.id), ] if record.state_ids: @@ -174,9 +176,9 @@ class AccountFiscalPosition(models.Model): if not country_id: return False base_domain = [ + *self._check_company_domain(self.env.company), ('auto_apply', '=', True), ('vat_required', '=', vat_required), - ('company_id', 'in', [self.env.company.id, False]), ] null_state_dom = state_domain = [('state_ids', '=', False)] null_zip_dom = zip_domain = [('zip_from', '=', False), ('zip_to', '=', False)] @@ -263,6 +265,7 @@ class AccountFiscalPositionTax(models.Model): _description = 'Tax Mapping of Fiscal Position' _rec_name = 'position_id' _check_company_auto = True + _check_company_domain = models.check_company_domain_parent_of position_id = fields.Many2one('account.fiscal.position', string='Fiscal Position', required=True, ondelete='cascade') @@ -282,16 +285,17 @@ class AccountFiscalPositionAccount(models.Model): _description = 'Accounts Mapping of Fiscal Position' _rec_name = 'position_id' _check_company_auto = True + _check_company_domain = models.check_company_domain_parent_of position_id = fields.Many2one('account.fiscal.position', string='Fiscal Position', required=True, ondelete='cascade') company_id = fields.Many2one('res.company', string='Company', related='position_id.company_id', store=True) account_src_id = fields.Many2one('account.account', string='Account on Product', check_company=True, required=True, - domain="[('deprecated', '=', False), ('company_id', '=', company_id)]") + domain="[('deprecated', '=', False)]") account_dest_id = fields.Many2one('account.account', string='Account to Use Instead', check_company=True, required=True, - domain="[('deprecated', '=', False), ('company_id', '=', company_id)]") + domain="[('deprecated', '=', False)]") _sql_constraints = [ ('account_src_dest_uniq', @@ -326,7 +330,7 @@ class ResPartner(models.Model): def _credit_debit_get(self): tables, where_clause, where_params = self.env['account.move.line']._where_calc([ ('parent_state', '=', 'posted'), - ('company_id', '=', self.env.company.id) + ('company_id', 'child_of', self.env.company.root_id.id) ]).get_sql() where_params = [tuple(self.ids)] + where_params @@ -371,17 +375,21 @@ class ResPartner(models.Model): sign = 1 if account_type == 'liability_payable': sign = -1 - res = self._cr.execute(''' - SELECT partner.id - FROM res_partner partner - LEFT JOIN account_move_line aml ON aml.partner_id = partner.id - JOIN account_move move ON move.id = aml.move_id - RIGHT JOIN account_account acc ON aml.account_id = acc.id - WHERE acc.account_type = %s - AND NOT acc.deprecated AND acc.company_id = %s - AND move.state = 'posted' - GROUP BY partner.id - HAVING %s * COALESCE(SUM(aml.amount_residual), 0) ''' + operator + ''' %s''', (account_type, self.env.company.id, sign, operand)) + res = self._cr.execute(f''' + SELECT aml.partner_id + FROM res_partner partner + LEFT JOIN account_move_line aml ON aml.partner_id = partner.id + JOIN account_move move ON move.id = aml.move_id + JOIN res_company line_company ON line_company.id = line.company_id + RIGHT JOIN account_account acc ON aml.account_id = acc.id + WHERE acc.account_type = %s + AND NOT acc.deprecated + AND SPLIT_PART(line_company.parent_path, '/', 1):int == %s + AND move.state = 'posted' + GROUP BY aml.partner_id + HAVING %s * COALESCE(SUM(aml.amount_residual), 0) {operator} %s''', + (account_type, self.env.company.root_id.id, sign, operand) + ) res = self._cr.fetchall() if not res: return [('id', '=', '0')] @@ -521,25 +529,22 @@ class ResPartner(models.Model): journal_item_count = fields.Integer(compute='_compute_journal_item_count', string="Journal Items") property_account_payable_id = fields.Many2one('account.account', company_dependent=True, string="Account Payable", - domain="[('account_type', '=', 'liability_payable'), ('deprecated', '=', False), ('company_id', '=', current_company_id)]", + domain="[('account_type', '=', 'liability_payable'), ('deprecated', '=', False)]", help="This account will be used instead of the default one as the payable account for the current partner", required=True) property_account_receivable_id = fields.Many2one('account.account', company_dependent=True, string="Account Receivable", - domain="[('account_type', '=', 'asset_receivable'), ('deprecated', '=', False), ('company_id', '=', current_company_id)]", + domain="[('account_type', '=', 'asset_receivable'), ('deprecated', '=', False)]", help="This account will be used instead of the default one as the receivable account for the current partner", required=True) property_account_position_id = fields.Many2one('account.fiscal.position', company_dependent=True, string="Fiscal Position", - domain="[('company_id', '=', current_company_id)]", help="The fiscal position determines the taxes/accounts used for this contact.") property_payment_term_id = fields.Many2one('account.payment.term', company_dependent=True, string='Customer Payment Terms', - domain="[('company_id', 'in', [current_company_id, False])]", help="This payment term will be used instead of the default one for sales orders and customer invoices") property_supplier_payment_term_id = fields.Many2one('account.payment.term', company_dependent=True, string='Vendor Payment Terms', - domain="[('company_id', 'in', [current_company_id, False])]", help="This payment term will be used instead of the default one for purchase orders and vendor bills") ref_company_ids = fields.One2many('res.company', 'partner_id', string='Companies that refers to partner') @@ -849,8 +854,9 @@ class ResPartner(models.Model): return None return self.env['res.partner'].search(domain + extra_domain, limit=1) + company = company or self.env.company for search_method in (search_with_vat, search_with_domain, search_with_phone_mail, search_with_name): - for extra_domain in ([('company_id', '=', (company or self.env.company).id)], []): + for extra_domain in (self.env['res.partner']._check_company_domain(company), []): partner = search_method(extra_domain) if partner: return partner diff --git a/addons/account/models/product.py b/addons/account/models/product.py index 9297ee2cfdd..c91679f90ff 100644 --- a/addons/account/models/product.py +++ b/addons/account/models/product.py @@ -26,10 +26,14 @@ class ProductTemplate(models.Model): _inherit = "product.template" taxes_id = fields.Many2many('account.tax', 'product_taxes_rel', 'prod_id', 'tax_id', help="Default taxes used when selling the product.", string='Customer Taxes', - domain=[('type_tax_use', '=', 'sale')], default=lambda self: self.env.company.account_sale_tax_id) + domain=[('type_tax_use', '=', 'sale')], + default=lambda self: self.env.company.account_sale_tax_id or self.env.company.root_id.account_sale_tax_id, + ) tax_string = fields.Char(compute='_compute_tax_string') supplier_taxes_id = fields.Many2many('account.tax', 'product_supplier_taxes_rel', 'prod_id', 'tax_id', string='Vendor Taxes', help='Default taxes used when buying the product.', - domain=[('type_tax_use', '=', 'purchase')], default=lambda self: self.env.company.account_purchase_tax_id) + domain=[('type_tax_use', '=', 'purchase')], + default=lambda self: self.env.company.account_purchase_tax_id or self.env.company.root_id.account_purchase_tax_id, + ) property_account_income_id = fields.Many2one('account.account', company_dependent=True, string="Income Account", domain=ACCOUNT_DOMAIN, @@ -229,6 +233,6 @@ class ProductProduct(models.Model): domain = expression.AND([ expression.OR(domains), - [('company_id', 'in', [False, company or self.env.company.id])], + self.env['product.product']._check_company_domain(company), ]) return self.env['product.product'].search(domain, limit=1) diff --git a/addons/account/models/res_config_settings.py b/addons/account/models/res_config_settings.py index 725706c11a5..af56aa2f108 100644 --- a/addons/account/models/res_config_settings.py +++ b/addons/account/models/res_config_settings.py @@ -14,26 +14,39 @@ class ResConfigSettings(models.TransientModel): comodel_name='account.journal', related='company_id.currency_exchange_journal_id', readonly=False, string="Currency Exchange Journal", - domain="[('company_id', '=', company_id), ('type', '=', 'general')]", + check_company=True, + domain="[('type', '=', 'general')]", help='The accounting journal where automatic exchange differences will be registered') income_currency_exchange_account_id = fields.Many2one( comodel_name="account.account", related="company_id.income_currency_exchange_account_id", string="Gain Exchange Rate Account", readonly=False, - domain="[('deprecated', '=', False), ('company_id', '=', company_id),\ - ('account_type', 'in', ('income', 'income_other'))]") + check_company=True, + domain="[('deprecated', '=', False), ('account_type', 'in', ('income', 'income_other'))]") expense_currency_exchange_account_id = fields.Many2one( comodel_name="account.account", related="company_id.expense_currency_exchange_account_id", string="Loss Exchange Rate Account", readonly=False, - domain="[('deprecated', '=', False), ('company_id', '=', company_id),\ - ('account_type', '=', 'expense')]") + check_company=True, + domain="[('deprecated', '=', False), ('account_type', '=', 'expense')]") has_chart_of_accounts = fields.Boolean(compute='_compute_has_chart_of_accounts', string='Company has a chart of accounts') chart_template = fields.Selection(selection=lambda self: self.env.company._chart_template_selection(), default=lambda self: self.env.company.chart_template) - sale_tax_id = fields.Many2one('account.tax', string="Default Sale Tax", related='company_id.account_sale_tax_id', readonly=False) - purchase_tax_id = fields.Many2one('account.tax', string="Default Purchase Tax", related='company_id.account_purchase_tax_id', readonly=False) + sale_tax_id = fields.Many2one( + 'account.tax', + string="Default Sale Tax", + related='company_id.account_sale_tax_id', + readonly=False, + check_company=True, + ) + purchase_tax_id = fields.Many2one( + 'account.tax', + string="Default Purchase Tax", + related='company_id.account_purchase_tax_id', + readonly=False, + check_company=True, + ) tax_calculation_rounding_method = fields.Selection( related='company_id.tax_calculation_rounding_method', string='Tax calculation rounding method', readonly=False) account_journal_suspense_account_id = fields.Many2one( @@ -41,7 +54,8 @@ class ResConfigSettings(models.TransientModel): string='Bank Suspense', readonly=False, related='company_id.account_journal_suspense_account_id', - domain="[('deprecated', '=', False), ('company_id', '=', company_id), ('account_type', 'in', ('asset_current', 'liability_current'))]", + check_company=True, + domain="[('deprecated', '=', False), ('account_type', 'in', ('asset_current', 'liability_current'))]", help='Bank Transactions are posted immediately after import or synchronization. ' 'Their counterparty is the bank suspense account.\n' 'Reconciliation replaces the latter by the definitive account(s).') @@ -49,8 +63,9 @@ class ResConfigSettings(models.TransientModel): comodel_name='account.account', string='Outstanding Receipts', readonly=False, + check_company=True, related='company_id.account_journal_payment_debit_account_id', - domain="[('deprecated', '=', False), ('company_id', '=', company_id), ('account_type', '=', 'asset_current')]", + domain="[('deprecated', '=', False), ('account_type', '=', 'asset_current')]", help='Incoming payments are posted on an Outstanding Receipts Account. ' 'In the bank reconciliation widget, they appear as blue lines.\n' 'Bank transactions are then reconciled on the Outstanding Receipts Accounts rather than the Receivable ' @@ -59,13 +74,15 @@ class ResConfigSettings(models.TransientModel): comodel_name='account.account', string='Outstanding Payments', readonly=False, + check_company=True, related='company_id.account_journal_payment_credit_account_id', - domain="[('deprecated', '=', False), ('company_id', '=', company_id), ('account_type', '=', 'asset_current')]", + domain="[('deprecated', '=', False), ('account_type', '=', 'asset_current')]", help='Outgoing Payments are posted on an Outstanding Payments Account. ' 'In the bank reconciliation widget, they appear as blue lines.\n' 'Bank transactions are then reconciled on the Outstanding Payments Account rather the Payable Account.') transfer_account_id = fields.Many2one('account.account', string="Internal Transfer", related='company_id.transfer_account_id', readonly=False, + check_company=True, domain=[ ('reconcile', '=', True), ('account_type', '=', 'asset_current'), @@ -100,11 +117,18 @@ class ResConfigSettings(models.TransientModel): module_account_invoice_extract = fields.Boolean(string="Document Digitization") module_snailmail_account = fields.Boolean(string="Snailmail") tax_exigibility = fields.Boolean(string='Cash Basis', related='company_id.tax_exigibility', readonly=False) - tax_cash_basis_journal_id = fields.Many2one('account.journal', related='company_id.tax_cash_basis_journal_id', string="Tax Cash Basis Journal", readonly=False) + tax_cash_basis_journal_id = fields.Many2one( + 'account.journal', + string="Tax Cash Basis Journal", + related='company_id.tax_cash_basis_journal_id', + readonly=False, + check_company=True, + ) account_cash_basis_base_account_id = fields.Many2one( comodel_name='account.account', string="Base Tax Received Account", readonly=False, + check_company=True, related='company_id.account_cash_basis_base_account_id', domain=[('deprecated', '=', False)]) account_fiscal_country_id = fields.Many2one(string="Fiscal Country Code", related="company_id.account_fiscal_country_id", readonly=False, store=False) @@ -154,15 +178,17 @@ class ResConfigSettings(models.TransientModel): help='Account for the difference amount after the expense discount has been granted', readonly=False, related='company_id.account_journal_early_pay_discount_loss_account_id', - domain="[('deprecated', '=', False), ('company_id', '=', company_id), ('account_type', 'in', ('expense', 'income', 'income_other'))]", + check_company=True, + domain="[('deprecated', '=', False), ('account_type', 'in', ('expense', 'income', 'income_other'))]", ) account_journal_early_pay_discount_gain_account_id = fields.Many2one( comodel_name='account.account', string='Cash Discount Gain', help='Account for the difference amount after the income discount has been granted', readonly=False, + check_company=True, related='company_id.account_journal_early_pay_discount_gain_account_id', - domain="[('deprecated', '=', False), ('company_id', '=', company_id), ('account_type', 'in', ('income', 'income_other', 'expense'))]", + domain="[('deprecated', '=', False), ('account_type', 'in', ('income', 'income_other', 'expense'))]", ) def set_values(self): @@ -208,7 +234,8 @@ class ResConfigSettings(models.TransientModel): def _onchange_tax_exigibility(self): res = {} tax = self.env['account.tax'].search([ - ('company_id', '=', self.env.company.id), ('tax_exigibility', '=', 'on_payment') + *self.env['account.tax']._check_company_domain(self.env.company), + ('tax_exigibility', '=', 'on_payment'), ], limit=1) if not self.tax_exigibility and tax: self.tax_exigibility = True diff --git a/addons/account/models/res_partner_bank.py b/addons/account/models/res_partner_bank.py index 1fb16ee673c..652f39d3653 100644 --- a/addons/account/models/res_partner_bank.py +++ b/addons/account/models/res_partner_bank.py @@ -16,6 +16,7 @@ class ResPartnerBank(models.Model): journal_id = fields.One2many( 'account.journal', 'bank_account_id', domain=[('type', '=', 'bank')], string='Account Journal', readonly=True, + check_company=True, help="The accounting journal corresponding to this bank account.") has_iban_warning = fields.Boolean( compute='_compute_display_account_warning', diff --git a/addons/account/models/template_generic_coa.py b/addons/account/models/template_generic_coa.py index b669c5868e6..24aa033e217 100644 --- a/addons/account/models/template_generic_coa.py +++ b/addons/account/models/template_generic_coa.py @@ -12,8 +12,6 @@ class AccountChartTemplate(models.AbstractModel): :return: all the values that are not stored but are used to instancieate the chart of accounts. Common keys are: - * anglo_saxon_accounting - * transfer_account_code_prefix * property_* * code_digits :rtype: dict @@ -21,9 +19,6 @@ class AccountChartTemplate(models.AbstractModel): return { 'name': "Generic Chart Template", 'country': None, - 'anglo_saxon_accounting': True, - 'transfer_account_code_prefix': '1017', - 'default_pos_receivable_account_id': 'pos_receivable', 'property_account_receivable_id': 'receivable', 'property_account_payable_id': 'payable', 'property_account_expense_id': 'expense', @@ -46,9 +41,11 @@ class AccountChartTemplate(models.AbstractModel): """ return { self.env.company.id: { + 'anglo_saxon_accounting': True, 'account_fiscal_country_id': 'base.us', 'bank_account_code_prefix': '1014', 'cash_account_code_prefix': '1015', + 'transfer_account_code_prefix': '1017', 'account_default_pos_receivable_account_id': 'pos_receivable', 'income_currency_exchange_account_id': 'income_currency_exchange', 'expense_currency_exchange_account_id': 'expense_currency_exchange', diff --git a/addons/account/populate/account_bank_statement.py b/addons/account/populate/account_bank_statement.py index 8d69a6281ab..23703c68157 100644 --- a/addons/account/populate/account_bank_statement.py +++ b/addons/account/populate/account_bank_statement.py @@ -75,7 +75,7 @@ class AccountBankStatementLine(models.Model): :return (list): the ids of partner the company has access to. """ return self.env['res.partner'].search([ - '|', ('company_id', '=', company_id), ('company_id', '=', False), + *self.env['res.company']._check_company_domain(company_id), ('id', 'in', self.env.registry.populated_models['res.partner']), ]).ids @@ -128,7 +128,7 @@ class AccountBankStatementLine(models.Model): ]) journal_ids = self.env['account.journal'].search([ - ('company_id', 'in', company_ids.ids), + *self.env['account.journal']._check_company_domain(company_ids), ('type', 'in', ('cash', 'bank')), ]).ids return [ diff --git a/addons/account/populate/account_move.py b/addons/account/populate/account_move.py index ad94caac53d..2482ea8f939 100644 --- a/addons/account/populate/account_move.py +++ b/addons/account/populate/account_move.py @@ -39,7 +39,10 @@ class AccountMove(models.Model): payable, receivable, liquidity, other, False. :return (Model): the recordset of accounts found. """ - domain = [('company_id', '=', company_id), ('account_type', '!=', 'off_balance')] + domain = [ + *self.env['account.account']._check_company_domain(company_id), + ('account_type', '!=', 'off_balance'), + ] if types: domain += [('account_type', 'in', types)] return self.env['account.account'].search(domain) @@ -56,7 +59,7 @@ class AccountMove(models.Model): :return (list): the ids of the journals of a company and a certain type """ return self.env['account.journal'].search([ - ('company_id', '=', company_id), + *self.env['account.journal']._check_company_domain(company_id), ('currency_id', 'in', (False, currency_id)), ('type', '=', journal_type), ]).ids @@ -70,7 +73,7 @@ class AccountMove(models.Model): :return (Model): all the products te company has access to """ return self.env['product.product'].search([ - ('company_id', 'in', (False, company_id)), + *self.env['product.product']._check_company_domain(company_id), ('id', 'in', self.env.registry.populated_models['product.product']), ]) @@ -83,7 +86,7 @@ class AccountMove(models.Model): :return (list): the ids of partner the company has access to. """ return self.env['res.partner'].search([ - '|', ('company_id', '=', company_id), ('company_id', '=', False), + *self.env['res.partner']._check_company_domain(company_id), ('id', 'in', self.env.registry.populated_models['res.partner']), ]).ids diff --git a/addons/account/populate/account_payment.py b/addons/account/populate/account_payment.py index e99d3b5c0f7..b5137dd242b 100644 --- a/addons/account/populate/account_payment.py +++ b/addons/account/populate/account_payment.py @@ -34,7 +34,7 @@ class AccountPayment(models.Model): :return (list): the ids of partner the company has access to. """ return self.env['res.partner'].search([ - '|', ('company_id', '=', company_id), ('company_id', '=', False), + *self.env['res.partner']._check_company_domain(company_id), ('id', 'in', self.env.registry.populated_models['res.partner']), ]).ids @@ -47,7 +47,7 @@ class AccountPayment(models.Model): :return (list): the ids of the bank and cash journals of a company """ return self.env['account.journal'].search([ - ('company_id', '=', company_id), + *self.env['account.journal']._check_company_domain(company_id), ('type', 'in', ('cash', 'bank')), ]).ids diff --git a/addons/account/populate/account_reconcile_model.py b/addons/account/populate/account_reconcile_model.py index 56ab85af792..ada1decb78a 100644 --- a/addons/account/populate/account_reconcile_model.py +++ b/addons/account/populate/account_reconcile_model.py @@ -62,7 +62,7 @@ class AccountReconcileModelLine(models.Model): asset, liability, equity, off_balance, False. :return (Model): the recordset of accounts found. """ - domain = [('company_id', '=', company_id)] + domain = self.env['account.account']._check_company_domain(company_id) if type: domain += [('account_type', '=', type)] if group: diff --git a/addons/account/security/account_security.xml b/addons/account/security/account_security.xml index 8e08ed1693e..b675fdcfb1f 100644 --- a/addons/account/security/account_security.xml +++ b/addons/account/security/account_security.xml @@ -125,49 +125,49 @@ Journal Group multi-company - [('company_id', 'in', company_ids)] + [('company_id', 'parent_of', company_ids)] Journal multi-company - [('company_id', 'in', company_ids)] + [('company_id', 'parent_of', company_ids)] Account multi-company - [('company_id', 'in', company_ids)] + [('company_id', 'parent_of', company_ids)] Account Group multi-company - [('company_id', 'in', company_ids)] + [('company_id', 'parent_of', company_ids)] Account Root multi-company - ['|',('company_id','=',False),('company_id', 'in', company_ids)] + ['|',('company_id','=',False), ('company_id', 'parent_of', company_ids)] Tax group multi-company - [('company_id', 'in', company_ids)] + [('company_id', 'parent_of', company_ids)] Tax multi-company - [('company_id', 'in', company_ids)] + [('company_id', 'parent_of', company_ids)] Tax Repartition multi-company - ['|',('company_id','=',False),('company_id', 'in', company_ids)] + ['|',('company_id','=',False), ('company_id', 'parent_of', company_ids)] @@ -179,7 +179,7 @@ Account fiscal Mapping company rule - [('company_id', 'in', company_ids)] + [('company_id', 'parent_of', company_ids)] @@ -197,13 +197,13 @@ Account reconcile model template company rule - [('company_id', 'in', company_ids)] + [('company_id', 'parent_of', company_ids)] Account reconcile model_line template company rule - [('company_id', 'in', company_ids)] + [('company_id', 'parent_of', company_ids)] diff --git a/addons/account/tests/__init__.py b/addons/account/tests/__init__.py index e8511720025..911d11c8df7 100644 --- a/addons/account/tests/__init__.py +++ b/addons/account/tests/__init__.py @@ -22,6 +22,7 @@ from . import test_account_invoice_report from . import test_account_move_line_tax_details from . import test_account_journal_dashboard from . import test_chart_template +from . import test_company_branch from . import test_digest from . import test_fiscal_position from . import test_sequence_mixin diff --git a/addons/account/tests/test_account_journal.py b/addons/account/tests/test_account_journal.py index dd68a75d750..a67b49cd5f9 100644 --- a/addons/account/tests/test_account_journal.py +++ b/addons/account/tests/test_account_journal.py @@ -24,13 +24,14 @@ class TestAccountJournal(AccountTestInvoicingCommon): def test_changing_journal_company(self): ''' Ensure you can't change the company of an account.journal if there are some journal entries ''' + self.company_data['default_journal_sale'].code = "DIFFERENT" self.env['account.move'].create({ 'move_type': 'entry', 'date': '2019-01-01', 'journal_id': self.company_data['default_journal_sale'].id, }) - with self.assertRaises(UserError), self.cr.savepoint(): + with self.assertRaisesRegex(UserError, "entries linked to it"), self.cr.savepoint(): self.company_data['default_journal_sale'].company_id = self.company_data_2['company'] def test_account_control_create_journal_entry(self): diff --git a/addons/account/tests/test_account_move_line_tax_details.py b/addons/account/tests/test_account_move_line_tax_details.py index b34eaba3ac5..705a6a964d9 100644 --- a/addons/account/tests/test_account_move_line_tax_details.py +++ b/addons/account/tests/test_account_move_line_tax_details.py @@ -1252,7 +1252,7 @@ class TestAccountTaxDetailsReport(AccountTestInvoicingCommon): def test_amounts_sign(self): for tax_sign in (1, -1): tax = self.env['account.tax'].create({ - 'name': "tax", + 'name': f"tax {tax_sign}", 'amount_type': 'percent', 'amount': tax_sign * 10.0, }) diff --git a/addons/account/tests/test_account_move_reconcile.py b/addons/account/tests/test_account_move_reconcile.py index 217ddf19a29..dc53d58eff6 100644 --- a/addons/account/tests/test_account_move_reconcile.py +++ b/addons/account/tests/test_account_move_reconcile.py @@ -4188,7 +4188,7 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): }) foreign_caba_tax = self.env['account.tax'].create({ - 'name': 'tax_1', + 'name': 'foreign tax_1', 'amount': 33.3333, 'company_id': self.company_data['company'].id, 'cash_basis_transition_account_id': self.cash_basis_transfer_account.id, diff --git a/addons/account/tests/test_company_branch.py b/addons/account/tests/test_company_branch.py new file mode 100644 index 00000000000..d741e51dcf0 --- /dev/null +++ b/addons/account/tests/test_company_branch.py @@ -0,0 +1,208 @@ +# -*- coding: utf-8 -*- + +from contextlib import nullcontext +from freezegun import freeze_time +from functools import partial + +from odoo import Command +from odoo.addons.account.tests.common import AccountTestInvoicingCommon +from odoo.exceptions import UserError +from odoo.tests import tagged, Form + + +@tagged('post_install', '-at_install') +class TestCompanyBranch(AccountTestInvoicingCommon): + + @classmethod + def setUpClass(cls, chart_template_ref=None): + super().setUpClass(chart_template_ref) + + cls.company_data['company'].write({ + 'child_ids': [ + Command.create({'name': 'Branch A'}), + Command.create({'name': 'Branch B'}), + ], + }) + cls.cr.precommit.run() # load the CoA + + cls.root_company = cls.company_data['company'] + cls.branch_a, cls.branch_b = cls.root_company.child_ids + + def test_chart_template_loading(self): + # Some company params have to be the same + self.assertEqual(self.root_company.currency_id, self.branch_a.currency_id) + self.assertEqual(self.root_company.fiscalyear_last_day, self.branch_a.fiscalyear_last_day) + self.assertEqual(self.root_company.fiscalyear_last_month, self.branch_a.fiscalyear_last_month) + + # The accounts are shared + root_accounts = self.env['account.account'].search([('company_id', 'parent_of', self.root_company.id)]) + branch_a_accounts = self.env['account.account'].search([('company_id', 'parent_of', self.branch_a.id)]) + self.assertTrue(root_accounts) + self.assertEqual(root_accounts, branch_a_accounts) + + # The journals are shared + root_journals = self.env['account.journal'].search([('company_id', 'parent_of', self.root_company.id)]) + branch_a_journals = self.env['account.journal'].search([('company_id', 'parent_of', self.branch_a.id)]) + self.assertTrue(root_journals) + self.assertEqual(root_journals, branch_a_journals) + + def test_reconciliation(self): + invoice = self.env['account.move'].create({ + 'move_type': 'out_invoice', + 'invoice_date': '2016-01-01', + 'company_id': self.branch_a.id, + 'partner_id': self.partner_a.id, + 'invoice_line_ids': [ + Command.create({ + 'name': 'product', + 'price_unit': 1000, + }) + ], + }) + invoice.action_post() + refund = self.env['account.move'].create({ + 'move_type': 'out_refund', + 'invoice_date': '2017-01-01', + 'company_id': self.root_company.id, + 'partner_id': self.partner_a.id, + 'invoice_line_ids': [ + Command.create({ + 'name': 'product', + 'price_unit': 1000, + }) + ], + }) + refund.action_post() + + payment_lines = (invoice + refund).line_ids.filtered(lambda l: l.display_type == 'payment_term') + payment_lines.reconcile() + self.assertEqual(payment_lines.mapped('amount_residual'), [0, 0]) + self.assertFalse(payment_lines.matched_debit_ids.exchange_move_id) + + # Can still open the invoice with only it's branch accessible + self.env.invalidate_all() + with Form(invoice.with_context(allowed_company_ids=self.branch_a.ids)): + pass + + def test_reconciliation_foreign_currency(self): + invoice = self.env['account.move'].create({ + 'move_type': 'out_invoice', + 'invoice_date': '2016-01-01', + 'company_id': self.branch_a.id, + 'currency_id': self.currency_data['currency'].id, + 'partner_id': self.partner_a.id, + 'invoice_line_ids': [ + Command.create({ + 'name': 'product', + 'price_unit': 1000, + }) + ], + }) + invoice.action_post() + refund = self.env['account.move'].create({ + 'move_type': 'out_refund', + 'invoice_date': '2017-01-01', + 'company_id': self.root_company.id, + 'currency_id': self.currency_data['currency'].id, + 'partner_id': self.partner_a.id, + 'invoice_line_ids': [ + Command.create({ + 'name': 'product', + 'price_unit': 1000, + }) + ], + }) + refund.action_post() + + payment_lines = (invoice + refund).line_ids.filtered(lambda l: l.display_type == 'payment_term') + payment_lines.reconcile() + self.assertEqual(payment_lines.mapped('amount_residual'), [0, 0]) + self.assertTrue(payment_lines.matched_debit_ids.exchange_move_id) + self.assertTrue(payment_lines.matched_debit_ids.exchange_move_id.journal_id.company_id, invoice.company_id) + + # Can still open the invoice with only it's branch accessible + self.env.invalidate_all() + with Form(invoice.with_context(allowed_company_ids=self.branch_a.ids)): + pass + + def test_lock_dates(self): + moves = self.env['account.move'].search([]) + moves.button_draft() + moves.unlink() + for lock in ['fiscalyear_lock_date', 'tax_lock_date']: + for root_lock, branch_lock, invoice_date, company, expected in ( + # before both locks + ('3021-01-01', '3022-01-01', '3020-01-01', self.root_company, 'fail'), + ('3021-01-01', '3022-01-01', '3020-01-01', self.branch_a, 'fail'), + # between root and branch lock + ('3020-01-01', '3022-01-01', '3021-01-01', self.root_company, 'success'), + ('3020-01-01', '3022-01-01', '3021-01-01', self.branch_a, 'fail'), + # between branch and root lock + ('3022-01-01', '3020-01-01', '3021-01-01', self.root_company, 'fail'), + ('3022-01-01', '3020-01-01', '3021-01-01', self.branch_a, 'fail'), + # after both locks + ('3020-01-01', '3021-01-01', '3022-01-01', self.root_company, 'success'), + ('3020-01-01', '3021-01-01', '3022-01-01', self.branch_a, 'success'), + ): + with self.subTest( + lock=lock, + root_lock=root_lock, + branch_lock=branch_lock, + invoice_date=invoice_date, + company=company.name, + ), self.env.cr.savepoint() as sp: + with freeze_time('4000-01-01'): # ensure we don't lock in the future + self.root_company[lock] = root_lock + self.branch_a[lock] = branch_lock + check = partial(self.assertRaises, UserError) if expected == 'fail' else nullcontext + with check(): + self.init_invoice( + 'out_invoice', amounts=[100], taxes=self.root_company.account_sale_tax_id, + invoice_date=invoice_date, post=True, company=company, + ) + sp.close() + + def test_change_record_company(self): + account = self.env['account.account'].create({ + 'name': 'volatile', + 'code': 'vola', + 'account_type': 'income', + 'company_id': self.branch_a.id, + }) + account_lines = [Command.create({ + 'account_id': account.id, + 'name': 'name', + })] + tax = self.env['account.tax'].create({ + 'name': 'volatile', + }) + tax_lines = [Command.create({ + 'account_id': self.root_company.account_journal_suspense_account_id.id, + 'tax_ids': [Command.set(tax.ids)], + 'name': 'name', + })] + for record, lines in ( + (account, account_lines), + (tax, tax_lines), + ): + with self.subTest(model=record._name): + self.env['account.move'].create({'company_id': self.branch_a.id, 'line_ids': lines}) + # Can switch to main + record.company_id = self.root_company + + # Can switch back + record.company_id = self.branch_a + + # Can't use in main if owned by a branch + with self.assertRaisesRegex(UserError, 'belongs to another company'): + self.env['account.move'].create({'company_id': self.root_company.id, 'line_ids': lines}) + + # Can still switch to main + record.company_id = self.root_company + + # Can use in main now + self.env['account.move'].create({'company_id': self.root_company.id, 'line_ids': lines}) + + # Can't switch back to branch if used in main + with self.assertRaisesRegex(UserError, 'journal items linked'): + record.company_id = self.branch_a diff --git a/addons/account/views/account_account_views.xml b/addons/account/views/account_account_views.xml index c39a4fe170d..87b6b1f1596 100644 --- a/addons/account/views/account_account_views.xml +++ b/addons/account/views/account_account_views.xml @@ -60,9 +60,9 @@ - + - + diff --git a/addons/account/views/account_move_views.xml b/addons/account/views/account_move_views.xml index 4d71d24a7ac..bd726eff21a 100644 --- a/addons/account/views/account_move_views.xml +++ b/addons/account/views/account_move_views.xml @@ -24,7 +24,7 @@ - + @@ -170,7 +170,7 @@ - + @@ -909,7 +909,7 @@ context="{'partner_id': partner_id, 'move_type': parent.move_type}" groups="account.group_account_readonly" options="{'no_quick_create': True}" - domain="[('deprecated', '=', False), ('account_type', 'not in', ('asset_receivable', 'liability_payable', 'off_balance')), ('company_id', '=', parent.company_id)]" + domain="[('deprecated', '=', False), ('account_type', 'not in', ('asset_receivable', 'liability_payable', 'off_balance')), ('company_id', 'parent_of', parent.company_id)]" attrs="{'required': [('display_type', 'not in', ('line_note', 'line_section'))]}"/> - + @@ -1084,7 +1084,7 @@ 'required': [('display_type', 'not in', ('line_section', 'line_note'))], 'invisible': [('display_type', 'in', ('line_section', 'line_note'))], }" - domain="[('deprecated', '=', False), ('company_id', '=', parent.company_id)]" /> + domain="[('company_id', 'parent_of', parent.company_id), ('deprecated', '=', False)]" />
- + diff --git a/addons/account/views/account_payment_view.xml b/addons/account/views/account_payment_view.xml index 992c97e43b6..2d29db140e1 100644 --- a/addons/account/views/account_payment_view.xml +++ b/addons/account/views/account_payment_view.xml @@ -18,6 +18,7 @@ + diff --git a/addons/account/views/account_reconcile_model_views.xml b/addons/account/views/account_reconcile_model_views.xml index 3ba1489b40e..97fd332539b 100644 --- a/addons/account/views/account_reconcile_model_views.xml +++ b/addons/account/views/account_reconcile_model_views.xml @@ -208,6 +208,7 @@ + - +
diff --git a/addons/account/wizard/account_automatic_entry_wizard.py b/addons/account/wizard/account_automatic_entry_wizard.py index bdb024e95c3..bafddbb3bb2 100644 --- a/addons/account/wizard/account_automatic_entry_wizard.py +++ b/addons/account/wizard/account_automatic_entry_wizard.py @@ -12,6 +12,7 @@ import json class AutomaticEntryWizard(models.TransientModel): _name = 'account.automatic.entry.wizard' _description = 'Create Automatic Entries' + _check_company_auto = True # General action = fields.Selection([('change_period', 'Change Period'), ('change_account', 'Change Account')], required=True) @@ -24,7 +25,8 @@ class AutomaticEntryWizard(models.TransientModel): percentage = fields.Float("Percentage", compute='_compute_percentage', readonly=False, store=True, help="Percentage of each line to execute the action on.") total_amount = fields.Monetary(compute='_compute_total_amount', store=True, readonly=False, currency_field='company_currency_id', help="Total amount impacted by the automatic entry.") journal_id = fields.Many2one('account.journal', required=True, readonly=False, string="Journal", - domain="[('company_id', '=', company_id), ('type', '=', 'general')]", + check_company=True, + domain="[('type', '=', 'general')]", compute="_compute_journal_id", inverse="_inverse_journal_id", help="Journal where to create the entry.") @@ -32,21 +34,21 @@ class AutomaticEntryWizard(models.TransientModel): # change period account_type = fields.Selection([('income', 'Revenue'), ('expense', 'Expense')], compute='_compute_account_type', store=True) expense_accrual_account = fields.Many2one('account.account', readonly=False, - domain="[('company_id', '=', company_id)," - "('account_type', 'not in', ('asset_receivable', 'liability_payable', 'off_balance'))]", + check_company=True, + domain="[('account_type', 'not in', ('asset_receivable', 'liability_payable', 'off_balance'))]", compute="_compute_expense_accrual_account", inverse="_inverse_expense_accrual_account", ) revenue_accrual_account = fields.Many2one('account.account', readonly=False, - domain="[('company_id', '=', company_id)," - "('account_type', 'not in', ('asset_receivable', 'liability_payable', 'off_balance'))]", + check_company=True, + domain="[('account_type', 'not in', ('asset_receivable', 'liability_payable', 'off_balance'))]", compute="_compute_revenue_accrual_account", inverse="_inverse_revenue_accrual_account", ) lock_date_message = fields.Char(string="Lock Date Message", compute="_compute_lock_date_message") # change account - destination_account_id = fields.Many2one(string="To", comodel_name='account.account', help="Account to transfer to.") + destination_account_id = fields.Many2one(string="To", comodel_name='account.account', help="Account to transfer to.", check_company=True) display_currency_helper = fields.Boolean(string="Currency Conversion Helper", compute='_compute_display_currency_helper') # Technical field. Used to indicate whether or not to display the currency conversion tooltip. The tooltip informs a currency conversion will be performed with the transfer. @@ -139,9 +141,9 @@ class AutomaticEntryWizard(models.TransientModel): raise UserError(_('You can only change the period/account for posted journal items.')) if any(move_line.reconciled for move_line in move_line_ids): raise UserError(_('You can only change the period/account for items that are not yet reconciled.')) - if any(line.company_id != move_line_ids[0].company_id for line in move_line_ids): + if any(line.company_id.root_id != move_line_ids[0].company_id.root_id for line in move_line_ids): raise UserError(_('You cannot use this wizard on journal entries belonging to different companies.')) - res['company_id'] = move_line_ids[0].company_id.id + res['company_id'] = move_line_ids[0].company_id.root_id.id allowed_actions = set(dict(self._fields['action'].selection)) if self.env.context.get('default_action'): diff --git a/addons/account/wizard/account_move_reversal.py b/addons/account/wizard/account_move_reversal.py index 2fb2b4d5db9..c7a6e6e728e 100644 --- a/addons/account/wizard/account_move_reversal.py +++ b/addons/account/wizard/account_move_reversal.py @@ -50,11 +50,13 @@ class AccountMoveReversal(models.TransientModel): for record in self: if record.move_ids: record.available_journal_ids = self.env['account.journal'].search([ - ('company_id', '=', record.company_id.id), + *self.env['account.journal']._check_company_domain(record.company_id), ('type', 'in', record.move_ids.journal_id.mapped('type')), ]) else: - record.available_journal_ids = self.env['account.journal'].search([('company_id', '=', record.company_id.id)]) + record.available_journal_ids = self.env['account.journal'].search([ + *self.env['account.journal']._check_company_domain(record.company_id), + ]) @api.constrains('journal_id', 'move_ids') def _check_journal_type(self): @@ -70,7 +72,7 @@ class AccountMoveReversal(models.TransientModel): if any(move.state != "posted" for move in move_ids): raise UserError(_('You can only reverse posted moves.')) if 'company_id' in fields: - res['company_id'] = move_ids.company_id.id or self.env.company.id + res['company_id'] = move_ids.company_id.root_id.id or self.env.company.id if 'move_ids' in fields: res['move_ids'] = [(6, 0, move_ids.ids)] return res diff --git a/addons/account/wizard/account_payment_register.py b/addons/account/wizard/account_payment_register.py index ebf3824fbb7..7d89b26520d 100644 --- a/addons/account/wizard/account_payment_register.py +++ b/addons/account/wizard/account_payment_register.py @@ -9,6 +9,7 @@ from odoo.tools import frozendict class AccountPaymentRegister(models.TransientModel): _name = 'account.payment.register' _description = 'Register Payment' + _check_company_auto = True # == Business fields == payment_date = fields.Date(string="Payment Date", required=True, @@ -30,6 +31,7 @@ class AccountPaymentRegister(models.TransientModel): journal_id = fields.Many2one( comodel_name='account.journal', compute='_compute_journal_id', store=True, readonly=False, precompute=True, + check_company=True, domain="[('id', 'in', available_journal_ids)]") available_journal_ids = fields.Many2many( comodel_name='account.journal', @@ -111,7 +113,8 @@ class AccountPaymentRegister(models.TransientModel): comodel_name='account.account', string="Difference Account", copy=False, - domain="[('deprecated', '=', False), ('company_id', '=', company_id)]", + domain="[('deprecated', '=', False)]", + check_company=True, compute='_compute_writeoff_account_id', store=True, readonly=False, @@ -153,7 +156,10 @@ class AccountPaymentRegister(models.TransientModel): """ payment_type = batch_result['payment_values']['payment_type'] company = batch_result['lines'].company_id - journals = self.env['account.journal'].search([('company_id', '=', company.id), ('type', 'in', ('bank', 'cash'))]) + journals = self.env['account.journal'].search([ + *self.env['account.journal']._check_company_domain(company), + ('type', 'in', ('bank', 'cash')), + ]) if payment_type == 'inbound': return journals.filtered('inbound_payment_method_line_ids') else: @@ -169,13 +175,16 @@ class AccountPaymentRegister(models.TransientModel): payment_values = batch_result['payment_values'] foreign_currency_id = payment_values['currency_id'] partner_bank_id = payment_values['partner_bank_id'] + company = batch_result['lines'].company_id + if len(company) > 1: + company = company._accessible_branches()[:1] currency_domain = [('currency_id', '=', foreign_currency_id)] partner_bank_domain = [('bank_account_id', '=', partner_bank_id)] default_domain = [ + *self.env['account.journal']._check_company_domain(company), ('type', 'in', ('bank', 'cash')), - ('company_id', '=', batch_result['lines'].company_id.id), ('id', 'in', self.available_journal_ids.ids) ] @@ -202,7 +211,7 @@ class AccountPaymentRegister(models.TransientModel): @api.model def _get_batch_available_partner_banks(self, batch_result, journal): payment_values = batch_result['payment_values'] - company = batch_result['lines'].company_id + company = batch_result['lines'].company_id._accessible_branches()[:1] # A specific bank account is set on the journal. The user must use this one. if payment_values['payment_type'] == 'inbound': @@ -244,7 +253,7 @@ class AccountPaymentRegister(models.TransientModel): lines = self.line_ids._origin - if len(lines.company_id) > 1: + if len(lines.company_id.root_id) > 1: raise UserError(_("You can't create payments for entries belonging to different companies.")) if not lines: raise UserError(_("You can't open the register payment wizard without at least one receivable/payable line.")) @@ -306,7 +315,7 @@ class AccountPaymentRegister(models.TransientModel): ''' payment_values = batch_result['payment_values'] lines = batch_result['lines'] - company = lines[0].company_id + company = lines[0].company_id._accessible_branches()[:1] source_amount = abs(sum(lines.mapped('amount_residual'))) if payment_values['currency_id'] == company.currency_id.id: @@ -369,7 +378,7 @@ class AccountPaymentRegister(models.TransientModel): else: # == Multiple batches: The wizard is not editable == wizard.update({ - 'company_id': batches[0]['lines'][0].company_id.id, + 'company_id': batches[0]['lines'][0].company_id._accessible_branches()[:1].id, 'partner_id': False, 'partner_type': False, 'payment_type': wizard_values_from_batch['payment_type'], @@ -414,7 +423,7 @@ class AccountPaymentRegister(models.TransientModel): wizard.available_journal_ids = wizard._get_batch_available_journals(batch) else: wizard.available_journal_ids = self.env['account.journal'].search([ - ('company_id', '=', wizard.company_id.id), + *self.env['account.journal']._check_company_domain(wizard.company_id), ('type', 'in', ('bank', 'cash')), ]) @@ -426,8 +435,8 @@ class AccountPaymentRegister(models.TransientModel): wizard.journal_id = wizard._get_batch_journal(batch) else: wizard.journal_id = self.env['account.journal'].search([ + *self.env['account.journal']._check_company_domain(wizard.company_id), ('type', 'in', ('bank', 'cash')), - ('company_id', '=', wizard.company_id.id), ('id', 'in', self.available_journal_ids.ids) ], limit=1) @@ -609,8 +618,10 @@ class AccountPaymentRegister(models.TransientModel): "The register payment wizard should only be called on account.move or account.move.line records." )) - if 'journal_id' in res and not self.env['account.journal'].browse(res['journal_id'])\ - .filtered_domain([('company_id', '=', lines.company_id.id), ('type', 'in', ('bank', 'cash'))]): + if 'journal_id' in res and not self.env['account.journal'].browse(res['journal_id']).filtered_domain([ + *self.env['account.journal']._check_company_domain(lines.company_id), + ('type', 'in', ('bank', 'cash')), + ]): # default can be inherited from the list view, should be computed instead del res['journal_id'] @@ -633,7 +644,7 @@ class AccountPaymentRegister(models.TransientModel): # Check. if not available_lines: raise UserError(_("You can't register a payment because there is nothing left to pay on the selected journal items.")) - if len(lines.company_id) > 1: + if len(lines.company_id.root_id) > 1: raise UserError(_("You can't create payments for entries belonging to different companies.")) if len(set(available_lines.mapped('account_type'))) > 1: raise UserError(_("You can't register payments for journal items being either all inbound, either all outbound.")) @@ -654,6 +665,7 @@ class AccountPaymentRegister(models.TransientModel): 'partner_type': self.partner_type, 'ref': self.communication, 'journal_id': self.journal_id.id, + 'company_id': self.company_id.id, 'currency_id': self.currency_id.id, 'partner_id': self.partner_id.id, 'partner_bank_id': self.partner_bank_id.id, @@ -725,6 +737,7 @@ class AccountPaymentRegister(models.TransientModel): 'partner_type': batch_values['partner_type'], 'ref': self._get_batch_communication(batch_result), 'journal_id': self.journal_id.id, + 'company_id': self.company_id.id, 'currency_id': batch_values['source_currency_id'], 'partner_id': batch_values['partner_id'], 'partner_bank_id': partner_bank_id, diff --git a/addons/account/wizard/account_tour_upload_bill.py b/addons/account/wizard/account_tour_upload_bill.py index 59482a869f1..38bb64d4ca2 100644 --- a/addons/account/wizard/account_tour_upload_bill.py +++ b/addons/account/wizard/account_tour_upload_bill.py @@ -47,8 +47,10 @@ class AccountTourUploadBill(models.TransientModel): record.preview_invoice = html def _selection_values(self): - journal_alias = self.env['account.journal'] \ - .search([('type', '=', 'purchase'), ('company_id', '=', self.env.company.id)], limit=1) + journal_alias = self.env['account.journal'].search([ + *self.env['account.journal']._check_company_domain(self.env.company), + ('type', '=', 'purchase'), + ], limit=1) values = [('sample', _('Try a sample vendor bill')), ('upload', _('Upload your own bill'))] if journal_alias.alias_name and journal_alias.alias_domain: diff --git a/addons/account/wizard/accrued_orders.py b/addons/account/wizard/accrued_orders.py index d9c26105c1b..5fdfda8cfd6 100644 --- a/addons/account/wizard/accrued_orders.py +++ b/addons/account/wizard/accrued_orders.py @@ -12,12 +12,13 @@ from odoo.tools.misc import formatLang class AccruedExpenseRevenue(models.TransientModel): _name = 'account.accrued.orders.wizard' _description = 'Accrued Orders Wizard' + _check_company_auto = True def _get_account_domain(self): if self.env.context.get('active_model') == 'purchase.order': - return [('account_type', '=', 'liability_current'), ('company_id', '=', self._get_default_company())] + return [('account_type', '=', 'liability_current')] else: - return [('account_type', '=', 'asset_current'), ('company_id', '=', self._get_default_company())] + return [('account_type', '=', 'asset_current')] def _get_default_company(self): if not self._context.get('active_model'): @@ -32,7 +33,7 @@ class AccruedExpenseRevenue(models.TransientModel): journal_id = fields.Many2one( comodel_name='account.journal', compute='_compute_journal_id', store=True, readonly=False, precompute=True, - domain="[('type', '=', 'general'), ('company_id', '=', company_id)]", + domain="[('type', '=', 'general')]", required=True, check_company=True, string='Journal', @@ -78,11 +79,11 @@ class AccruedExpenseRevenue(models.TransientModel): @api.depends('company_id') def _compute_journal_id(self): - journal = self.env['account.journal'].search( - [('type', '=', 'general'), ('company_id', '=', self.company_id.id)], limit=1 - ) for record in self: - record.journal_id = journal + record.journal_id = self.env['account.journal'].search([ + *self.env['account.journal']._check_company_domain(record.company_id), + ('type', '=', 'general') + ], limit=1) @api.depends('date', 'journal_id', 'account_id', 'amount') def _compute_preview_data(self): diff --git a/addons/account/wizard/setup_wizards.py b/addons/account/wizard/setup_wizards.py index 3028f131fe7..6345fdb893b 100644 --- a/addons/account/wizard/setup_wizards.py +++ b/addons/account/wizard/setup_wizards.py @@ -74,7 +74,8 @@ class SetupBarBankConfigWizard(models.TransientModel): linked_journal_id = fields.Many2one(string="Journal", comodel_name='account.journal', inverse='set_linked_journal_id', compute="_compute_linked_journal_id", - domain=lambda self: [('type', '=', 'bank'), ('bank_account_id', '=', False), ('company_id', '=', self.env.company.id)]) + check_company=True, + domain=[('type', '=', 'bank'), ('bank_account_id', '=', False)]) bank_bic = fields.Char(related='bank_id.bic', readonly=False, string="Bic") num_journals_without_account = fields.Integer(default=lambda self: self._number_unlinked_journal()) diff --git a/addons/account_edi_ubl_cii/models/account_edi_common.py b/addons/account_edi_ubl_cii/models/account_edi_common.py index c3b2125badb..f590fdd7ddd 100644 --- a/addons/account_edi_ubl_cii/models/account_edi_common.py +++ b/addons/account_edi_ubl_cii/models/account_edi_common.py @@ -412,7 +412,7 @@ class AccountEdiCommon(models.AbstractModel): tax_ids = [] for tax_categ_percent_el in allow_el.findall(tax_xpath): tax = self.env['account.tax'].search([ - ('company_id', '=', invoice.company_id.id), + *self.env['account.tax']._check_company_domain(invoice.company_id), ('amount', '=', float(tax_categ_percent_el.text)), ('amount_type', '=', 'percent'), ('type_tax_use', '=', invoice.journal_id.type), # Journal type is ensured by _create_invoice_from_xml_tree to be either 'sale' or 'purchase' @@ -654,7 +654,7 @@ class AccountEdiCommon(models.AbstractModel): 4. price_include matching the amount """ base_domain = [ - ('company_id', '=', invoice_line.company_id.id), + *self.env['account.journal']._check_company_domain(invoice_line.company_id), ('amount_type', '=', 'fixed'), ('amount', '=', fixed_tax_vals['tax_amount']), ] @@ -675,7 +675,7 @@ class AccountEdiCommon(models.AbstractModel): for tax_node in tax_nodes: amount = float(tax_node.text) domain = [ - ('company_id', '=', invoice_line.company_id.id), + *self.env['account.journal']._check_company_domain(invoice_line.company_id), ('amount_type', '=', 'percent'), ('type_tax_use', '=', invoice_line.move_id.journal_id.type), ('amount', '=', amount), diff --git a/addons/account_edi_ubl_cii/models/account_edi_xml_ubl_20.py b/addons/account_edi_ubl_cii/models/account_edi_xml_ubl_20.py index f9e0770883d..6a8a865f6cb 100644 --- a/addons/account_edi_ubl_cii/models/account_edi_xml_ubl_20.py +++ b/addons/account_edi_ubl_cii/models/account_edi_xml_ubl_20.py @@ -616,7 +616,7 @@ class AccountEdiXmlUBL20(models.AbstractModel): # Product product = self._import_retrieve_info_from_map( tree, - self._import_retrieve_product_map(invoice_line.move_id.journal_id), + self._import_retrieve_product_map(invoice_line.move_id.company_id), ) if product is not None: invoice_line.product_id = product @@ -740,9 +740,9 @@ class AccountEdiXmlUBL20(models.AbstractModel): return self.env['product.product'].search(extra_domain + [('name', 'ilike', name_node.text)], limit=1) return { - 10: lambda tree: with_code_barcode(tree, [('company_id', '=', company.id)]), + 10: lambda tree: with_code_barcode(tree, self.env['product.product']._check_company_domain(company)), 20: lambda tree: with_code_barcode(tree, []), - 30: lambda tree: with_name(tree, [('company_id', '=', company.id)]), + 30: lambda tree: with_name(tree, self.env['product.product']._check_company_domain(company)), 40: lambda tree: with_name(tree, []), } diff --git a/addons/account_payment/models/account_journal.py b/addons/account_payment/models/account_journal.py index 2778499ad93..3ff52778abe 100644 --- a/addons/account_payment/models/account_journal.py +++ b/addons/account_payment/models/account_journal.py @@ -33,8 +33,8 @@ class AccountJournal(models.Model): for journal in self: to_remove = [] - available_providers = installed_providers.filtered( - lambda p: p.company_id == journal.company_id + available_providers = installed_providers.filtered_domain( + self.env['payment.provider']._check_company_domain(journal.company_id) ).mapped('code') available = payment_method.code in available_providers diff --git a/addons/account_payment/models/account_payment.py b/addons/account_payment/models/account_payment.py index 3bc076527ff..c6d5f3f7ac6 100644 --- a/addons/account_payment/models/account_payment.py +++ b/addons/account_payment/models/account_payment.py @@ -64,7 +64,7 @@ class AccountPayment(models.Model): for payment in self: if payment.use_electronic_payment_method: payment.suitable_payment_token_ids = self.env['payment.token'].sudo().search([ - ('company_id', '=', payment.company_id.id), + *self.env['payment.token']._check_company_domain(payment.company_id), ('provider_id.capture_manually', '=', False), ('partner_id', '=', payment.partner_id.id), ('provider_id', '=', payment.payment_method_line_id.payment_provider_id.id), @@ -103,7 +103,7 @@ class AccountPayment(models.Model): return self.payment_token_id = self.env['payment.token'].search([ - ('company_id', '=', self.company_id.id), + *self.env['payment.token']._check_company_domain(self.company_id), ('partner_id', '=', self.partner_id.id), ('provider_id.capture_manually', '=', False), ('provider_id', '=', self.payment_method_line_id.payment_provider_id.id), diff --git a/addons/account_payment/models/account_payment_method_line.py b/addons/account_payment/models/account_payment_method_line.py index 3f36614414e..a7c7f0c552f 100644 --- a/addons/account_payment/models/account_payment_method_line.py +++ b/addons/account_payment/models/account_payment_method_line.py @@ -21,8 +21,8 @@ class AccountPaymentMethodLine(models.Model): @api.depends('payment_method_id') def _compute_payment_provider_id(self): providers = self.env['payment.provider'].sudo().search([ + *self.env['payment.provider']._check_company_domain(self.journal_id.company_id), ('code', 'in', self.mapped('code')), - ('company_id', 'in', self.journal_id.company_id.ids), ]) # Make sure to pick the active provider, if any. @@ -37,7 +37,10 @@ class AccountPaymentMethodLine(models.Model): for line in self: code = line.payment_method_id.code company = line.journal_id.company_id - line.payment_provider_id = providers_map.get((code, company), False) + line.payment_provider_id = False + while not line.payment_provider_id and company: + line.payment_provider_id = providers_map.get((code, company), False) + company = company.parent_id @api.model def _get_payment_method_domain(self, code): @@ -49,7 +52,7 @@ class AccountPaymentMethodLine(models.Model): if unique: company_ids = self.env['payment.provider'].sudo().search([('code', '=', code)]).mapped('company_id') if company_ids: - domain = expression.AND([domain, [('company_id', 'in', company_ids.ids)]]) + domain = expression.AND([domain, self.env['payment.provider']._check_company_domain(company_ids)]) return domain diff --git a/addons/account_payment/models/payment_provider.py b/addons/account_payment/models/payment_provider.py index 406db422fbe..4de80b835fb 100644 --- a/addons/account_payment/models/payment_provider.py +++ b/addons/account_payment/models/payment_provider.py @@ -12,7 +12,8 @@ class PaymentProvider(models.Model): comodel_name='account.journal', compute='_compute_journal_id', inverse='_inverse_journal_id', - domain='[("type", "=", "bank"), ("company_id", "=", company_id)]', + check_company=True, + domain='[("type", "=", "bank")]', ) #=== COMPUTE METHODS ===# @@ -21,7 +22,7 @@ class PaymentProvider(models.Model): def _compute_journal_id(self): for provider in self: payment_method = self.env['account.payment.method.line'].search([ - ('journal_id.company_id', '=', provider.company_id.id), + *self.env['account.payment.method.line']._check_company_domain(provider.company_id), ('code', '=', provider._get_code()) ], limit=1) if payment_method: @@ -33,13 +34,14 @@ class PaymentProvider(models.Model): for provider in self: code = provider._get_code() payment_method_line = self.env['account.payment.method.line'].search([ - ('journal_id.company_id', '=', provider.company_id.id), + *self.env['account.payment.method.line']._check_company_domain(provider.company_id), ('code', '=', code), ], limit=1) if provider.journal_id: if not payment_method_line: default_payment_method_id = provider._get_default_payment_method_id(code) existing_payment_method_line = self.env['account.payment.method.line'].search([ + *self.env['account.payment.method.line']._check_company_domain(provider.company_id), ('payment_method_id', '=', default_payment_method_id), ('journal_id', '=', provider.journal_id.id), ], limit=1) diff --git a/addons/account_payment/wizards/account_payment_register.py b/addons/account_payment/wizards/account_payment_register.py index 4c0e9e2c6d6..f695ed6dfea 100644 --- a/addons/account_payment/wizards/account_payment_register.py +++ b/addons/account_payment/wizards/account_payment_register.py @@ -37,7 +37,7 @@ class AccountPaymentRegister(models.TransientModel): for wizard in self: if wizard.can_edit_wizard and wizard.use_electronic_payment_method: wizard.suitable_payment_token_ids = self.env['payment.token'].sudo().search([ - ('company_id', '=', wizard.company_id.id), + *self.env['payment.tokeb']._check_company_domain(wizard.company_id), ('provider_id.capture_manually', '=', False), ('partner_id', '=', wizard.partner_id.id), ('provider_id', '=', wizard.payment_method_line_id.payment_provider_id.id), @@ -63,7 +63,7 @@ class AccountPaymentRegister(models.TransientModel): and wizard.partner_id: wizard.payment_token_id = self.env['payment.token'].sudo().search([ - ('company_id', '=', wizard.company_id.id), + *self.env['payment.token']._check_company_domain(wizard.company_id), ('partner_id', '=', wizard.partner_id.id), ('provider_id.capture_manually', '=', False), ('provider_id', '=', wizard.payment_method_line_id.payment_provider_id.id), diff --git a/addons/account_peppol/models/account_edi_proxy_user.py b/addons/account_peppol/models/account_edi_proxy_user.py index 9e7a14cc74c..6245716666b 100644 --- a/addons/account_peppol/models/account_edi_proxy_user.py +++ b/addons/account_peppol/models/account_edi_proxy_user.py @@ -121,10 +121,10 @@ class AccountEdiProxyClientUser(models.Model): # use the first purchase journal if the Peppol journal is not set up # to create the move anyway if not journal_id: - journal_id = self.env['account.journal'].search( - [('company_id', '=', company.id), ('type', '=', 'purchase')], - limit=1, - ) + journal_id = self.env['account.journal'].search([ + *self.env['account.journal']._check_company_domain(company), + ('type', '=', 'purchase') + ], limit=1) attachment_vals = { 'name': f'{filename}.xml', diff --git a/addons/account_peppol/models/res_company.py b/addons/account_peppol/models/res_company.py index c9b21e2b30d..91e968df011 100644 --- a/addons/account_peppol/models/res_company.py +++ b/addons/account_peppol/models/res_company.py @@ -151,10 +151,10 @@ class ResCompany(models.Model): def _compute_peppol_purchase_journal_id(self): for company in self: if company.is_account_peppol_participant and not company.peppol_purchase_journal_id: - company.peppol_purchase_journal_id = self.env['account.journal'].search( - [('company_id', '=', company.id), ('type', '=', 'purchase')], - limit=1, - ) + company.peppol_purchase_journal_id = self.env['account.journal'].search([ + *self.env['account.journal']._check_company_domain(company), + ('type', '=', 'purchase'), + ], limit=1) else: company.peppol_purchase_journal_id = False diff --git a/addons/analytic/models/analytic_account.py b/addons/analytic/models/analytic_account.py index 2445f7a926c..2059845b5b0 100644 --- a/addons/analytic/models/analytic_account.py +++ b/addons/analytic/models/analytic_account.py @@ -5,6 +5,7 @@ from collections import defaultdict import itertools from odoo import api, fields, models, _ from odoo.exceptions import UserError +from odoo.tools import groupby class AccountAnalyticAccount(models.Model): @@ -96,24 +97,12 @@ class AccountAnalyticAccount(models.Model): @api.constrains('company_id') def _check_company_consistency(self): - analytic_accounts = self.filtered('company_id') - - if not analytic_accounts: - return - - self.flush_recordset(['company_id']) - self.env['account.analytic.line'].flush_model(['account_id', 'company_id']) - - self._cr.execute(''' - SELECT line.account_id - FROM account_analytic_line line - JOIN account_analytic_account account ON line.account_id = account.id - WHERE line.company_id != account.company_id and account.company_id IS NOT NULL - AND account.id IN %s - ''', [tuple(self.ids)]) - - if self._cr.fetchone(): - raise UserError(_("You can't set a different company on your analytic account since there are some analytic items linked to it.")) + for company, accounts in groupby(self, lambda account: account.company_id): + if company and self.env['account.analytic.line'].search([ + ('account_id', 'in', [account.id for account in accounts]), + '!', ('company_id', 'child_of', company.id), + ], limit=1): + raise UserError(_("You can't set a different company on your analytic account since there are some analytic items linked to it.")) @api.depends('code', 'partner_id') def _compute_display_name(self): diff --git a/addons/analytic/models/analytic_plan.py b/addons/analytic/models/analytic_plan.py index 82466b95ab0..dfa8ca3a16c 100644 --- a/addons/analytic/models/analytic_plan.py +++ b/addons/analytic/models/analytic_plan.py @@ -21,7 +21,8 @@ class AccountAnalyticPlan(models.Model): 'account.analytic.plan', string="Parent", ondelete='cascade', - domain="[('id', '!=', id), ('company_id', 'in', [False, company_id])]", + check_company=True, + domain="[('id', '!=', id)]", ) parent_path = fields.Char( index='btree', @@ -135,8 +136,8 @@ class AccountAnalyticPlan(models.Model): company_id = kwargs.get('company_id', self.env.company.id) record_account_ids = kwargs.get('existing_account_ids', []) all_plans = self.search([ + *self._check_company_domain(company_id), ('account_ids', '!=', False), - '|', ('company_id', '=', company_id), ('company_id', '=', False), ]) root_plans = self.browse({ int(plan.parent_path.split('/')[0]) diff --git a/addons/analytic/views/analytic_distribution_model_views.xml b/addons/analytic/views/analytic_distribution_model_views.xml index 669f6f9457c..1f35f16977c 100644 --- a/addons/analytic/views/analytic_distribution_model_views.xml +++ b/addons/analytic/views/analytic_distribution_model_views.xml @@ -7,6 +7,7 @@ + @@ -29,6 +30,7 @@ + diff --git a/addons/base_setup/models/res_config_settings.py b/addons/base_setup/models/res_config_settings.py index b2b98a91988..9700eb9b158 100644 --- a/addons/base_setup/models/res_config_settings.py +++ b/addons/base_setup/models/res_config_settings.py @@ -11,6 +11,7 @@ class ResConfigSettings(models.TransientModel): company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.company) + is_root_company = fields.Boolean(compute='_compute_is_root_company') user_default_rights = fields.Boolean( "Default Access Rights", config_parameter='base_setup.default_user_rights') @@ -119,3 +120,8 @@ class ResConfigSettings(models.TransientModel): for record in self: record.company_informations = informations + + @api.depends('company_id') + def _compute_is_root_company(self): + for record in self: + record.is_root_company = not record.company_id.parent_id diff --git a/addons/base_setup/views/res_config_settings_views.xml b/addons/base_setup/views/res_config_settings_views.xml index 223acd2e7ef..cc8fdb9afb4 100644 --- a/addons/base_setup/views/res_config_settings_views.xml +++ b/addons/base_setup/views/res_config_settings_views.xml @@ -7,6 +7,7 @@ +
diff --git a/addons/base_vat/models/account_fiscal_position.py b/addons/base_vat/models/account_fiscal_position.py index f0619c97426..6291b5629c9 100644 --- a/addons/base_vat/models/account_fiscal_position.py +++ b/addons/base_vat/models/account_fiscal_position.py @@ -33,9 +33,9 @@ class AccountFiscalPosition(models.Model): # If the company has a fiscal position with a foreign vat in Europe, in the same country as the partner, then the VIES validity applies if self.search_count([ + *self._check_company_domain(company), ('foreign_vat', '!=', False), ('country_id', '=', delivery.country_id.id), - ('company_id', '=', company.id) ]) or company.country_id in eu_countries: return super()._get_vat_valid(delivery, company) and delivery.vies_valid diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index fd4e0aa5796..e5ddd9df9d4 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -110,14 +110,13 @@ class Lead(models.Model): compute='_compute_name', readonly=False, store=True) user_id = fields.Many2one( 'res.users', string='Salesperson', default=lambda self: self.env.user, - domain="['&', ('share', '=', False), ('company_ids', 'in', user_company_ids)]", + domain="[('share', '=', False)]", check_company=True, index=True, tracking=True) user_company_ids = fields.Many2many( 'res.company', compute='_compute_user_company_ids', help='UX: Limit to lead company or all if no company') team_id = fields.Many2one( 'crm.team', string='Sales Team', check_company=True, index=True, tracking=True, - domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", compute='_compute_team_id', ondelete="set null", readonly=False, store=True, precompute=True) lead_properties = fields.Properties( 'Properties', definition='team_id.lead_properties_definition', @@ -175,7 +174,6 @@ class Lead(models.Model): # Customer / contact partner_id = fields.Many2one( 'res.partner', string='Customer', check_company=True, index=True, tracking=10, - domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", help="Linked partner (optional). Usually created when converting the lead. You can find a partner by its Name, TIN, Email or Internal Reference.") partner_is_blacklisted = fields.Boolean('Partner is blacklisted', related='partner_id.is_blacklisted', readonly=True) contact_name = fields.Char( diff --git a/addons/crm/tests/test_crm_lead_multicompany.py b/addons/crm/tests/test_crm_lead_multicompany.py index 8f26eb18872..7b6118d5b3a 100644 --- a/addons/crm/tests/test_crm_lead_multicompany.py +++ b/addons/crm/tests/test_crm_lead_multicompany.py @@ -205,6 +205,7 @@ class TestCRMLeadMultiCompany(TestCrmCommon): # writing current user on lead would imply putting its team and team's company # on lead (aka self.company_2), and this clashes with company restriction on # customer + self.env.user.company_ids -= self.company_main with self.assertRaises(UserError): lead.write({ 'user_id': self.env.user, diff --git a/addons/delivery/models/sale_order.py b/addons/delivery/models/sale_order.py index d4025e2844e..a2d4f491e97 100644 --- a/addons/delivery/models/sale_order.py +++ b/addons/delivery/models/sale_order.py @@ -7,7 +7,7 @@ from odoo.exceptions import UserError class SaleOrder(models.Model): _inherit = 'sale.order' - carrier_id = fields.Many2one('delivery.carrier', string="Delivery Method", domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", help="Fill this field if you plan to invoice the shipping based on picking.") + carrier_id = fields.Many2one('delivery.carrier', string="Delivery Method", check_company=True, help="Fill this field if you plan to invoice the shipping based on picking.") delivery_message = fields.Char(readonly=True, copy=False) delivery_rating_success = fields.Boolean(copy=False) delivery_set = fields.Boolean(compute='_compute_delivery_state') diff --git a/addons/delivery/wizard/choose_delivery_carrier.py b/addons/delivery/wizard/choose_delivery_carrier.py index 595244999cb..46cfb335fe3 100644 --- a/addons/delivery/wizard/choose_delivery_carrier.py +++ b/addons/delivery/wizard/choose_delivery_carrier.py @@ -61,7 +61,7 @@ class ChooseDeliveryCarrier(models.TransientModel): @api.depends('partner_id') def _compute_available_carrier(self): for rec in self: - carriers = self.env['delivery.carrier'].search(['|', ('company_id', '=', False), ('company_id', '=', rec.order_id.company_id.id)]) + carriers = self.env['delivery.carrier'].search(self.env['delivery.carrier']._check_company_domain(rec.order_id.company_id)) rec.available_carrier_ids = carriers.available_carriers(rec.order_id.partner_shipping_id) if rec.partner_id else carriers def _get_shipment_rate(self): diff --git a/addons/event/models/event_event.py b/addons/event/models/event_event.py index a7bfa97cb06..ecdc1ac5228 100644 --- a/addons/event/models/event_event.py +++ b/addons/event/models/event_event.py @@ -127,7 +127,7 @@ class EventEvent(models.Model): organizer_id = fields.Many2one( 'res.partner', string='Organizer', tracking=True, default=lambda self: self.env.company.partner_id, - domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") + check_company=True) event_type_id = fields.Many2one('event.type', string='Template', ondelete='set null') event_mail_ids = fields.One2many( 'event.mail', 'event_id', string='Mail Schedule', copy=True, @@ -207,7 +207,9 @@ class EventEvent(models.Model): # Location and communication address_id = fields.Many2one( 'res.partner', string='Venue', default=lambda self: self.env.company.partner_id.id, - tracking=True, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") + check_company=True, + tracking=True + ) address_search = fields.Many2one( 'res.partner', string='Address', compute='_compute_address_search', search='_search_address_search') address_inline = fields.Char( diff --git a/addons/event_crm/models/event_lead_rule.py b/addons/event_crm/models/event_lead_rule.py index b0edf0b1ea2..70d3a0fc250 100644 --- a/addons/event_crm/models/event_lead_rule.py +++ b/addons/event_crm/models/event_lead_rule.py @@ -93,7 +93,7 @@ class EventLeadRule(models.Model): help='Filter the attendees to include those of this specific event category. If not set, no event category restriction will be applied.') event_id = fields.Many2one( 'event.event', string='Event', - domain="[('company_id', 'in', [company_id or current_company_id, False])]", + check_company=True, help='Filter the attendees to include those of this specific event. If not set, no event restriction will be applied.') company_id = fields.Many2one( 'res.company', string='Company', diff --git a/addons/fleet/models/fleet_vehicle.py b/addons/fleet/models/fleet_vehicle.py index 84cae82379e..668ec2935b6 100644 --- a/addons/fleet/models/fleet_vehicle.py +++ b/addons/fleet/models/fleet_vehicle.py @@ -47,7 +47,7 @@ class FleetVehicle(models.Model): vin_sn = fields.Char('Chassis Number', help='Unique number written on the vehicle motor (VIN/SN number)', copy=False) trailer_hook = fields.Boolean(default=False, string='Trailer Hitch', compute='_compute_model_fields', store=True, readonly=False) driver_id = fields.Many2one('res.partner', 'Driver', tracking=True, help='Driver address of the vehicle', copy=False) - future_driver_id = fields.Many2one('res.partner', 'Future Driver', tracking=True, help='Next Driver Address of the vehicle', copy=False, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") + future_driver_id = fields.Many2one('res.partner', 'Future Driver', tracking=True, help='Next Driver Address of the vehicle', copy=False, check_company=True) model_id = fields.Many2one('fleet.vehicle.model', 'Model', tracking=True, required=True) diff --git a/addons/hr/models/hr_department.py b/addons/hr/models/hr_department.py index b7ffe4b70d6..2ed7d22cede 100644 --- a/addons/hr/models/hr_department.py +++ b/addons/hr/models/hr_department.py @@ -17,9 +17,9 @@ class Department(models.Model): complete_name = fields.Char('Complete Name', compute='_compute_complete_name', recursive=True, store=True) active = fields.Boolean('Active', default=True) company_id = fields.Many2one('res.company', string='Company', index=True, default=lambda self: self.env.company) - parent_id = fields.Many2one('hr.department', string='Parent Department', index=True, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") + parent_id = fields.Many2one('hr.department', string='Parent Department', index=True, check_company=True) child_ids = fields.One2many('hr.department', 'parent_id', string='Child Departments') - manager_id = fields.Many2one('hr.employee', string='Manager', tracking=True, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") + manager_id = fields.Many2one('hr.employee', string='Manager', tracking=True, check_company=True) member_ids = fields.One2many('hr.employee', 'department_id', string='Members', readonly=True) total_employee = fields.Integer(compute='_compute_total_employee', string='Total Employee') jobs_ids = fields.One2many('hr.job', 'department_id', string='Jobs') diff --git a/addons/hr/models/hr_employee.py b/addons/hr/models/hr_employee.py index c07e50b05bb..c9a03f809d1 100644 --- a/addons/hr/models/hr_employee.py +++ b/addons/hr/models/hr_employee.py @@ -404,7 +404,7 @@ class HrEmployeePrivate(models.Model): if vals['work_contact_id']: self._message_subscribe([vals['work_contact_id']]) if 'user_id' in vals: - # Update the profile pictures with user, except if provided + # Update the profile pictures with user, except if provided vals.update(self._sync_user(self.env['res.users'].browse(vals['user_id']), (bool(self.image_1920)))) if 'work_permit_expiration_date' in vals: diff --git a/addons/hr/models/hr_employee_base.py b/addons/hr/models/hr_employee_base.py index 0de5022953d..c89a72dec6c 100644 --- a/addons/hr/models/hr_employee_base.py +++ b/addons/hr/models/hr_employee_base.py @@ -19,28 +19,29 @@ class HrEmployeeBase(models.AbstractModel): name = fields.Char() active = fields.Boolean("Active") color = fields.Integer('Color Index', default=0) - department_id = fields.Many2one('hr.department', 'Department', domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") + department_id = fields.Many2one('hr.department', 'Department', check_company=True) member_of_department = fields.Boolean("Member of department", compute='_compute_part_of_department', search='_search_part_of_department', help="Whether the employee is a member of the active user's department or one of it's child department.") - job_id = fields.Many2one('hr.job', 'Job Position', domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") + job_id = fields.Many2one('hr.job', 'Job Position', check_company=True) job_title = fields.Char("Job Title", compute="_compute_job_title", store=True, readonly=False) company_id = fields.Many2one('res.company', 'Company') address_id = fields.Many2one('res.partner', 'Work Address', compute="_compute_address_id", store=True, readonly=False, - domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") + check_company=True) work_phone = fields.Char('Work Phone', compute="_compute_phones", store=True, readonly=False) mobile_phone = fields.Char('Work Mobile', compute="_compute_work_contact_details", store=True, inverse='_inverse_work_contact_details') work_email = fields.Char('Work Email', compute="_compute_work_contact_details", store=True, inverse='_inverse_work_contact_details') work_contact_id = fields.Many2one('res.partner', 'Work Contact', copy=False) work_location_id = fields.Many2one('hr.work.location', 'Work Location', compute="_compute_work_location_id", store=True, readonly=False, - domain="[('address_id', '=', address_id), '|', ('company_id', '=', False), ('company_id', '=', company_id)]") + check_company=True, + domain="[('address_id', '=', address_id)]") user_id = fields.Many2one('res.users') resource_id = fields.Many2one('resource.resource') - resource_calendar_id = fields.Many2one('resource.calendar', domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") + resource_calendar_id = fields.Many2one('resource.calendar', check_company=True) parent_id = fields.Many2one('hr.employee', 'Manager', compute="_compute_parent_id", store=True, readonly=False, - domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") + check_company=True) coach_id = fields.Many2one( 'hr.employee', 'Coach', compute='_compute_coach', store=True, readonly=False, - domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", + check_company=True, help='Select the "Employee" who is the coach of this employee.\n' 'The "Coach" has no specific rights or responsibilities by default.') tz = fields.Selection( diff --git a/addons/hr/models/hr_job.py b/addons/hr/models/hr_job.py index 18e14606f65..6698a441d84 100644 --- a/addons/hr/models/hr_job.py +++ b/addons/hr/models/hr_job.py @@ -26,7 +26,7 @@ class Job(models.Model): employee_ids = fields.One2many('hr.employee', 'job_id', string='Employees', groups='base.group_user') description = fields.Html(string='Job Description', sanitize_attributes=False) requirements = fields.Text('Requirements') - department_id = fields.Many2one('hr.department', string='Department', domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") + department_id = fields.Many2one('hr.department', string='Department', check_company=True) company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company) contract_type_id = fields.Many2one('hr.contract.type', string='Employment Type') diff --git a/addons/hr/models/hr_plan.py b/addons/hr/models/hr_plan.py index 2e994a46a26..1f234226ed9 100644 --- a/addons/hr/models/hr_plan.py +++ b/addons/hr/models/hr_plan.py @@ -15,7 +15,7 @@ class HrPlan(models.Model): plan_activity_type_ids = fields.One2many( 'hr.plan.activity.type', 'plan_id', string='Activities', - domain="[('company_id', '=', company_id)]") + check_company=True) active = fields.Boolean(default=True) steps_count = fields.Integer(compute='_compute_steps_count') diff --git a/addons/hr/models/hr_work_location.py b/addons/hr/models/hr_work_location.py index 8b5da355b16..e576bfda468 100644 --- a/addons/hr/models/hr_work_location.py +++ b/addons/hr/models/hr_work_location.py @@ -12,5 +12,5 @@ class WorkLocation(models.Model): active = fields.Boolean(default=True) name = fields.Char(string="Work Location", required=True) company_id = fields.Many2one('res.company', required=True, default=lambda self: self.env.company) - address_id = fields.Many2one('res.partner', required=True, string="Work Address", domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") + address_id = fields.Many2one('res.partner', required=True, string="Work Address", check_company=True) location_number = fields.Char() diff --git a/addons/hr/models/res_users.py b/addons/hr/models/res_users.py index 880c8e04384..cc95ada4fee 100644 --- a/addons/hr/models/res_users.py +++ b/addons/hr/models/res_users.py @@ -242,7 +242,10 @@ class User(models.Model): # Raise meaningful error message raise AccessError(_("You are only allowed to update your preferences. Please contact a HR officer to update other information.")) - employee_domain = [('user_id', 'in', self.ids), ('company_id', '=', self.env.company.id)] + employee_domain = [ + *self.env['hr.employee']._check_company_domain(self.env.company), + ('user_id', 'in', self.ids), + ] if hr_fields: employees = self.env['hr.employee'].sudo().search(employee_domain) get_field = self.env['ir.model.fields']._get diff --git a/addons/hr/models/resource.py b/addons/hr/models/resource.py index 05af1c176cf..47fd0fb6152 100644 --- a/addons/hr/models/resource.py +++ b/addons/hr/models/resource.py @@ -8,4 +8,4 @@ class ResourceResource(models.Model): _inherit = "resource.resource" user_id = fields.Many2one(copy=False) - employee_id = fields.One2many('hr.employee', 'resource_id', domain="[('company_id', '=', company_id)]") + employee_id = fields.One2many('hr.employee', 'resource_id', check_company=True) diff --git a/addons/hr/wizard/hr_plan_wizard.py b/addons/hr/wizard/hr_plan_wizard.py index af80aef2018..b51e4390ab9 100644 --- a/addons/hr/wizard/hr_plan_wizard.py +++ b/addons/hr/wizard/hr_plan_wizard.py @@ -30,7 +30,8 @@ class HrPlanWizard(models.TransientModel): ], limit=1) plan_id = fields.Many2one('hr.plan', default=lambda self: self._default_plan_id(), - domain="[('company_id', 'in', [False, company_id]), '|', ('department_id', '=', department_id), ('department_id', '=', False)]") + check_company=True, + domain="['|', ('department_id', '=', department_id), ('department_id', '=', False)]") department_id = fields.Many2one('hr.department', compute='_compute_department_id') employee_ids = fields.Many2many( 'hr.employee', 'hr_employee_hr_plan_wizard_rel', 'employee_id', 'plan_wizard_id', string='Employee', required=True, diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index 4ba95779481..e841f595aa5 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -12,7 +12,6 @@ from odoo.tools import email_split, float_repr, float_compare, is_html_empty class HrExpense(models.Model): - _name = "hr.expense" _inherit = ['mail.thread.main.attachment', 'mail.activity.mixin', 'analytic.mixin'] _description = "Expense" @@ -56,7 +55,15 @@ class HrExpense(models.Model): states={'approved': [('readonly', True)], 'done': [('readonly', True)]}, default=_default_employee_id, domain=lambda self: self._get_employee_id_domain(), check_company=True) # product_id not required to allow create an expense without product via mail alias, but should be required on the view. - product_id = fields.Many2one('product.product', string='Category', tracking=True, states={'done': [('readonly', True)]}, domain="[('can_be_expensed', '=', True), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", ondelete='restrict') + product_id = fields.Many2one( + 'product.product', + string='Category', + tracking=True, + states={'done': [('readonly', True)]}, + check_company=True, + domain="[('can_be_expensed', '=', True)]", + ondelete='restrict', + ) product_description = fields.Html(compute='_compute_product_description') product_uom_id = fields.Many2one('uom.uom', string='Unit of Measure', compute='_compute_product_uom_id', store=True, precompute=True, copy=True, readonly=True, @@ -68,7 +75,8 @@ class HrExpense(models.Model): quantity = fields.Float(required=True, states={'done': [('readonly', True)]}, digits='Product Unit of Measure', default=1) tax_ids = fields.Many2many('account.tax', 'expense_tax', 'expense_id', 'tax_id', compute='_compute_tax_ids', store=True, readonly=False, precompute=True, - domain="[('company_id', '=', company_id), ('type_tax_use', '=', 'purchase')]", string='Included taxes', + check_company=True, + domain="[('type_tax_use', '=', 'purchase')]", string='Included taxes', help="Both price-included and price-excluded taxes will behave as price-included taxes for expenses.") amount_tax = fields.Monetary(string='Tax amount in Currency', help="Tax amount in currency", compute='_compute_amount_tax', store=True, currency_field='currency_id') amount_tax_company = fields.Monetary('Tax amount', help="Tax amount in company currency", compute='_compute_total_amount_company', store=True, currency_field='company_currency_id') @@ -80,8 +88,14 @@ class HrExpense(models.Model): company_id = fields.Many2one('res.company', string='Company', required=True, readonly=True, states={'draft': [('readonly', False)], 'refused': [('readonly', False)]}, default=lambda self: self.env.company) currency_id = fields.Many2one('res.currency', string='Currency', required=True, readonly=False, store=True, states={'reported': [('readonly', True)], 'approved': [('readonly', True)], 'done': [('readonly', True)]}, compute='_compute_currency_id', default=lambda self: self.env.company.currency_id) currency_rate = fields.Float(compute='_compute_currency_rate', tracking=True) - account_id = fields.Many2one('account.account', compute='_compute_account_id', store=True, readonly=False, precompute=True, string='Account', - domain="[('account_type', 'not in', ('asset_receivable','liability_payable','asset_cash','liability_credit_card')), ('company_id', '=', company_id)]", help="An expense account is expected") + account_id = fields.Many2one( + 'account.account', + compute='_compute_account_id', store=True, readonly=False, precompute=True, + string='Account', + check_company=True, + domain="[('account_type', 'not in', ('asset_receivable','liability_payable','asset_cash','liability_credit_card'))]", + help="An expense account is expected", + ) description = fields.Text('Internal Notes', readonly=True, states={'draft': [('readonly', False)], 'reported': [('readonly', False)], 'refused': [('readonly', False)]}) payment_mode = fields.Selection([ ("own_account", "Employee (to reimburse)"), @@ -144,7 +158,7 @@ class HrExpense(models.Model): def _compute_product_has_cost(self): for expense in self: expense.product_has_cost = expense.product_id and (float_compare(expense.product_id.standard_price, 0.0, precision_digits=2) != 0) - tax_ids = expense.product_id.supplier_taxes_id.filtered(lambda tax: tax.company_id == expense.company_id) + tax_ids = expense.product_id.supplier_taxes_id.filtered_domain(self.env['account.tax']._check_company_domain(expense.company_id)) expense.product_has_tax = bool(tax_ids) @api.depends('sheet_id', 'sheet_id.account_move_ids', 'sheet_id.state') @@ -310,7 +324,7 @@ class HrExpense(models.Model): def _compute_tax_ids(self): for expense in self: expense = expense.with_company(expense.company_id) - expense.tax_ids = expense.product_id.supplier_taxes_id.filtered(lambda tax: tax.company_id == expense.company_id) # taxes only from the same company + expense.tax_ids = expense.product_id.supplier_taxes_id.filtered_domain(self.env['account.tax']._check_company_domain(expense.company_id)) @api.depends('product_id', 'company_id') @@ -959,12 +973,21 @@ class HrExpenseSheet(models.Model): if company_journal_id: return company_journal_id.id default_company_id = self.default_get(['company_id'])['company_id'] - journal = self.env['account.journal'].search([('type', '=', 'purchase'), ('company_id', '=', default_company_id)], limit=1) + journal = self.env['account.journal'].search([ + *self.env['account.journal']._check_company_domain(default_company_id), + ('type', '=', 'purchase'), + ], limit=1) return journal.id name = fields.Char('Expense Report Summary', required=True, tracking=True) expense_line_ids = fields.One2many('hr.expense', 'sheet_id', string='Expense Lines', copy=False) - product_ids = fields.Many2many('product.product', compute='_compute_product_ids', search='_search_product_ids', string='Categories') + product_ids = fields.Many2many( + 'product.product', + string='Categories', + compute='_compute_product_ids', + search='_search_product_ids', + check_company=True, + ) expense_number = fields.Integer(compute='_compute_expense_number', string='Number of Expenses') nb_account_move = fields.Integer( string='Number of Journal Entries', @@ -1015,8 +1038,14 @@ class HrExpenseSheet(models.Model): tracking=True, related='expense_line_ids.payment_mode', readonly=True) employee_journal_id = fields.Many2one( - 'account.journal', string='Journal', states={'done': [('readonly', True)], 'post': [('readonly', True)]}, check_company=True, domain="[('type', '=', 'purchase'), ('company_id', '=', company_id)]", - default=_default_journal_id, help="The journal used when the expense is paid by employee.") + 'account.journal', + string='Journal', + states={'done': [('readonly', True)], 'post': [('readonly', True)]}, + check_company=True, + domain="[('type', '=', 'purchase')]", + default=_default_journal_id, + help="The journal used when the expense is paid by employee.", + ) selectable_payment_method_line_ids = fields.Many2many('account.payment.method.line', compute='_compute_selectable_payment_method_line_ids') payment_method_line_id = fields.Many2one( comodel_name='account.payment.method.line', @@ -1029,7 +1058,13 @@ class HrExpenseSheet(models.Model): ) accounting_date = fields.Date("Accounting Date") account_move_ids = fields.One2many('account.move', 'expense_sheet_id', string='Journal Entries', readonly=True) - journal_id = fields.Many2one('account.journal', compute='_compute_journal_id', string="Expense Journal", store=True) + journal_id = fields.Many2one( + 'account.journal', + compute='_compute_journal_id', + string="Expense Journal", + store=True, + check_company=True, + ) # === Security fields === # can_reset = fields.Boolean('Can Reset', compute='_compute_can_reset') diff --git a/addons/hr_expense/models/res_company.py b/addons/hr_expense/models/res_company.py index 4cf9f410025..740910dfc6d 100644 --- a/addons/hr_expense/models/res_company.py +++ b/addons/hr_expense/models/res_company.py @@ -16,12 +16,12 @@ class ResCompany(models.Model): "account.journal", string="Default Expense Journal", check_company=True, - domain="[('type', '=', 'purchase'), ('company_id', '=', company_id)]", + domain="[('type', '=', 'purchase')]", help="The company's default journal used when an employee expense is created.", ) company_expense_allowed_payment_method_line_ids = fields.Many2many( "account.payment.method.line", string="Payment methods available for expenses paid by company", check_company=True, - domain="[('payment_type', '=', 'outbound'), ('company_id', '=', company_id),('journal_id', '!=', False)]", + domain="[('payment_type', '=', 'outbound'), ('journal_id', '!=', False)]", ) diff --git a/addons/hr_expense/models/res_config_settings.py b/addons/hr_expense/models/res_config_settings.py index f59562b0cd4..3dedabc0964 100644 --- a/addons/hr_expense/models/res_config_settings.py +++ b/addons/hr_expense/models/res_config_settings.py @@ -16,7 +16,7 @@ class ResConfigSettings(models.TransientModel): module_hr_payroll_expense = fields.Boolean(string='Reimburse Expenses in Payslip') module_hr_expense_extract = fields.Boolean(string='Send bills to OCR to generate expenses') expense_product_id = fields.Many2one('product.product', related='company_id.expense_product_id', readonly=False) - expense_journal_id = fields.Many2one('account.journal', related='company_id.expense_journal_id', readonly=False) + expense_journal_id = fields.Many2one('account.journal', related='company_id.expense_journal_id', readonly=False, check_company=True) company_expense_allowed_payment_method_line_ids = fields.Many2many( comodel_name='account.payment.method.line', related='company_id.company_expense_allowed_payment_method_line_ids', diff --git a/addons/hr_expense/wizard/hr_expense_split.py b/addons/hr_expense/wizard/hr_expense_split.py index c57f65d22dc..150930bd469 100644 --- a/addons/hr_expense/wizard/hr_expense_split.py +++ b/addons/hr_expense/wizard/hr_expense_split.py @@ -6,10 +6,10 @@ from odoo.tools import float_compare class HrExpenseSplit(models.TransientModel): - _name = 'hr.expense.split' _inherit = ['analytic.mixin'] _description = 'Expense Split' + _check_company_auto = True def default_get(self, fields): result = super(HrExpenseSplit, self).default_get(fields) @@ -28,8 +28,12 @@ class HrExpenseSplit(models.TransientModel): name = fields.Char('Description', required=True) wizard_id = fields.Many2one('hr.expense.split.wizard') expense_id = fields.Many2one('hr.expense', string='Expense') - product_id = fields.Many2one('product.product', string='Product', required=True) - tax_ids = fields.Many2many('account.tax', domain="[('company_id', '=', company_id), ('type_tax_use', '=', 'purchase')]") + product_id = fields.Many2one('product.product', string='Product', required=True, check_company=True) + tax_ids = fields.Many2many( + 'account.tax', + check_company=True, + domain="[('type_tax_use', '=', 'purchase')]", + ) total_amount = fields.Monetary("Total In Currency", required=True, compute='_compute_from_product_id', store=True, readonly=False) amount_tax = fields.Monetary(string='Tax amount in Currency', compute='_compute_amount_tax') employee_id = fields.Many2one('hr.employee', string="Employee", required=True) @@ -57,12 +61,12 @@ class HrExpenseSplit(models.TransientModel): In case we switch to the product without taxes defined on it, taxes should be removed. Computed method won't be good for this purpose, as we don't want to recompute and reset taxes in case they are removed on purpose during splitting. """ - self.tax_ids = self.tax_ids if self.product_has_tax and self.tax_ids else self.product_id.supplier_taxes_id.filtered(lambda tax: tax.company_id == self.company_id) + self.tax_ids = self.tax_ids if self.product_has_tax and self.tax_ids else self.product_id.supplier_taxes_id.filtered_domain(self.env['account.tax']._check_company_domain(self.company_id)) @api.depends('product_id') def _compute_product_has_tax(self): for split in self: - split.product_has_tax = split.product_id and split.product_id.supplier_taxes_id.filtered(lambda tax: tax.company_id == split.company_id) + split.product_has_tax = split.product_id and split.product_id.supplier_taxes_id.filtered_domain(self.env['account.tax']._check_company_domain(split.company_id)) def _get_values(self): self.ensure_one() diff --git a/addons/l10n_ar/data/template/account.tax-ar_ex.csv b/addons/l10n_ar/data/template/account.tax-ar_ex.csv index 7ebaa1a8322..a05948abe04 100644 --- a/addons/l10n_ar/data/template/account.tax-ar_ex.csv +++ b/addons/l10n_ar/data/template/account.tax-ar_ex.csv @@ -1,5 +1,5 @@ "id","name","description","invoice_label","sequence","active","amount_type","amount","type_tax_use","tax_group_id","repartition_line_ids/repartition_type","repartition_line_ids/document_type","repartition_line_ids/account_id","description@es","invoice_label@es" -ri_tax_percepcion_iva_aplicada,"0%","Perception VAT applied","Perc VAT A","4","False","fixed","0.0","sale","tax_group_percepcion_iva","base","invoice","","Percepción IVA Aplicada","Perc IVA A" +ri_tax_percepcion_iva_aplicada,"0% EX","Perception VAT applied","Perc VAT A","4","False","fixed","0.0","sale","tax_group_percepcion_iva","base","invoice","","Percepción IVA Aplicada","Perc IVA A" "","","","","","","","","","","tax","invoice","ri_percepcion_iva_aplicada","","" "","","","","","","","","","","base","refund","","","" "","","","","","","","","","","tax","refund","ri_percepcion_iva_aplicada","","" diff --git a/addons/l10n_ar/demo/exento_demo.xml b/addons/l10n_ar/demo/exento_demo.xml index a5219142a09..96b0e7ba1ce 100644 --- a/addons/l10n_ar/demo/exento_demo.xml +++ b/addons/l10n_ar/demo/exento_demo.xml @@ -17,7 +17,6 @@ - (AR) Exento diff --git a/addons/l10n_ar/demo/mono_demo.xml b/addons/l10n_ar/demo/mono_demo.xml index e2df06578da..2f66888e4c1 100644 --- a/addons/l10n_ar/demo/mono_demo.xml +++ b/addons/l10n_ar/demo/mono_demo.xml @@ -17,7 +17,6 @@ - (AR) Monotributista diff --git a/addons/l10n_ar/demo/respinsc_demo.xml b/addons/l10n_ar/demo/respinsc_demo.xml index 506ad06e05a..464e7c13098 100644 --- a/addons/l10n_ar/demo/respinsc_demo.xml +++ b/addons/l10n_ar/demo/respinsc_demo.xml @@ -17,7 +17,6 @@ - (AR) Responsable Inscripto diff --git a/addons/l10n_ar/models/account_move.py b/addons/l10n_ar/models/account_move.py index 656da797106..ab5fbb0afc5 100644 --- a/addons/l10n_ar/models/account_move.py +++ b/addons/l10n_ar/models/account_move.py @@ -182,7 +182,11 @@ class AccountMove(models.Model): for rec in self.filtered(lambda x: x.company_id.account_fiscal_country_id.code == "AR" and x.journal_id.type == 'sale' and x.l10n_latam_use_documents and x.partner_id.l10n_ar_afip_responsibility_type_id): res_code = rec.partner_id.l10n_ar_afip_responsibility_type_id.code - domain = [('company_id', '=', rec.company_id.id), ('l10n_latam_use_documents', '=', True), ('type', '=', 'sale')] + domain = [ + *self.env['account.journal']._check_company_domain(rec.company_id), + ('l10n_latam_use_documents', '=', True), + ('type', '=', 'sale'), + ] journal = self.env['account.journal'] msg = False if res_code in ['9', '10'] and rec.journal_id.l10n_ar_afip_pos_system not in expo_journals: diff --git a/addons/l10n_ar/tests/common.py b/addons/l10n_ar/tests/common.py index 2678c35b6d4..60fbd1cfd4d 100644 --- a/addons/l10n_ar/tests/common.py +++ b/addons/l10n_ar/tests/common.py @@ -17,7 +17,6 @@ class TestAr(AccountTestInvoicingCommon): # ==== Company ==== cls.company_data['company'].write({ - 'parent_id': cls.env.ref('base.main_company').id, 'currency_id': cls.env.ref('base.ARS').id, 'name': '(AR) Responsable Inscripto (Unit Tests)', "l10n_ar_afip_start_date": time.strftime('%Y-01-01'), @@ -45,7 +44,6 @@ class TestAr(AccountTestInvoicingCommon): # ==== Company MONO ==== cls.company_mono = cls.setup_company_data('(AR) Monotributista (Unit Tests)', chart_template=chart_template_ref)['company'] cls.company_mono.write({ - 'parent_id': cls.env.ref('base.main_company').id, 'currency_id': cls.env.ref('base.ARS').id, 'name': '(AR) Monotributista (Unit Tests)', "l10n_ar_afip_start_date": time.strftime('%Y-01-01'), diff --git a/addons/l10n_cl/demo/account_demo.py b/addons/l10n_cl/demo/account_demo.py index 2475e0a64b5..aacc57042e3 100644 --- a/addons/l10n_cl/demo/account_demo.py +++ b/addons/l10n_cl/demo/account_demo.py @@ -31,8 +31,8 @@ class AccountChartTemplate(models.AbstractModel): if company.account_fiscal_country_id.code == "CL": foreign = ref('l10n_cl.dc_fe_dte').id self.env['account.journal'].search([ + *self.env['account.journal']._check_company_domain(company), ('type', '=', 'purchase'), - ('company_id', '=', self.env.company.id), ]).l10n_latam_use_documents = False move_data['demo_invoice_1']['l10n_latam_document_type_id'] = foreign move_data['demo_invoice_2']['l10n_latam_document_type_id'] = foreign diff --git a/addons/l10n_de/models/datev.py b/addons/l10n_de/models/datev.py index 51a01cfc711..0554ed8df06 100644 --- a/addons/l10n_de/models/datev.py +++ b/addons/l10n_de/models/datev.py @@ -36,13 +36,21 @@ class ProductTemplate(models.Model): company = self.env.company if company.account_fiscal_country_id.code == "DE": if not self.property_account_income_id: - taxes = self.taxes_id.filtered(lambda t: t.company_id == company) + taxes = self.taxes_id.filtered_domain(self.env['account.tax']._check_company_domain(company)) if not result['income'] or (result['income'].tax_ids and taxes and taxes[0] not in result['income'].tax_ids): - result['income'] = self.env['account.account'].search([('internal_group', '=', 'income'), ('deprecated', '=', False), - ('tax_ids', 'in', taxes.ids)], limit=1) + result['income'] = self.env['account.account'].search([ + *self.env['account.account']._check_company_domain(company), + ('internal_group', '=', 'income'), + ('deprecated', '=', False), + ('tax_ids', 'in', taxes.ids) + ], limit=1) if not self.property_account_expense_id: - supplier_taxes = self.supplier_taxes_id.filtered(lambda t: t.company_id == company) + supplier_taxes = self.supplier_taxes_id.filtered_domain(self.env['account.tax']._check_company_domain(company)) if not result['expense'] or (result['expense'].tax_ids and supplier_taxes and supplier_taxes[0] not in result['expense'].tax_ids): - result['expense'] = self.env['account.account'].search([('internal_group', '=', 'expense'), ('deprecated', '=', False), - ('tax_ids', 'in', supplier_taxes.ids)], limit=1) + result['expense'] = self.env['account.account'].search([ + *self.env['account.account']._check_company_domain(company), + ('internal_group', '=', 'expense'), + ('deprecated', '=', False), + ('tax_ids', 'in', supplier_taxes.ids), + ], limit=1) return result diff --git a/addons/l10n_do/data/template/account.tax-do.csv b/addons/l10n_do/data/template/account.tax-do.csv index be282bd001c..52ca9462b66 100644 --- a/addons/l10n_do/data/template/account.tax-do.csv +++ b/addons/l10n_do/data/template/account.tax-do.csv @@ -139,7 +139,7 @@ "tax_group_nonformal","60","75%","Withholding from Informal Suppliers of Goods (75%)","Withholding from Informal Suppliers of Goods (75%)","18.0","group","purchase","","tax_group_ret","tax_18_purch,ret_75_tax_nonformal","","","","","","Retención a Proveedores Informales de Bienes (75%)" "tax_group_person_construction","61","2% Mat.","Withholding to Individuals for Services with Materials (2%)","Withholding to Individuals for Services with Materials (2%)","18.0","group","purchase","","tax_group_ret","tax_18_purch,ret_100_tax_person,ret_2_income_person","","","","","","Retención a Jurídicas por Servicios Profesionales (30%)" "tax_group_person_services","58","10% Serv.","Withholding to Individuals for Person Services","Withholding to Individuals for Person Services","18.0","group","purchase","","tax_group_ret","tax_18_purch,ret_100_tax_person,ret_10_income_person","","","","","","Retención a Físicas por Honorarios por Servicios (10%)" -"tax_group_moral_services","58","2% Mat.","Withholding to Individuals for Moral Services","Withholding to Individuals for Moral Services","18.0","group","purchase","","tax_group_ret","tax_18_purch,ret_30_tax_moral","","","","","","Retención a Jurídicas por Servicios Profesionales (30%)" +"tax_group_moral_services","58","2% Serv P.","Withholding to Individuals for Moral Services","Withholding to Individuals for Moral Services","18.0","group","purchase","","tax_group_ret","tax_18_purch,ret_30_tax_moral","","","","","","Retención a Jurídicas por Servicios Profesionales (30%)" "tax_group_restaurant_sale","64","Restaurant","Restaurant Sales","Restaurant Sales","18.0","group","sale","","tax_group_ret","tax_18_sale,tax_tip_sale","","","","","","Ventas del Restaurante" "tax_group_restaurant_purch","65","Restaurant","Restaurant Purchases","Restaurant Purchases","18.0","group","purchase","","tax_group_ret","tax_18_purch,tax_tip_purch","","","","","","Compras a Restaurantes" "tax_18_10_total_mount","","18% of 10%","18% ITBIS on 10% of the Total Amount","18% ITBIS on 10% of the Total Amount","1.8","percent","purchase","False","tax_group_itbis","","","base","invoice","","","18% ITBIS sobre el 10% del Monto Total" diff --git a/addons/l10n_es_edi_facturae/__init__.py b/addons/l10n_es_edi_facturae/__init__.py index cf432c87e49..89d548178a3 100644 --- a/addons/l10n_es_edi_facturae/__init__.py +++ b/addons/l10n_es_edi_facturae/__init__.py @@ -16,7 +16,7 @@ def _edit_tax_types(env, template_data): ] if not concerned_company_ids: return - current_taxes = env['account.tax'].search([('company_id', 'in', concerned_company_ids)]) + current_taxes = env['account.tax'].search(env['account.tax']._check_company_domain(concerned_company_ids)) if not current_taxes: return xmlid2tax = { diff --git a/addons/l10n_eu_oss/models/res_company.py b/addons/l10n_eu_oss/models/res_company.py index fffce504a67..113fb593b83 100644 --- a/addons/l10n_eu_oss/models/res_company.py +++ b/addons/l10n_eu_oss/models/res_company.py @@ -27,22 +27,21 @@ class Company(models.Model): for company in self: invoice_repartition_lines, refund_repartition_lines = company._get_repartition_lines_oss() taxes = self.env['account.tax'].search([ + *self.env['account.tax']._check_company_domain(company), ('type_tax_use', '=', 'sale'), ('amount_type', '=', 'percent'), - ('company_id', '=', company.id), ('country_id', '=', company.account_fiscal_country_id.id), ('tax_group_id', 'not in', oss_tax_groups.mapped('res_id'))]) multi_tax_reports_countries_fpos = self.env['account.fiscal.position'].search([ - ('company_id', '=', company.id), ('foreign_vat', '!=', False), ]) oss_countries = eu_countries - company.account_fiscal_country_id - multi_tax_reports_countries_fpos.country_id for destination_country in oss_countries: mapping = [] fpos = self.env['account.fiscal.position'].search([ + *self.env['account.fiscal.position']._check_company_domain(company), ('country_id', '=', destination_country.id), - ('company_id', '=', company.id), ('auto_apply', '=', True), ('vat_required', '=', False), ('foreign_vat', '=', False)], limit=1) @@ -62,7 +61,9 @@ class Company(models.Model): if not foreign_taxes.get(tax_amount, False): oss_tax_group_local_xml_id = f"{company.id}_oss_tax_group_{str(tax_amount).replace('.', '_')}_{company.account_fiscal_country_id.code}" if not self.env.ref(f"account.{oss_tax_group_local_xml_id}", raise_if_not_found=False): - tg = self.env['account.tax.group'].search([('company_id', '=', company.id)]) + tg = self.env['account.tax.group'].search([ + *self.env['account.tax.group']._check_company_domain(company), + ]) self.env['ir.model.data'].create({ 'name': oss_tax_group_local_xml_id, 'module': 'account', @@ -113,8 +114,8 @@ class Company(models.Model): self.ensure_one() if not self.env.ref(f'l10n_eu_oss.oss_tax_account_company_{self.id}', raise_if_not_found=False): sales_tax_accounts = self.env['account.tax'].search([ + *self.env['account.tax']._check_company_domain(self), ('type_tax_use', '=', 'sale'), - ('company_id', '=', self.id) ]).invoice_repartition_line_ids.mapped('account_id') if not sales_tax_accounts: return False diff --git a/addons/l10n_fr/__init__.py b/addons/l10n_fr/__init__.py index a672a456855..9b98c07a6db 100644 --- a/addons/l10n_fr/__init__.py +++ b/addons/l10n_fr/__init__.py @@ -22,5 +22,5 @@ def _setup_inalterability(env): fr_companies._create_secure_sequence(['l10n_fr_closing_sequence_id']) for fr_company in fr_companies: - fr_journals = env['account.journal'].search([('company_id', '=', fr_company.id)]) + fr_journals = env['account.journal'].search(env['account.journal']._check_company_domain(fr_company)) fr_journals.filtered(lambda x: not x.secure_sequence_id)._create_secure_sequence(['secure_sequence_id']) diff --git a/addons/l10n_fr/models/res_company.py b/addons/l10n_fr/models/res_company.py index ceb530d7ad4..39dd96d722c 100644 --- a/addons/l10n_fr/models/res_company.py +++ b/addons/l10n_fr/models/res_company.py @@ -11,8 +11,8 @@ class ResCompany(models.Model): siret = fields.Char(related='partner_id.siret', string='SIRET', size=14, readonly=False) ape = fields.Char(string='APE') - l10n_fr_rounding_difference_loss_account_id = fields.Many2one('account.account') - l10n_fr_rounding_difference_profit_account_id = fields.Many2one('account.account') + l10n_fr_rounding_difference_loss_account_id = fields.Many2one('account.account', check_company=True) + l10n_fr_rounding_difference_profit_account_id = fields.Many2one('account.account', check_company=True) @api.model def _get_unalterable_country(self): diff --git a/addons/l10n_fr_fec/wizard/account_fr_fec.py b/addons/l10n_fr_fec/wizard/account_fr_fec.py index af2becc3d68..4173b25774f 100644 --- a/addons/l10n_fr_fec/wizard/account_fr_fec.py +++ b/addons/l10n_fr_fec/wizard/account_fr_fec.py @@ -26,7 +26,7 @@ class AccountFrFec(models.TransientModel): ('official', 'Official FEC report (posted entries only)'), ('nonofficial', 'Non-official FEC report (posted and unposted entries)'), ], string='Export Type', required=True, default='official') - excluded_journal_ids = fields.Many2many('account.journal', string="Excluded Journals", domain="[('company_id', '=', current_company_id)]") + excluded_journal_ids = fields.Many2many('account.journal', string="Excluded Journals", domain="[('company_id', 'parent_of', current_company_id)]") @api.onchange('test_file') def _onchange_export_file(self): @@ -152,8 +152,8 @@ class AccountFrFec(models.TransientModel): rows_to_write = [header] # INITIAL BALANCE unaffected_earnings_account = self.env['account.account'].search([ + *self.env['account.account']._check_company_domain(company), ('account_type', '=', 'equity_unaffected'), - ('company_id', '=', company.id) ], limit=1) unaffected_earnings_line = True # used to make sure that we add the unaffected earning initial balance only once if unaffected_earnings_account: @@ -236,8 +236,9 @@ class AccountFrFec(models.TransientModel): and (unaffected_earnings_results[11] != '0,00' or unaffected_earnings_results[12] != '0,00')): #search an unaffected earnings account - unaffected_earnings_account = self.env['account.account'].search([('account_type', '=', 'equity_unaffected'), - ('company_id', '=', company.id)], limit=1) + unaffected_earnings_account = self.env['account.account'].search([ + ('account_type', '=', 'equity_unaffected') + ], limit=1) if unaffected_earnings_account: unaffected_earnings_results[4] = unaffected_earnings_account.code unaffected_earnings_results[5] = unaffected_earnings_account.name diff --git a/addons/l10n_fr_pos_cert/models/account_closing.py b/addons/l10n_fr_pos_cert/models/account_closing.py index 9e46246fc7c..173c62d7f5c 100644 --- a/addons/l10n_fr_pos_cert/models/account_closing.py +++ b/addons/l10n_fr_pos_cert/models/account_closing.py @@ -41,8 +41,9 @@ class AccountClosing(models.Model): JOIN account_journal j ON aml.journal_id = j.id JOIN account_account acc ON acc.id = aml.account_id JOIN account_move m ON m.id = aml.move_id + JOIN res_company move_company ON move_company.id = m.company_id WHERE j.type = 'sale' - AND aml.company_id = %(company_id)s + AND SPLIT_PART(move_company.parent_path, '/', 1)::int = %(company_id)s AND m.state = 'posted' AND acc.account_type = 'asset_receivable' ''' diff --git a/addons/l10n_in_edi_ewaybill/demo/chart_template.py b/addons/l10n_in_edi_ewaybill/demo/chart_template.py index 4ad86e5a449..47f4c707bbb 100644 --- a/addons/l10n_in_edi_ewaybill/demo/chart_template.py +++ b/addons/l10n_in_edi_ewaybill/demo/chart_template.py @@ -11,8 +11,9 @@ class AccountChartTemplate(models.AbstractModel): company = company or self.env.company """We need to deactivate einvoice here, as we can not send e-invoice and e-waybill in the same demo company""" if company == self.env.ref('l10n_in_edi_ewaybill.demo_company_in_ewaybill'): - val = self.env['account.journal'].search([ + sales_journals = self.env['account.journal'].search([ + *self.env['account.journal']._check_company_domain(company), ('type', '=', 'sale'), - ('company_id', '=', company.id)]) - val.write({'edi_format_ids': [Command.unlink(self.env.ref('l10n_in_edi.edi_in_einvoice_json_1_03').id)]}) + ]) + sales_journals.write({'edi_format_ids': [Command.unlink(self.env.ref('l10n_in_edi.edi_in_einvoice_json_1_03').id)]}) return super()._get_demo_data(company) diff --git a/addons/l10n_it_edi/models/account_edi_format.py b/addons/l10n_it_edi/models/account_edi_format.py index 9f46bfbf7e1..2ff3f5a2f76 100644 --- a/addons/l10n_it_edi/models/account_edi_format.py +++ b/addons/l10n_it_edi/models/account_edi_format.py @@ -378,9 +378,10 @@ class AccountEdiFormat(models.Model): def _l10n_it_get_partner_invoice(self, tree, company): # Partner (first step to avoid warning 'Warning! You must first select a partner.'). <1.2> elements = tree.xpath('//CedentePrestatore//IdCodice') - partner = elements and self.env['res.partner'].search( - ['&', ('vat', 'ilike', elements[0].text), '|', ('company_id', '=', company.id), ('company_id', '=', False)], - limit=1) + partner = elements and self.env['res.partner'].search([ + *self.env['res.partner']._check_company_domain(company), + ('vat', 'ilike', elements[0].text) + ], limit=1) if not partner: elements = tree.xpath('//CedentePrestatore//CodiceFiscale') if elements: @@ -391,13 +392,16 @@ class AccountEdiFormat(models.Model): elif re.match(r'^IT[0-9]{11}$', codice): domains.append([('l10n_it_codice_fiscale', '=', self.env['res.partner']._l10n_it_edi_normalized_codice_fiscale(codice))]) - partner = elements and self.env['res.partner'].search( - AND([OR(domains), OR([[('company_id', '=', company.id)], [('company_id', '=', False)]])]), limit=1) + partner = elements and self.env['res.partner'].search(AND([ + *self.env['res.partner']._check_company_domain(company), + OR(domains) + ]), limit=1) if not partner: elements = tree.xpath('//DatiTrasmissione//Email') - partner = elements and self.env['res.partner'].search( - ['&', '|', ('email', '=', elements[0].text), ('l10n_it_pec_email', '=', elements[0].text), '|', - ('company_id', '=', company.id), ('company_id', '=', False)], limit=1) + partner = elements and self.env['res.partner'].search([ + *self.env['res.partner']._check_company_domain(company), + '|', ('email', '=', elements[0].text), ('l10n_it_pec_email', '=', elements[0].text) + ], limit=1) return partner @@ -415,8 +419,8 @@ class AccountEdiFormat(models.Model): def _l10n_it_edi_search_tax_for_import(self, company, percentage, extra_domain=None): """ Returns the VAT, Withholding or Pension Fund tax that suits the conditions given and matches the percentage found in the XML for the company. """ - conditions = [ - ('company_id', '=', company.id), + domain = [ + *self.env['account.tax']._check_company_domain(company), ('amount', '=', percentage), ('amount_type', '=', 'percent'), ('type_tax_use', '=', 'purchase'), @@ -425,7 +429,7 @@ class AccountEdiFormat(models.Model): # As we're importing vendor bills, we're excluding Reverse Charge Taxes # which have a [100.0, 100.0, -100.0] repartition lines factor_percent distribution. # We only allow for taxes that have all positive repartition lines factor_percent distribution. - taxes = self.env['account.tax'].search(conditions).filtered( + taxes = self.env['account.tax'].search(domain).filtered( lambda tax: all([rep_line.factor_percent >= 0 for rep_line in tax.invoice_repartition_line_ids])) return taxes[0] if taxes else taxes diff --git a/addons/l10n_it_edi/tests/common.py b/addons/l10n_it_edi/tests/common.py index 16e7a682aa4..b64ab4f191a 100644 --- a/addons/l10n_it_edi/tests/common.py +++ b/addons/l10n_it_edi/tests/common.py @@ -92,7 +92,7 @@ class TestItEdi(AccountEdiTestCommon): }) cls.default_tax = cls.env['account.tax'].with_company(cls.company).create({ - 'name': "22%", + 'name': "22% default", 'amount': 22.0, 'amount_type': 'percent', }) diff --git a/addons/l10n_latam_check/wizards/l10n_latam_payment_mass_transfer.py b/addons/l10n_latam_check/wizards/l10n_latam_payment_mass_transfer.py index e254088ed0d..650d0bbb427 100644 --- a/addons/l10n_latam_check/wizards/l10n_latam_payment_mass_transfer.py +++ b/addons/l10n_latam_check/wizards/l10n_latam_payment_mass_transfer.py @@ -7,6 +7,7 @@ from odoo.exceptions import UserError class L10nLatamPaymentMassTransfer(models.TransientModel): _name = 'l10n_latam.payment.mass.transfer' _description = 'Checks Mass Transfers' + _check_company_auto = True payment_date = fields.Date( string="Payment Date", @@ -16,13 +17,15 @@ class L10nLatamPaymentMassTransfer(models.TransientModel): destination_journal_id = fields.Many2one( comodel_name='account.journal', string='Destination Journal', - domain="[('type', 'in', ('bank', 'cash')), ('company_id', '=', company_id), ('id', '!=', journal_id)]", + check_company=True, + domain="[('type', 'in', ('bank', 'cash')), ('id', '!=', journal_id)]", ) communication = fields.Char( string="Memo", ) journal_id = fields.Many2one( 'account.journal', + check_company=True, compute='_compute_journal_company' ) company_id = fields.Many2one( @@ -31,6 +34,7 @@ class L10nLatamPaymentMassTransfer(models.TransientModel): ) check_ids = fields.Many2many( 'account.payment', + check_company=True, ) @api.depends('check_ids') diff --git a/addons/l10n_pe/demo/account_demo.py b/addons/l10n_pe/demo/account_demo.py index be473c8c47c..69551ef8b5e 100644 --- a/addons/l10n_pe/demo/account_demo.py +++ b/addons/l10n_pe/demo/account_demo.py @@ -11,7 +11,7 @@ class AccountChartTemplate(models.AbstractModel): def _get_demo_data_move(self, company=False): def _get_tax_by_code(code, type_tax='sale'): taxes = self.env['account.tax'].search([ - ('company_id', '=', company.id), ('type_tax_use', '=', type_tax), ('l10n_pe_edi_tax_code', '=', code)], limit=1) + ('type_tax_use', '=', type_tax), ('l10n_pe_edi_tax_code', '=', code)], limit=1) return [Command.set(taxes.ids)] move_data = super()._get_demo_data_move(company) @@ -53,7 +53,7 @@ class AccountChartTemplate(models.AbstractModel): move_data['demo_invoice_5']['invoice_line_ids'] = [ Command.create({'product_id': ref('product.consu_delivery_03').id, 'quantity': 1.0, 'price_unit': 500.0, 'tax_ids': _get_tax_by_code('1000', 'purchase')}), ] - igv_gyng_tax = self.env['account.tax'].search([('company_id', '=', company.id), ('type_tax_use', '=', 'purchase'), ('tax_group_id.name', '=', 'IGV GyNG')], limit=1) + igv_gyng_tax = self.env['account.tax'].search([('type_tax_use', '=', 'purchase'), ('tax_group_id.name', '=', 'IGV GyNG')], limit=1) move_data['demo_invoice_equipment_purchase']['partner_id'] = ref('base.res_partner_2').id move_data['demo_invoice_equipment_purchase']['invoice_date'] = last_month_date move_data['demo_invoice_equipment_purchase']['l10n_latam_document_number'] = 'FFF-100003' diff --git a/addons/l10n_sa_edi/models/res_company.py b/addons/l10n_sa_edi/models/res_company.py index e5db7e0a752..f18c2ccb0f4 100644 --- a/addons/l10n_sa_edi/models/res_company.py +++ b/addons/l10n_sa_edi/models/res_company.py @@ -44,12 +44,18 @@ class ResCompany(models.Model): l10n_sa_additional_identification_number = fields.Char( related='partner_id.l10n_sa_additional_identification_number', readonly=False) + def _get_company_root_delegated_field_names(self): + return super()._get_company_root_delegated_field_names() + [ + 'l10n_sa_api_mode', + 'l10n_sa_private_key', + ] + def write(self, vals): for company in self: if 'l10n_sa_api_mode' in vals: if company.l10n_sa_api_mode == 'prod' and vals['l10n_sa_api_mode'] != 'prod': raise UserError("You cannot change the ZATCA Submission Mode once it has been set to Production") - journals = self.env['account.journal'].search([('company_id', '=', company.id)]) + journals = self.env['account.journal'].search(self.env['account.journal']._check_company_domain(company)) journals._l10n_sa_reset_certificates() journals.l10n_sa_latest_submission_hash = False return super().write(vals) diff --git a/addons/mrp/models/mrp_bom.py b/addons/mrp/models/mrp_bom.py index 1bde4b6b350..bc0c0990aa8 100644 --- a/addons/mrp/models/mrp_bom.py +++ b/addons/mrp/models/mrp_bom.py @@ -32,11 +32,11 @@ class MrpBom(models.Model): product_tmpl_id = fields.Many2one( 'product.template', 'Product', check_company=True, index=True, - domain="[('type', 'in', ['product', 'consu']), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", required=True) + domain="[('type', 'in', ['product', 'consu'])]", required=True) product_id = fields.Many2one( 'product.product', 'Product Variant', check_company=True, index=True, - domain="['&', ('product_tmpl_id', '=', product_tmpl_id), ('type', 'in', ['product', 'consu']), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", + domain="['&', ('product_tmpl_id', '=', product_tmpl_id), ('type', 'in', ['product', 'consu'])]", help="If a product variant is defined the BOM is available only for this product.") bom_line_ids = fields.One2many('mrp.bom.line', 'bom_id', 'BoM Lines', copy=True) byproduct_ids = fields.One2many('mrp.bom.byproduct', 'bom_id', 'By-products', copy=True) @@ -56,7 +56,7 @@ class MrpBom(models.Model): ('asap', 'When components for 1st operation are available')], string='Manufacturing Readiness', default='all_available', required=True) picking_type_id = fields.Many2one( - 'stock.picking.type', 'Operation Type', domain="[('code', '=', 'mrp_operation'), ('company_id', '=', company_id)]", + 'stock.picking.type', 'Operation Type', domain="[('code', '=', 'mrp_operation')]", check_company=True, help=u"When a procurement has a ‘produce’ route with a operation type set, it will try to create " "a Manufacturing Order for that product using a BoM of the same operation type. That allows " diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index b17ac2e2eab..8ba01e8ba23 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -60,13 +60,7 @@ class MrpProduction(models.Model): product_id = fields.Many2one( 'product.product', 'Product', - domain="""[ - ('type', 'in', ['product', 'consu']), - '|', - ('company_id', '=', False), - ('company_id', '=', company_id) - ] - """, + domain="[('type', 'in', ['product', 'consu'])]", compute='_compute_product_id', store=True, copy=True, precompute=True, readonly=True, required=True, check_company=True, states={'draft': [('readonly', False)]}) @@ -84,14 +78,14 @@ class MrpProduction(models.Model): domain="[('category_id', '=', product_uom_category_id)]") lot_producing_id = fields.Many2one( 'stock.lot', string='Lot/Serial Number', copy=False, - domain="[('product_id', '=', product_id), ('company_id', '=', company_id)]", check_company=True) + domain="[('product_id', '=', product_id)]", check_company=True) qty_producing = fields.Float(string="Quantity Producing", digits='Product Unit of Measure', copy=False) product_uom_category_id = fields.Many2one(related='product_id.uom_id.category_id') product_uom_qty = fields.Float(string='Total Quantity', compute='_compute_product_uom_qty', store=True) picking_type_id = fields.Many2one( 'stock.picking.type', 'Operation Type', copy=True, readonly=False, compute='_compute_picking_type_id', store=True, precompute=True, - domain="[('code', '=', 'mrp_operation'), ('company_id', '=', company_id)]", + domain="[('code', '=', 'mrp_operation')]", required=True, check_company=True, index=True) use_create_components_lots = fields.Boolean(related='picking_type_id.use_create_components_lots') use_auto_consume_components_lots = fields.Boolean(related='picking_type_id.use_auto_consume_components_lots') @@ -99,7 +93,7 @@ class MrpProduction(models.Model): 'stock.location', 'Components Location', compute='_compute_locations', store=True, check_company=True, readonly=False, required=True, precompute=True, - domain="[('usage','=','internal'), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", + domain="[('usage','=','internal')]", help="Location where the system will look for components.") # this field was added to be passed a default in view for manual raw moves warehouse_id = fields.Many2one(related='location_src_id.warehouse_id') @@ -107,7 +101,7 @@ class MrpProduction(models.Model): 'stock.location', 'Finished Products Location', compute='_compute_locations', store=True, check_company=True, readonly=False, required=True, precompute=True, - domain="[('usage','=','internal'), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", + domain="[('usage','=','internal')]", help="Location where the system will stock the finished products.") date_deadline = fields.Datetime( 'Deadline', copy=False, store=True, readonly=True, compute='_compute_date_deadline', diff --git a/addons/mrp/models/mrp_unbuild.py b/addons/mrp/models/mrp_unbuild.py index 85f92ed8a24..b5bfa888da6 100644 --- a/addons/mrp/models/mrp_unbuild.py +++ b/addons/mrp/models/mrp_unbuild.py @@ -18,7 +18,7 @@ class MrpUnbuild(models.Model): name = fields.Char('Reference', copy=False, readonly=True, default=lambda x: _('New')) product_id = fields.Many2one( 'product.product', 'Product', check_company=True, - domain="[('type', 'in', ['product', 'consu']), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", + domain="[('type', 'in', ['product', 'consu'])]", required=True, states={'done': [('readonly', True)]}) company_id = fields.Many2one( 'res.company', 'Company', @@ -48,22 +48,22 @@ class MrpUnbuild(models.Model): states={'done': [('readonly', True)]}, check_company=True) mo_id = fields.Many2one( 'mrp.production', 'Manufacturing Order', - domain="[('state', '=', 'done'), ('company_id', '=', company_id), ('product_id', '=?', product_id), ('bom_id', '=?', bom_id)]", + domain="[('state', '=', 'done'), ('product_id', '=?', product_id), ('bom_id', '=?', bom_id)]", states={'done': [('readonly', True)]}, check_company=True) mo_bom_id = fields.Many2one('mrp.bom', 'Bill of Material used on the Production Order', related='mo_id.bom_id') lot_id = fields.Many2one( 'stock.lot', 'Lot/Serial Number', - domain="[('product_id', '=', product_id), ('company_id', '=', company_id)]", check_company=True) + domain="[('product_id', '=', product_id)]", check_company=True) has_tracking=fields.Selection(related='product_id.tracking', readonly=True) location_id = fields.Many2one( 'stock.location', 'Source Location', - domain="[('usage','=','internal'), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", + domain="[('usage','=','internal')]", check_company=True, compute='_compute_location_id', store=True, readonly=False, precompute=True, required=True, states={'done': [('readonly', True)]}, help="Location where the product you want to unbuild is.") location_dest_id = fields.Many2one( 'stock.location', 'Destination Location', - domain="[('usage','=','internal'), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", + domain="[('usage','=','internal')]", check_company=True, compute='_compute_location_id', store=True, readonly=False, precompute=True, required=True, states={'done': [('readonly', True)]}, help="Location where you want to send the components resulting from the unbuild order.") diff --git a/addons/mrp_account/models/product.py b/addons/mrp_account/models/product.py index d3e7258fab3..79978b36696 100644 --- a/addons/mrp_account/models/product.py +++ b/addons/mrp_account/models/product.py @@ -118,7 +118,7 @@ class ProductCategory(models.Model): property_stock_account_production_cost_id = fields.Many2one( 'account.account', 'Production Account', company_dependent=True, - domain="[('company_id', '=', allowed_company_ids[0]), ('deprecated', '=', False)]", check_company=True, + domain="[('deprecated', '=', False)]", check_company=True, help="""This account will be used as a valuation counterpart for both components and final products for manufacturing orders. If there are any workcenter/employee costs, this value will remain on the account once the production is completed.""") diff --git a/addons/payment/models/payment_provider.py b/addons/payment/models/payment_provider.py index 73cc1582b5a..cb8daf82330 100644 --- a/addons/payment/models/payment_provider.py +++ b/addons/payment/models/payment_provider.py @@ -15,6 +15,7 @@ class PaymentProvider(models.Model): _name = 'payment.provider' _description = 'Payment Provider' _order = 'module_state, state desc, sequence, name' + _check_company_auto = True def _valid_field_parameter(self, field, name): return name == 'required_if_provider' or super()._valid_field_parameter(field, name) @@ -476,7 +477,10 @@ class PaymentProvider(models.Model): :rtype: recordset of `payment.provider` """ # Compute the base domain for compatible providers. - domain = ['&', ('state', 'in', ['enabled', 'test']), ('company_id', '=', company_id)] + domain = [ + *self.env['payment.provider']._check_company_domain(company_id), + ('state', 'in', ['enabled', 'test']), + ] # Handle the is_published state. if not self.env.user._is_internal(): diff --git a/addons/payment/models/payment_token.py b/addons/payment/models/payment_token.py index fcb81f4d507..9746d1d2e94 100644 --- a/addons/payment/models/payment_token.py +++ b/addons/payment/models/payment_token.py @@ -10,6 +10,7 @@ class PaymentToken(models.Model): _name = 'payment.token' _order = 'partner_id, id desc' _description = 'Payment Token' + _check_company_auto = True provider_id = fields.Many2one(string="Provider", comodel_name='payment.provider', required=True) provider_code = fields.Selection(related='provider_id.code') diff --git a/addons/payment/models/res_company.py b/addons/payment/models/res_company.py index 6ca7745ed01..6b5416d0438 100644 --- a/addons/payment/models/res_company.py +++ b/addons/payment/models/res_company.py @@ -33,13 +33,15 @@ class ResCompany(models.Model): new_env = api.Environment(self.env.cr, self.env.uid, self.env.context) # Configure Stripe - default_journal = new_env['account.journal'].search( - [('type', '=', 'bank'), ('company_id', '=', new_env.company.id)], limit=1 - ) + default_journal = new_env['account.journal'].search([ + *self.env['account.journal']._check_company_domain(new_env.company), + ('type', '=', 'bank'), + ], limit=1) - stripe_provider = new_env['payment.provider'].search( - [('company_id', '=', self.env.company.id), ('code', '=', 'stripe')], limit=1 - ) + stripe_provider = new_env['payment.provider'].search([ + *self.env['payment.provider']._check_company_domain(self.env.company), + ('code', '=', 'stripe') + ], limit=1) if not stripe_provider: base_provider = self.env.ref('payment.payment_provider_stripe') # Use sudo to access payment provider record that can be in different company. diff --git a/addons/payment/security/payment_security.xml b/addons/payment/security/payment_security.xml index 58b5f69772c..0d7e82a8884 100644 --- a/addons/payment/security/payment_security.xml +++ b/addons/payment/security/payment_security.xml @@ -6,7 +6,7 @@ Access providers in own companies only - [('company_id', 'in', company_ids)] + [('company_id', 'parent_of', company_ids)] @@ -31,7 +31,7 @@ Access tokens in own companies only - [('company_id', 'in', company_ids)] + [('company_id', 'parent_of', company_ids)] diff --git a/addons/payment/wizards/payment_onboarding_wizard.py b/addons/payment/wizards/payment_onboarding_wizard.py index 90d539cae4a..666e253058b 100644 --- a/addons/payment/wizards/payment_onboarding_wizard.py +++ b/addons/payment/wizards/payment_onboarding_wizard.py @@ -39,8 +39,10 @@ class PaymentWizard(models.TransientModel): if env is None: env = self.env module_id = env.ref('base.module_payment_custom').id - return env['payment.provider'].search([('module_id', '=', module_id), - ('company_id', '=', env.company.id)], limit=1) + return env['payment.provider'].search([ + *env['payment.provider']._check_company_domain(self.env.company), + ('module_id', '=', module_id), + ], limit=1) def _get_default_payment_provider_onboarding_value(self, key): if not self.env.is_admin(): @@ -59,9 +61,11 @@ class PaymentWizard(models.TransientModel): ]).mapped('name') if 'payment_paypal' in installed_modules: - provider = self.env['payment.provider'].search( - [('company_id', '=', self.env.company.id), ('code', '=', 'paypal')], limit=1 - ) + provider = self.env['payment.provider'].search([ + *self.env['payment.provider']._check_company_domain(self.env.company), + ('code', '=', 'paypal'), + + ], limit=1) self._payment_provider_onboarding_cache['paypal_email_account'] = provider['paypal_email_account'] or self.env.company.email self._payment_provider_onboarding_cache['paypal_pdt_token'] = provider['paypal_pdt_token'] else: @@ -92,16 +96,18 @@ class PaymentWizard(models.TransientModel): new_env = api.Environment(self.env.cr, self.env.uid, self.env.context) if self.payment_method == 'paypal': - provider = new_env['payment.provider'].search( - [('company_id', '=', self.env.company.id), ('code', '=', 'paypal')], limit=1 - ) + provider = new_env['payment.provider'].search([ + *self.env['payment.provider']._check_company_domain(self.env.company), + ('code', '=', 'paypal') + ], limit=1) if not provider: base_provider = self.env.ref('payment.payment_provider_paypal') # Use sudo to access payment provider record that can be in different company. provider = base_provider.sudo().copy(default={'company_id':self.env.company.id}) - default_journal = new_env['account.journal'].search( - [('type', '=', 'bank'), ('company_id', '=', new_env.company.id)], limit=1 - ) + default_journal = new_env['account.journal'].search([ + *self.env['account.journal']._check_company_domain(new_env.company), + ('type', '=', 'bank') + ], limit=1) provider.write({ 'paypal_email_account': self.paypal_email_account, 'paypal_pdt_token': self.paypal_pdt_token, diff --git a/addons/payment_custom/models/payment_provider.py b/addons/payment_custom/models/payment_provider.py index 7b8888fd4d0..626b48ba861 100644 --- a/addons/payment_custom/models/payment_provider.py +++ b/addons/payment_custom/models/payment_provider.py @@ -51,7 +51,8 @@ class PaymentProvider(models.Model): for provider in self.filtered(lambda p: p.custom_mode == 'wire_transfer'): company_id = provider.company_id.id accounts = self.env['account.journal'].search([ - ('type', '=', 'bank'), ('company_id', '=', company_id) + *self.env['account.journal']._check_company_domain(company_id), + ('type', '=', 'bank'), ]).bank_account_id account_names = "".join(f"
  • {account.display_name}
  • " for account in accounts) provider.pending_msg = f'
    ' \ diff --git a/addons/point_of_sale/models/chart_template.py b/addons/point_of_sale/models/chart_template.py index fcda3d655a5..01cc718ae58 100644 --- a/addons/point_of_sale/models/chart_template.py +++ b/addons/point_of_sale/models/chart_template.py @@ -14,8 +14,8 @@ class AccountChartTemplate(models.AbstractModel): """ reload_template = template_code == company.chart_template if not reload_template: - self.env['pos.payment.method'].search([('company_id', '=', company.id)]).unlink() - self.env["pos.config"].search([('company_id', '=', company.id)]).write({ + self.env['pos.payment.method'].search(self.env['pos.payment.method']._check_company_domain(company)).unlink() + self.env["pos.config"].search(self.env['pos.config']._check_company_domain(company)).write({ 'journal_id': False, 'invoice_journal_id': False, }) diff --git a/addons/point_of_sale/models/pos_config.py b/addons/point_of_sale/models/pos_config.py index 51530d93cb3..90ec61a5491 100644 --- a/addons/point_of_sale/models/pos_config.py +++ b/addons/point_of_sale/models/pos_config.py @@ -6,31 +6,40 @@ from uuid import uuid4 import pytz from odoo import api, fields, models, _ +from odoo.osv.expression import OR from odoo.exceptions import ValidationError, UserError class PosConfig(models.Model): _name = 'pos.config' _description = 'Point of Sale Configuration' + _check_company_auto = True def _default_warehouse_id(self): - return self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1).id + return self.env['stock.warehouse'].search(self.env['stock.warehouse']._check_company_domain(self.env.company), limit=1).id def _default_picking_type_id(self): - return self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1).pos_type_id.id + return self.env['stock.warehouse'].search(self.env['stock.warehouse']._check_company_domain(self.env.company), limit=1).pos_type_id.id def _default_sale_journal(self): - return self.env['account.journal'].search([('type', 'in', ('sale', 'general')), ('company_id', '=', self.env.company.id), ('code', '=', 'POSS')], limit=1) + return self.env['account.journal'].search([ + *self.env['account.journal']._check_company_domain(self.env.company), + ('type', 'in', ('sale', 'general')), + ('code', '=', 'POSS'), + ], limit=1) def _default_invoice_journal(self): - return self.env['account.journal'].search([('type', '=', 'sale'), ('company_id', '=', self.env.company.id)], limit=1) + return self.env['account.journal'].search([ + *self.env['account.journal']._check_company_domain(self.env.company), + ('type', '=', 'sale'), + ], limit=1) def _default_payment_methods(self): """ Should only default to payment methods that are compatible to this config's company and currency. """ domain = [ + *self.env['pos.payment.method']._check_company_domain(self.env.company), ('split_transactions', '=', False), - ('company_id', '=', self.env.company.id), '|', ('journal_id', '=', False), ('journal_id.currency_id', 'in', (False, self.env.company.currency_id.id)), @@ -61,11 +70,13 @@ class PosConfig(models.Model): journal_id = fields.Many2one( 'account.journal', string='Point of Sale Journal', domain=[('type', 'in', ('general', 'sale'))], + check_company=True, help="Accounting journal used to post POS session journal entries and POS invoice payments.", default=_default_sale_journal, ondelete='restrict') invoice_journal_id = fields.Many2one( 'account.journal', string='Invoice Journal', + check_company=True, domain=[('type', '=', 'sale')], help="Accounting journal used to create invoices.", default=_default_invoice_journal) @@ -271,24 +282,12 @@ class PosConfig(models.Model): value=selection_value, )) - @api.constrains('company_id', 'journal_id') - def _check_company_journal(self): - for config in self: - if config.journal_id and config.journal_id.company_id.id != config.company_id.id: - raise ValidationError(_("The sales journal of the point of sale %s must belong to its company.", config.name)) - def _check_profit_loss_cash_journal(self): if self.cash_control and self.payment_method_ids: for method in self.payment_method_ids: if method.is_cash_count and (not method.journal_id.loss_account_id or not method.journal_id.profit_account_id): raise ValidationError(_("You need a loss and profit account on your cash journal.")) - @api.constrains('company_id', 'invoice_journal_id') - def _check_company_invoice_journal(self): - for config in self: - if config.invoice_journal_id and config.invoice_journal_id.company_id.id != config.company_id.id: - raise ValidationError(_("The invoice journal of the point of sale %s must belong to the same company.", config.name)) - @api.constrains('company_id', 'payment_method_ids') def _check_company_payment(self): for config in self: @@ -349,10 +348,7 @@ class PosConfig(models.Model): @api.constrains('payment_method_ids') def _check_payment_method_ids_journal(self): - cash_journal = self.env['account.journal'].search( - [('company_id', '=', self.env.company.id), ('type', '=', 'cash')]) - - for cash_method in self.payment_method_ids.filtered(lambda s: s.journal_id.id in cash_journal.ids): + for cash_method in self.payment_method_ids.filtered(lambda m: m.journal_id.type == 'cash'): if self.env['pos.config'].search([('id', '!=', self.id), ('payment_method_ids', 'in', cash_method.ids)]): raise ValidationError(_("This cash payment method is already used in another Point of Sale.\n" "A new cash payment method should be created for this Point of Sale.")) @@ -521,8 +517,6 @@ class PosConfig(models.Model): def _check_before_creating_new_session(self): self._check_company_has_template() self._check_pricelists() - self._check_company_journal() - self._check_company_invoice_journal() self._check_company_payment() self._check_currencies() self._check_profit_loss_cash_journal() @@ -605,8 +599,14 @@ class PosConfig(models.Model): for pos_config in self: if pos_config.payment_method_ids or pos_config.has_active_session: continue - cash_journal = self.env['account.journal'].search([('company_id', '=', company.id), ('type', '=', 'cash')], limit=1) - bank_journal = self.env['account.journal'].search([('company_id', '=', company.id), ('type', '=', 'bank')], limit=1) + cash_journal = self.env['account.journal'].search([ + *self.env['account.journal']._check_company_domain(company), + ('type', '=', 'cash'), + ], limit=1) + bank_journal = self.env['account.journal'].search([ + *self.env['account.journal']._check_company_domain(company), + ('type', '=', 'bank'), + ], limit=1) payment_methods = self.env['pos.payment.method'] if cash_journal and len(cash_journal.pos_payment_method_ids.ids) == 0: payment_methods |= payment_methods.create({ @@ -631,7 +631,10 @@ class PosConfig(models.Model): for pos_config in self: if pos_config.journal_id: continue - pos_journal = self.env['account.journal'].search([('company_id', '=', company.id), ('code', '=', 'POSS')]) + pos_journal = self.env['account.journal'].search([ + *self.env['account.journal']._check_company_domain(company), + ('code', '=', 'POSS'), + ]) if not pos_journal: pos_journal = self.env['account.journal'].create({ 'type': 'general', @@ -644,43 +647,46 @@ class PosConfig(models.Model): def setup_invoice_journal(self, company): for pos_config in self: - invoice_journal_id = pos_config.invoice_journal_id or self.env['account.journal'].search([('type', '=', 'sale'), ('company_id', '=', company.id)], limit=1) + invoice_journal_id = pos_config.invoice_journal_id or self.env['account.journal'].search([ + *self.env['account.journal']._check_company_domain(company), + ('type', '=', 'sale'), + ], limit=1) if invoice_journal_id: pos_config.write({'invoice_journal_id': invoice_journal_id.id}) + def _get_availlable_product_domain(self): + domain = [ + *self.env['product.product']._check_company_domain(self.company_id), + ('available_in_pos', '=', True), + ('sale_ok', '=', True), + ] + if self.iface_available_categ_ids: + domain.append(('pos_category_id', 'in', self.iface_available_categ_ids.ids)) + if self.iface_tipproduct: + domain = OR([domain, [('id', '=', self.tip_product_id.id)]]) + return domain + def get_limited_products_loading(self, fields): - query = """ + tables, where_clause, params = self.env['product.product']._where_calc( + self._get_availlable_product_domain() + ).get_sql() + query = f""" WITH pm AS ( SELECT product_id, Max(write_date) date FROM stock_quant GROUP BY product_id ) - SELECT p.id - FROM product_product p - LEFT JOIN product_template t ON product_tmpl_id=t.id - LEFT JOIN pm ON p.id=pm.product_id - WHERE ( - t.available_in_pos - AND t.sale_ok - AND (t.company_id=%(company_id)s OR t.company_id IS NULL) - AND (%(available_categ_ids)s IS NULL OR EXISTS ( - SELECT 1 FROM pos_category_product_template_rel - WHERE product_template_id = t.id - AND pos_category_id = ANY(%(available_categ_ids)s))) - ) OR p.id=%(tip_product_id)s - ORDER BY t.priority DESC, - t.detailed_type DESC, - COALESCE(pm.date,p.write_date) DESC - LIMIT %(limit)s + SELECT product_product.id + FROM {tables} + LEFT JOIN pm ON product_product.id=pm.product_id + WHERE {where_clause} + ORDER BY product_product__product_tmpl_id.priority DESC, + product_product__product_tmpl_id.detailed_type DESC, + COALESCE(pm.date, product_product.write_date) DESC + LIMIT %s """ - params = { - 'company_id': self.company_id.id, - 'available_categ_ids': self.iface_available_categ_ids.mapped('id') if self.iface_available_categ_ids else None, - 'tip_product_id': self.tip_product_id.id if self.tip_product_id else None, - 'limit': 20000 - } - self.env.cr.execute(query, params) + self.env.cr.execute(query, params + [20000]) product_ids = self.env.cr.fetchall() products = self.env['product.product'].search_read([('id', 'in', product_ids)], fields=fields) return products diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index 0cd7aa4f04d..3d0070a353d 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -29,7 +29,7 @@ class PosOrder(models.Model): @api.model def _amount_line_tax(self, line, fiscal_position_id): - taxes = line.tax_ids.filtered(lambda t: t.company_id.id == line.order_id.company_id.id) + taxes = line.tax_ids.filtered_domain(self.env['account.tax']._check_company_domain(line.order_id.company_id)) taxes = fiscal_position_id.map_tax(taxes) price = line.price_unit * (1 - (line.discount or 0.0) / 100.0) taxes = taxes.compute_all(price, line.order_id.currency_id, line.qty, product=line.product_id, partner=line.order_id.partner_id or False)['taxes'] @@ -1436,7 +1436,7 @@ class PosOrderLine(models.Model): price = self.order_id.pricelist_id._get_product_price( self.product_id, self.qty or 1.0, currency=self.currency_id ) - self.tax_ids = self.product_id.taxes_id.filtered(lambda r: not self.company_id or r.company_id == self.company_id) + self.tax_ids = self.product_id.taxes_id.filtered_domain(self.env['account.tax']._check_company_domain(self.company_id)) tax_ids_after_fiscal_position = self.order_id.fiscal_position_id.map_tax(self.tax_ids) self.price_unit = self.env['account.tax']._fix_tax_included_price_company(price, self.tax_ids, tax_ids_after_fiscal_position, self.company_id) self._onchange_qty() diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py index 11b048a5384..ea782937f9d 100644 --- a/addons/point_of_sale/models/pos_session.py +++ b/addons/point_of_sale/models/pos_session.py @@ -9,7 +9,6 @@ from markupsafe import Markup, escape from odoo import api, fields, models, _, Command from odoo.exceptions import AccessError, UserError, ValidationError from odoo.tools import float_is_zero, float_compare, convert -from odoo.osv.expression import AND, OR from odoo.service.common import exp_version @@ -180,7 +179,7 @@ class PosSession(models.Model): for record in self: company = record.config_id.journal_id.company_id start_date = record.start_at.date() - if (company.period_lock_date and start_date <= company.period_lock_date) or (company.fiscalyear_lock_date and start_date <= company.fiscalyear_lock_date): + if (company.period_lock_date and start_date <= company.period_lock_date) or (start_date <= company._get_user_fiscal_lock_date()): raise ValidationError(_("You cannot create a session before the accounting lock date.")) def _check_invoices_are_posted(self): @@ -1730,7 +1729,7 @@ class PosSession(models.Model): def _loader_params_account_tax(self): return { 'search_params': { - 'domain': [('company_id', '=', self.company_id.id)], + 'domain': self.env['account.tax']._check_company_domain(self.company_id), 'fields': [ 'name', 'price_include', 'include_base_amount', 'is_base_affected', 'amount_type', 'children_tax_ids', 'amount', 'id' @@ -1924,18 +1923,9 @@ class PosSession(models.Model): return self.env['pos.category'].search_read(**params['search_params']) def _loader_params_product_product(self): - domain = [ - '&', '&', ('sale_ok', '=', True), ('available_in_pos', '=', True), '|', - ('company_id', '=', self.config_id.company_id.id), ('company_id', '=', False) - ] - if self.config_id.limit_categories and self.config_id.iface_available_categ_ids: - domain = AND([domain, [('pos_categ_ids', 'in', self.config_id.iface_available_categ_ids.ids)]]) - if self.config_id.iface_tipproduct: - domain = OR([domain, [('id', '=', self.config_id.tip_product_id.id)]]) - return { 'search_params': { - 'domain': domain, + 'domain': self.config_id._get_availlable_product_domain(), 'fields': [ 'display_name', 'lst_price', 'standard_price', 'categ_id', 'pos_categ_ids', 'taxes_id', 'barcode', 'default_code', 'to_weight', 'uom_id', 'description_sale', 'description', 'product_tmpl_id', 'tracking', diff --git a/addons/point_of_sale/models/res_company.py b/addons/point_of_sale/models/res_company.py index 328434bf783..277903632bb 100644 --- a/addons/point_of_sale/models/res_company.py +++ b/addons/point_of_sale/models/res_company.py @@ -29,13 +29,9 @@ class ResCompany(models.Model): for record in self: sessions_in_period = pos_session_model.search( [ - "&", - "&", - ("company_id", "=", record.id), + ("company_id", "child_of", record.id), ("state", "!=", "closed"), - "|", - ("start_at", "<=", record.period_lock_date), - ("start_at", "<=", record.fiscalyear_lock_date), + ("start_at", "<=", record._get_user_fiscal_lock_date()), ] ) if sessions_in_period: diff --git a/addons/point_of_sale/models/res_config_settings.py b/addons/point_of_sale/models/res_config_settings.py index 0963fb75d65..3fd30d9c66a 100644 --- a/addons/point_of_sale/models/res_config_settings.py +++ b/addons/point_of_sale/models/res_config_settings.py @@ -256,7 +256,10 @@ class ResConfigSettings(models.TransientModel): def _compute_pos_pricelist_id(self): for res_config in self: currency_id = res_config.pos_journal_id.currency_id.id if res_config.pos_journal_id.currency_id else res_config.pos_config_id.company_id.currency_id.id - pricelists_in_current_currency = self.env['product.pricelist'].search([('company_id', 'in', (False, res_config.pos_config_id.company_id.id)), ('currency_id', '=', currency_id)]) + pricelists_in_current_currency = self.env['product.pricelist'].search([ + *self.env['product.pricelist']._check_company_domain(res_config.pos_config_id.company_id), + ('currency_id', '=', currency_id), + ]) if not res_config.pos_use_pricelist: res_config.pos_available_pricelist_ids = pricelists_in_current_currency[:1] res_config.pos_pricelist_id = pricelists_in_current_currency[:1] diff --git a/addons/pos_restaurant/models/pos_config.py b/addons/pos_restaurant/models/pos_config.py index 6c7d39cc277..2015029eeb1 100644 --- a/addons/pos_restaurant/models/pos_config.py +++ b/addons/pos_restaurant/models/pos_config.py @@ -92,12 +92,19 @@ class PosConfig(models.Model): def add_cash_payment_method(self): companies = self.env['res.company'].search([]) for company in companies.filtered('chart_template'): - pos_configs = self.search([('company_id', '=', company.id), ('module_pos_restaurant', '=', True)]) + pos_configs = self.search([ + *self._check_company_domain(company), + ('module_pos_restaurant', '=', True) + ]) journal_counter = 2 for pos_config in pos_configs: if pos_config.payment_method_ids.filtered('is_cash_count'): continue - cash_journal = self.env['account.journal'].search([('company_id', '=', company.id), ('type', '=', 'cash'), ('pos_payment_method_ids', '=', False)], limit=1) + cash_journal = self.env['account.journal'].search([ + *self.env['account.journal']._check_company_domain(company), + ('type', '=', 'cash'), + ('pos_payment_method_ids', '=', False), + ], limit=1) if not cash_journal: cash_journal = self.env['account.journal'].create({ 'name': 'Cash %s' % journal_counter, @@ -120,10 +127,12 @@ class PosConfig(models.Model): if not companies: companies = self.env['res.company'].search([]) for company in companies.filtered('chart_template'): - pos_configs = self.search([('company_id', '=', company.id), ('module_pos_restaurant', '=', True)]) + pos_configs = self.search([ + *self.env['account.journal']._check_company_domain(company), + ('module_pos_restaurant', '=', True), + ]) if not pos_configs: - self = self.with_company(company) - pos_configs = self.env['pos.config'].create({ + pos_configs = self.env['pos.config'].with_company(company).create({ 'name': 'Bar', 'company_id': company.id, 'module_pos_restaurant': True, diff --git a/addons/pos_sale_product_configurator/models/product.py b/addons/pos_sale_product_configurator/models/product.py index c810fcf92cd..3d49c929b7d 100644 --- a/addons/pos_sale_product_configurator/models/product.py +++ b/addons/pos_sale_product_configurator/models/product.py @@ -21,6 +21,7 @@ class ProductProduct(models.Model): def _optional_product_pos_domain(self): return [ - '&', '&', ['sale_ok', '=', True], ['available_in_pos', '=', True], - '|', ['company_id', '=', self.env.company], ['company_id', '=', False] + *self.env['product.product']._check_company_domain(self.env.company), + ['sale_ok', '=', True], + ['available_in_pos', '=', True], ] diff --git a/addons/product/security/product_security.xml b/addons/product/security/product_security.xml index 4cbff24b335..1fbc4234f11 100644 --- a/addons/product/security/product_security.xml +++ b/addons/product/security/product_security.xml @@ -34,31 +34,31 @@ Product multi-company - ['|', ('company_id', 'in', company_ids), ('company_id', '=', False)] + ['|', ('company_id', 'parent_of', company_ids), ('company_id', '=', False)] product pricelist company rule - ['|', ('company_id', 'in', company_ids), ('company_id', '=', False)] + ['|', ('company_id', 'parent_of', company_ids), ('company_id', '=', False)] product pricelist item company rule - ['|', ('company_id', 'in', company_ids), ('company_id', '=', False)] + ['|', ('company_id', 'parent_of', company_ids), ('company_id', '=', False)] product supplierinfo company rule - ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] + ['|', ('company_id', '=', False), ('company_id', 'parent_of', company_ids)] product packaging company rule - ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] + ['|', ('company_id', '=', False), ('company_id', 'parent_of', company_ids)] diff --git a/addons/purchase/models/account_invoice.py b/addons/purchase/models/account_invoice.py index 10b7992d0bc..b71325a7a67 100644 --- a/addons/purchase/models/account_invoice.py +++ b/addons/purchase/models/account_invoice.py @@ -94,8 +94,8 @@ class AccountMove(models.Model): if self.partner_id and self.move_type in ['in_invoice', 'in_refund'] and self.currency_id != currency_id: if not self.env.context.get('default_journal_id'): journal_domain = [ + *self.env['account.journal']._check_company_domain(self.company_id), ('type', '=', 'purchase'), - ('company_id', '=', self.company_id.id), ('currency_id', '=', currency_id.id), ] default_journal_id = self.env['account.journal'].search(journal_domain, limit=1) diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py index 8d5fa67f4a3..ef290a254ef 100644 --- a/addons/purchase/models/purchase.py +++ b/addons/purchase/models/purchase.py @@ -1036,7 +1036,7 @@ class PurchaseOrderLine(models.Model): line = line.with_company(line.company_id) fpos = line.order_id.fiscal_position_id or line.order_id.fiscal_position_id._get_fiscal_position(line.order_id.partner_id) # filter taxes by company - taxes = line.product_id.supplier_taxes_id.filtered(lambda r: r.company_id == line.env.company) + taxes = line.product_id.supplier_taxes_id.filtered_domain(self.env['account.tax']._check_company_domain(line.company_id)) line.taxes_id = fpos.map_tax(taxes) @api.depends('invoice_lines.move_id.state', 'invoice_lines.quantity', 'qty_received', 'product_uom_qty', 'order_id.state') diff --git a/addons/purchase/views/purchase_views.xml b/addons/purchase/views/purchase_views.xml index 59c65c32a0e..c531330d624 100644 --- a/addons/purchase/views/purchase_views.xml +++ b/addons/purchase/views/purchase_views.xml @@ -221,6 +221,7 @@ + @@ -270,6 +271,7 @@ + diff --git a/addons/purchase_stock/wizard/product_replenish.py b/addons/purchase_stock/wizard/product_replenish.py index 50adced44f9..f578f4118e1 100644 --- a/addons/purchase_stock/wizard/product_replenish.py +++ b/addons/purchase_stock/wizard/product_replenish.py @@ -17,17 +17,18 @@ class ProductReplenish(models.TransientModel): if res.get('product_id'): product_id = self.env['product.product'].browse(res['product_id']) product_tmpl_id = product_id.product_tmpl_id + company = product_tmpl_id.company_id or self.env.company if 'warehouse_id' not in res: - company = product_tmpl_id.company_id or self.env.company - res['warehouse_id'] = self.env['stock.warehouse'].search([('company_id', '=', company.id)], limit=1).id + res['warehouse_id'] = self.env['stock.warehouse'].search([ + *self.env['stock.warehouse']._check_company_domain(company), + ], limit=1).id orderpoint = self.env['stock.warehouse.orderpoint'].search([('product_id', 'in', [product_tmpl_id.product_variant_id.id, product_id.id]), ("warehouse_id", "=", res['warehouse_id'])], limit=1) if orderpoint: res['supplier_id'] = orderpoint.supplier_id.id elif product_tmpl_id.seller_ids: res['supplier_id'] = product_tmpl_id.seller_ids[0].id if not product_tmpl_id.seller_ids: - company = product_tmpl_id.company_id or self.env.company - domain = ['|', ('company_id', '=', False), ('company_id', '=', company.id)] + self._get_allowed_route_domain() + domain = self.env['stock.route']._check_company_domain(company) + self._get_allowed_route_domain() domain = AND([domain, [('id', '!=', self.env.ref('purchase_stock.route_warehouse0_buy', raise_if_not_found=False).id)]]) res['route_id'] = self.env['stock.route'].search(domain, limit=1).id return res diff --git a/addons/repair/models/repair.py b/addons/repair/models/repair.py index 2f264d99ff1..5c2dab407b5 100644 --- a/addons/repair/models/repair.py +++ b/addons/repair/models/repair.py @@ -21,6 +21,7 @@ class Repair(models.Model): _description = 'Repair Order' _inherit = ['mail.thread', 'mail.activity.mixin'] _order = 'priority desc, create_date desc' + _check_company_auto = True name = fields.Char( 'Repair Reference', @@ -30,8 +31,9 @@ class Repair(models.Model): description = fields.Char('Repair Description') product_id = fields.Many2one( 'product.product', string='Product to Repair', - domain="[('type', 'in', ['product', 'consu']), '|', ('company_id', '=', company_id), ('company_id', '=', False)]", - readonly=True, required=True, states={'draft': [('readonly', False)]}, check_company=True) + check_company=True, + domain="[('type', 'in', ['product', 'consu'])]", + readonly=True, required=True, states={'draft': [('readonly', False)]}) product_qty = fields.Float( 'Product Quantity', default=1.0, digits='Product Unit of Measure', @@ -666,6 +668,7 @@ class Repair(models.Model): class RepairLine(models.Model): _name = 'repair.line' _description = 'Repair Line (parts)' + _check_company_auto = True name = fields.Text('Description', required=True) repair_id = fields.Many2one( @@ -680,14 +683,14 @@ class RepairLine(models.Model): ('remove', 'Remove')], 'Type', default='add', required=True) product_id = fields.Many2one( 'product.product', 'Product', required=True, check_company=True, - domain="[('type', 'in', ['product', 'consu']), '|', ('company_id', '=', company_id), ('company_id', '=', False)]") + domain="[('type', 'in', ['product', 'consu'])]") invoiced = fields.Boolean('Invoiced', copy=False, readonly=True) price_unit = fields.Float('Unit Price', required=True, digits='Product Price') price_subtotal = fields.Float('Subtotal', compute='_compute_price_total_and_subtotal', store=True, digits=0) price_total = fields.Float('Total', compute='_compute_price_total_and_subtotal', store=True, digits=0) tax_id = fields.Many2many( 'account.tax', 'repair_operation_line_tax', 'repair_operation_line_id', 'tax_id', 'Taxes', - domain="[('type_tax_use','=','sale'), ('company_id', '=', company_id)]", check_company=True) + domain="[('type_tax_use','=','sale')]", check_company=True) product_uom_qty = fields.Float( 'Quantity', default=1.0, digits='Product Unit of Measure', required=True) @@ -712,7 +715,7 @@ class RepairLine(models.Model): copy=False, readonly=True) lot_id = fields.Many2one( 'stock.lot', 'Lot/Serial', - domain="[('product_id','=', product_id), ('company_id', '=', company_id)]", check_company=True) + domain="[('product_id','=', product_id)]", check_company=True) state = fields.Selection([ ('draft', 'Draft'), ('confirmed', 'Confirmed'), @@ -804,6 +807,7 @@ class RepairLine(models.Model): class RepairFee(models.Model): _name = 'repair.fee' _description = 'Repair Fees' + _check_company_auto = True repair_id = fields.Many2one( 'repair.order', 'Repair Order Reference', @@ -815,7 +819,7 @@ class RepairFee(models.Model): name = fields.Text('Description', index=True, required=True) product_id = fields.Many2one( 'product.product', 'Product', check_company=True, - domain="[('type', '=', 'service'), '|', ('company_id', '=', company_id), ('company_id', '=', False)]") + domain="[('type', '=', 'service')]") product_uom_qty = fields.Float('Quantity', digits='Product Unit of Measure', required=True, default=1.0) price_unit = fields.Float('Unit Price', required=True, digits='Product Price') product_uom = fields.Many2one( @@ -827,7 +831,7 @@ class RepairFee(models.Model): price_total = fields.Float('Total', compute='_compute_price_total_and_subtotal', store=True, digits=0) tax_id = fields.Many2many( 'account.tax', 'repair_fee_line_tax', 'repair_fee_line_id', 'tax_id', 'Taxes', - domain="[('type_tax_use','=','sale'), ('company_id', '=', company_id)]", check_company=True) + domain="[('type_tax_use','=','sale')]", check_company=True) invoice_line_id = fields.Many2one('account.move.line', 'Invoice Line', copy=False, readonly=True, check_company=True) invoiced = fields.Boolean('Invoiced', copy=False, readonly=True) diff --git a/addons/sale/models/product_template.py b/addons/sale/models/product_template.py index 2c0a3ac4cee..b7796332a7b 100644 --- a/addons/sale/models/product_template.py +++ b/addons/sale/models/product_template.py @@ -67,7 +67,7 @@ class ProductTemplate(models.Model): if target_company: # don't prevent writing `False`, should always work subquery_products = self.env['product.product'].sudo().with_context(active_test=False)._search([('product_tmpl_id', 'in', self.ids)]) so_lines = self.env['sale.order.line'].sudo().search_read( - [('product_id', 'in', subquery_products), ('company_id', '!=', target_company.id)], + [('product_id', 'in', subquery_products), '!', ('company_id', 'child_of', target_company.root_id.id)], fields=['id', 'product_id'], ) used_products = list(map(lambda sol: sol['product_id'][1], so_lines)) diff --git a/addons/sale/models/sale_order.py b/addons/sale/models/sale_order.py index 65f8e905d62..469a8b2f48d 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -446,9 +446,11 @@ class SaleOrder(models.Model): key = (default_team_id, user_id, company_id) if key not in cached_teams: cached_teams[key] = self.env['crm.team'].with_context( - default_team_id=default_team_id + default_team_id=default_team_id, )._get_default_team_id( - user_id=user_id, domain=[('company_id', 'in', [company_id, False])]) + user_id=user_id, + domain=self.env['crm.team']._check_company_domain(company_id), + ) order.team_id = cached_teams[key] @api.depends('order_line.price_subtotal', 'order_line.price_tax', 'order_line.price_total') diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index bdbfe2882c2..2909513c6c1 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -82,7 +82,7 @@ class SaleOrderLine(models.Model): comodel_name='product.product', string="Product", change_default=True, ondelete='restrict', check_company=True, index='btree_not_null', - domain="[('sale_ok', '=', True), '|', ('company_id', '=', False), ('company_id', '=', company_id)]") + domain="[('sale_ok', '=', True)]") product_template_id = fields.Many2one( string="Product Template", comodel_name='product.template', @@ -413,7 +413,10 @@ class SaleOrderLine(models.Model): taxes_by_product_company[(product, tax.company_id)] += tax for company, lines in lines_by_company.items(): for line in lines.with_company(company): - taxes = taxes_by_product_company[(line.product_id, company)] + taxes, comp = None, company + while not taxes and comp: + taxes = taxes_by_product_company[(line.product_id, comp)] + comp = comp.parent_id if not line.product_id or not taxes: # Nothing to map line.tax_id = False diff --git a/addons/sale/tests/test_sale_order_down_payment.py b/addons/sale/tests/test_sale_order_down_payment.py index fb0c6f62119..c2b4569c233 100644 --- a/addons/sale/tests/test_sale_order_down_payment.py +++ b/addons/sale/tests/test_sale_order_down_payment.py @@ -1,3 +1,5 @@ +import uuid + from odoo.tests import tagged from odoo import Command from .common import TestSaleCommon @@ -66,7 +68,7 @@ class TestSaleOrderDownPayment(TestSaleCommon): @classmethod def create_tax(cls, amount, values=None): vals = { - 'name': 'Tax %s' % amount, + 'name': f'Tax {amount} {uuid.uuid4()}', 'amount_type': 'percent', 'amount': amount, 'type_tax_use': 'sale', diff --git a/addons/sale/tests/test_sale_to_invoice.py b/addons/sale/tests/test_sale_to_invoice.py index c1d2f261790..b5b881da438 100644 --- a/addons/sale/tests/test_sale_to_invoice.py +++ b/addons/sale/tests/test_sale_to_invoice.py @@ -235,7 +235,10 @@ class TestSaleToInvoice(TestSaleCommon): """ # Confirm the SO self.sale_order.action_confirm() - tax_downpayment = self.company_data['default_tax_sale'].copy({'price_include': True}) + tax_downpayment = self.company_data['default_tax_sale'].copy({ + 'name': 'default price included', + 'price_include': True, + }) # Let's do an invoice for a deposit of 100 product_id = self.env.company.sale_down_payment_product_id product_id.taxes_id = tax_downpayment.ids diff --git a/addons/sale/wizard/sale_make_invoice_advance.py b/addons/sale/wizard/sale_make_invoice_advance.py index 99f3d4b849c..cbf75e6b7d1 100644 --- a/addons/sale/wizard/sale_make_invoice_advance.py +++ b/addons/sale/wizard/sale_make_invoice_advance.py @@ -68,11 +68,13 @@ class SaleAdvancePaymentInv(models.TransientModel): comodel_name='account.account', string="Income Account", domain=[('deprecated', '=', False)], + check_company=True, help="Account used for deposits") deposit_taxes_id = fields.Many2many( comodel_name='account.tax', string="Customer Taxes", domain=[('type_tax_use', '=', 'sale')], + check_company=True, help="Taxes used for deposits") # UI diff --git a/addons/sale_management/models/sale_order_template_line.py b/addons/sale_management/models/sale_order_template_line.py index b1609a5ce12..850532b4956 100644 --- a/addons/sale_management/models/sale_order_template_line.py +++ b/addons/sale_management/models/sale_order_template_line.py @@ -36,7 +36,7 @@ class SaleOrderTemplateLine(models.Model): product_id = fields.Many2one( comodel_name='product.product', check_company=True, - domain="[('sale_ok', '=', True), ('company_id', 'in', [company_id, False])]") + domain="[('sale_ok', '=', True)]") name = fields.Text( string="Description", diff --git a/addons/sale_management/models/sale_order_template_option.py b/addons/sale_management/models/sale_order_template_option.py index 392815c1596..102ace459b0 100644 --- a/addons/sale_management/models/sale_order_template_option.py +++ b/addons/sale_management/models/sale_order_template_option.py @@ -21,7 +21,7 @@ class SaleOrderTemplateOption(models.Model): product_id = fields.Many2one( comodel_name='product.product', required=True, check_company=True, - domain="[('sale_ok', '=', True), ('company_id', 'in', [company_id, False])]") + domain="[('sale_ok', '=', True)]") name = fields.Text( string="Description", diff --git a/addons/sale_project/models/product.py b/addons/sale_project/models/product.py index a93eeead4c0..29dd9a66f03 100644 --- a/addons/sale_project/models/product.py +++ b/addons/sale_project/models/product.py @@ -33,10 +33,10 @@ class ProductTemplate(models.Model): creating a new project based on the selected template.") project_id = fields.Many2one( 'project.project', 'Project', company_dependent=True, - domain="['|', ('company_id', '=', False), '&', ('company_id', '=?', company_id), ('company_id', '=', current_company_id)]") + ) project_template_id = fields.Many2one( - 'project.project', 'Project Template', copy=True, - domain="['|', ('company_id', '=', False), '&', ('company_id', '=?', company_id), ('company_id', '=', current_company_id)]") + 'project.project', 'Project Template', company_dependent=True, copy=True, + ) service_policy = fields.Selection('_selection_service_policy', string="Service Invoicing Policy", compute='_compute_service_policy', inverse='_inverse_service_policy') service_type = fields.Selection(selection_add=[ ('milestones', 'Project Milestones'), diff --git a/addons/sale_timesheet/models/project.py b/addons/sale_timesheet/models/project.py index 9dc6cd32566..2ceb3c6f6ae 100644 --- a/addons/sale_timesheet/models/project.py +++ b/addons/sale_timesheet/models/project.py @@ -48,8 +48,9 @@ class Project(models.Model): ('detailed_type', '=', 'service'), ('invoice_policy', '=', 'delivery'), ('service_type', '=', 'timesheet'), - '|', ('company_id', '=', False), ('company_id', '=', company_id)]""", + ]""", help='Service that will be used by default when invoicing the time spent on a task. It can be modified on each task individually by selecting a specific sales order item.', + check_company=True, compute="_compute_timesheet_product_id", store=True, readonly=False, default=_default_timesheet_product_id) warning_employee_rate = fields.Boolean(compute='_compute_warning_employee_rate', compute_sudo=True) diff --git a/addons/spreadsheet_account/models/account.py b/addons/spreadsheet_account/models/account.py index 43b6e52639e..cbabac7779a 100644 --- a/addons/spreadsheet_account/models/account.py +++ b/addons/spreadsheet_account/models/account.py @@ -133,8 +133,8 @@ class AccountMove(models.Model): def get_account_group(self, account_types): data = self._read_group( [ + *self._check_company_domain(self.env.company), ("account_type", "in", account_types), - ("company_id", "=", self.env.company.id), ], ['account_type'], ['code:array_agg'], diff --git a/addons/stock/models/product_strategy.py b/addons/stock/models/product_strategy.py index 0395a3dd1d9..4e64aa9a524 100644 --- a/addons/stock/models/product_strategy.py +++ b/addons/stock/models/product_strategy.py @@ -28,7 +28,7 @@ class StockPutawayRule(models.Model): if self.env.context.get('active_model') == 'stock.location': return self.env.context.get('active_id') if not self.env.user.has_group('stock.group_stock_multi_warehouses'): - wh = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1) + wh = self.env['stock.warehouse'].search(self.env['stock.warehouse']._check_company_domain(self.env.company), limit=1) input_loc, _ = wh._get_input_output_locations(wh.reception_steps, wh.delivery_steps) return input_loc @@ -51,7 +51,7 @@ class StockPutawayRule(models.Model): return [] def _domain_product_id(self): - domain = "[('type', '!=', 'service'), '|', ('company_id', '=', False), ('company_id', '=', company_id)]" + domain = "[('type', '!=', 'service')]" if self.env.context.get('active_model') == 'product.template': return [('product_tmpl_id', '=', self.env.context.get('active_id'))] return domain @@ -63,11 +63,11 @@ class StockPutawayRule(models.Model): default=_default_category_id, domain=_domain_category_id, ondelete='cascade') location_in_id = fields.Many2one( 'stock.location', 'When product arrives in', check_company=True, - domain="[('child_ids', '!=', False), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", + domain="[('child_ids', '!=', False)]", default=_default_location_id, required=True, ondelete='cascade', index=True) location_out_id = fields.Many2one( 'stock.location', 'Store to sublocation', check_company=True, - domain="[('id', 'child_of', location_in_id), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", + domain="[('id', 'child_of', location_in_id)]", required=True, ondelete='cascade') sequence = fields.Integer('Priority', help="Give to the more specialized category, a higher priority to have them in top of the list.") company_id = fields.Many2one( diff --git a/addons/stock/models/stock_lot.py b/addons/stock/models/stock_lot.py index 356a2c91607..344a004604a 100644 --- a/addons/stock/models/stock_lot.py +++ b/addons/stock/models/stock_lot.py @@ -91,9 +91,6 @@ class StockLot(models.Model): domain = [ "('tracking', '!=', 'none')", "('type', '=', 'product')", - "'|'", - "('company_id', '=', False)", - "('company_id', '=', company_id)" ] if self.env.context.get('default_product_tmpl_id'): domain.insert(0, diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 289fc7a83ae..73cf7d03938 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -45,7 +45,7 @@ class StockMove(models.Model): product_id = fields.Many2one( 'product.product', 'Product', check_company=True, - domain="[('type', 'in', ['product', 'consu']), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", index=True, required=True, + domain="[('type', 'in', ['product', 'consu'])]", index=True, required=True, states={'done': [('readonly', True)]}) description_picking = fields.Text('Description of Picking') product_qty = fields.Float( diff --git a/addons/stock/models/stock_move_line.py b/addons/stock/models/stock_move_line.py index bba1ad7d4ea..55951852287 100644 --- a/addons/stock/models/stock_move_line.py +++ b/addons/stock/models/stock_move_line.py @@ -25,7 +25,7 @@ class StockMoveLine(models.Model): 'stock.move', 'Stock Operation', check_company=True, index=True) company_id = fields.Many2one('res.company', string='Company', readonly=True, required=True, index=True) - product_id = fields.Many2one('product.product', 'Product', ondelete="cascade", check_company=True, domain="[('type', '!=', 'service'), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", index=True) + product_id = fields.Many2one('product.product', 'Product', ondelete="cascade", check_company=True, domain="[('type', '!=', 'service')]", index=True) product_uom_id = fields.Many2one( 'uom.uom', 'Unit of Measure', required=True, domain="[('category_id', '=', product_uom_category_id)]", compute="_compute_product_uom_id", store=True, readonly=False, precompute=True, @@ -46,7 +46,7 @@ class StockMoveLine(models.Model): package_level_id = fields.Many2one('stock.package_level', 'Package Level', check_company=True) lot_id = fields.Many2one( 'stock.lot', 'Lot/Serial Number', - domain="[('product_id', '=', product_id), ('company_id', '=', company_id)]", check_company=True) + domain="[('product_id', '=', product_id)]", check_company=True) lot_name = fields.Char('Lot/Serial Number Name') result_package_id = fields.Many2one( 'stock.quant.package', 'Destination Package', diff --git a/addons/stock/models/stock_orderpoint.py b/addons/stock/models/stock_orderpoint.py index 20feb1a5e06..a551f6442bc 100644 --- a/addons/stock/models/stock_orderpoint.py +++ b/addons/stock/models/stock_orderpoint.py @@ -32,7 +32,7 @@ class StockWarehouseOrderpoint(models.Model): elif self.env.context.get('default_product_id', False): product_id = self.env.context.get('default_product_id', False) domain = f"('id', '=', {product_id})" - return f"[{domain}, '|', ('company_id', '=', False), ('company_id', '=', company_id)]" + return f"[{domain}]" name = fields.Char( 'Name', copy=False, required=True, readonly=True, diff --git a/addons/stock/models/stock_quant.py b/addons/stock/models/stock_quant.py index 3ae3c0726c5..49842d02277 100644 --- a/addons/stock/models/stock_quant.py +++ b/addons/stock/models/stock_quant.py @@ -30,11 +30,7 @@ class StockQuant(models.Model): def _domain_lot_id(self): if not self._is_inventory_mode(): return - domain = [ - "'|'", - "('company_id', '=', company_id)", - "('company_id', '=', False)" - ] + domain = [] if self.env.context.get('active_model') == 'product.product': domain.insert(0, "('product_id', '=', %s)" % self.env.context.get('active_id')) elif self.env.context.get('active_model') == 'product.template': diff --git a/addons/stock/models/stock_scrap.py b/addons/stock/models/stock_scrap.py index 39f16db7588..ac8566cf92b 100644 --- a/addons/stock/models/stock_scrap.py +++ b/addons/stock/models/stock_scrap.py @@ -20,7 +20,7 @@ class StockScrap(models.Model): company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company, required=True, states={'done': [('readonly', True)]}) origin = fields.Char(string='Source Document') product_id = fields.Many2one( - 'product.product', 'Product', domain="[('type', 'in', ['product', 'consu']), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", + 'product.product', 'Product', domain="[('type', 'in', ['product', 'consu'])]", required=True, states={'done': [('readonly', True)]}, check_company=True) product_uom_id = fields.Many2one( 'uom.uom', 'Unit of Measure', @@ -30,7 +30,7 @@ class StockScrap(models.Model): tracking = fields.Selection(string='Product Tracking', readonly=True, related="product_id.tracking") lot_id = fields.Many2one( 'stock.lot', 'Lot/Serial', - states={'done': [('readonly', True)]}, domain="[('product_id', '=', product_id), ('company_id', '=', company_id)]", check_company=True) + states={'done': [('readonly', True)]}, domain="[('product_id', '=', product_id)]", check_company=True) package_id = fields.Many2one( 'stock.quant.package', 'Package', states={'done': [('readonly', True)]}, check_company=True) @@ -40,11 +40,11 @@ class StockScrap(models.Model): location_id = fields.Many2one( 'stock.location', 'Source Location', compute='_compute_location_id', store=True, required=True, precompute=True, states={'done': [('readonly', True)]}, - domain="[('usage', '=', 'internal'), ('company_id', 'in', [company_id, False])]", check_company=True, readonly=False) + domain="[('usage', '=', 'internal')]", check_company=True, readonly=False) scrap_location_id = fields.Many2one( 'stock.location', 'Scrap Location', compute='_compute_scrap_location_id', store=True, required=True, precompute=True, states={'done': [('readonly', True)]}, - domain="[('scrap_location', '=', True), ('company_id', 'in', [company_id, False])]", check_company=True, readonly=False) + domain="[('scrap_location', '=', True)]", check_company=True, readonly=False) scrap_qty = fields.Float( 'Quantity', required=True, states={'done': [('readonly', True)]}, digits='Product Unit of Measure', compute='_compute_scrap_qty', default=0.0, readonly=False, store=True) @@ -125,11 +125,11 @@ class StockScrap(models.Model): 'scrap_id': self.id, 'location_dest_id': self.scrap_location_id.id, 'move_line_ids': [(0, 0, {'product_id': self.product_id.id, - 'product_uom_id': self.product_uom_id.id, + 'product_uom_id': self.product_uom_id.id, 'qty_done': self.scrap_qty, 'location_id': self.location_id.id, 'location_dest_id': self.scrap_location_id.id, - 'package_id': self.package_id.id, + 'package_id': self.package_id.id, 'owner_id': self.owner_id.id, 'lot_id': self.lot_id.id, })], # 'restrict_partner_id': self.owner_id.id, diff --git a/addons/stock/models/stock_storage_category.py b/addons/stock/models/stock_storage_category.py index f58b21e060b..dc5a4620a4c 100644 --- a/addons/stock/models/stock_storage_category.py +++ b/addons/stock/models/stock_storage_category.py @@ -60,7 +60,7 @@ class StorageCategoryProductCapacity(models.Model): elif self.env.context.get('default_product_id', False): product_id = self.env.context.get('default_product_id', False) domain = f"('id', '=', {product_id})" - return f"[{domain}, '|', ('company_id', '=', False), ('company_id', '=', company_id)]" + return f"[{domain}]" storage_category_id = fields.Many2one('stock.storage.category', ondelete='cascade', required=True, index=True) product_id = fields.Many2one('product.product', 'Product', domain=lambda self: self._domain_product_id(), ondelete='cascade', check_company=True) diff --git a/addons/stock/wizard/product_replenish.py b/addons/stock/wizard/product_replenish.py index 1a63c24bd25..02718125390 100644 --- a/addons/stock/wizard/product_replenish.py +++ b/addons/stock/wizard/product_replenish.py @@ -12,6 +12,7 @@ from odoo.tools.misc import clean_context class ProductReplenish(models.TransientModel): _name = 'product.replenish' _description = 'Product Replenish' + _check_company_auto = True product_id = fields.Many2one('product.product', string='Product', required=True) product_tmpl_id = fields.Many2one('product.template', string='Product Template', required=True) @@ -23,11 +24,13 @@ class ProductReplenish(models.TransientModel): date_planned = fields.Datetime('Scheduled Date', required=True, help="Date at which the replenishment should take place.") warehouse_id = fields.Many2one( 'stock.warehouse', string='Warehouse', required=True, - domain="[('company_id', '=', company_id)]") + check_company=True, + ) route_id = fields.Many2one( 'stock.route', string='Preferred Route', help="Apply specific route for the replenishment instead of product's default routes.", - domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") + check_company=True, + ) company_id = fields.Many2one('res.company') forecasted_quantity = fields.Float(string="Forecasted Quantity", compute="_compute_forecasted_quantity") allowed_route_ids = fields.Many2many("stock.route", compute="_compute_allowed_route_ids") diff --git a/addons/stock/wizard/stock_rules_report.py b/addons/stock/wizard/stock_rules_report.py index 6113e54fe34..0aee05a7d30 100644 --- a/addons/stock/wizard/stock_rules_report.py +++ b/addons/stock/wizard/stock_rules_report.py @@ -32,7 +32,7 @@ class StockRulesReport(models.TransientModel): res['product_has_variants'] = True if 'warehouse_ids' in fields: company = product_tmpl_id.company_id or self.env.company - warehouse_id = self.env['stock.warehouse'].search([('company_id', '=', company.id)], limit=1).id + warehouse_id = self.env['stock.warehouse'].search(self.env['stock.warehouse']._check_company_domain(company), limit=1).id res['warehouse_ids'] = [(6, 0, [warehouse_id])] return res diff --git a/addons/stock/wizard/stock_warn_insufficient_qty.py b/addons/stock/wizard/stock_warn_insufficient_qty.py index 19dccd23c8a..aeebc9dd24b 100644 --- a/addons/stock/wizard/stock_warn_insufficient_qty.py +++ b/addons/stock/wizard/stock_warn_insufficient_qty.py @@ -20,10 +20,11 @@ class StockWarnInsufficientQty(models.AbstractModel): @api.depends('product_id') def _compute_quant_ids(self): for quantity in self: + company = quantity._get_reference_document_company_id() quantity.quant_ids = self.env['stock.quant'].search([ + *self.env['stock.quant']._check_company_domain(company), ('product_id', '=', quantity.product_id.id), ('location_id.usage', '=', 'internal'), - ('company_id', '=', quantity._get_reference_document_company_id().id) ]) def action_done(self): diff --git a/addons/stock_account/models/product.py b/addons/stock_account/models/product.py index 1f541dd0de7..2a0ae743616 100644 --- a/addons/stock_account/models/product.py +++ b/addons/stock_account/models/product.py @@ -118,13 +118,17 @@ class ProductProduct(models.Model): company_id = self.env.company self.company_currency_id = company_id.currency_id domain = [ + *self.env['stock.valuation.layer']._check_company_domain(company_id), ('product_id', 'in', self.ids), - ('company_id', '=', company_id.id), ] 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, ['product_id'], ['value:sum', 'quantity:sum']) + groups = self.env['stock.valuation.layer']._read_group( + domain, + groupby=['product_id'], + aggregates=['value:sum', 'quantity:sum'], + ) remaining = self # Browse all products and compute products' quantities_dict in batch. for product, value_sum, quantity_sum in groups: @@ -740,22 +744,21 @@ class ProductCategory(models.Model): """) property_stock_journal = fields.Many2one( 'account.journal', 'Stock Journal', company_dependent=True, - domain="[('company_id', '=', allowed_company_ids[0])]", check_company=True, help="When doing automated inventory valuation, this is the Accounting Journal in which entries will be automatically posted when stock moves are processed.") property_stock_account_input_categ_id = fields.Many2one( 'account.account', 'Stock Input Account', company_dependent=True, - domain="[('company_id', '=', allowed_company_ids[0]), ('deprecated', '=', False)]", check_company=True, + domain="[('deprecated', '=', False)]", check_company=True, help="""Counterpart journal items for all incoming stock moves will be posted in this account, unless there is a specific valuation account set on the source location. This is the default value for all products in this category. It can also directly be set on each product.""") property_stock_account_output_categ_id = fields.Many2one( 'account.account', 'Stock Output Account', company_dependent=True, - domain="[('company_id', '=', allowed_company_ids[0]), ('deprecated', '=', False)]", check_company=True, + domain="[('deprecated', '=', False)]", check_company=True, help="""When doing automated inventory valuation, counterpart journal items for all outgoing stock moves will be posted in this account, unless there is a specific valuation account set on the destination location. This is the default value for all products in this category. It can also directly be set on each product.""") property_stock_valuation_account_id = fields.Many2one( 'account.account', 'Stock Valuation Account', company_dependent=True, - domain="[('company_id', '=', allowed_company_ids[0]), ('deprecated', '=', False)]", check_company=True, + domain="[('deprecated', '=', False)]", check_company=True, help="""When automated inventory valuation is enabled on a product, this account will hold the current value of the products.""",) @api.model diff --git a/addons/website_payment/models/payment_provider.py b/addons/website_payment/models/payment_provider.py index 0c3ecf2df92..7da47039c63 100644 --- a/addons/website_payment/models/payment_provider.py +++ b/addons/website_payment/models/payment_provider.py @@ -9,7 +9,7 @@ class PaymentProvider(models.Model): website_id = fields.Many2one( "website", - domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", + check_company=True, ondelete="restrict", ) diff --git a/addons/website_payment/models/res_config_settings.py b/addons/website_payment/models/res_config_settings.py index 091f072a02f..d80e41819c5 100644 --- a/addons/website_payment/models/res_config_settings.py +++ b/addons/website_payment/models/res_config_settings.py @@ -53,9 +53,10 @@ class ResConfigSettings(models.TransientModel): def action_configure_first_provider(self): self.ensure_one() - stripe = self.env['payment.provider'].search( - [('company_id', '=', self.env.company.id), ('code', '=', 'stripe')], limit=1 - ) + stripe = self.env['payment.provider'].search([ + *self.env['payment.provider']._check_company_domain(self.env.company), + ('code', '=', 'stripe') + ], limit=1) providers = self._get_activated_providers() return { 'name': self.first_provider_label, diff --git a/addons/website_sale/models/product_template.py b/addons/website_sale/models/product_template.py index 3b2adc492ca..a77c7803755 100644 --- a/addons/website_sale/models/product_template.py +++ b/addons/website_sale/models/product_template.py @@ -268,7 +268,7 @@ class ProductTemplate(models.Model): fpos_id = self.env['website'].sudo()._get_current_fiscal_position_id(partner) fiscal_position = self.env['account.fiscal.position'].sudo().browse(fpos_id) - product_taxes = product.sudo().taxes_id.filtered(lambda x: x.company_id == company_id) + product_taxes = product.sudo().taxes_id.filtered_domain(self.env['account.tax']._check_company_domain(company_id)) taxes = fiscal_position.map_tax(product_taxes) price = self._price_with_tax_computed( diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py index 1d21faea6ce..e770cf6bb94 100644 --- a/addons/website_sale/models/sale_order.py +++ b/addons/website_sale/models/sale_order.py @@ -340,18 +340,17 @@ class SaleOrder(models.Model): def _cart_accessories(self): """ Suggest accessories based on 'Accessory Products' of products in cart """ - products = self.website_order_line.product_id + product_ids = set(self.website_order_line.product_id.ids) all_accessory_products = self.env['product.product'] for line in self.website_order_line.filtered('product_id'): accessory_products = line.product_id.product_tmpl_id._get_website_accessory_product() if accessory_products: # Do not read ptavs if there is no accessory products to filter combination = line.product_id.product_template_attribute_value_ids + line.product_no_variant_attribute_value_ids - all_accessory_products |= accessory_products.filtered( - lambda product: - product not in products and - (not product.company_id or product.company_id == line.company_id) and - product._is_variant_possible(parent_combination=combination) + all_accessory_products |= accessory_products.filtered(lambda product: + product.id not in product_ids + and product.filtered_domain(self.env['product.product']._check_company_domain(line.company_id)) + and product._is_variant_possible(parent_combination=combination) ) return random.sample(all_accessory_products, len(all_accessory_products)) diff --git a/addons/website_sale/models/website_visitor.py b/addons/website_sale/models/website_visitor.py index 48dd536814a..4689fdbbab0 100644 --- a/addons/website_sale/models/website_visitor.py +++ b/addons/website_sale/models/website_visitor.py @@ -20,10 +20,10 @@ class WebsiteVisitor(models.Model): @api.depends('website_track_ids') def _compute_product_statistics(self): - 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:array_agg', '__count']) + results = self.env['website.track']._read_group([ + ('visitor_id', 'in', self.ids), ('product_id', '!=', False), + ('product_id', 'any', self.env['product.product']._check_company_domain(self.env.companies)), + ], ['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