Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package trytond for openSUSE:Factory checked 
in at 2026-08-14 22:09:25
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/trytond (Old)
 and      /work/SRC/openSUSE:Factory/.trytond.new.1258 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "trytond"

Fri Aug 14 22:09:25 2026 rev:119 rq:1371190 version:7.0.55

Changes:
--------
--- /work/SRC/openSUSE:Factory/trytond/trytond.changes  2026-06-12 
19:28:57.937808912 +0200
+++ /work/SRC/openSUSE:Factory/.trytond.new.1258/trytond.changes        
2026-08-14 22:09:36.704695754 +0200
@@ -1,0 +2,5 @@
+Fri Aug 14 09:23:34 UTC 2026 - Axel Braun <[email protected]>
+
+- Version 7.0.55 - Bugfix Release
+
+-------------------------------------------------------------------

Old:
----
  trytond-7.0.51.tar.gz

New:
----
  trytond-7.0.55.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ trytond.spec ++++++
--- /var/tmp/diff_new_pack.VBsB9k/_old  2026-08-14 22:09:37.601728653 +0200
+++ /var/tmp/diff_new_pack.VBsB9k/_new  2026-08-14 22:09:37.603728726 +0200
@@ -30,7 +30,7 @@
 ##%%endif
 
 Name:           trytond
-Version:        %{majorver}.51
+Version:        %{majorver}.55
 Release:        0
 Summary:        An Enterprise Resource Planning (ERP) system
 License:        GPL-3.0-or-later

++++++ trytond-7.0.51.tar.gz -> trytond-7.0.55.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/trytond-7.0.51/CHANGELOG new/trytond-7.0.55/CHANGELOG
--- old/trytond-7.0.51/CHANGELOG        2026-06-02 18:46:47.000000000 +0200
+++ new/trytond-7.0.55/CHANGELOG        2026-08-04 12:05:58.000000000 +0200
@@ -1,4 +1,26 @@
 
+Version 7.0.55 - 2026-08-04
+---------------------------
+* Bug fixes (see mercurial logs for details)
+* Restrict protocols allowed for weasyprint to http(s) (issue14947)
+
+Version 7.0.54 - 2026-07-15
+---------------------------
+* Bug fixes (see mercurial logs for details)
+
+
+Version 7.0.53 - 2026-07-01
+---------------------------
+* Bug fixes (see mercurial logs for details)
+* Enforce access right on the email template record (issue14907)
+* Add ModelAccessProxy (issue14907)
+* Restrict Genshi evaluation (issue14869, issue5160)
+
+Version 7.0.52 - 2026-06-18
+---------------------------
+* Bug fixes (see mercurial logs for details)
+
+
 Version 7.0.51 - 2026-06-02
 ---------------------------
 * Bug fixes (see mercurial logs for details)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/trytond-7.0.51/PKG-INFO new/trytond-7.0.55/PKG-INFO
--- old/trytond-7.0.51/PKG-INFO 2026-06-02 18:46:51.011286300 +0200
+++ new/trytond-7.0.55/PKG-INFO 2026-08-04 12:06:02.040366600 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: trytond
-Version: 7.0.51
+Version: 7.0.55
 Summary: Tryton server
 Home-page: http://www.tryton.org/
 Download-URL: http://downloads.tryton.org/7.0/
@@ -77,7 +77,7 @@
 Provides-Extra: html2text
 Requires-Dist: html2text; extra == "html2text"
 Provides-Extra: weasyprint
-Requires-Dist: weasyprint; extra == "weasyprint"
+Requires-Dist: weasyprint>=68.0; extra == "weasyprint"
 Provides-Extra: coroutine
 Requires-Dist: gevent>=1.1; extra == "coroutine"
 Provides-Extra: image
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/trytond-7.0.51/doc/ref/models.rst 
new/trytond-7.0.55/doc/ref/models.rst
--- old/trytond-7.0.51/doc/ref/models.rst       2025-09-22 21:34:39.000000000 
+0200
+++ new/trytond-7.0.55/doc/ref/models.rst       2026-07-08 08:36:10.000000000 
+0200
@@ -775,6 +775,15 @@
      the string
 
 
+ModelAccessProxy
+================
+
+.. class:: ModelAccessProxy(record[, context])
+
+   A class proxying instance of :class:`ModelStorage` with check access using
+   ``context``.
+
+
 convert_from
 ------------
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/trytond-7.0.51/setup.py new/trytond-7.0.55/setup.py
--- old/trytond-7.0.51/setup.py 2025-09-22 21:34:39.000000000 +0200
+++ new/trytond-7.0.55/setup.py 2026-08-04 10:39:24.000000000 +0200
@@ -154,7 +154,7 @@
         'BCrypt': ['passlib[bcrypt]'],
         'Argon2': ['passlib[argon2]'],
         'html2text': ['html2text'],
-        'weasyprint': ['weasyprint'],
+        'weasyprint': ['weasyprint >= 68.0'],
         'coroutine': ['gevent>=1.1'],
         'image': ['pillow'],
         'barcode': ['python-barcode[images]'],
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/trytond-7.0.51/tox.ini new/trytond-7.0.55/tox.ini
--- old/trytond-7.0.51/tox.ini  2025-09-22 21:34:39.000000000 +0200
+++ new/trytond-7.0.55/tox.ini  2026-07-30 08:12:46.000000000 +0200
@@ -16,7 +16,7 @@
 deps =
     coverage
     unittest-xml-reporting
