diff --git a/pos_attribute_image_lazy/README.md b/pos_attribute_image_lazy/README.md new file mode 100644 index 0000000..a2f4c7a --- /dev/null +++ b/pos_attribute_image_lazy/README.md @@ -0,0 +1,40 @@ +# POS Attribute Image Lazy Loading + +Keeps `product.attribute.value` images out of the `pos.session.load_pos_data` +payload. + +## The problem + +`pos.session._get_attributes_by_ptal_id()` inlines the base64 `image` of every +attribute value of a `create_variant='no_variant'` attribute, **once per +attribute line** referencing it. With a handful of photo sized images this grows +without bound and the request dies while serialising the response: + +``` +File "werkzeug/wrappers/response.py", line 327, in set_data + value = value.encode(self.charset) +MemoryError +``` + +The browser only sees `ConnectionLostError` on +`/web/dataset/call_kw/pos.session/load_pos_data`, with no traceback in the POS. + +Note that `product.attribute.value.image` is capped at 70x70 upstream, but +`product.template.attribute.value.image` is a related field declared without +`max_width`/`max_height`, so uploading through a product template's attribute +lines stores the original untouched. + +## The fix + +The POS front end never reads those bytes. `point_of_sale.ColorProductAttribute` +only tests the field for truthiness and renders the swatch from +`/web/image/product.template.attribute.value//image`. Reading under +`bin_size` returns the attachment size instead of its content, which is still +truthy, so nothing changes visually. + +Measured on a production database with 224 images of ~1 MB: + +| | `load_pos_data` payload | +|---|---| +| Without the module | 145.93 MB | +| With the module | 0.51 MB | diff --git a/pos_attribute_image_lazy/__init__.py b/pos_attribute_image_lazy/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/pos_attribute_image_lazy/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/pos_attribute_image_lazy/__manifest__.py b/pos_attribute_image_lazy/__manifest__.py new file mode 100644 index 0000000..870e3fc --- /dev/null +++ b/pos_attribute_image_lazy/__manifest__.py @@ -0,0 +1,12 @@ +{ + "name": "POS Attribute Image Lazy Loading", + "version": "17.0.1.0.0", + "summary": "Avoid embedding attribute value images in the POS session payload", + "author": "Criptomart", + "website": "https://criptomart.net", + "license": "AGPL-3", + "category": "Point of Sale", + "depends": ["point_of_sale"], + "data": [], + "installable": True, +} diff --git a/pos_attribute_image_lazy/models/__init__.py b/pos_attribute_image_lazy/models/__init__.py new file mode 100644 index 0000000..f7116e3 --- /dev/null +++ b/pos_attribute_image_lazy/models/__init__.py @@ -0,0 +1 @@ +from . import pos_session diff --git a/pos_attribute_image_lazy/models/pos_session.py b/pos_attribute_image_lazy/models/pos_session.py new file mode 100644 index 0000000..d227689 --- /dev/null +++ b/pos_attribute_image_lazy/models/pos_session.py @@ -0,0 +1,27 @@ +from odoo import models + + +class PosSession(models.Model): + _inherit = "pos.session" + + def _get_attributes_by_ptal_id(self): + """Return the attribute values without inlining their image content. + + ``point_of_sale`` reads the ``image`` field of every + ``product.attribute.value`` belonging to a ``no_variant`` attribute and + ships it, base64 encoded, inside the ``load_pos_data`` response -- + repeated once per attribute line using that value. + + The POS front end never uses those bytes: the only reader is + ``point_of_sale.ColorProductAttribute``, which merely tests the field + for truthiness and then renders the swatch from + ``/web/image/product.template.attribute.value//image``. + + Reading with ``bin_size`` makes the ORM return the human readable + attachment size (e.g. ``b'827.41 Kb'``) instead of its content, so the + attachment is never loaded into memory. The value stays truthy, which + keeps the swatch rendering identical. + """ + return super( + PosSession, self.with_context(bin_size=True) + )._get_attributes_by_ptal_id()