[IMP] models: use slots for BaseModel

This restricts the attributes of a BaseModel instance to `env`, `_ids`
and `_prefetch_ids`.  This way, one can only assign fields on a record;
other assignments are programming errors.

This also reduces the memory footprint of records from 168 to 64 bytes
(-62%), and makes their instanciation faster.

closes odoo/odoo#51075

Related: odoo/enterprise#10529
Signed-off-by: Raphael Collet (rco) <rco@openerp.com>
This commit is contained in:
Raphael Collet
2020-05-18 09:51:43 +00:00
parent 2e56191c8b
commit ea3e39506a
19 changed files with 51 additions and 75 deletions
@@ -1252,7 +1252,7 @@ class AccountBankStatementLine(models.Model):
# Update the payment date to match the current bank statement line's date.
if counterpart_line.payment_id:
counterpart_line.payment_id.payment_date = self.date
counterpart_line.payment_id.date = self.date
# -------------------------------------------------------------------------
# BUSINESS METHODS
@@ -1249,15 +1249,15 @@ class TestAccountBankStatement(TestAccountBankStatementCommon):
test_line_1 = test_move.line_ids.filtered(lambda line: line.name == 'test line 1')
test_line_2 = test_move.line_ids.filtered(lambda line: line.name == 'test line 2')
def _prepare_reconciliation_with_payments(lines_vals_list):
statement_line_wo_patch = self.env['account.bank.statement.line'].browse(self.statement_line.id)
return statement_line_wo_patch._prepare_reconciliation(lines_vals_list, create_payment_for_invoice=True)
statement_line = self.statement_line
StatementLine_prepare_reconciliation = type(statement_line)._prepare_reconciliation
with patch.object(
self.statement_line,
'_prepare_reconciliation',
lambda *args, **kwargs: _prepare_reconciliation_with_payments(*args)
):
def _prepare_reconciliation(self, lines_vals_list, create_payment_for_invoice=False):
if self == statement_line:
create_payment_for_invoice = True
return StatementLine_prepare_reconciliation(self, lines_vals_list, create_payment_for_invoice)
with patch.object(type(statement_line), '_prepare_reconciliation', _prepare_reconciliation):
self.statement_line.reconcile([
{'id': test_line_1.id, 'balance': -50.0},
{'id': test_line_2.id},
@@ -76,29 +76,27 @@ class HrHolidaySummaryReport(models.AbstractModel):
res[(date_from-start_date).days]['color'] = holiday.holiday_status_id.color_name
date_from += timedelta(1)
count += holiday.number_of_days
self.sum = count
return res
employee = self.env['hr.employee'].browse(empid)
return {'emp': employee.name, 'display': res, 'sum': count}
def _get_data_from_report(self, data):
res = []
Employee = self.env['hr.employee']
if 'depts' in data:
for department in self.env['hr.department'].browse(data['depts']):
res.append({'dept' : department.name, 'data': [], 'color': self._get_day(data['date_from'])})
for emp in Employee.search([('department_id', '=', department.id)]):
res[len(res)-1]['data'].append({
'emp': emp.name,
'display': self._get_leaves_summary(data['date_from'], emp.id, data['holiday_type']),
'sum': self.sum
})
elif 'emp' in data:
res.append({'data':[]})
for emp in Employee.browse(data['emp']):
res[0]['data'].append({
'emp': emp.name,
'display': self._get_leaves_summary(data['date_from'], emp.id, data['holiday_type']),
'sum': self.sum
res.append({
'dept': department.name,
'data': [
self._get_leaves_summary(data['date_from'], emp.id, data['holiday_type'])
for emp in Employee.search([('department_id', '=', department.id)])
],
'color': self._get_day(data['date_from']),
})
elif 'emp' in data:
res.append({'data': [
self._get_leaves_summary(data['date_from'], emp.id, data['holiday_type'])
for emp in Employee.browse(data['emp'])
]})
return res
def _get_holidays_status(self):
+1 -1
View File
@@ -9,7 +9,7 @@ class TestUBL(common.TransactionCase):
super(TestUBL, self).setUp()
# Force user Belgium country.
self.env.user.company_id = self.env['res.company'].create({'name': 'MyCompany'})
self.env.user.company_id.country = self.env.ref('base.be')
self.env.user.company_id.country_id = self.env.ref('base.be')
self.env.ref('l10n_be.l10nbe_chart_template').try_loading()
self.partner_id = self.env['res.partner'].create({'name': 'TestUser', 'vat': 'BE0123456789'})
@@ -1264,20 +1264,6 @@ class TestWorkOrderProcess(TestWorkOrderProcessCommon):
production sooner """
self.workcenter_1.alternative_workcenter_ids = self.wc_alt_1 | self.wc_alt_2
self.env['mrp.workcenter'].search([]).write({'tz': 'UTC'}) # compute all date in UTC
mrp_routing_0 = self.env['mrp.routing'].create({
'name': 'Primary Assembly',
})
mrp_workcenter_3 = self.env['mrp.workcenter'].create({
'name': 'Assembly Line 1',
'resource_calendar_id': self.env.ref('resource.resource_calendar_std').id,
})
mrp_routing_workcenter_0 = self.env['mrp.routing.workcenter'].create({
'routing_id': mrp_routing_0.id,
'workcenter_id': mrp_workcenter_3.id,
'name': 'Manual Assembly',
'time_cycle': 60,
})
self.planning_bom.routing = mrp_routing_0
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = self.product_4
mo_form.bom_id = self.planning_bom
@@ -31,13 +31,14 @@ class PaymentWizard(models.TransientModel):
acc_number = fields.Char("Account Number", default=lambda self: self._get_default_payment_acquirer_onboarding_value('acc_number'))
manual_post_msg = fields.Html("Payment Instructions")
_data_fetched = fields.Boolean(store=False)
@api.onchange('journal_name', 'acc_number')
def _set_manual_post_msg_value(self):
self.manual_post_msg = _('<h3>Please make a payment to: </h3><ul><li>Bank: %s</li><li>Account Number: %s</li><li>Account Holder: %s</li></ul>') %\
(self.journal_name or _("Bank") , self.acc_number or _("Account"), self.env.company.name)
_payment_acquirer_onboarding_cache = {}
_data_fetched = False
def _get_manual_payment_acquirer(self, env=None):
if env is None:
@@ -51,7 +51,6 @@ class TestAngloSaxonCommon(common.TransactionCase):
self.cash_journal = self.env['account.journal'].create({'name': 'CASH journal', 'type': 'cash', 'code': 'CSH00'})
self.sale_journal = self.env['account.journal'].create({'name': 'SALE journal', 'type': 'sale', 'code': 'INV00'})
self.pos_config.invoice_journal_id = self.sale_journal
self.pos_config.journal_ids = [self.cash_journal.id]
self.cash_payment_method = self.env['pos.payment.method'].create({
'name': 'Cash Test',
'is_cash_count': True,
@@ -494,8 +494,7 @@ class TestStockValuationWithCOA(AccountTestCommon):
})
self.product1.product_tmpl_id.categ_id.property_cost_method = 'fifo'
self.product1.product_tmpl_id.categ_id.valuation = 'real_time'
self.product1.product_tmpl_id.categ_id.property_valuation = 'real_time'
# Receive 10@10 ; create the vendor bill
po1 = self.env['purchase.order'].create({
+2 -2
View File
@@ -140,9 +140,9 @@ class TestRobustness(SavepointCase):
move1._action_confirm()
move1._action_assign()
move1.result_package_id = False
self.assertEqual(move1.move_line_ids.package_id, package)
package.unpack()
self.assertEqual(move1.move_line_ids.package_id, self.env['stock.quant.package'])
# unreserve
move1._do_unreserve()
@@ -108,7 +108,7 @@ class TestStockValuationCommon(SavepointCase):
'picking_type_id': self.picking_type_out.id,
})
if unit_cost:
dropshipped.unit_cost = unit_cost
dropshipped.price_unit = unit_cost
dropshipped._action_confirm()
dropshipped._action_assign()
dropshipped.move_line_ids.qty_done = quantity
@@ -183,7 +183,7 @@ class TestStockValuation(StockAccountTestCommon):
def test_dropship_fifo_perpetual_continental_ordered(self):
self.env.company.anglo_saxon_accounting = False
self.product1.product_tmpl_id.categ_id.proprty_cost_method = 'fifo'
self.product1.product_tmpl_id.categ_id.property_cost_method = 'fifo'
self.product1.product_tmpl_id.standard_price = 10
self.product1.product_tmpl_id.categ_id.property_valuation = 'real_time'
self.product1.product_tmpl_id.invoice_policy = 'order'
@@ -334,8 +334,6 @@ class StockLandedCostLine(models.Model):
@api.onchange('product_id')
def onchange_product_id(self):
if not self.product_id:
self.quantity = 0.0
self.name = self.product_id.name or ''
self.split_method = self.product_id.product_tmpl_id.split_method_landed_cost or self.split_method or 'equal'
self.price_unit = self.product_id.standard_price or 0.0
@@ -181,9 +181,6 @@ class TestActivityFlow(TestActivityCommon):
'datas': 'testtest',
})
# Adding the attachments to the activity
activity.attachment_ids = attachments
# Checking if the attachment has been forwarded to the message
# when marking an activity as "Done"
activity.action_feedback()
-4
View File
@@ -405,10 +405,6 @@ class View(models.Model):
func = getattr(values['main_object'], 'get_backend_menu_id', False)
values['backend_menu_id'] = func and func() or self.env.ref('website.menu_website_configuration').id
# Fallback incase main_object dont't inherit 'website.seo.metadata'
if not hasattr(values['main_object'], 'get_website_meta'):
values['main_object'].get_website_meta = lambda: {}
if self._context != new_context:
self = self.with_context(new_context)
return super(View, self)._render(values, engine=engine, minimal_qcontext=minimal_qcontext)
+5
View File
@@ -1011,3 +1011,8 @@ class BaseModel(models.AbstractModel):
return self.website_id._get_http_domain()
else:
return super(BaseModel, self).get_base_url()
def get_website_meta(self):
# dummy version of 'get_website_meta' above; this is a graceful fallback
# for models that don't inherit from 'website.seo.metadata'
return {}
+1
View File
@@ -234,6 +234,7 @@ class frozendict(dict):
class QWeb(object):
__slots__ = ()
_void_elements = frozenset([
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen',
+2
View File
@@ -14,6 +14,8 @@ _logger = logging.getLogger(__name__)
class ResConfigModuleInstallationMixin(object):
__slots__ = ()
@api.model
def _install_modules(self, modules):
""" Install the requested modules.
@@ -1,25 +1,12 @@
# -*- coding: utf-8 -*-
from odoo.tests import common
ANSWER_TO_ULTIMATE_QUESTION = 42
class TestAttributes(common.TransactionCase):
def test_we_can_add_attributes(self):
def test_we_cannot_add_attributes(self):
Model = self.env['test_new_api.category']
instance = Model.create({'name': 'Foo'})
# assign an unknown attribute
instance.unknown = ANSWER_TO_ULTIMATE_QUESTION
# Does the attribute exist in the instance of the model ?
self.assertTrue(hasattr(instance, 'unknown'))
# Is it the right type ?
self.assertIsInstance(instance.unknown, int)
# Is it the right value, in case of, we don't know ;-)
self.assertEqual(instance.unknown, ANSWER_TO_ULTIMATE_QUESTION)
# We are paranoiac !
self.assertEqual(getattr(instance, 'unknown'), ANSWER_TO_ULTIMATE_QUESTION)
with self.assertRaises(AttributeError):
instance.unknown = 42
+9 -2
View File
@@ -53,7 +53,7 @@ from . import api
from . import tools
from .exceptions import AccessError, MissingError, ValidationError, UserError
from .osv.query import Query
from .tools import frozendict, lazy_classproperty, lazy_property, ormcache, \
from .tools import frozendict, lazy_classproperty, ormcache, \
Collector, LastOrderedSet, OrderedSet, IterableGenerator, \
groupby, unique
from .tools.config import config
@@ -151,6 +151,10 @@ class MetaModel(api.Meta):
module_to_models = defaultdict(list)
def __new__(meta, name, bases, attrs):
attrs.setdefault('__slots__', ())
return super().__new__(meta, name, bases, attrs)
def __init__(self, name, bases, attrs):
if not self._register:
self._register = True
@@ -253,6 +257,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
To create a class that should not be instantiated,
the :attr:`~odoo.models.BaseModel._register` attribute may be set to False.
"""
__slots__ = ['env', '_ids', '_prefetch_ids']
_auto = False
"""Whether a database table should be created (default: ``True``).
@@ -5607,7 +5612,7 @@ Record ids: %(records)s
# Cache and recomputation management
#
@lazy_property
@property
def _cache(self):
""" Return the cache of ``self``, mapping field names to values. """
return RecordCache(self)
@@ -6226,6 +6231,8 @@ collections.Sequence.register(BaseModel)
class RecordCache(MutableMapping):
""" A mapping from field names to values, to read and update the cache of a record. """
__slots__ = ['_record']
def __init__(self, record):
assert len(record) == 1, "Unexpected RecordCache(%s)" % record
self._record = record