-    postgresql: psycopg2 >= 2.7.0
+    postgresql: psycopg2-binary >= 2.7.0
 passenv = *
 setenv =
     sqlite: TRYTOND_DATABASE_URI={env:SQLITE_URI:sqlite://}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/trytond-7.0.51/trytond/__init__.py 
new/trytond-7.0.55/trytond/__init__.py
--- old/trytond-7.0.51/trytond/__init__.py      2026-05-20 23:22:06.000000000 
+0200
+++ new/trytond-7.0.55/trytond/__init__.py      2026-07-15 18:24:00.000000000 
+0200
@@ -10,7 +10,9 @@
 import __main__
 from lxml import etree, objectify
 
-__version__ = "7.0.51"
+from ._safe_genshi import genshi_patch
+
+__version__ = "7.0.55"
 
 if not os.environ.get('TRYTOND_APPNAME'):
     os.environ['TRYTOND_APPNAME'] = os.path.basename(
@@ -31,6 +33,8 @@
 etree.set_default_parser(etree.XMLParser(resolve_entities=False))
 objectify.set_default_parser(objectify.makeparser(resolve_entities=False))
 
+genshi_patch()
+
 
 class _RequestPatchFinder:
     def find_spec(self, fullname, path, target=None):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/trytond-7.0.51/trytond/_safe_genshi.py 
new/trytond-7.0.55/trytond/_safe_genshi.py
--- old/trytond-7.0.51/trytond/_safe_genshi.py  1970-01-01 01:00:00.000000000 
+0100
+++ new/trytond-7.0.55/trytond/_safe_genshi.py  2026-07-14 19:21:23.000000000 
+0200
@@ -0,0 +1,108 @@
+# This file is part of Tryton.  The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
+
+from genshi.template.astutil import ASTTransformer
+from genshi.template.eval import (
+    BUILTINS, Code, ExpressionASTTransformer, TemplateASTTransformer)
+
+
+class _SafeASTTransformer(ASTTransformer):
+
+    def visit_Attribute(self, node):
+        if (node.attr.startswith('_')
+                and node.attr not in {
+                    '__class__', '__name__', '__url__', '__href__'}):
+            raise ValueError(f"invalid attribute {node.attr!r}")
+        return super().visit_Attribute(node)
+
+    def visit_Import(self, node):
+        raise ValueError("invalid import")
+
+    def visit_ImportFrom(self, node):
+        raise ValueError("invalid import from")
+
+    def visit_Name(self, node):
+        if (node.id.startswith('_')
+                and not node.id.startswith('__relatorio_')
+                and node.id not in {'_'}):
+            raise ValueError(f"invalid name {node.id!r}")
+        return super().visit_Name(node)
+
+
+class SafeExpressionASTTransformer(
+        _SafeASTTransformer, ExpressionASTTransformer):
+    pass
+
+
+class SafeTemplateASTTransformer(_SafeASTTransformer, TemplateASTTransformer):
+    pass
+
+
+ALLOWED_BUILTINS = {
+    'False',
+    'True',
+    'None',
+    'abs',
+    'all',
+    'any',
+    'ascii',
+    'bin',
+    'bool',
+    'bytearray',
+    'bytes',
+    'chr',
+    'complex',
+    'dict',
+    'dir',
+    'divmod',
+    'enumerate',
+    'filter',
+    'float',
+    'format',
+    'frozenset',
+    'hasattr',
+    'hash',
+    'hex',
+    'int',
+    'iter',
+    'len',
+    'list',
+    'map',
+    'max',
+    'min',
+    'next',
+    'oct',
+    'ord',
+    'pow',
+    'range',
+    'repr',
+    'reversed',
+    'round',
+    'set',
+    'slice',
+    'sorted',
+    'str',
+    'sum',
+    'tuple',
+    'zip',
+    }
+
+
+def genshi_patch():
+
+    code__init__ = Code.__init__
+
+    def patched_code__init__(
+            self, source, filename=None, lineno=-1, lookup='strict',
+            xform=None):
+        if self.mode == 'eval':
+            xform = SafeExpressionASTTransformer
+        else:
+            xform = SafeTemplateASTTransformer
+        code__init__(
+            self, source, filename=filename, lineno=lineno, lookup=lookup,
+            xform=xform)
+    Code.__init__ = patched_code__init__
+
+    for name in BUILTINS.keys() - ALLOWED_BUILTINS:
+        BUILTINS.pop(name)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/trytond-7.0.51/trytond/ir/action.py 
new/trytond-7.0.55/trytond/ir/action.py
--- old/trytond-7.0.51/trytond/ir/action.py     2025-12-14 17:59:46.000000000 
+0100
+++ new/trytond-7.0.55/trytond/ir/action.py     2026-07-30 23:56:51.000000000 
+0200
@@ -484,7 +484,7 @@
         'get_report_content', setter='set_report_content')
     report_content_name = fields.Function(fields.Char('Content Name'),
         'on_change_with_report_content_name')
-    report_content_html = fields.Function(fields.Binary(
+    report_content_html = fields.Function(fields.Text(
             "Content HTML",
             states={
                 'invisible': ~Eval('template_extension').in_(
@@ -646,11 +646,18 @@
 
     @classmethod
     def get_report_content_html(cls, reports, name):
-        return cls.get_report_content(reports, name[:-5])
+        contents = cls.get_report_content(reports, name[:-5])
+        for report_id, content in contents.items():
+            if isinstance(content, bytes):
+                try:
+                    contents[report_id] = content.decode('utf-8')
+                except UnicodeDecodeError:
+                    pass
+        return contents
 
     @classmethod
     def set_report_content_html(cls, reports, name, value):
-        if value is not None:
+        if isinstance(value, str):
             value = value.encode('utf-8')
         cls.set_report_content(reports, name[:-5], value)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/trytond-7.0.51/trytond/ir/email_.py 
new/trytond-7.0.55/trytond/ir/email_.py
--- old/trytond-7.0.51/trytond/ir/email_.py     2025-09-22 21:34:39.000000000 
+0200
+++ new/trytond-7.0.55/trytond/ir/email_.py     2026-07-08 08:36:10.000000000 
+0200
@@ -19,7 +19,8 @@
 
 from trytond.config import config
 from trytond.i18n import gettext
-from trytond.model import EvalEnvironment, ModelSQL, ModelView, fields
+from trytond.model import (
+    EvalEnvironment, ModelAccessProxy, ModelSQL, ModelView, fields)
 from trytond.model.exceptions import AccessError, ValidationError
 from trytond.pool import Pool
 from trytond.pyson import Bool, Eval, PYSONDecoder
@@ -418,7 +419,7 @@
     def get(self, record):
         pool = Pool()
         Model = pool.get(self.model.model)
-        record = Model(int(record))
+        record = ModelAccessProxy(Model(int(record)))
 
         values = {}
         for attr, key in [
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/trytond-7.0.51/trytond/ir/lang.py 
new/trytond-7.0.55/trytond/ir/lang.py
--- old/trytond-7.0.51/trytond/ir/lang.py       2026-04-14 18:58:51.000000000 
+0200
+++ new/trytond-7.0.55/trytond/ir/lang.py       2026-06-16 15:37:57.000000000 
+0200
@@ -4,6 +4,7 @@
 import datetime
 from ast import literal_eval
 from decimal import Decimal
+from functools import partial
 from itertools import takewhile
 from locale import CHAR_MAX
 
@@ -506,6 +507,7 @@
         Formats val according to the currency settings in lang.
         """
         # Code from currency in locale.py
+        conv = partial(getattr, self)
 
         # check for illegal values
         if digits is None:
@@ -522,10 +524,8 @@
 
         if symbol:
             smb = currency.symbol
-            precedes = (val < 0 and self.n_cs_precedes
-                or self.p_cs_precedes)
-            separated = (val < 0 and self.n_sep_by_space
-                or self.p_sep_by_space)
+            precedes = conv(val < 0 and 'n_cs_precedes' or 'p_cs_precedes')
+            separated = conv(val < 0 and 'n_sep_by_space' or 'p_sep_by_space')
             if not smb and hasattr(currency, 'code'):
                 smb = currency.code
                 separated = True
@@ -535,8 +535,8 @@
             else:
                 s = s + (separated and ' ' or '') + smb
 
-        sign_pos = val < 0 and self.n_sign_posn or self.p_sign_posn
-        sign = val < 0 and self.negative_sign or self.positive_sign
+        sign_pos = conv(val < 0 and 'n_sign_posn' or 'p_sign_posn')
+        sign = conv(val < 0 and 'negative_sign' or 'positive_sign')
 
         if sign_pos == 0:
             s = '(' + s + ')'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/trytond-7.0.51/trytond/ir/model.py 
new/trytond-7.0.55/trytond/ir/model.py
--- old/trytond-7.0.51/trytond/ir/model.py      2026-02-27 18:14:46.000000000 
+0100
+++ new/trytond-7.0.55/trytond/ir/model.py      2026-07-08 08:36:10.000000000 
+0200
@@ -530,13 +530,12 @@
     @classmethod
     def get_access(cls, models):
         'Return access for models'
-        # root user above constraint
-        if Transaction().user == 0:
-            return defaultdict(lambda: defaultdict(lambda: True))
-
         pool = Pool()
         Model = pool.get('ir.model')
-        User = pool.get('res.user')
+        try:
+            User = pool.get('res.user')
+        except KeyError:
+            return defaultdict(lambda: defaultdict(lambda: True))
         cursor = Transaction().connection.cursor()
         model_access = cls.__table__()
         ir_model = Model.__table__()
@@ -627,9 +626,8 @@
         Model = pool.get(model_name)
         assert mode in ['read', 'write', 'create', 'delete'], \
             'Invalid access mode for security'
-        transaction = Transaction()
-        if (transaction.user == 0
-                or (raise_exception and not transaction.check_access)):
+
+        if not Transaction().check_access:
             return True
 
         access = cls.get_access([model_name])[model_name][mode]
@@ -749,15 +747,14 @@
     @classmethod
     def get_access(cls, models):
         'Return fields access for models'
-        # root user above constraint
-        if Transaction().user == 0:
-            return defaultdict(lambda: defaultdict(
-                    lambda: defaultdict(lambda: True)))
-
         pool = Pool()
         Model = pool.get('ir.model')
         ModelField = pool.get('ir.model.field')
-        User = pool.get('res.user')
+        try:
+            User = pool.get('res.user')
+        except KeyError:
+            return defaultdict(lambda: defaultdict(
+                    lambda: defaultdict(lambda: True)))
         field_access = cls.__table__()
         ir_model = Model.__table__()
         model_field = ModelField.__table__()
@@ -807,8 +804,7 @@
         return accesses
 
     @classmethod
-    def check(cls, model_name, fields, mode='read', raise_exception=True,
-            access=False):
+    def check(cls, model_name, fields, mode='read', raise_exception=True):
         '''
         Check access for fields on model_name.
         '''
@@ -816,17 +812,12 @@
         Model = pool.get(model_name)
         assert mode in ('read', 'write', 'create', 'delete'), \
             'Invalid access mode'
-        transaction = Transaction()
-        if (transaction.user == 0
-                or (raise_exception and not transaction.check_access)):
-            if access:
-                return dict((x, True) for x in fields)
+
+        if not Transaction().check_access:
             return True
 
         accesses = dict((f, a[mode])
             for f, a in cls.get_access([model_name])[model_name].items())
-        if access:
-            return accesses
         for field in fields:
             if not accesses.get(field, True):
                 if raise_exception:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/trytond-7.0.51/trytond/ir/resource.py 
new/trytond-7.0.55/trytond/ir/resource.py
--- old/trytond-7.0.51/trytond/ir/resource.py   2026-05-31 17:47:57.000000000 
+0200
+++ new/trytond-7.0.55/trytond/ir/resource.py   2026-07-08 08:36:10.000000000 
+0200
@@ -69,13 +69,14 @@
     @classmethod
     def search(
             cls, domain, offset=0, limit=None, order=None, count=False,
-            query=False):
+            **kwargs):
         transaction = Transaction()
         enforce_access = (
-            not query and transaction.user and transaction.check_access)
+            not kwargs.get('query', False)
+            and transaction.user and transaction.check_access)
         result = super().search(
             domain, offset=offset, limit=limit, order=order,
-            count=False if enforce_access else count, query=query)
+            count=False if enforce_access else count, **kwargs)
         if not enforce_access:
             return result
 
@@ -110,7 +111,7 @@
             loop += 1
             result = super().search(
                 domain, offset=offset + loop * limit, limit=limit, order=order,
-                count=False, query=False)
+                count=False)
             if not result:
                 break
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/trytond-7.0.51/trytond/ir/rule.py 
new/trytond-7.0.55/trytond/ir/rule.py
--- old/trytond-7.0.51/trytond/ir/rule.py       2025-09-22 21:34:39.000000000 
+0200
+++ new/trytond-7.0.55/trytond/ir/rule.py       2026-07-08 08:36:10.000000000 
+0200
@@ -7,10 +7,11 @@
 from trytond.cache import Cache
 from trytond.i18n import gettext
 from trytond.model import Check, Index, ModelSQL, ModelView, fields
-from trytond.model.exceptions import ValidationError
+from trytond.model.exceptions import AccessError, ValidationError
 from trytond.pool import Pool
 from trytond.pyson import PYSONDecoder
-from trytond.transaction import Transaction, inactive_records
+from trytond.transaction import (
+    Transaction, inactive_records, without_check_access)
 
 
 class DomainError(ValidationError):
@@ -205,9 +206,6 @@
         model_names = list(model_names)
 
         cursor = transaction.connection.cursor()
-        # root user above constraint
-        if transaction.user == 0:
-            return {}, {}
         cursor.execute(*rule_table.join(rule_group,
                 condition=rule_group.id == rule_table.rule_group
                 ).join(model,
@@ -236,8 +234,8 @@
 
         clause = defaultdict(lambda: ['OR'])
         clause_global = defaultdict(lambda: ['OR'])
-        # Use root user without context to prevent recursion
-        with transaction.set_user(0), transaction.set_context(user=0):
+        # Without check access to prevent recursion
+        with without_check_access():
             rules = cls.browse(ids)
         for rule in rules:
             decoder = PYSONDecoder(
@@ -265,9 +263,8 @@
     @classmethod
     def domain_get(cls, model_name, mode='read'):
         pool = Pool()
-        transaction = Transaction()
-        # root user above constraint
-        if transaction.user == 0 or not transaction.check_access:
+
+        if not Transaction().check_access:
             return []
 
         assert mode in cls.modes
@@ -312,6 +309,47 @@
             return Model.search(domain, order=[], query=True)
 
     @classmethod
+    def check(cls, model_name, ids, mode='read'):
+        pool = Pool()
+        Model = pool.get(model_name)
+        transaction = Transaction()
+
+        def test_domain(ids, domain):
+            # Use root to prevent infinite recursion
+            with transaction.set_user(0, set_context=True), \
+                    inactive_records(), \
+                    without_check_access():
+                records = Model.search([
+                        ('id', 'in', ids),
+                        domain,
+                        ], order=[])
+            return list(set(ids).difference(map(int, records)))
+
+        domain = cls.domain_get(model_name, mode=mode)
+        if not domain:
+            return
+        forbidden = test_domain(ids, domain)
+        if forbidden:
+            ids = ', '.join(map(str, forbidden[:5]))
+            if len(forbidden) > 5:
+                ids += '...'
+            rules = []
+            clause, clause_global = cls.get(model_name, mode=mode)
+            if clause:
+                dom = list(clause.values())
+                dom.insert(0, 'OR')
+                if test_domain(forbidden, dom):
+                    rules.extend(clause.keys())
+                for rule, dom in clause_global.items():
+                    if test_domain(forbidden, dom):
+                        rules.append(rule)
+            raise AccessError(gettext(
+                    f'ir.msg_{mode}_rule_error',
+                    ids=ids,
+                    rules='\n'.join(r.name for r in rules),
+                    **Model.__names__()))
+
+    @classmethod
     def delete(cls, rules):
         super(Rule, cls).delete(rules)
         # Restart the cache on the domain_get method of ir.rule
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/trytond-7.0.51/trytond/model/__init__.py 
new/trytond-7.0.55/trytond/model/__init__.py
--- old/trytond-7.0.51/trytond/model/__init__.py        2025-09-22 
21:34:39.000000000 +0200
+++ new/trytond-7.0.55/trytond/model/__init__.py        2026-07-08 
08:36:10.000000000 +0200
@@ -9,7 +9,7 @@
 from .model import Model
 from .modelsingleton import ModelSingleton
 from .modelsql import Check, Exclude, Index, ModelSQL, Unique, convert_from
-from .modelstorage import EvalEnvironment, ModelStorage
+from .modelstorage import EvalEnvironment, ModelAccessProxy, ModelStorage
 from .modelview import ModelView
 from .multivalue import MultiValueMixin, ValueMixin
 from .order import sequence_ordered, sort
@@ -23,4 +23,4 @@
     'Workflow', 'DictSchemaMixin', 'MatchMixin', 'UnionMixin', 'dualmethod',
     'MultiValueMixin', 'ValueMixin', 'SymbolMixin', 'DigitsMixin',
     'EvalEnvironment', 'sequence_ordered', 'sort', 'DeactivableMixin', 'tree',
-    'sum_tree', 'avatar_mixin']
+    'sum_tree', 'avatar_mixin', 'ModelAccessProxy']
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/trytond-7.0.51/trytond/model/fields/many2one.py 
new/trytond-7.0.55/trytond/model/fields/many2one.py
--- old/trytond-7.0.51/trytond/model/fields/many2one.py 2025-09-22 
21:34:39.000000000 +0200
+++ new/trytond-7.0.55/trytond/model/fields/many2one.py 2026-07-08 
08:36:10.000000000 +0200
@@ -250,7 +250,10 @@
                 target_id, = query.columns
                 if isinstance(target_id, As):
                     target_id = target_id.expression
-                query.where &= target_id == column
+                if query.where is None:
+                    query.where = target_id == column
+                else:
+                    query.where &= target_id == column
                 expression = column.in_(query)
                 if operator.startswith('not'):
                     return ~expression
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/trytond-7.0.51/trytond/model/modelstorage.py 
new/trytond-7.0.55/trytond/model/modelstorage.py
--- old/trytond-7.0.51/trytond/model/modelstorage.py    2026-02-27 
21:27:54.000000000 +0100
+++ new/trytond-7.0.55/trytond/model/modelstorage.py    2026-07-08 
08:36:10.000000000 +0200
@@ -28,7 +28,8 @@
 from trytond.tools.domain_inversion import domain_inversion, eval_domain
 from trytond.tools.domain_inversion import parse as domain_parse
 from trytond.transaction import (
-    Transaction, inactive_records, record_cache_size, without_check_access)
+    Transaction, check_access, inactive_records, record_cache_size,
+    without_check_access)
 
 from . import fields
 from .descriptors import dualmethod
@@ -1705,11 +1706,11 @@
 
         if load_eager or multiple_getter:
             FieldAccess = Pool().get('ir.model.field.access')
-            fread_accesses = {}
-            fread_accesses.update(FieldAccess.check(self.__name__,
-                list(self._fields.keys()), 'read', access=True))
-            to_remove = set(x for x, y in fread_accesses.items()
-                    if not y and x != name)
+            fields_access = FieldAccess.get_access(
+                [self.__name__])[self.__name__]
+            to_remove = {
+                f for f, a in fields_access.items() if not a['read']}
+            to_remove.discard(name)
 
             def not_cached(item):
                 fname, field = item
@@ -2090,3 +2091,42 @@
 
 
 _pyson_encoder = PYSONEncoder()
+
+
+def ModelAccessProxy(record, context=None):
+    pool = Pool()
+    ModelAccess = pool.get('ir.model.access')
+    FieldAccess = pool.get('ir.model.field.access')
+    Rule = pool.get('ir.rule')
+
+    model = record.__class__
+    with Transaction().set_context(context), check_access():
+        ModelAccess.check(model.__name__)
+        Rule.check(model.__name__, [record.id])
+
+    class _ModelAccessProxy:
+        __class__ = model
+
+        def __init__(self, id):
+            self.id = id
+
+        def __getattr__(self, name):
+            if name in model._fields:
+                with Transaction().set_context(context), check_access():
+                    FieldAccess.check(model.__name__, [name])
+            value = getattr(record, name)
+            if isinstance(value, Model):
+                value = ModelAccessProxy(value, context)
+            elif isinstance(value, (list, tuple)):
+                value = [
+                    ModelAccessProxy(r, context)
+                    if isinstance(r, Model) else r for r in value]
+            return value
+
+        def __int__(self):
+            return int(self.id)
+
+        def __str__(self):
+            return f'{model.__name__},{self.id}'
+
+    return _ModelAccessProxy(record.id)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/trytond-7.0.51/trytond/model/modelview.py 
new/trytond-7.0.55/trytond/model/modelview.py
--- old/trytond-7.0.51/trytond/model/modelview.py       2025-09-22 
21:34:39.000000000 +0200
+++ new/trytond-7.0.55/trytond/model/modelview.py       2026-07-08 
08:36:10.000000000 +0200
@@ -408,10 +408,9 @@
         tree_root = tree.getroottree().getroot()
 
         # Find field without read access
-        fread_accesses = FieldAccess.check(cls.__name__,
-                list(cls._fields.keys()), 'read', access=True)
-        fields_to_remove = set(
-            x for x, y in fread_accesses.items() if not y)
+        fields_access = FieldAccess.get_access([cls.__name__])[cls.__name__]
+        fields_to_remove = {
+            f for f, a in fields_access.items() if not a['read']}
 
         # Find relation field without read access
         for name, field in cls._fields.items():
@@ -631,8 +630,8 @@
             button_groups = Button.get_groups(cls.__name__, button_name)
             if ((button_groups and not groups & button_groups)
                     or (not button_groups
-                        and not ModelAccess.check(
-                            cls.__name__, 'write', raise_exception=False))):
+                        and not ModelAccess.get_access(
+                            [cls.__name__])[cls.__name__]['write'])):
                 states = states.copy()
                 states['readonly'] = True
             element.set('states', encoder.encode(states))
@@ -667,8 +666,8 @@
                 action = None
             if (not action
                     or not action.res_model
-                    or not ModelAccess.check(
-                        action.res_model, 'read', raise_exception=False)):
+                    or not ModelAccess.get_access(
+                        [action.res_model])[action.res_model]['read']):
                 element.tag = 'label'
                 colspan = element.attrib.get('colspan')
                 link_name = element.attrib['name']
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/trytond-7.0.51/trytond/report/report.py 
new/trytond-7.0.55/trytond/report/report.py
--- old/trytond-7.0.51/trytond/report/report.py 2026-02-27 18:00:45.000000000 
+0100
+++ new/trytond-7.0.55/trytond/report/report.py 2026-08-04 10:40:12.000000000 
+0200
@@ -386,7 +386,12 @@
         if (weasyprint
                 and input_format in {'html', 'xhtml'}
                 and output_format == 'pdf'):
-            return output_format, weasyprint.HTML(string=data).write_pdf()
+            return output_format, weasyprint.HTML(
+                string=data,
+                url_fetcher=weasyprint.URLFetcher(
+                    allowed_protocols={'http', 'https'},
+                    ),
+                ).write_pdf()
 
         if input_format == output_format and output_format in MIMETYPES:
             return output_format, data
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/trytond-7.0.51/trytond/res/user.py 
new/trytond-7.0.55/trytond/res/user.py
--- old/trytond-7.0.51/trytond/res/user.py      2025-12-14 17:59:10.000000000 
+0100
+++ new/trytond-7.0.55/trytond/res/user.py      2026-07-08 08:36:10.000000000 
+0200
@@ -636,6 +636,10 @@
         Group = pool.get('res.group')
 
         transaction = Transaction()
+
+        if '_groups' in transaction.context:
+            return transaction.context['_groups']
+
         user = transaction.user
         groups = cls._get_groups_cache.get(user)
         if groups is not None:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/trytond-7.0.51/trytond/tests/test_access.py 
new/trytond-7.0.55/trytond/tests/test_access.py
--- old/trytond-7.0.51/trytond/tests/test_access.py     2025-12-14 
21:34:25.000000000 +0100
+++ new/trytond-7.0.55/trytond/tests/test_access.py     2026-07-08 
08:36:10.000000000 +0200
@@ -3,6 +3,7 @@
 # this repository contains the full copyright notices and license terms.
 import unittest
 
+from trytond.model import ModelAccessProxy
 from trytond.model.exceptions import AccessError
 from trytond.pool import Pool
 from trytond.tests.test_tryton import activate_module, with_transaction
@@ -404,6 +405,39 @@
         with self.assertRaises(AccessError):
             TestAccess.search([], order=[('relate.value', 'ASC')])
 
+    @with_transaction()
+    def test_model_access_proxy(self):
+        "Test model access proxy"
+        pool = Pool()
+        Model = pool.get('ir.model')
+        ModelAccess = pool.get('ir.model.access')
+        TestAccess = pool.get(self.model_name)
+        record, = TestAccess.create([{}])
+        model, = Model.search([('model', '=', self.model_name)])
+        ModelAccess.create([{
+                    'model': model.id,
+                    'perm_read': True,
+                    }])
+
+        ModelAccessProxy(record, {})
+
+    @with_transaction()
+    def test_model_access_proxy_no_access(self):
+        "Test model access proxy without access"
+        pool = Pool()
+        Model = pool.get('ir.model')
+        ModelAccess = pool.get('ir.model.access')
+        TestAccess = pool.get(self.model_name)
+        record, = TestAccess.create([{}])
+        model, = Model.search([('model', '=', self.model_name)])
+        ModelAccess.create([{
+                    'model': model.id,
+                    'perm_read': False,
+                    }])
+
+        with self.assertRaises(AccessError):
+            ModelAccessProxy(record, {})
+
 
 class ModelAccessWriteTestCase(_ModelAccessTestCase):
     _perm = 'perm_write'
@@ -1105,6 +1139,49 @@
         with self.assertRaises(AccessError):
             TestAccess.search([('relate', 'child_of', 42, 'parent')])
 
+    @with_transaction()
+    def test_model_access_proxy(self):
+        "Test model access proxy"
+        pool = Pool()
+        Field = pool.get('ir.model.field')
+        FieldAccess = pool.get('ir.model.field.access')
+        TestAccess = pool.get('test.access')
+        record, = TestAccess.create([{'field1': "foo"}])
+        field, = Field.search([
+                ('model.model', '=', 'test.access'),
+                ('name', '=', 'field1'),
+                ])
+        FieldAccess.create([{
+                    'field': field.id,
+                    'perm_read': True,
+                    }])
+
+        proxy = ModelAccessProxy(record, {})
+
+        self.assertEqual(proxy.field1, 'foo')
+
+    @with_transaction()
+    def test_model_access_proxy_no_access(self):
+        "Test model access proxy without access"
+        pool = Pool()
+        Field = pool.get('ir.model.field')
+        FieldAccess = pool.get('ir.model.field.access')
+        TestAccess = pool.get('test.access')
+        record, = TestAccess.create([{'field1': "foo"}])
+        field, = Field.search([
+                ('model.model', '=', 'test.access'),
+                ('name', '=', 'field1'),
+                ])
+        FieldAccess.create([{
+                    'field': field.id,
+                    'perm_read': False,
+                    }])
+
+        proxy = ModelAccessProxy(record, {})
+
+        with self.assertRaises(AccessError):
+            ModelAccessProxy(proxy.field1)
+
 
 class ModelFieldAccessWriteTestCase(_ModelFieldAccessTestCase):
     _perm = 'perm_write'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/trytond-7.0.51/trytond/tests/test_field_many2one.py 
new/trytond-7.0.55/trytond/tests/test_field_many2one.py
--- old/trytond-7.0.51/trytond/tests/test_field_many2one.py     2025-09-22 
21:34:39.000000000 +0200
+++ new/trytond-7.0.55/trytond/tests/test_field_many2one.py     2026-07-08 
08:36:10.000000000 +0200
@@ -274,6 +274,27 @@
 
         self.assertEqual(record.target.context, 'foo')
 
+    @with_transaction()
+    def test_search_where_target_union(self):
+        "Test search on Many2One using where and UNION-ed query on target"
+        pool = Pool()
+        Target = pool.get('test.many2one_target')
+        Many2One = pool.get('test.many2one')
+        target1, target2 = Target.create([
+                {'value': 1},
+                {'value': 2},
+                ])
+        many2one1, many2one2 = Many2One.create([
+                {'many2one': target1},
+                {'many2one': target2},
+                ])
+
+        many2ones = Many2One.search([
+                ('many2one', 'where',
+                    ['OR', ('value', '=', 1), ('write_uid.id', '=', -1)]),
+                ])
+        self.assertListEqual(many2ones, [many2one1])
+
 
 class FieldMany2OneTreeTestCase(unittest.TestCase):
     "Test Field Many2One Tree"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/trytond-7.0.51/trytond/tests/test_genshi.py 
new/trytond-7.0.55/trytond/tests/test_genshi.py
--- old/trytond-7.0.51/trytond/tests/test_genshi.py     1970-01-01 
01:00:00.000000000 +0100
+++ new/trytond-7.0.55/trytond/tests/test_genshi.py     2026-07-08 
08:36:10.000000000 +0200
@@ -0,0 +1,36 @@
+# This file is part of Tryton.  The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
+
+import unittest
+
+from genshi.template import MarkupTemplate, TextTemplate
+from genshi.template.eval import UndefinedError
+
+
+class GenshiTestCase(unittest.TestCase):
+
+    def test_no_builtins(self):
+        "Test no builtins"
+        with self.assertRaises(UndefinedError):
+            str(TextTemplate("${open('%s').read()}" % __file__).generate())
+
+    def test_no_private_name(self):
+        "Test no private name"
+        with self.assertRaisesRegex(ValueError, r"invalid name '__import__'"):
+            str(TextTemplate("${__import__('os')}").generate())
+
+    def test_no_private_attribute(self):
+        "Test no private attribute"
+        with self.assertRaisesRegex(
+                ValueError, r"invalid attribute '__getattribute__'"):
+            str(TextTemplate("${True.__getattribute__}").generate())
+
+    def test_no_import(self):
+        "Test no import"
+        with self.assertRaisesRegex(ValueError, r"invalid import"):
+            str(MarkupTemplate("<?python import os?>").generate())
+
+    def test_no_import_from(self):
+        "Test no import from"
+        with self.assertRaisesRegex(ValueError, r"invalid import from"):
+            str(MarkupTemplate("<?python from os import popen?>").generate())
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/trytond-7.0.51/trytond/tests/test_rule.py 
new/trytond-7.0.55/trytond/tests/test_rule.py
--- old/trytond-7.0.51/trytond/tests/test_rule.py       2025-09-22 
21:34:39.000000000 +0200
+++ new/trytond-7.0.55/trytond/tests/test_rule.py       2026-07-08 
08:36:10.000000000 +0200
@@ -3,6 +3,7 @@
 import json
 import unittest
 
+from trytond.model import ModelAccessProxy
 from trytond.model.exceptions import AccessError
 from trytond.pool import Pool
 from trytond.tests.test_tryton import activate_module, with_transaction
@@ -708,3 +709,52 @@
 
         with self.assertRaisesRegex(AccessError, "Field different from foo"):
             TestRuleModel.read([test.id], ['name'])
+
+    @with_transaction()
+    def test_model_access_proxy(self):
+        "Test model access proxy"
+        pool = Pool()
+        TestRule = pool.get('test.rule')
+        RuleGroup = pool.get('ir.rule.group')
+        Model = pool.get('ir.model')
+
+        model, = Model.search([('model', '=', 'test.rule')])
+        rule_group, = RuleGroup.create([{
+                    'name': "Field different from foo",
+                    'model': model.id,
+                    'global_p': True,
+                    'perm_read': False,
+                    'rules': [('create', [{
+                                    'domain': json.dumps(
+                                        [('field', '!=', 'foo')]),
+                                    }])],
+                    }])
+        record, = TestRule.create([{'field': 'foo'}])
+
+        proxy = ModelAccessProxy(record, {})
+
+        self.assertEqual(proxy.field, 'foo')
+
+    @with_transaction()
+    def test_model_access_proxy_no_access(self):
+        "Test model access proxy without access"
+        pool = Pool()
+        TestRule = pool.get('test.rule')
+        RuleGroup = pool.get('ir.rule.group')
+        Model = pool.get('ir.model')
+
+        model, = Model.search([('model', '=', 'test.rule')])
+        rule_group, = RuleGroup.create([{
+                    'name': "Field different from foo",
+                    'model': model.id,
+                    'global_p': True,
+                    'perm_read': True,
+                    'rules': [('create', [{
+                                    'domain': json.dumps(
+                                        [('field', '!=', 'foo')]),
+                                    }])],
+                    }])
+        record, = TestRule.create([{'field': 'foo'}])
+
+        with self.assertRaises(AccessError):
+            ModelAccessProxy(record, {})
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/trytond-7.0.51/trytond/tools/misc.py 
new/trytond-7.0.55/trytond/tools/misc.py
--- old/trytond-7.0.51/trytond/tools/misc.py    2026-04-14 20:36:56.000000000 
+0200
+++ new/trytond-7.0.55/trytond/tools/misc.py    2026-07-08 08:36:10.000000000 
+0200
@@ -210,8 +210,15 @@
 def resolve(name):
     "Resolve a dotted name to a global object."
     name = name.split('.')
-    used = name.pop(0)
-    found = importlib.import_module(used)
+    if name[:2] == ['trytond', 'modules']:
+        # import the Tryton module using entry point
+        name = name[2:]
+        used = name.pop(0)
+        found = import_module(used)
+        used = found.__name__
+    else:
+        used = name.pop(0)
+        found = importlib.import_module(used)
     for n in name:
         used = used + '.' + n
         try:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/trytond-7.0.51/trytond/transaction.py 
new/trytond-7.0.55/trytond/transaction.py
--- old/trytond-7.0.51/trytond/transaction.py   2026-04-30 21:39:01.000000000 
+0200
+++ new/trytond-7.0.55/trytond/transaction.py   2026-07-08 08:36:10.000000000 
+0200
@@ -406,7 +406,9 @@
 
     @property
     def check_access(self):
-        return self.context.get('_check_access', False)
+        return (
+            self.context.get('_check_access', False)
+            and self.user != 0)
 
     @property
     def active_records(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/trytond-7.0.51/trytond.egg-info/PKG-INFO 
new/trytond-7.0.55/trytond.egg-info/PKG-INFO
--- old/trytond-7.0.51/trytond.egg-info/PKG-INFO        2026-06-02 
18:46:50.000000000 +0200
+++ new/trytond-7.0.55/trytond.egg-info/PKG-INFO        2026-08-04 
12:06:01.000000000 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: trytond
-Version: 7.0.51
+Version: 7.0.55
 Summary: Tryton server
 Home-page: http://www.tryton.org/
 Download-URL: http://downloads.tryton.org/7.0/
@@ -77,7 +77,7 @@
 Provides-Extra: html2text
 Requires-Dist: html2text; extra == "html2text"
 Provides-Extra: weasyprint
-Requires-Dist: weasyprint; extra == "weasyprint"
+Requires-Dist: weasyprint>=68.0; extra == "weasyprint"
 Provides-Extra: coroutine
 Requires-Dist: gevent>=1.1; extra == "coroutine"
 Provides-Extra: image
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/trytond-7.0.51/trytond.egg-info/SOURCES.txt 
new/trytond-7.0.55/trytond.egg-info/SOURCES.txt
--- old/trytond-7.0.51/trytond.egg-info/SOURCES.txt     2026-06-02 
18:46:50.000000000 +0200
+++ new/trytond-7.0.55/trytond.egg-info/SOURCES.txt     2026-08-04 
12:06:01.000000000 +0200
@@ -91,6 +91,7 @@
 doc/tutorial/module/wizard.rst
 doc/tutorial/module/workflow.rst
 trytond/__init__.py
+trytond/_safe_genshi.py
 trytond/admin.py
 trytond/application.py
 trytond/bus.py
@@ -507,6 +508,7 @@
 trytond/tests/test_field_time.py
 trytond/tests/test_field_timedelta.py
 trytond/tests/test_filestore.py
+trytond/tests/test_genshi.py
 trytond/tests/test_history.py
 trytond/tests/test_i18n.py
 trytond/tests/test_importdata.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/trytond-7.0.51/trytond.egg-info/requires.txt 
new/trytond-7.0.55/trytond.egg-info/requires.txt
--- old/trytond-7.0.51/trytond.egg-info/requires.txt    2026-06-02 
18:46:50.000000000 +0200
+++ new/trytond-7.0.55/trytond.egg-info/requires.txt    2026-08-04 
12:06:01.000000000 +0200
@@ -56,4 +56,4 @@
 pillow
 
 [weasyprint]
-weasyprint
+weasyprint>=68.0

Reply via email to