# HG changeset patch
# User Cédric Krier <ced@b2ck.com>
# Date 1678209025 -3600
#      Tue Mar 07 18:10:25 2023 +0100
# Branch 6.6
# Node ID a18e8ac55bc051e50c489041d5eadfe832b2d6fe
# Parent  dec3259fb5ec59e896ff32080276a56d88ede4bd
Add trailing / to URLs of DPD shipping services

Closes #12098
(grafted from a6b41e6401ec070f5cdf97115bfd2c22c3dccdac)

diff --git a/modules/stock_package_shipping_dpd/configuration.py b/modules/stock_package_shipping_dpd/configuration.py
--- a/modules/stock_package_shipping_dpd/configuration.py
+++ b/modules/stock_package_shipping_dpd/configuration.py
@@ -12,8 +12,8 @@
     'production': 'https://public-ws.dpd.com/services/',
     }
 
-LOGIN_SERVICE = 'LoginService/V2_0?wsdl'
-SHIPMENT_SERVICE = 'ShipmentService/V3_2?wsdl'
+LOGIN_SERVICE = 'LoginService/V2_0/?wsdl'
+SHIPMENT_SERVICE = 'ShipmentService/V3_2/?wsdl'
 TIMEOUT = config.get(
     'stock_package_shipping_dpd', 'requests_timeout', default=300)
 
# HG changeset patch
# User Sergi Almacellas Abellana <sergi@koolpi.com>
# Date 1678365432 -3600
#      Thu Mar 09 13:37:12 2023 +0100
# Branch 6.6
# Node ID 70609fc9c0a74a93460655605519f459e13617cf
# Parent  a18e8ac55bc051e50c489041d5eadfe832b2d6fe
Use inventory company to complete lines

Closes #12127
(grafted from 5282d0322326c6abe1b98cb1dee1565064257e4f)

diff --git a/modules/stock/inventory.py b/modules/stock/inventory.py
--- a/modules/stock/inventory.py
+++ b/modules/stock/inventory.py
@@ -256,7 +256,9 @@
             if inventory.state == 'done':
                 continue
             # Compute product quantities
