mirror of
https://gitlab.com/hibou-io/hibou-odoo/suite.git
synced 2025-01-20 12:37:31 +02:00
[ADD] product_attribute_lazy: performance module for product attribute template rel
This commit is contained in:
1
product_attribute_lazy/__init__.py
Normal file
1
product_attribute_lazy/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import models
|
||||||
29
product_attribute_lazy/__manifest__.py
Normal file
29
product_attribute_lazy/__manifest__.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
'name': 'Product Attribute Lazy',
|
||||||
|
'author': 'Hibou Corp.',
|
||||||
|
'version': '16.0.1.0.0',
|
||||||
|
'category': 'Product',
|
||||||
|
'sequence': 95,
|
||||||
|
'summary': 'Performance module to change behavior of attribute-product-template relationship.',
|
||||||
|
'description': """
|
||||||
|
Performance module to change behavior of attribute-product-template relationship.
|
||||||
|
|
||||||
|
Adds scheduled action to reindex all attributes.
|
||||||
|
|
||||||
|
Adds scheduled action that will kill queries to this table,
|
||||||
|
you can deactive if you're not having issues with long running table queries.
|
||||||
|
|
||||||
|
By default, a new pure SQL version of the index is created. If you would like to use the slower ORM
|
||||||
|
computation, set system config parameter `product_attribute_lazy.indexer_use_sql` to '0'.
|
||||||
|
""",
|
||||||
|
'website': 'https://hibou.io/',
|
||||||
|
'depends': [
|
||||||
|
'product',
|
||||||
|
],
|
||||||
|
'data': [
|
||||||
|
'data/product_data.xml',
|
||||||
|
],
|
||||||
|
'installable': True,
|
||||||
|
'application': False,
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
}
|
||||||
50
product_attribute_lazy/data/product_data.xml
Normal file
50
product_attribute_lazy/data/product_data.xml
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
|
||||||
|
<record id="action_product_attribute_index" model="ir.actions.server">
|
||||||
|
<field name="name">Reindex Related Products</field>
|
||||||
|
<field name="model_id" ref="product.model_product_attribute"/>
|
||||||
|
<field name="binding_model_id" ref="product.model_product_attribute"/>
|
||||||
|
<field name="state">code</field>
|
||||||
|
<field name="code">
|
||||||
|
records.run_indexer_manual()
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Product Attribute Indexer -->
|
||||||
|
<record forcecreate="True" id="ir_cron_product_attribute_indexer" model="ir.cron">
|
||||||
|
<field name="name">Product Attribute Indexer</field>
|
||||||
|
<field name="model_id" ref="model_product_attribute"/>
|
||||||
|
<field name="state">code</field>
|
||||||
|
<field name="code">
|
||||||
|
model.run_indexer(True)
|
||||||
|
</field>
|
||||||
|
<field name="user_id" ref="base.user_root"/>
|
||||||
|
<field name="interval_number">1</field>
|
||||||
|
<field name="interval_type">days</field>
|
||||||
|
<field name="numbercall">-1</field>
|
||||||
|
<field eval="False" name="doall"/>
|
||||||
|
<field name="priority" eval="5"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Product Attribute Indexer -->
|
||||||
|
<record forcecreate="True" id="ir_cron_product_attribute_rel_query_watchdog" model="ir.cron">
|
||||||
|
<field name="name">Query Watchdog: product_attribute_product_template_rel</field>
|
||||||
|
<field name="model_id" ref="model_product_attribute"/>
|
||||||
|
<field name="state">code</field>
|
||||||
|
<field name="code">
|
||||||
|
dbname = env.cr.dbname
|
||||||
|
query = f'select pg_cancel_backend(pid) from pg_stat_activity where state=\'active\' and datname = \'{dbname}\' and pid != pg_backend_pid() and query like \'%product_attribute_product_template_rel%\';'
|
||||||
|
env.cr.execute(query)
|
||||||
|
</field>
|
||||||
|
<field name="user_id" ref="base.user_root"/>
|
||||||
|
<field name="interval_number">1</field>
|
||||||
|
<field name="interval_type">minutes</field>
|
||||||
|
<field name="numbercall">-1</field>
|
||||||
|
<field eval="False" name="doall"/>
|
||||||
|
<field name="priority" eval="10"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
2
product_attribute_lazy/models/__init__.py
Normal file
2
product_attribute_lazy/models/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from . import product_attribute_patch
|
||||||
|
from . import product_attribute
|
||||||
158
product_attribute_lazy/models/product_attribute.py
Normal file
158
product_attribute_lazy/models/product_attribute.py
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from odoo import api, fields, models, _, registry, tools
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ProductAttribute(models.Model):
|
||||||
|
_inherit = 'product.attribute'
|
||||||
|
|
||||||
|
number_related_products = fields.Integer(compute=False, store=True) # compute='_compute_number_related_products')
|
||||||
|
product_tmpl_ids = fields.Many2many('product.template', string="Related Products", compute=False, store=True) #compute='_compute_products', store=True)
|
||||||
|
|
||||||
|
def action_open_related_products(self):
|
||||||
|
self.ensure_one()
|
||||||
|
self.env.cr.execute('SELECT product_template_id FROM product_attribute_product_template_rel WHERE product_attribute_id = %s' % (self.id, ))
|
||||||
|
tmpl_res = self.env.cr.fetchall()
|
||||||
|
tmpl_ids = [t[0] for t in tmpl_res]
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'name': _("Related Products"),
|
||||||
|
'res_model': 'product.template',
|
||||||
|
'view_mode': 'tree,form',
|
||||||
|
'domain': [('id', 'in', tmpl_ids)],
|
||||||
|
}
|
||||||
|
|
||||||
|
def _compute_products_sql(self):
|
||||||
|
if not self:
|
||||||
|
raise UserError('Index with SQL requires attributes.')
|
||||||
|
# ORIGINAL for replicating logic
|
||||||
|
# for pa in self:
|
||||||
|
# product_tmpls = pa.attribute_line_ids.product_tmpl_id
|
||||||
|
# pa.with_context(active_test=False).product_tmpl_ids = product_tmpls
|
||||||
|
# pa.number_related_products = len(product_tmpls)
|
||||||
|
self_ids = ','.join(str(i) for i in self.ids)
|
||||||
|
query = """
|
||||||
|
WITH current_rel AS (
|
||||||
|
SELECT
|
||||||
|
attribute_id AS product_attribute_id,
|
||||||
|
product_tmpl_id AS product_template_id
|
||||||
|
FROM product_template_attribute_line
|
||||||
|
WHERE attribute_id in (%s)
|
||||||
|
GROUP BY 1, 2
|
||||||
|
),
|
||||||
|
expanded_rel AS (
|
||||||
|
SELECT
|
||||||
|
cr.product_attribute_id as cr_product_attribute_id,
|
||||||
|
cr.product_template_id as cr_product_template_id,
|
||||||
|
paptr.product_attribute_id as paptr_product_attribute_id,
|
||||||
|
paptr.product_template_id as paptr_product_template_id
|
||||||
|
FROM current_rel cr
|
||||||
|
LEFT JOIN product_attribute_product_template_rel paptr ON paptr.product_attribute_id = cr.product_attribute_id AND paptr.product_template_id = cr.product_template_id
|
||||||
|
),
|
||||||
|
not_in_rel AS (
|
||||||
|
SELECT
|
||||||
|
cr_product_attribute_id as product_attribute_id,
|
||||||
|
cr_product_template_id as product_template_id
|
||||||
|
FROM expanded_rel
|
||||||
|
WHERE paptr_product_attribute_id is null and paptr_product_template_id is null
|
||||||
|
)
|
||||||
|
|
||||||
|
INSERT INTO product_attribute_product_template_rel (product_attribute_id, product_template_id)
|
||||||
|
SELECT product_attribute_id, product_template_id
|
||||||
|
FROM not_in_rel;
|
||||||
|
""" % (self_ids, )
|
||||||
|
query += """
|
||||||
|
WITH needs_del AS (
|
||||||
|
SELECT
|
||||||
|
t.product_attribute_id,
|
||||||
|
t.product_template_id
|
||||||
|
FROM product_attribute_product_template_rel AS t
|
||||||
|
LEFT JOIN product_template_attribute_line AS real ON
|
||||||
|
real.attribute_id = t.product_attribute_id
|
||||||
|
AND real.product_tmpl_id = t.product_template_id
|
||||||
|
WHERE t.product_attribute_id in (%s)
|
||||||
|
AND real.product_tmpl_id is null
|
||||||
|
AND real.attribute_id is null
|
||||||
|
)
|
||||||
|
DELETE FROM product_attribute_product_template_rel
|
||||||
|
WHERE product_attribute_id in (%s) AND (
|
||||||
|
(product_attribute_product_template_rel.product_attribute_id,
|
||||||
|
product_attribute_product_template_rel.product_template_id) IN (SELECT * FROM needs_del)
|
||||||
|
);
|
||||||
|
""" % (self_ids, self_ids)
|
||||||
|
for i in self.ids:
|
||||||
|
query += """
|
||||||
|
UPDATE product_attribute
|
||||||
|
SET number_related_products = (SELECT COUNT(*) FROM product_attribute_product_template_rel WHERE product_attribute_id = %s)
|
||||||
|
WHERE id = %s;
|
||||||
|
""" % (i, i)
|
||||||
|
|
||||||
|
self.env.cr.execute(query)
|
||||||
|
|
||||||
|
def _run_indexer(self, use_new_cursor=False):
|
||||||
|
indexer_use_sql = self.env['ir.config_parameter'].sudo().get_param('product_attribute_lazy.indexer_use_sql', '1') != '0'
|
||||||
|
for pa in self:
|
||||||
|
if indexer_use_sql:
|
||||||
|
pa._compute_products_sql()
|
||||||
|
else:
|
||||||
|
pa._compute_products()
|
||||||
|
if use_new_cursor:
|
||||||
|
pa._cr.commit()
|
||||||
|
_logger.info("_run_indexer is finished and committed for %s" % (pa.id, ))
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def run_indexer(self, use_new_cursor=False):
|
||||||
|
start_time = datetime.now()
|
||||||
|
attributes = None
|
||||||
|
try:
|
||||||
|
if use_new_cursor:
|
||||||
|
cr = registry(self._cr.dbname).cursor()
|
||||||
|
self = self.with_env(self.env(cr=cr))
|
||||||
|
|
||||||
|
# We want to freeze the cron that kills long running relationship queries...
|
||||||
|
watchdog_cron = self.sudo().env.ref('product_attribute_lazy.ir_cron_product_attribute_rel_query_watchdog', raise_if_not_found=False)
|
||||||
|
if watchdog_cron:
|
||||||
|
try:
|
||||||
|
with tools.mute_logger('odoo.sql_db'):
|
||||||
|
self._cr.execute("SELECT id FROM ir_cron WHERE id = %s FOR UPDATE NOWAIT", (watchdog_cron.id, ))
|
||||||
|
except Exception:
|
||||||
|
_logger.info('Attempt to run indexer aborted, as the query watchdog is already running')
|
||||||
|
self._cr.rollback()
|
||||||
|
raise UserError('Attempt to run indexer aborted, as the query watchdog is already running')
|
||||||
|
|
||||||
|
# if we could tell that it needs re-indexed....
|
||||||
|
attributes = self.env['product.attribute'].search([])
|
||||||
|
attributes._run_indexer(use_new_cursor=use_new_cursor)
|
||||||
|
except Exception:
|
||||||
|
_logger.error("Error during product attribute indexer", exc_info=True)
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
if use_new_cursor:
|
||||||
|
try:
|
||||||
|
self._cr.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if attributes:
|
||||||
|
_logger.warning('Indexer took %s seconds total for %s attributes.' % ((datetime.now()-start_time).seconds, len(attributes)))
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def run_indexer_manual(self):
|
||||||
|
# intended to be called by server action. Don't allow it to overlap with cron
|
||||||
|
if not self.exists():
|
||||||
|
raise UserError('One or more selected Product Attributes are required.')
|
||||||
|
|
||||||
|
indexer_cron = self.sudo().env.ref('product_attribute_lazy.ir_cron_product_attribute_indexer')
|
||||||
|
# Avoid repeated and overlapping index processes
|
||||||
|
try:
|
||||||
|
with tools.mute_logger('odoo.sql_db'):
|
||||||
|
self._cr.execute("SELECT id FROM ir_cron WHERE id = %s FOR UPDATE NOWAIT", (indexer_cron.id, ))
|
||||||
|
except Exception:
|
||||||
|
_logger.info('Attempt to run indexer aborted, as already running')
|
||||||
|
self._cr.rollback()
|
||||||
|
raise UserError('Attempt to run indexer aborted, as already running')
|
||||||
|
|
||||||
|
self._run_indexer(True)
|
||||||
79
product_attribute_lazy/models/product_attribute_patch.py
Normal file
79
product_attribute_lazy/models/product_attribute_patch.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
from odoo import api, _
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
from odoo.addons.product.models.product_attribute import ProductAttribute
|
||||||
|
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
"""Override to make sure attribute type can't be changed if it's used on
|
||||||
|
a product template.
|
||||||
|
|
||||||
|
This is important to prevent because changing the type would make
|
||||||
|
existing combinations invalid without recomputing them, and recomputing
|
||||||
|
them might take too long and we don't want to change products without
|
||||||
|
the user knowing about it."""
|
||||||
|
if 'create_variant' in vals:
|
||||||
|
for pa in self:
|
||||||
|
|
||||||
|
if vals['create_variant'] != pa.create_variant:
|
||||||
|
if not pa.number_related_products:
|
||||||
|
# index this attribute, this will be free if there are truely no related products
|
||||||
|
pa._compute_products()
|
||||||
|
if pa.number_related_products:
|
||||||
|
raise UserError(
|
||||||
|
_("You cannot change the Variants Creation Mode of the attribute %s because it is used on the following products:\n%s") %
|
||||||
|
(pa.display_name, ", ".join(pa.product_tmpl_ids.mapped('display_name')))
|
||||||
|
)
|
||||||
|
invalidate = 'sequence' in vals and any(record.sequence != vals['sequence'] for record in self)
|
||||||
|
res = super(ProductAttribute, self).write(vals)
|
||||||
|
if invalidate:
|
||||||
|
# prefetched o2m have to be resequenced
|
||||||
|
# (eg. product.template: attribute_line_ids)
|
||||||
|
self.env.flush_all()
|
||||||
|
self.env.invalidate_all()
|
||||||
|
return res
|
||||||
|
|
||||||
|
ProductAttribute.write = write
|
||||||
|
|
||||||
|
# eventually may want to only display if count is <100 otherwise this takes a long long time
|
||||||
|
@api.ondelete(at_uninstall=False)
|
||||||
|
def _unlink_except_used_on_product(self):
|
||||||
|
for pa in self:
|
||||||
|
if not pa.number_related_products:
|
||||||
|
# index this attribute, this will be free if there are truely no related products
|
||||||
|
pa._compute_products()
|
||||||
|
if pa.number_related_products:
|
||||||
|
if pa.number_related_products > 100:
|
||||||
|
raise UserError(
|
||||||
|
_("You cannot delete the attribute %s because it is used on many products.") %
|
||||||
|
(pa.display_name, )
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise UserError(
|
||||||
|
_("You cannot delete the attribute %s because it is used on the following products:\n%s") %
|
||||||
|
(pa.display_name, ", ".join(pa.product_tmpl_ids.mapped('display_name')))
|
||||||
|
)
|
||||||
|
|
||||||
|
ProductAttribute._unlink_except_used_on_product = _unlink_except_used_on_product
|
||||||
|
|
||||||
|
# this method should never be called as it is not a computed field anymore
|
||||||
|
# replace it to replace decoration
|
||||||
|
# @api.depends('product_tmpl_ids')
|
||||||
|
def _compute_number_related_products(self):
|
||||||
|
pass
|
||||||
|
# raise Exception('in patched _compute_number_related_products')
|
||||||
|
# for pa in self:
|
||||||
|
# pa.number_related_products = len(pa.product_tmpl_ids)
|
||||||
|
|
||||||
|
ProductAttribute._compute_number_related_products = _compute_number_related_products
|
||||||
|
|
||||||
|
# this method should be called on a schedule to "index" these...
|
||||||
|
# replace it to replace decoration
|
||||||
|
# @api.depends('attribute_line_ids.active', 'attribute_line_ids.product_tmpl_id')
|
||||||
|
def _compute_products(self):
|
||||||
|
for pa in self:
|
||||||
|
product_tmpls = pa.attribute_line_ids.product_tmpl_id
|
||||||
|
pa.with_context(active_test=False).product_tmpl_ids = product_tmpls
|
||||||
|
pa.number_related_products = len(product_tmpls)
|
||||||
|
|
||||||
|
|
||||||
|
ProductAttribute._compute_products = _compute_products
|
||||||
Reference in New Issue
Block a user