-            with Transaction().set_context(stock_date_end=inventory.date):
+            with Transaction().set_context(
+                    company=inventory.company.id,
+                    stock_date_end=inventory.date):
                 if fill:
                     pbl = Product.products_by_location(
                         [inventory.location.id],
# HG changeset patch
# User Cédric Krier <ced@b2ck.com>
# Date 1678449180 -3600
#      Fri Mar 10 12:53:00 2023 +0100
# Branch 6.6
# Node ID 32fc398251b01a46fde9f6d995dd3514e100996e
# Parent  70609fc9c0a74a93460655605519f459e13617cf
Test if anchor has href attribute before converting it

It is valid HTML to have anchor without href attribute.

Closes #12134
(grafted from f3a2d9b2258d6004fffc5431136bd36b010b7f9b)

diff --git a/modules/marketing_automation/marketing_automation.py b/modules/marketing_automation/marketing_automation.py
--- a/modules/marketing_automation/marketing_automation.py
+++ b/modules/marketing_automation/marketing_automation.py
@@ -567,7 +567,7 @@
             for kind, data, pos in stream:
                 if kind is START:
                     tag, attrs = data
-                    if tag == 'a':
+                    if tag == 'a' and attrs.get('href'):
                         href = attrs.get('href')
                         attrs -= 'href'
                         if href.startswith('unsubscribe'):
diff --git a/modules/marketing_email/marketing.py b/modules/marketing_email/marketing.py
--- a/modules/marketing_email/marketing.py
+++ b/modules/marketing_email/marketing.py
@@ -451,7 +451,7 @@
                 for kind, data, pos in stream:
                     if kind is START:
                         tag, attrs = data
-                        if tag == 'a':
+                        if tag == 'a' and attrs.get('href'):
                             href = attrs.get('href')
                             attrs -= 'href'
                             href = short(href, str(message))
# HG changeset patch
# User Cédric Krier <ced@b2ck.com>
# Date 1678743883 -3600
#      Mon Mar 13 22:44:43 2023 +0100
# Branch 6.6
# Node ID f861f5087fd0bd2b57a480ae8b044853e8a7400f
# Parent  32fc398251b01a46fde9f6d995dd3514e100996e
Ensure price ratio is Decimal for product kit

When the ratio is computed based on number of components instead of price
list.

Closes #12135
(grafted from de0b876c414875b7f317b3531859d4b7ac25e8d5)

diff --git a/modules/product_kit/common.py b/modules/product_kit/common.py
--- a/modules/product_kit/common.py
+++ b/modules/product_kit/common.py
@@ -311,7 +311,7 @@
                 if sum_:
                     ratio = list_prices[component] / sum_
                 else:
-                    ratio = 1 / len(components)
+                    ratio = Decimal(1) / len(components)
                 if component.fixed:
                     ratio /= Decimal(str(component.quantity / quantity))
                 else:
# HG changeset patch
# User Cédric Krier <ced@b2ck.com>
# Date 1678892950 -3600
#      Wed Mar 15 16:09:10 2023 +0100
# Branch 6.6
# Node ID 943b19e831957035d57f666aa2bfe12751059917
# Parent  f861f5087fd0bd2b57a480ae8b044853e8a7400f
Do not copy password hash nor reset from user
(grafted from c94f99afeadbc10e483ed3d34d707635f77ac945)

diff --git a/modules/web_user/user.py b/modules/web_user/user.py
--- a/modules/web_user/user.py
+++ b/modules/web_user/user.py
@@ -162,6 +162,13 @@
         cls.save(users)
 
     @classmethod
+    def copy(cls, users, default=None):
+        default = default.copy() if default is not None else {}
+        default['password_hash'] = None
+        default['reset_password_token'] = None
+        return super().copy(users, default=default)
+
+    @classmethod
     def create(cls, vlist):
         users = super(User, cls).create(vlist)
         cls._format_email(users)
diff --git a/trytond/trytond/res/user.py b/trytond/trytond/res/user.py
--- a/trytond/trytond/res/user.py
+++ b/trytond/trytond/res/user.py
@@ -439,7 +439,8 @@
             default = {}
         default = default.copy()
 
-        default['password'] = ''
+        default['password_hash'] = None
+        default['password_reset'] = None
         default.setdefault('warnings')
         default.setdefault('applications')
 
# HG changeset patch
# User Cédric Krier <ced@b2ck.com>
# Date 1678870382 -3600
#      Wed Mar 15 09:53:02 2023 +0100
# Branch 6.6
# Node ID 3f2e658af5e4edd0a1e557746f79ee26594aab63
# Parent  943b19e831957035d57f666aa2bfe12751059917
Reset next number of invoice sequences when renew fiscal year
(grafted from fcf3710fa6cf50cc25de17b6e65acdc7f96a3510)

diff --git a/modules/account_invoice/account.py b/modules/account_invoice/account.py
--- a/modules/account_invoice/account.py
+++ b/modules/account_invoice/account.py
@@ -399,11 +399,13 @@
                 sequence = getattr(invoice_sequence, field, None)
                 sequences[sequence.id] = sequence
         copies = Sequence.copy(list(sequences.values()), default={
-                'next_number': 1,
                 'name': lambda data: data['name'].replace(
                     self.start.previous_fiscalyear.name,
                     self.start.name)
                 })
+        Sequence.write(copies, {
+                'number_next': Sequence.default_number_next(),
+                })
 
         mapping = {}
         for previous_id, new_sequence in zip(sequences.keys(), copies):
# HG changeset patch
# User Cédric Krier <ced@b2ck.com>
# Date 1678200233 -3600
#      Tue Mar 07 15:43:53 2023 +0100
# Branch 6.6
# Node ID c249ee8605b14801e80bda4e1ce1762ce4475cc9
# Parent  3f2e658af5e4edd0a1e557746f79ee26594aab63
Keep all transaction caches still referenced

The LRUDict is replaced by a WeakValueDictionary to keep all used transaction
caches such as they can be cleared by ModelStorage.
But we still keep the latest cache used so they are not removed from the
WeakValueDictionary too soon when such cache has been created to be pre-filled
like in ModelSQL.search.

Closes #12119
(grafted from 72f5f8ed9b1a9be670ba9c3dd29532f328373e7d)

diff --git a/trytond/trytond/transaction.py b/trytond/trytond/transaction.py
--- a/trytond/trytond/transaction.py
+++ b/trytond/trytond/transaction.py
@@ -2,8 +2,9 @@
 # this repository contains the full copyright notices and license terms.
 import logging
 import time
-from collections import defaultdict
+from collections import defaultdict, deque
 from threading import local
+from weakref import WeakValueDictionary
 
 from sql import Flavor
 
@@ -67,19 +68,11 @@
     started_at = None
 
     def __new__(cls, new=False):
-        from trytond.cache import LRUDict
-        from trytond.pool import Pool
         transactions = cls._local.transactions
         if new or not transactions:
             instance = super(Transaction, cls).__new__(cls)
-            instance.cache = LRUDict(
-                _cache_transaction,
-                lambda: LRUDict(
-                    _cache_model,
-                    lambda name: LRUDict(
-                        record_cache_size(instance),
-                        Pool().get(name)._record),
-                    default_factory_with_key=True))
+            instance.cache = WeakValueDictionary()
+            instance._cache_deque = deque(maxlen=_cache_transaction)
             instance._atexit = []
             transactions.append(instance)
         else:
@@ -98,10 +91,21 @@
         return self._local.tasks
 
     def get_cache(self):
+        from trytond.cache import LRUDict
+        from trytond.pool import Pool
         keys = tuple(((key, self.context[key])
                 for key in sorted(self.cache_keys)
                 if key in self.context))
-        return self.cache[(self.user, keys)]
+        cache = self.cache.setdefault(
+            (self.user, keys), LRUDict(
+                _cache_model,
+                lambda name: LRUDict(
+                    record_cache_size(self),
+                    Pool().get(name)._record),
+                default_factory_with_key=True))
+        # Keep last used cache references to allow to pre-fill them
+        self._cache_deque.append(cache)
+        return cache
 
     def start(self, database_name, user, readonly=False, context=None,
             close=False, autocommit=False):
# HG changeset patch
# User Cédric Krier <ced@b2ck.com>
# Date 1678203238 -3600
#      Tue Mar 07 16:33:58 2023 +0100
# Branch 6.6
# Node ID f22d879b713323fc9d17d7c420478df035c8c0d2
# Parent  c249ee8605b14801e80bda4e1ce1762ce4475cc9
Remove password hash also from the transaction cache

Closes #12120
(grafted from 7b5157206d328bfe020fabece59fb7747dd2dd9d)

diff --git a/trytond/trytond/res/user.py b/trytond/trytond/res/user.py
--- a/trytond/trytond/res/user.py
+++ b/trytond/trytond/res/user.py
@@ -364,12 +364,29 @@
     @classmethod
     def read(cls, ids, fields_names):
         result = super(User, cls).read(ids, fields_names)
-        if not fields_names or 'password_hash' in fields_names:
-            for values in result:
+        cache = Transaction().get_cache()[cls.__name__]
+        for values in result:
+            if 'password_hash' in values:
                 values['password_hash'] = None
+            if values['id'] in cache:
+                cache[values['id']]['password_hash'] = None
         return result
 
     @classmethod
+    def search(
+            cls, domain, offset=0, limit=None, order=None, count=False,
+            query=False):
+        users = super().search(
+            domain, offset=offset, limit=limit, order=order, count=count,
+            query=query)
+        cache = Transaction().get_cache()[cls.__name__]
+        if not count and not query:
+            for user in users:
+                if user.id in cache:
+                    cache[user.id]['password_hash'] = None
+        return users
+
+    @classmethod
     def create(cls, vlist):
         vlist = [cls._convert_vals(vals) for vals in vlist]
         return super(User, cls).create(vlist)
# HG changeset patch
# User Cédric Krier <ced@b2ck.com>
# Date 1678317666 -3600
#      Thu Mar 09 00:21:06 2023 +0100
# Branch 6.6
# Node ID b07f0b7fd83bd52bed960bd86ce5e1daaac1bccb
# Parent  f22d879b713323fc9d17d7c420478df035c8c0d2
Call record saved after saving the current record of the screen

Since 403bc7a95b0a record_saved has been implemented but it is not called by
the Screen when saving record.

Closes #12117, #12137
(grafted from 4cca7cca2fc1f4a8cced8951dda60dde35e36a7b)

diff --git a/sao/src/screen.js b/sao/src/screen.js
--- a/sao/src/screen.js
+++ b/sao/src/screen.js
@@ -1600,7 +1600,9 @@
             }
             const display = () => {
                 // Return the original promise to keep succeed/rejected state
-                return this.display().then(() => prm, () => prm);
+                return this.display()
+                    .always(() => this.record_saved())
+                    .then(() => prm, () => prm);
             };
             return prm.then(current_record => {
                 if (path && current_record && current_record.id) {
# HG changeset patch
# User Cédric Krier <ced@b2ck.com>
# Date 1678983214 -3600
#      Thu Mar 16 17:13:34 2023 +0100
# Branch 6.6
# Node ID a3f4a38095c50536bb9ad099614276d1bd02d77a
# Parent  b07f0b7fd83bd52bed960bd86ce5e1daaac1bccb
Do not create marketing parameter when context value is an empty string

Closes #12145
(grafted from c3ad06a9ac25f378aebaf93489ad2a68302f77b5)

diff --git a/modules/marketing_campaign/marketing.py b/modules/marketing_campaign/marketing.py
--- a/modules/marketing_campaign/marketing.py
+++ b/modules/marketing_campaign/marketing.py
@@ -108,7 +108,9 @@
         default = super().default_get(
             fields_names, with_rec_name=with_rec_name)
         for fname in cls.marketing_campaign_fields():
-            if isinstance(context.get(fname), str) and not default.get(fname):
+            if (isinstance(context.get(fname), str)
+                    and context[fname]
+                    and not default.get(fname)):
                 field = getattr(cls, fname)
                 Target = field.get_target()
                 target = Target.from_name(
# HG changeset patch
# User Sergi Almacellas Abellana <sergi@koolpi.com>
# Date 1679644569 -3600
#      Fri Mar 24 08:56:09 2023 +0100
# Branch 6.6
# Node ID 9a6bb51bcefbe62fbfb51c01db1bbc3eaca95220
# Parent  a3f4a38095c50536bb9ad099614276d1bd02d77a
Do not copy templates relation when duplicating a category

Closes #12158
(grafted from 17f94a972a316d3ea1aebdeb1615b1fa16883220)

diff --git a/modules/product/category.py b/modules/product/category.py
--- a/modules/product/category.py
+++ b/modules/product/category.py
@@ -36,3 +36,9 @@
             'pyson_domain': PYSONEncoder().encode(
                 [('id', '=', categories[0].id)]),
              }
+
+    @classmethod
+    def copy(cls, categories, default=None):
+        default = default.copy() if default is not None else {}
+        default.setdefault('templates')
+        return super().copy(categories, default=default)