[tryton-commits] changeset in trytond:6.0 Add support for SQL expression to sqlit...

2021-06-23 Thread Nicolas Évrard
changeset 269dae43fc45 in trytond:6.0
details: https://hg.tryton.org/trytond?cmd=changeset=269dae43fc45
description:
Add support for SQL expression to sqlite's TRIM

issue10510
review352121003
(grafted from 3b899a065dce3c18d07ab1829bfb943f576ba9e9)
diffstat:

 trytond/backend/sqlite/database.py |  7 ++-
 trytond/tests/test_backend.py  |  3 ++-
 2 files changed, 8 insertions(+), 2 deletions(-)

diffs (37 lines):

diff -r 7838e3871ab7 -r 269dae43fc45 trytond/backend/sqlite/database.py
--- a/trytond/backend/sqlite/database.pySun Jun 06 09:27:28 2021 +0200
+++ b/trytond/backend/sqlite/database.pyWed Jun 16 14:55:46 2021 +0200
@@ -236,7 +236,12 @@
 
 @property
 def params(self):
-return [self.string, self.characters]
+if isinstance(self.string, str):
+params = [self.string]
+else:
+params = list(self.string.params)
+params.append(self.characters)
+return params
 
 
 def sign(value):
diff -r 7838e3871ab7 -r 269dae43fc45 trytond/tests/test_backend.py
--- a/trytond/tests/test_backend.py Sun Jun 06 09:27:28 2021 +0200
+++ b/trytond/tests/test_backend.py Wed Jun 16 14:55:46 2021 +0200
@@ -5,7 +5,7 @@
 import unittest
 
 from sql import Select
-from sql import functions
+from sql import functions, Literal
 from sql.functions import CurrentTimestamp, ToChar
 
 from trytond.tests.test_tryton import activate_module, with_transaction
@@ -127,6 +127,7 @@
 # (functions.Substring('Thomas', '...$'), 'mas'),
 # (functions.Substring('Thomas', '%#"o_a#"_', '#'), 'oma'),
 (functions.Trim('yxTomxx', 'BOTH', 'xyz'), 'Tom'),
+(functions.Trim(Literal('yxTomxxx'), 'BOTH', 'xyz'), "Tom"),
 (functions.Upper('tom'), 'TOM'),
 ]
 for func, result in tests:



[tryton-commits] changeset in trytond:5.8 Add support for SQL expression to sqlit...

2021-06-23 Thread Nicolas Évrard
changeset 4938d32f33fc in trytond:5.8
details: https://hg.tryton.org/trytond?cmd=changeset=4938d32f33fc
description:
Add support for SQL expression to sqlite's TRIM

issue10510
review352121003
(grafted from 3b899a065dce3c18d07ab1829bfb943f576ba9e9)
diffstat:

 trytond/backend/sqlite/database.py |  7 ++-
 trytond/tests/test_backend.py  |  3 ++-
 2 files changed, 8 insertions(+), 2 deletions(-)

diffs (37 lines):

diff -r b5fec49cfc79 -r 4938d32f33fc trytond/backend/sqlite/database.py
--- a/trytond/backend/sqlite/database.pySun Jun 06 09:27:28 2021 +0200
+++ b/trytond/backend/sqlite/database.pyWed Jun 16 14:55:46 2021 +0200
@@ -233,7 +233,12 @@
 
 @property
 def params(self):
-return [self.string, self.characters]
+if isinstance(self.string, str):
+params = [self.string]
+else:
+params = list(self.string.params)
+params.append(self.characters)
+return params
 
 
 def sign(value):
diff -r b5fec49cfc79 -r 4938d32f33fc trytond/tests/test_backend.py
--- a/trytond/tests/test_backend.py Sun Jun 06 09:27:28 2021 +0200
+++ b/trytond/tests/test_backend.py Wed Jun 16 14:55:46 2021 +0200
@@ -5,7 +5,7 @@
 import unittest
 
 from sql import Select
-from sql import functions
+from sql import functions, Literal
 from sql.functions import CurrentTimestamp, ToChar
 
 from trytond.tests.test_tryton import activate_module, with_transaction
@@ -118,6 +118,7 @@
 # (functions.Substring('Thomas', '...$'), 'mas'),
 # (functions.Substring('Thomas', '%#"o_a#"_', '#'), 'oma'),
 (functions.Trim('yxTomxx', 'BOTH', 'xyz'), 'Tom'),
+(functions.Trim(Literal('yxTomxxx'), 'BOTH', 'xyz'), "Tom"),
 (functions.Upper('tom'), 'TOM'),
 ]
 for func, result in tests:



[tryton-commits] changeset in modules/account:default Search active period with S...

2021-05-19 Thread Nicolas Évrard
changeset 6fa943bab6bf in modules/account:default
details: https://hg.tryton.org/modules/account?cmd=changeset=6fa943bab6bf
description:
Search active period with SQL clause instead of a subquery

issue10344
review351841002
diffstat:

 common.py |  16 +++-
 1 files changed, 7 insertions(+), 9 deletions(-)

diffs (52 lines):

diff -r 05a3dd4ca183 -r 6fa943bab6bf common.py
--- a/common.py Sun May 16 17:58:08 2021 +0200
+++ b/common.py Wed May 19 15:28:21 2021 +0200
@@ -2,6 +2,7 @@
 # this repository contains the full copyright notices and license terms.
 import datetime
 
+from sql import Literal
 from sql.conditionals import Coalesce
 
 from trytond.model import Model, fields
@@ -66,8 +67,7 @@
 
 class ActivePeriodMixin(PeriodMixin):
 
-active = fields.Function(
-fields.Boolean("Active"), 'get_active', searcher='search_active')
+active = fields.Function(fields.Boolean("Active"), 'get_active')
 
 @classmethod
 def _active_dates(cls):
@@ -119,8 +119,8 @@
 or (from_date <= start_date and end_date <= to_date))
 
 @classmethod
-def search_active(cls, name, domain):
-table = cls.__table__()
+def domain_active(cls, domain, tables):
+table, _ = tables[None]
 _, operator, value = domain
 
 if operator in {'=', '!='}:
@@ -134,16 +134,14 @@
 elif False in value and True not in value:
 operator = 'not in'
 else:
-return []
+return Literal(True)
 else:
-return []
+return Literal(True)
 
 from_date, to_date = cls._active_dates()
 start_date = Coalesce(table.start_date, datetime.date.min)
 end_date = Coalesce(table.end_date, datetime.date.max)
 
-query = table.select(table.id,
-where=((start_date <= to_date) & (end_date >= to_date))
+return (((start_date <= to_date) & (end_date >= to_date))
 | ((start_date <= from_date) & (end_date >= from_date))
 | ((start_date >= from_date) & (end_date <= to_date)))
-return [('id', operator, query)]



[tryton-commits] changeset in sao:default Search and get keys in one request for ...

2021-05-19 Thread Nicolas Évrard
changeset 8cfda2f5b7db in sao:default
details: https://hg.tryton.org/sao?cmd=changeset=8cfda2f5b7db
description:
Search and get keys in one request for Dict fields

issue10332
review349751003
diffstat:

 src/model.js |  11 ---
 1 files changed, 4 insertions(+), 7 deletions(-)

diffs (29 lines):

diff -r aea9ffab0b8b -r 8cfda2f5b7db src/model.js
--- a/src/model.js  Tue May 18 22:08:59 2021 +0200
+++ b/src/model.js  Wed May 19 15:41:39 2021 +0200
@@ -2770,10 +2770,6 @@
 var batchlen = Math.min(10, Sao.config.limit);
 
 keys = jQuery.extend([], keys);
-var get_keys = function(key_ids) {
-return this.schema_model.execute('get_keys',
-[key_ids], context).then(update_keys);
-}.bind(this);
 var update_keys = function(values) {
 for (var i = 0, len = values.length; i < len; i++) {
 var k = values[i];
@@ -2784,10 +2780,11 @@
 var prms = [];
 while (keys.length > 0) {
 var sub_keys = keys.splice(0, batchlen);
-prms.push(this.schema_model.execute('search',
+prms.push(this.schema_model.execute('search_get_keys',
 [[['name', 'in', sub_keys], domain],
-0, Sao.config.limit, null], context)
-.then(get_keys));
+Sao.config.limit],
+context)
+.then(update_keys));
 }
 return jQuery.when.apply(jQuery, prms);
 },



[tryton-commits] changeset in tryton:default Search and get keys in one request f...

2021-05-19 Thread Nicolas Évrard
changeset 8d3c52dc5e69 in tryton:default
details: https://hg.tryton.org/tryton?cmd=changeset=8d3c52dc5e69
description:
Search and get keys in one request for Dict fields

issue10332
review349751003
diffstat:

 tryton/gui/window/view_form/model/field.py |  13 +++--
 1 files changed, 3 insertions(+), 10 deletions(-)

diffs (23 lines):

diff -r bdcdda37d2ad -r 8d3c52dc5e69 tryton/gui/window/view_form/model/field.py
--- a/tryton/gui/window/view_form/model/field.pyTue May 18 22:32:45 
2021 +0200
+++ b/tryton/gui/window/view_form/model/field.pyWed May 19 15:41:39 
2021 +0200
@@ -1088,16 +1088,9 @@
 for i in range(0, len(keys), batchlen):
 sub_keys = keys[i:i + batchlen]
 try:
-key_ids = RPCExecute('model', schema_model, 'search',
-[('name', 'in', sub_keys), domain], 0,
-CONFIG['client.limit'], None, context=context)
-except RPCException:
-key_ids = []
-if not key_ids:
-continue
-try:
-values = RPCExecute('model', schema_model,
-'get_keys', key_ids, context=context)
+values = RPCExecute('model', schema_model, 'search_get_keys',
+[('name', 'in', sub_keys), domain], CONFIG['client.limit'],
+context=context)
 except RPCException:
 values = []
 if not values:



[tryton-commits] changeset in trytond:default Search and get keys in one request ...

2021-05-19 Thread Nicolas Évrard
changeset 89c0a05e8d4b in trytond:default
details: https://hg.tryton.org/trytond?cmd=changeset=89c0a05e8d4b
description:
Search and get keys in one request for Dict fields

issue10332
review349751003
diffstat:

 CHANGELOG   |  1 +
 trytond/model/dictschema.py |  6 ++
 2 files changed, 7 insertions(+), 0 deletions(-)

diffs (31 lines):

diff -r c433e259ed6a -r 89c0a05e8d4b CHANGELOG
--- a/CHANGELOG Tue May 18 22:34:43 2021 +0200
+++ b/CHANGELOG Wed May 19 15:41:39 2021 +0200
@@ -1,3 +1,4 @@
+* Combine search and get_keys in DictSchemaMixin
 * Make language code unique
 * Support base64 encoded data in ModelStorage.import_data
 * Add BOOL_AND and BOOL_OR to SQLite backend
diff -r c433e259ed6a -r 89c0a05e8d4b trytond/model/dictschema.py
--- a/trytond/model/dictschema.py   Tue May 18 22:34:43 2021 +0200
+++ b/trytond/model/dictschema.py   Wed May 19 15:41:39 2021 +0200
@@ -91,6 +91,7 @@
 super(DictSchemaMixin, cls).__setup__()
 cls.__rpc__.update({
 'get_keys': RPC(instantiate=0),
+'search_get_keys': RPC(),
 })
 
 @staticmethod
@@ -179,6 +180,11 @@
 return keys
 
 @classmethod
+def search_get_keys(cls, domain, limit=None):
+schemas = cls.search(domain, limit=limit)
+return cls.get_keys(schemas)
+
+@classmethod
 def get_relation_fields(cls):
 if not config.get('dict', cls.__name__, default=True):
 return {}



[tryton-commits] changeset in modules/account:default Use the right order of colu...

2021-04-28 Thread Nicolas Évrard
changeset 04efbab05b97 in modules/account:default
details: https://hg.tryton.org/modules/account?cmd=changeset=04efbab05b97
description:
Use the right order of columns in the index used for 
account.account.party

issue10216
review 348241002
diffstat:

 move.py |  2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)

diffs (12 lines):

diff -r 0abedc0e447f -r 04efbab05b97 move.py
--- a/move.py   Thu Apr 22 07:55:16 2021 +0200
+++ b/move.py   Wed Apr 28 10:15:54 2021 +0200
@@ -774,7 +774,7 @@
 table_h.index_action(['move', 'account'], 'add')
 # Index for account.account.party
 table_h.index_action(
-['party', 'account', 'id'], 'add', where=table.party != Null)
+['account', 'party', 'id'], 'add', where=table.party != Null)
 
 @classmethod
 def default_date(cls):



[tryton-commits] changeset in sao:default Set loaded fields first after O2M on_ch...

2021-02-04 Thread Nicolas Évrard
changeset 10156791f1ed in sao:default
details: https://hg.tryton.org/sao?cmd=changeset;node=10156791f1ed
description:
Set loaded fields first after O2M on_change calls

issue9930
review302811002
diffstat:

 src/model.js |  34 +++---
 1 files changed, 27 insertions(+), 7 deletions(-)

diffs (72 lines):

diff -r d8485f101974 -r 10156791f1ed src/model.js
--- a/src/model.js  Wed Feb 03 19:39:33 2021 +0100
+++ b/src/model.js  Thu Feb 04 18:05:18 2021 +0100
@@ -2277,7 +2277,7 @@
 if (value.add || value.update) {
 var context = this.get_context(record);
 fields = record._values[this.name].model.fields;
-var field_names = {};
+var new_field_names = {};
 var adding_values = [];
 if (value.add) {
 for (var i=0; i < value.add.length; i++) {
@@ -2291,25 +2291,25 @@
 if (!(f in fields) &&
 (f != 'id') &&
 (!~f.indexOf('.'))) {
-field_names[f] = true;
+new_field_names[f] = true;
 }
 });
 });
 }
 });
-if (!jQuery.isEmptyObject(field_names)) {
+if (!jQuery.isEmptyObject(new_field_names)) {
 var args = {
 'method': 'model.' + this.description.relation +
 '.fields_get',
-'params': [Object.keys(field_names), context]
+'params': [Object.keys(new_field_names), context]
 };
 try {
-fields = Sao.rpc(args, record.model.session, false);
+new_fields = Sao.rpc(args, record.model.session, 
false);
 } catch (e) {
 return;
 }
 } else {
-fields = {};
+new_fields = {};
 }
 }
 
@@ -2332,7 +2332,27 @@
 }
 
 if (value.add || value.update) {
-group.add_fields(fields);
+// First set already added fields to prevent triggering a
+// second on_change call
+if (value.update) {
+value.update.forEach(function(vals) {
+if (!vals.id) {
+return;
+}
+var record2 = group.get(vals.id);
+if (record2) {
+var vals_to_set = {};
+for (var key in vals) {
+if (!(key in new_field_names)) {
+vals_to_set[key] = vals[key];
+}
+}
+record2.set_on_change(vals_to_set);
+}
+});
+}
+
+group.add_fields(new_fields);
 if (value.add) {
 value.add.forEach(function(vals) {
 var new_record;



[tryton-commits] changeset in tryton:default Set loaded fields first after O2M on...

2021-02-04 Thread Nicolas Évrard
changeset 65002d9965d4 in tryton:default
details: https://hg.tryton.org/tryton?cmd=changeset;node=65002d9965d4
description:
Set loaded fields first after O2M on_change calls

issue9930
review302811002
diffstat:

 tryton/gui/window/view_form/model/field.py |  17 ++---
 1 files changed, 14 insertions(+), 3 deletions(-)

diffs (38 lines):

diff -r a49fcbe0973a -r 65002d9965d4 tryton/gui/window/view_form/model/field.py
--- a/tryton/gui/window/view_form/model/field.pyWed Feb 03 19:39:33 
2021 +0100
+++ b/tryton/gui/window/view_form/model/field.pyThu Feb 04 18:05:18 
2021 +0100
@@ -763,12 +763,12 @@
 for f in v if f not in fields and f != 'id' and '.' not in f)
 if field_names:
 try:
-fields = RPCExecute('model', self.attrs['relation'],
+new_fields = RPCExecute('model', self.attrs['relation'],
 'fields_get', list(field_names), context=context)
 except RPCException:
 return
 else:
-fields = {}
+new_fields = {}
 
 group = record.value[self.name]
 if value and value.get('delete'):
@@ -787,7 +787,18 @@
 force_remove=False)
 
 if value and (value.get('add') or value.get('update', [])):
-record.value[self.name].add_fields(fields)
+# First set already added fields to prevent triggering a
+# second on_change call
+for vals in value.get('update', []):
+if 'id' not in vals:
+continue
+record2 = group.get(vals['id'])
+if record2 is not None:
+vals_to_set = {
+k: v for k, v in vals.items() if k not in new_fields}
+record2.set_on_change(vals_to_set)
+
+record.value[self.name].add_fields(new_fields)
 for index, vals in value.get('add', []):
 new_record = None
 id_ = vals.pop('id', None)



[tryton-commits] changeset in sao:default Do not specify mode to load in sao's O2...

2021-02-11 Thread Nicolas Évrard
changeset 3357e86c8ccd in sao:default
details: https://hg.tryton.org/sao?cmd=changeset;node=3357e86c8ccd
description:
Do not specify mode to load in sao's O2M widget

issue10080
review335561002
diffstat:

 src/view/form.js |  2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)

diffs (12 lines):

diff -r 10156791f1ed -r 3357e86c8ccd src/view/form.js
--- a/src/view/form.js  Thu Feb 04 18:05:18 2021 +0100
+++ b/src/view/form.js  Thu Feb 11 11:19:51 2021 +0100
@@ -3034,7 +3034,7 @@
 this.screen.pre_validate = attributes.pre_validate == 1;
 
 this.screen.message_callback = this.record_label.bind(this);
-this.prm = this.screen.switch_view(modes[0]).done(function() {
+this.prm = this.screen.switch_view().done(function() {
 this.content.append(this.screen.screen_container.el);
 }.bind(this));
 



[tryton-commits] changeset in modules/sale_subscription:default Raise ValueError ...

2021-03-11 Thread Nicolas Évrard
changeset a373093b65c0 in modules/sale_subscription:default
details: 
https://hg.tryton.org/modules/sale_subscription?cmd=changeset;node=a373093b65c0
description:
Raise ValueError on incorrect weekday

issue1
review323051007
diffstat:

 recurrence.py |  2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)

diffs (12 lines):

diff -r a587b9b8523c -r a373093b65c0 recurrence.py
--- a/recurrence.py Thu Mar 11 12:53:00 2021 +0100
+++ b/recurrence.py Thu Mar 11 20:28:37 2021 +0100
@@ -170,7 +170,7 @@
 try:
 cls = WEEKDAYS[weekday[:2]]
 except KeyError:
-ValueError('Invalid weekday')
+raise ValueError('Invalid weekday')
 if not weekday[2:]:
 byweekday.append(cls)
 else:



[tryton-commits] changeset in tryton:default Ensure dependencies of fields are al...

2021-03-11 Thread Nicolas Évrard
changeset dc0520e83643 in tryton:default
details: https://hg.tryton.org/tryton?cmd=changeset;node=dc0520e83643
description:
Ensure dependencies of fields are also loaded on Form display

issue9630
review298641002
diffstat:

 tryton/gui/window/view_form/view/form.py |  20 +---
 1 files changed, 13 insertions(+), 7 deletions(-)

diffs (30 lines):

diff -r 870ec68b29e4 -r dc0520e83643 tryton/gui/window/view_form/view/form.py
--- a/tryton/gui/window/view_form/view/form.py  Thu Mar 11 19:14:07 2021 +0100
+++ b/tryton/gui/window/view_form/view/form.py  Thu Mar 11 23:34:44 2021 +0100
@@ -510,13 +510,19 @@
 if record:
 # Force to set fields in record
 # Get first the lazy one from the view to reduce number of requests
-fields = ((name, record.group.fields[name])
-for name in self.widgets)
-fields = (
-(name,
-field.attrs.get('loading', 'eager') == 'eager',
-len(field.views))
-for name, field in fields)
+field_names = set()
+for name in self.widgets:
+field = record.group.fields[name]
+field_names.add(name)
+field_names.update(f for f in field.attrs.get('depends', [])
+if (not f.startswith('_parent')
+and f in record.group.fields))
+fields = []
+for name in field_names:
+field = record.group.fields[name]
+fields.append(
+(name, field.attrs.get('loading', 'eager') == 'eager',
+len(field.views)))
 fields = sorted(fields, key=operator.itemgetter(1, 2))
 for field, _, _ in fields:
 record[field].get(record)



[tryton-commits] changeset in trytond:default Ensure dependencies of fields are a...

2021-03-11 Thread Nicolas Évrard
changeset ccaef9daa811 in trytond:default
details: https://hg.tryton.org/trytond?cmd=changeset;node=ccaef9daa811
description:
Ensure dependencies of fields are also loaded on Form display

issue9630
review298641002
diffstat:

 trytond/model/fields/field.py |  1 +
 1 files changed, 1 insertions(+), 0 deletions(-)

diffs (11 lines):

diff -r 9ce501abab88 -r ccaef9daa811 trytond/model/fields/field.py
--- a/trytond/model/fields/field.py Thu Mar 11 23:12:06 2021 +0100
+++ b/trytond/model/fields/field.py Thu Mar 11 23:34:44 2021 +0100
@@ -441,6 +441,7 @@
 'context': encoder.encode(self.context),
 'loading': self.loading,
 'name': self.name,
+'depends': self.depends,
 'on_change': list(self.on_change),
 'on_change_with': list(self.on_change_with),
 'readonly': self.readonly,



[tryton-commits] changeset in sao:default Ensure dependencies of fields are also ...

2021-03-11 Thread Nicolas Évrard
changeset 9f7cab2d5c3a in sao:default
details: https://hg.tryton.org/sao?cmd=changeset;node=9f7cab2d5c3a
description:
Ensure dependencies of fields are also loaded on Form display

issue9630
review298641002
diffstat:

 src/view/form.js |  22 ++
 1 files changed, 18 insertions(+), 4 deletions(-)

diffs (42 lines):

diff -r 8b4f7a8fced0 -r 9f7cab2d5c3a src/view/form.js
--- a/src/view/form.js  Thu Mar 11 19:14:07 2021 +0100
+++ b/src/view/form.js  Thu Mar 11 23:34:44 2021 +0100
@@ -261,20 +261,34 @@
 display: function() {
 var record = this.record;
 var field;
+var depends;
 var name;
 var promesses = [];
 if (record) {
 // Force to set fields in record
 // Get first the lazy one from the view to reduce number of 
requests
-var fields = [];
+var field_names = new Set();
 for (name in this.widgets) {
-field = record.model.fields[name];
+depends = (
+record.model.fields[name].description.depends || []);
+field_names.add(name);
+for (var i = 0; i < depends.length; i++) {
+var depend = depends[i];
+if (!depend.startsWith('_parent') &&
+depend in record.model.fields) {
+field_names.add(depend);
+}
+}
+}
+var fields = [];
+field_names.forEach(function(fname) {
+field = record.model.fields[fname];
 fields.push([
-name,
+fname,
 field.description.loading || 'eager' == 'eager',
 field.views.size,
 ]);
-}
+});
 fields.sort(function(a, b) {
 if (!a[1] && b[1]) {
 return -1;



[tryton-commits] changeset in modules/sale_subscription:5.0 Raise ValueError on i...

2021-03-15 Thread Nicolas Évrard
changeset e35375d5233c in modules/sale_subscription:5.0
details: 
https://hg.tryton.org/modules/sale_subscription?cmd=changeset=e35375d5233c
description:
Raise ValueError on incorrect weekday

issue1
review323051007
(grafted from a373093b65c09bb34fb4f1c6e94b8ee758dfe497)
diffstat:

 recurrence.py |  2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)

diffs (12 lines):

diff -r b04baa2c2371 -r e35375d5233c recurrence.py
--- a/recurrence.py Fri Jan 01 17:32:58 2021 +0100
+++ b/recurrence.py Thu Mar 11 20:28:37 2021 +0100
@@ -162,7 +162,7 @@
 try:
 cls = WEEKDAYS[weekday[:2]]
 except KeyError:
-ValueError('Invalid weekday')
+raise ValueError('Invalid weekday')
 if not weekday[2:]:
 byweekday.append(cls)
 else:



[tryton-commits] changeset in modules/sale_subscription:5.8 Raise ValueError on i...

2021-03-15 Thread Nicolas Évrard
changeset 5694b87815da in modules/sale_subscription:5.8
details: 
https://hg.tryton.org/modules/sale_subscription?cmd=changeset=5694b87815da
description:
Raise ValueError on incorrect weekday

issue1
review323051007
(grafted from a373093b65c09bb34fb4f1c6e94b8ee758dfe497)
diffstat:

 recurrence.py |  2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)

diffs (12 lines):

diff -r 349f1c862421 -r 5694b87815da recurrence.py
--- a/recurrence.py Fri Jan 01 16:38:23 2021 +0100
+++ b/recurrence.py Thu Mar 11 20:28:37 2021 +0100
@@ -165,7 +165,7 @@
 try:
 cls = WEEKDAYS[weekday[:2]]
 except KeyError:
-ValueError('Invalid weekday')
+raise ValueError('Invalid weekday')
 if not weekday[2:]:
 byweekday.append(cls)
 else:



[tryton-commits] changeset in modules/sale_subscription:5.6 Raise ValueError on i...

2021-03-15 Thread Nicolas Évrard
changeset ba19580a0a61 in modules/sale_subscription:5.6
details: 
https://hg.tryton.org/modules/sale_subscription?cmd=changeset=ba19580a0a61
description:
Raise ValueError on incorrect weekday

issue1
review323051007
(grafted from a373093b65c09bb34fb4f1c6e94b8ee758dfe497)
diffstat:

 recurrence.py |  2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)

diffs (12 lines):

diff -r b52a8764b0bc -r ba19580a0a61 recurrence.py
--- a/recurrence.py Fri Jan 01 17:14:14 2021 +0100
+++ b/recurrence.py Thu Mar 11 20:28:37 2021 +0100
@@ -165,7 +165,7 @@
 try:
 cls = WEEKDAYS[weekday[:2]]
 except KeyError:
-ValueError('Invalid weekday')
+raise ValueError('Invalid weekday')
 if not weekday[2:]:
 byweekday.append(cls)
 else:



[tryton-commits] changeset in trytond:default Compute default values on creation ...

2021-03-12 Thread Nicolas Évrard
changeset 36c76fc52954 in trytond:default
details: https://hg.tryton.org/trytond?cmd=changeset;node=36c76fc52954
description:
Compute default values on creation only once per schema of missing 
default values

issue10131
review341491002
diffstat:

 trytond/model/modelsql.py |  42 +++---
 1 files changed, 23 insertions(+), 19 deletions(-)

diffs (60 lines):

diff -r ccaef9daa811 -r 36c76fc52954 trytond/model/modelsql.py
--- a/trytond/model/modelsql.py Thu Mar 11 23:34:44 2021 +0100
+++ b/trytond/model/modelsql.py Fri Mar 12 12:33:30 2021 +0100
@@ -563,6 +563,7 @@
 table = cls.__table__()
 modified_fields = set()
 defaults_cache = {}  # Store already computed default values
+missing_defaults = {}  # Store missing default values by schema
 new_ids = []
 vlist = [v.copy() for v in vlist]
 for values in vlist:
@@ -574,26 +575,29 @@
 modified_fields |= set(values.keys())
 
 # Get default values
-default = []
-for fname, field in cls._fields.items():
-if fname in values:
-continue
-if fname in [
-'create_uid', 'create_date',
-'write_uid', 'write_date', 'id']:
-continue
-if isinstance(field, fields.Function) and not field.setter:
-continue
-if fname in defaults_cache:
-values[fname] = defaults_cache[fname]
-else:
-default.append(fname)
+values_schema = tuple(sorted(values))
+if values_schema not in missing_defaults:
+default = []
+missing_defaults[values_schema] = default_values = {}
+for fname, field in cls._fields.items():
+if fname in values:
+continue
+if fname in [
+'create_uid', 'create_date',
+'write_uid', 'write_date', 'id']:
+continue
+if isinstance(field, fields.Function) and not field.setter:
+continue
+if fname in defaults_cache:
+default_values[fname] = defaults_cache[fname]
+else:
+default.append(fname)
 
-if default:
-defaults = cls.default_get(default, with_rec_name=False)
-defaults = cls._clean_defaults(defaults)
-values.update(defaults)
-defaults_cache.update(defaults)
+if default:
+defaults = cls.default_get(default, with_rec_name=False)
+default_values.update(cls._clean_defaults(defaults))
+defaults_cache.update(default_values)
+values.update(missing_defaults[values_schema])
 
 insert_columns = [table.create_uid, table.create_date]
 insert_values = [transaction.user, CurrentTimestamp()]



[tryton-commits] changeset in sao:5.6 Do not specify mode to load in sao's O2M wi...

2021-02-24 Thread Nicolas Évrard
changeset 47c5563d4f29 in sao:5.6
details: https://hg.tryton.org/sao?cmd=changeset;node=47c5563d4f29
description:
Do not specify mode to load in sao's O2M widget

issue10080
review335561002
(grafted from 3357e86c8ccd23175f9d5d3b544045dbb14b7fb5)
diffstat:

 src/view/form.js |  2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)

diffs (12 lines):

diff -r 8624202d2562 -r 47c5563d4f29 src/view/form.js
--- a/src/view/form.js  Fri Feb 19 21:24:02 2021 +0100
+++ b/src/view/form.js  Thu Feb 11 11:19:51 2021 +0100
@@ -3130,7 +3130,7 @@
 this.screen.pre_validate = attributes.pre_validate == 1;
 
 this.screen.message_callback = this.record_label.bind(this);
-this.prm = this.screen.switch_view(modes[0]).done(function() {
+this.prm = this.screen.switch_view().done(function() {
 this.content.append(this.screen.screen_container.el);
 }.bind(this));
 



[tryton-commits] changeset in sao:5.0 Do not specify mode to load in sao's O2M wi...

2021-02-24 Thread Nicolas Évrard
changeset f8d2431c6bbe in sao:5.0
details: https://hg.tryton.org/sao?cmd=changeset;node=f8d2431c6bbe
description:
Do not specify mode to load in sao's O2M widget

issue10080
review335561002
(grafted from 3357e86c8ccd23175f9d5d3b544045dbb14b7fb5)
diffstat:

 src/view/form.js |  2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)

diffs (12 lines):

diff -r b9b71621fc00 -r f8d2431c6bbe src/view/form.js
--- a/src/view/form.js  Fri Feb 19 21:24:26 2021 +0100
+++ b/src/view/form.js  Thu Feb 11 11:19:51 2021 +0100
@@ -2900,7 +2900,7 @@
 this.screen.pre_validate = attributes.pre_validate == 1;
 
 this.screen.message_callback = this.record_label.bind(this);
-this.prm = this.screen.switch_view(modes[0]).done(function() {
+this.prm = this.screen.switch_view().done(function() {
 this.content.append(this.screen.screen_container.el);
 }.bind(this));
 



[tryton-commits] changeset in sao:5.8 Do not specify mode to load in sao's O2M wi...

2021-02-24 Thread Nicolas Évrard
changeset 376206f6ae41 in sao:5.8
details: https://hg.tryton.org/sao?cmd=changeset;node=376206f6ae41
description:
Do not specify mode to load in sao's O2M widget

issue10080
review335561002
(grafted from 3357e86c8ccd23175f9d5d3b544045dbb14b7fb5)
diffstat:

 src/view/form.js |  2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)

diffs (12 lines):

diff -r 9337ba832d82 -r 376206f6ae41 src/view/form.js
--- a/src/view/form.js  Fri Feb 19 21:23:35 2021 +0100
+++ b/src/view/form.js  Thu Feb 11 11:19:51 2021 +0100
@@ -3034,7 +3034,7 @@
 this.screen.pre_validate = attributes.pre_validate == 1;
 
 this.screen.message_callback = this.record_label.bind(this);
-this.prm = this.screen.switch_view(modes[0]).done(function() {
+this.prm = this.screen.switch_view().done(function() {
 this.content.append(this.screen.screen_container.el);
 }.bind(this));
 



[tryton-commits] changeset in tryton:default Remove useless code in Notebook

2021-03-07 Thread Nicolas Évrard
changeset 09dd58c192e7 in tryton:default
details: https://hg.tryton.org/tryton?cmd=changeset;node=09dd58c192e7
description:
Remove useless code in Notebook

issue10152
review332161002
diffstat:

 tryton/gui/window/view_form/view/form_gtk/state_widget.py |  12 +---
 1 files changed, 1 insertions(+), 11 deletions(-)

diffs (22 lines):

diff -r efa1ec2c4f2b -r 09dd58c192e7 
tryton/gui/window/view_form/view/form_gtk/state_widget.py
--- a/tryton/gui/window/view_form/view/form_gtk/state_widget.py Sun Mar 07 
10:05:33 2021 +0100
+++ b/tryton/gui/window/view_form/view/form_gtk/state_widget.py Sun Mar 07 
15:40:40 2021 +0100
@@ -90,17 +90,7 @@
 
 
 class Notebook(StateMixin, Gtk.Notebook):
-
-def state_set(self, record):
-super(Notebook, self).state_set(record)
-if record:
-state_changes = record.expr_eval(self.attrs.get('states', {}))
-else:
-state_changes = {}
-if state_changes.get('readonly', self.attrs.get('readonly')):
-for widgets in self.widgets.values():
-for widget in widgets:
-widget._readonly_set(True)
+pass
 
 
 class Expander(StateMixin, Gtk.Expander):



[tryton-commits] changeset in trytond:default Call Function.get by bunch of cache...

2021-02-23 Thread Nicolas Évrard
changeset 0afa2a5de45a in trytond:default
details: https://hg.tryton.org/trytond?cmd=changeset;node=0afa2a5de45a
description:
Call Function.get by bunch of cache size

issue9927
review324701002
diffstat:

 trytond/model/modelsql.py  |  14 +-
 trytond/tests/test_modelsql.py |  17 +
 2 files changed, 26 insertions(+), 5 deletions(-)

diffs (58 lines):

diff -r 5a852119fe53 -r 0afa2a5de45a trytond/model/modelsql.py
--- a/trytond/model/modelsql.py Sun Feb 21 22:24:54 2021 +0100
+++ b/trytond/model/modelsql.py Tue Feb 23 18:50:27 2021 +0100
@@ -856,11 +856,15 @@
 date_result = date_results[fname]
 row[fname] = date_result[row['id']]
 else:
-getter_results = field.get(ids, cls, field_list, values=result)
-for fname in field_list:
-getter_result = getter_results[fname]
-for row in result:
-row[fname] = getter_result[row['id']]
+for sub_results in grouped_slice(result, cache_size()):
+sub_results = list(sub_results)
+sub_ids = [r['id'] for r in sub_results]
+getter_results = field.get(
+sub_ids, cls, field_list, values=sub_results)
+for fname in field_list:
+getter_result = getter_results[fname]
+for row in sub_results:
+row[fname] = getter_result[row['id']]
 
 def read_related(field, Target, rows, fields):
 name = field.name
diff -r 5a852119fe53 -r 0afa2a5de45a trytond/tests/test_modelsql.py
--- a/trytond/tests/test_modelsql.pySun Feb 21 22:24:54 2021 +0100
+++ b/trytond/tests/test_modelsql.pyTue Feb 23 18:50:27 2021 +0100
@@ -2,6 +2,7 @@
 # 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 random
 import unittest
 import time
 from unittest.mock import patch, call
@@ -47,6 +48,22 @@
 self.assertEqual(values, [{'id': record.id, 'name': "Record"}])
 
 @with_transaction()
+def test_read_function_field_bigger_than_cache(self):
+"Test reading a Function field on a list bigger then the cache size"
+pool = Pool()
+Model = pool.get('test.modelsql.read')
+
+records = Model.create([{'name': str(i)} for i in range(10)])
+records_created = {m.id: m.name for m in records}
+record_ids = [r.id for r in records]
+random.shuffle(record_ids)
+
+with Transaction().set_context(_record_cache_size=2):
+records_read = {r['id']: r['rec_name']
+for r in Model.read(record_ids, ['rec_name'])}
+self.assertEqual(records_read, records_created)
+
+@with_transaction()
 def test_read_related_2one(self):
 "Test read with related Many2One"
 pool = Pool()



[tryton-commits] changeset in trytond:default Update MPTT only for affected fields

2021-03-05 Thread Nicolas Évrard
changeset f6615c7c6c21 in trytond:default
details: https://hg.tryton.org/trytond?cmd=changeset;node=f6615c7c6c21
description:
Update MPTT only for affected fields

issue10130
review353331002
diffstat:

 trytond/model/modelsql.py |  89 +-
 trytond/model/modelstorage.py |  11 +
 2 files changed, 55 insertions(+), 45 deletions(-)

diffs (151 lines):

diff -r 318efe295241 -r f6615c7c6c21 trytond/model/modelsql.py
--- a/trytond/model/modelsql.py Tue Mar 02 08:38:10 2021 +0100
+++ b/trytond/model/modelsql.py Fri Mar 05 18:22:55 2021 +0100
@@ -1,7 +1,7 @@
 # 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 datetime
-from itertools import islice, chain, product, groupby
+from itertools import islice, chain, product, groupby, repeat
 from collections import OrderedDict, defaultdict
 from functools import wraps
 
@@ -665,8 +665,9 @@
 
 cls._insert_history(new_ids)
 
-field_names = list(cls._fields.keys())
-cls._update_mptt(field_names, [new_ids] * len(field_names))
+if cls._mptt_fields:
+field_names = list(sorted(cls._mptt_fields))
+cls._update_mptt(field_names, repeat(new_ids, len(field_names)))
 
 cls.__check_domain_rule(new_ids, 'create')
 records = cls.browse(new_ids)
@@ -1027,9 +1028,12 @@
 if hasattr(field, 'set'):
 fields_to_set.setdefault(fname, []).extend((ids, value))
 
-field_names = list(values.keys())
-cls._update_mptt(field_names, [ids] * len(field_names), values)
-all_field_names |= set(field_names)
+mptt_fields = cls._mptt_fields & set(values)
+if mptt_fields:
+cls._update_mptt(
+list(sorted(mptt_fields)), repeat(ids, len(mptt_fields)),
+values)
+all_field_names |= set(values)
 
 for fname in sorted(fields_to_set, key=cls.index_set_field):
 fargs = fields_to_set[fname]
@@ -1067,20 +1071,18 @@
 cls.__check_timestamp(ids)
 cls.__check_domain_rule(ids, 'delete')
 
-has_translation = False
 tree_ids = {}
-for fname, field in cls._fields.items():
-if (isinstance(field, fields.Many2One)
-and field.model_name == cls.__name__
-and field.left and field.right):
-tree_ids[fname] = []
-for sub_ids in grouped_slice(ids):
-where = reduce_ids(field.sql_column(table), sub_ids)
-cursor.execute(*table.select(table.id, where=where))
-tree_ids[fname] += [x[0] for x in cursor.fetchall()]
-if (getattr(field, 'translate', False)
-and not hasattr(field, 'set')):
-has_translation = True
+for fname in cls._mptt_fields:
+field = cls._fields[fname]
+tree_ids[fname] = []
+for sub_ids in grouped_slice(ids):
+where = reduce_ids(field.sql_column(table), sub_ids)
+cursor.execute(*table.select(table.id, where=where))
+tree_ids[fname] += [x[0] for x in cursor.fetchall()]
+
+has_translation = any(
+getattr(f, 'translate', False) and not hasattr(f, 'set')
+for f in cls._fields.values())
 
 foreign_keys_tocheck = []
 foreign_keys_toupdate = []
@@ -1463,34 +1465,31 @@
 cursor = Transaction().connection.cursor()
 for field_name, ids in zip(field_names, list_ids):
 field = cls._fields[field_name]
-if (isinstance(field, fields.Many2One)
-and field.model_name == cls.__name__
-and field.left and field.right):
-if (values is not None
-and (field.left in values or field.right in values)):
-raise Exception('ValidateError',
-'You can not update fields: "%s", "%s"' %
-(field.left, field.right))
+if (values is not None
+and (field.left in values or field.right in values)):
+raise Exception('ValidateError',
+'You can not update fields: "%s", "%s"' %
+(field.left, field.right))
 
-# Nested creation require a rebuild
-# because initial values are 0
-# and thus _update_tree can not find the children
-table = cls.__table__()
-parent = cls.__table__()
-cursor.execute(*table.join(parent,
-condition=Column(table, field_name) == parent.id
-).select(table.id,
-where=(Column(parent, field.left) == 0)
-& 

[tryton-commits] changeset in modules/account:default Call super() method of view...

2021-04-09 Thread Nicolas Évrard
changeset 8454816a931c in modules/account:default
details: https://hg.tryton.org/modules/account?cmd=changeset=8454816a931c
description:
Call super() method of view_attributes

issue10151
review349551002
diffstat:

 account.py |  6 +++---
 party.py   |  2 +-
 2 files changed, 4 insertions(+), 4 deletions(-)

diffs (42 lines):

diff -r 100594419a86 -r 8454816a931c account.py
--- a/account.pyMon Apr 05 17:02:37 2021 +0200
+++ b/account.pyFri Apr 09 10:52:03 2021 +0200
@@ -341,7 +341,7 @@
 
 @classmethod
 def view_attributes(cls):
-return [
+return super().view_attributes() + [
 ('/tree/field[@name="amount_cmp"]', 'tree_invisible',
 ~Eval('comparison', False)),
 ]
@@ -2242,7 +2242,7 @@
 
 @classmethod
 def view_attributes(cls):
-return [
+return super().view_attributes() + [
 ('/form/separator[@id="comparison"]', 'states', {
 'invisible': ~Eval('comparison', False),
 }),
@@ -2360,7 +2360,7 @@
 
 @classmethod
 def view_attributes(cls):
-return [
+return super().view_attributes() + [
 ('/form/separator[@id="comparison"]', 'states', {
 'invisible': ~Eval('comparison', False),
 }),
diff -r 100594419a86 -r 8454816a931c party.py
--- a/party.py  Mon Apr 05 17:02:37 2021 +0200
+++ b/party.py  Fri Apr 09 10:52:03 2021 +0200
@@ -270,7 +270,7 @@
 
 @classmethod
 def view_attributes(cls):
-return [
+return super().view_attributes() + [
 ('/tree/field[@name="receivable_today"]',
 'visual', If(Eval('receivable_today', 0) > 0, 'danger', '')),
 ('/tree/field[@name="payable_today"]',



[tryton-commits] changeset in modules/marketing_automation:default Call super() m...

2021-04-09 Thread Nicolas Évrard
changeset 5b63ac56f903 in modules/marketing_automation:default
details: 
https://hg.tryton.org/modules/marketing_automation?cmd=changeset=5b63ac56f903
description:
Call super() method of view_attributes

issue10151
review349551002
diffstat:

 marketing_automation.py |  2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)

diffs (12 lines):

diff -r 877a827c069c -r 5b63ac56f903 marketing_automation.py
--- a/marketing_automation.py   Sun Mar 21 16:09:13 2021 +0100
+++ b/marketing_automation.py   Fri Apr 09 10:52:03 2021 +0200
@@ -362,7 +362,7 @@
 
 @classmethod
 def view_attributes(cls):
-return [
+return super().view_attributes() + [
 ('//group[@id="email"]', 'states', {
 'invisible': Eval('action') != 'send_email',
 }),



[tryton-commits] changeset in modules/account_invoice:default Call super() method...

2021-04-09 Thread Nicolas Évrard
changeset 7e707a5468bd in modules/account_invoice:default
details: 
https://hg.tryton.org/modules/account_invoice?cmd=changeset=7e707a5468bd
description:
Call super() method of view_attributes

issue10151
review349551002
diffstat:

 invoice.py |  4 ++--
 1 files changed, 2 insertions(+), 2 deletions(-)

diffs (21 lines):

diff -r f73cfaad3dac -r 7e707a5468bd invoice.py
--- a/invoice.pySat Apr 03 14:27:10 2021 +0200
+++ b/invoice.pyFri Apr 09 10:52:03 2021 +0200
@@ -1189,7 +1189,7 @@
 
 @classmethod
 def view_attributes(cls):
-return [
+return super().view_attributes() + [
 ('/form//field[@name="comment"]', 'spell', Eval('party_lang')),
 ('/tree', 'visual',
 If((
@@ -2229,7 +2229,7 @@
 
 @classmethod
 def view_attributes(cls):
-return [
+return super().view_attributes() + [
 ('/form//field[@name="note"]|/form//field[@name="description"]',
 'spell', Eval('party_lang'))]
 



[tryton-commits] changeset in modules/account_statement:default Call super() meth...

2021-04-09 Thread Nicolas Évrard
changeset c4db2f037591 in modules/account_statement:default
details: 
https://hg.tryton.org/modules/account_statement?cmd=changeset=c4db2f037591
description:
Call super() method of view_attributes

issue10151
review349551002
diffstat:

 statement.py |  2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)

diffs (12 lines):

diff -r fdd9e3568577 -r c4db2f037591 statement.py
--- a/statement.py  Mon Apr 05 17:48:14 2021 +0200
+++ b/statement.py  Fri Apr 09 10:52:03 2021 +0200
@@ -424,7 +424,7 @@
 
 @classmethod
 def view_attributes(cls):
-return [
+return super().view_attributes() + [
 ('/tree', 'visual', If(Eval('state') == 'cancelled', 'muted', '')),
 ]
 



[tryton-commits] changeset in modules/account_statement_rule:default Call super()...

2021-04-09 Thread Nicolas Évrard
changeset fb1c71e09d67 in modules/account_statement_rule:default
details: 
https://hg.tryton.org/modules/account_statement_rule?cmd=changeset=fb1c71e09d67
description:
Call super() method of view_attributes

issue10151
review349551002
diffstat:

 account.py |  2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)

diffs (12 lines):

diff -r 2ca0aca64521 -r fb1c71e09d67 account.py
--- a/account.pySat Mar 13 12:27:47 2021 +0100
+++ b/account.pyFri Apr 09 10:52:03 2021 +0200
@@ -202,7 +202,7 @@
 
 @classmethod
 def view_attributes(cls):
-return [
+return super().view_attributes() + [
 ('//group[@id="%s"]' % type_, 'states', {
 'invisible': Eval('key_type') != type_,
 }) for type_ in ['integer', 'float', 'number']]



[tryton-commits] changeset in modules/purchase:default Call super() method of vie...

2021-04-09 Thread Nicolas Évrard
changeset d04a7d920667 in modules/purchase:default
details: https://hg.tryton.org/modules/purchase?cmd=changeset=d04a7d920667
description:
Call super() method of view_attributes

issue10151
review349551002
diffstat:

 purchase.py |  4 ++--
 1 files changed, 2 insertions(+), 2 deletions(-)

diffs (21 lines):

diff -r 547822f452ce -r d04a7d920667 purchase.py
--- a/purchase.py   Mon Apr 05 16:24:13 2021 +0200
+++ b/purchase.py   Fri Apr 09 10:52:03 2021 +0200
@@ -696,7 +696,7 @@
 
 @classmethod
 def view_attributes(cls):
-attributes = [
+attributes = super().view_attributes() + [
 ('/form//field[@name="comment"]', 'spell', Eval('party_lang')),
 ('/tree', 'visual', If(Eval('state') == 'cancelled', 'muted', '')),
 ('/tree/field[@name="invoice_state"]', 'visual',
@@ -1731,7 +1731,7 @@
 
 @classmethod
 def view_attributes(cls):
-return [
+return super().view_attributes() + [
 ('/form//field[@name="note"]|/form//field[@name="description"]',
 'spell', Eval('_parent_purchase', {}).get('party_lang')),
 ('//label[@id="delivery_date"]', 'states', {



[tryton-commits] changeset in modules/purchase_request:default Call super() metho...

2021-04-09 Thread Nicolas Évrard
changeset bf31be74debc in modules/purchase_request:default
details: 
https://hg.tryton.org/modules/purchase_request?cmd=changeset=bf31be74debc
description:
Call super() method of view_attributes

issue10151
review349551002
diffstat:

 purchase_request.py |  2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)

diffs (12 lines):

diff -r 79249d1938e4 -r bf31be74debc purchase_request.py
--- a/purchase_request.py   Mon Apr 05 16:24:13 2021 +0200
+++ b/purchase_request.py   Fri Apr 09 10:52:03 2021 +0200
@@ -310,7 +310,7 @@
 
 @classmethod
 def view_attributes(cls):
-return [
+return super().view_attributes() + [
 ('/tree', 'visual', If(Eval('state') == 'cancelled', 'muted', '')),
 ]
 



[tryton-commits] changeset in modules/sale_complaint:default Call super() method ...

2021-04-09 Thread Nicolas Évrard
changeset 78038eb7c59b in modules/sale_complaint:default
details: 
https://hg.tryton.org/modules/sale_complaint?cmd=changeset=78038eb7c59b
description:
Call super() method of view_attributes

issue10151
review349551002
diffstat:

 complaint.py |  2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)

diffs (12 lines):

diff -r 27fdcf6bc98b -r 78038eb7c59b complaint.py
--- a/complaint.py  Sat Apr 03 14:27:10 2021 +0200
+++ b/complaint.py  Fri Apr 09 10:52:03 2021 +0200
@@ -249,7 +249,7 @@
 
 @classmethod
 def view_attributes(cls):
-return [
+return super().view_attributes() + [
 ('/tree', 'visual', If(Eval('state') == 'cancelled', 'muted', '')),
 ]
 



[tryton-commits] changeset in modules/sale:default Call super() method of view_at...

2021-04-09 Thread Nicolas Évrard
changeset 0c8a414e80c8 in modules/sale:default
details: https://hg.tryton.org/modules/sale?cmd=changeset=0c8a414e80c8
description:
Call super() method of view_attributes

issue10151
review349551002
diffstat:

 sale.py |  4 ++--
 1 files changed, 2 insertions(+), 2 deletions(-)

diffs (21 lines):

diff -r f51789ea2880 -r 0c8a414e80c8 sale.py
--- a/sale.py   Mon Apr 05 16:24:13 2021 +0200
+++ b/sale.py   Fri Apr 09 10:52:03 2021 +0200
@@ -702,7 +702,7 @@
 
 @classmethod
 def view_attributes(cls):
-attributes = [
+attributes = super().view_attributes() + [
 ('/form//field[@name="comment"]', 'spell', Eval('party_lang')),
 ('/tree', 'visual', If(Eval('state') == 'cancelled', 'muted', '')),
 ('/tree/field[@name="invoice_state"]', 'visual',
@@ -1715,7 +1715,7 @@
 
 @classmethod
 def view_attributes(cls):
-return [
+return super().view_attributes() + [
 ('/form//field[@name="note"]|/form//field[@name="description"]',
 'spell', Eval('_parent_sale', {}).get('party_lang'))]
 



[tryton-commits] changeset in modules/purchase_requisition:default Call super() m...

2021-04-09 Thread Nicolas Évrard
changeset 25ac2a496d23 in modules/purchase_requisition:default
details: 
https://hg.tryton.org/modules/purchase_requisition?cmd=changeset=25ac2a496d23
description:
Call super() method of view_attributes

issue10151
review349551002
diffstat:

 purchase.py |  2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)

diffs (12 lines):

diff -r dde7ed22e861 -r 25ac2a496d23 purchase.py
--- a/purchase.py   Mon Apr 05 16:24:13 2021 +0200
+++ b/purchase.py   Fri Apr 09 10:52:03 2021 +0200
@@ -315,7 +315,7 @@
 
 @classmethod
 def view_attributes(cls):
-return [
+return super().view_attributes() + [
 ('/tree', 'visual',
 If(Eval('state') == 'cancelled', 'muted', '')),
 ]



[tryton-commits] changeset in modules/sale_opportunity:default Call super() metho...

2021-04-09 Thread Nicolas Évrard
changeset bbd25a079f89 in modules/sale_opportunity:default
details: 
https://hg.tryton.org/modules/sale_opportunity?cmd=changeset=bbd25a079f89
description:
Call super() method of view_attributes

issue10151
review349551002
diffstat:

 opportunity.py |  2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)

diffs (12 lines):

diff -r 780803562c5b -r bbd25a079f89 opportunity.py
--- a/opportunity.pyMon Apr 05 16:24:13 2021 +0200
+++ b/opportunity.pyFri Apr 09 10:52:03 2021 +0200
@@ -253,7 +253,7 @@
 
 @classmethod
 def view_attributes(cls):
-return [
+return super().view_attributes() + [
 ('/tree', 'visual', If(Eval('state') == 'cancelled', 'muted', '')),
 ]
 



[tryton-commits] changeset in modules/sale_subscription:default Call super() meth...

2021-04-09 Thread Nicolas Évrard
changeset bd509929e911 in modules/sale_subscription:default
details: 
https://hg.tryton.org/modules/sale_subscription?cmd=changeset=bd509929e911
description:
Call super() method of view_attributes

issue10151
review349551002
diffstat:

 subscription.py |  2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)

diffs (12 lines):

diff -r 95d455d8e327 -r bd509929e911 subscription.py
--- a/subscription.py   Mon Apr 05 16:24:13 2021 +0200
+++ b/subscription.py   Fri Apr 09 10:52:03 2021 +0200
@@ -309,7 +309,7 @@
 
 @classmethod
 def view_attributes(cls):
-return [
+return super().view_attributes() + [
 ('/tree', 'visual',
 If(Eval('state') == 'cancelled', 'muted', '')),
 ]



[tryton-commits] changeset in modules/stock:default Call super() method of view_a...

2021-04-09 Thread Nicolas Évrard
changeset 1f805acff803 in modules/stock:default
details: https://hg.tryton.org/modules/stock?cmd=changeset=1f805acff803
description:
Call super() method of view_attributes

issue10151
review349551002
diffstat:

 inventory.py |  2 +-
 move.py  |  2 +-
 shipment.py  |  2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diffs (36 lines):

diff -r 31d952c5014c -r 1f805acff803 inventory.py
--- a/inventory.py  Thu Apr 08 15:47:35 2021 +0200
+++ b/inventory.py  Fri Apr 09 10:52:03 2021 +0200
@@ -129,7 +129,7 @@
 
 @classmethod
 def view_attributes(cls):
-return [
+return super().view_attributes() + [
 ('/tree', 'visual', If(Eval('state') == 'cancelled', 'muted', '')),
 ]
 
diff -r 31d952c5014c -r 1f805acff803 move.py
--- a/move.py   Thu Apr 08 15:47:35 2021 +0200
+++ b/move.py   Fri Apr 09 10:52:03 2021 +0200
@@ -600,7 +600,7 @@
 
 @classmethod
 def view_attributes(cls):
-return [
+return super().view_attributes() + [
 ('/tree', 'visual', If(Eval('state') == 'cancelled', 'muted', '')),
 ]
 
diff -r 31d952c5014c -r 1f805acff803 shipment.py
--- a/shipment.py   Thu Apr 08 15:47:35 2021 +0200
+++ b/shipment.py   Fri Apr 09 10:52:03 2021 +0200
@@ -24,7 +24,7 @@
 
 @classmethod
 def view_attributes(cls):
-return [
+return super().view_attributes() + [
 ('/tree', 'visual', If(Eval('state') == 'cancelled', 'muted', '')),
 ]
 



[tryton-commits] changeset in modules/account:default Use subquery to gain perfor...

2021-04-12 Thread Nicolas Évrard
changeset ac039369474a in modules/account:default
details: https://hg.tryton.org/modules/account?cmd=changeset=ac039369474a
description:
Use subquery to gain performance in account.account.party

issue10216
review353411002
diffstat:

 account.py |  21 +++--
 move.py|   8 ++--
 2 files changed, 17 insertions(+), 12 deletions(-)

diffs (59 lines):

diff -r 1ce785a9f7c3 -r ac039369474a account.py
--- a/account.pySun Apr 11 20:51:49 2021 +0200
+++ b/account.pyMon Apr 12 20:48:57 2021 +0200
@@ -1321,25 +1321,26 @@
 Account = pool.get('account.account')
 line = Line.__table__()
 account = Account.__table__()
+
+account_party = line.select(
+Min(line.id).as_('id'), line.account, line.party,
+where=line.party != Null,
+group_by=[line.account, line.party])
+
 columns = []
-group_by = []
 for fname, field in cls._fields.items():
 if not hasattr(field, 'set'):
-if fname == 'id':
-column = Min(line.id)
-elif fname in {'account', 'party'}:
-column = Column(line, fname)
+if fname in {'id', 'account', 'party'}:
+column = Column(account_party, fname)
 else:
 column = Column(account, fname)
 columns.append(column.as_(fname))
-if fname != 'id':
-group_by.append(column)
 return (
-line.join(account, condition=line.account == account.id)
+account_party.join(
+account, condition=account_party.account == account.id)
 .select(
 *columns,
-where=(line.party != Null) & account.party_required,
-group_by=group_by))
+where=account.party_required))
 
 @classmethod
 def get_balance(cls, records, name):
diff -r 1ce785a9f7c3 -r ac039369474a move.py
--- a/move.py   Sun Apr 11 20:51:49 2021 +0200
+++ b/move.py   Mon Apr 12 20:48:57 2021 +0200
@@ -768,9 +768,13 @@
 def __register__(cls, module_name):
 super(Line, cls).__register__(module_name)
 
-table = cls.__table_handler__(module_name)
+table = cls.__table__()
+table_h = cls.__table_handler__(module_name)
 # Index for General Ledger
-table.index_action(['move', 'account'], 'add')
+table_h.index_action(['move', 'account'], 'add')
+# Index for account.account.party
+table_h.index_action(
+['party', 'account', 'id'], 'add', where=table.party != Null)
 
 @classmethod
 def default_date(cls):



[tryton-commits] changeset in tryton:default Use specific search domain for refer...

2021-04-12 Thread Nicolas Évrard
changeset 558aeef898cd in tryton:default
details: https://hg.tryton.org/tryton?cmd=changeset=558aeef898cd
description:
Use specific search domain for reference field searches

issue9997
review324781002
diffstat:

 tryton/common/domain_inversion.py  |  57 ++---
 tryton/gui/window/view_form/model/field.py |   3 +-
 2 files changed, 53 insertions(+), 7 deletions(-)

diffs (86 lines):

diff -r e440811334aa -r 558aeef898cd tryton/common/domain_inversion.py
--- a/tryton/common/domain_inversion.py Sun Apr 11 18:28:37 2021 +0200
+++ b/tryton/common/domain_inversion.py Mon Apr 12 20:54:03 2021 +0200
@@ -170,15 +170,60 @@
 
 def prepare_reference_domain(domain, reference):
 "convert domain to replace reference fields by their local part"
+
+def value2reference(value):
+model, ref_id = None, None
+if isinstance(value, str) and ',' in value:
+model, ref_id = value.split(',', 1)
+if ref_id != '%':
+try:
+ref_id = int(ref_id)
+except ValueError:
+model, ref_id = None, value
+elif (isinstance(value, (list, tuple))
+and len(value) == 2
+and isinstance(value[0], str)
+and (isinstance(value[1], int) or value[1] == '%')):
+model, ref_id = value
+else:
+ref_id = value
+return model, ref_id
+
 if domain in ('AND', 'OR'):
 return domain
 elif is_leaf(domain):
-# When a Reference field is using the dotted notation the model
-# specified must be removed from the clause
-if domain[0].count('.') and len(domain) > 3:
-local_name, target_name = domain[0].split('.', 1)
-if local_name == reference:
-return [target_name] + list(domain[1:3] + domain[4:])
+if domain[0] == reference:
+if domain[1] in {'=', '!='}:
+model, ref_id = value2reference(domain[2])
+if model is not None:
+if ref_id == '%':
+if domain[1] == '=':
+return [reference + '.id', '!=', None, model]
+else:
+return [reference, 'not like', domain[2]]
+return [reference + '.id', domain[1], ref_id, model]
+elif domain[1] in {'in', 'not in'}:
+model_values = {}
+for value in domain[2]:
+model, ref_id = value2reference(value)
+if model is None:
+break
+model_values.setdefault(model, []).append(ref_id)
+else:
+new_domain = ['OR'] if domain[1] == 'in' else ['AND']
+for model, ref_ids in model_values.items():
+if '%' in ref_ids:
+if domain[1] == 'in':
+new_domain.append(
+[reference + '.id', '!=', None, model])
+else:
+new_domain.append(
+[reference, 'not like', model + ',%'])
+else:
+new_domain.append(
+[reference + '.id', domain[1], ref_ids, model])
+return new_domain
+return []
 return domain
 else:
 return [prepare_reference_domain(d, reference) for d in domain]
diff -r e440811334aa -r 558aeef898cd tryton/gui/window/view_form/model/field.py
--- a/tryton/gui/window/view_form/model/field.pySun Apr 11 18:28:37 
2021 +0200
+++ b/tryton/gui/window/view_form/model/field.pyMon Apr 12 20:54:03 
2021 +0200
@@ -971,10 +971,11 @@
 screen_domain = filter_leaf(screen_domain, self.name, model)
 screen_domain = prepare_reference_domain(screen_domain, self.name)
 return concat(localize_domain(
-screen_domain, strip_target=True), attr_domain)
+screen_domain, self.name, strip_target=True), attr_domain)
 
 def get_models(self, record):
 screen_domain, attr_domain = self.domains_get(record)
+screen_domain = prepare_reference_domain(screen_domain, self.name)
 return extract_reference_models(
 concat(screen_domain, attr_domain), self.name)
 



[tryton-commits] changeset in trytond:default Use specific search domain for refe...

2021-04-12 Thread Nicolas Évrard
changeset ee23aca64b26 in trytond:default
details: https://hg.tryton.org/trytond?cmd=changeset=ee23aca64b26
description:
Use specific search domain for reference field searches

issue9997
review324781002
diffstat:

 trytond/tests/test_tools.py   |  59 --
 trytond/tools/domain_inversion.py |  57 +---
 2 files changed, 106 insertions(+), 10 deletions(-)

diffs (145 lines):

diff -r dd0bb6655e6d -r ee23aca64b26 trytond/tests/test_tools.py
--- a/trytond/tests/test_tools.py   Mon Apr 12 20:39:23 2021 +0200
+++ b/trytond/tests/test_tools.py   Mon Apr 12 20:54:03 2021 +0200
@@ -739,16 +739,67 @@
 domain = [['x', 'like', 'A%']]
 self.assertEqual(
 prepare_reference_domain(domain, 'x'),
-[['x', 'like', 'A%']])
+[[]])
 
-domain = [['x.y', 'like', 'A%', 'model']]
+domain = [['x', '=', 'A']]
 self.assertEqual(
-prepare_reference_domain(domain, 'x'), [['y', 'like', 'A%']])
+prepare_reference_domain(domain, 'x'),
+[[]])
 
 domain = [['x.y', 'child_of', [1], 'model', 'parent']]
 self.assertEqual(
 prepare_reference_domain(domain, 'x'),
-[['y', 'child_of', [1], 'parent']])
+[['x.y', 'child_of', [1], 'model', 'parent']])
+
+domain = [['x.y', 'like', 'A%', 'model']]
+self.assertEqual(
+prepare_reference_domain(domain, 'x'),
+[['x.y', 'like', 'A%', 'model']])
+
+domain = [['x', '=', 'model,1']]
+self.assertEqual(
+prepare_reference_domain(domain, 'x'),
+[['x.id', '=', 1, 'model']])
+
+domain = [['x', '!=', 'model,1']]
+self.assertEqual(
+prepare_reference_domain(domain, 'x'),
+[['x.id', '!=', 1, 'model']])
+
+domain = [['x', '=', 'model,%']]
+self.assertEqual(
+prepare_reference_domain(domain, 'x'),
+[['x.id', '!=', None, 'model']])
+
+domain = [['x', '!=', 'model,%']]
+self.assertEqual(
+prepare_reference_domain(domain, 'x'),
+[['x', 'not like', 'model,%']])
+
+domain = [['x', 'in',
+['model_a,1', 'model_b,%', 'model_c,3', 'model_a,2']]]
+self.assertEqual(
+prepare_reference_domain(domain, 'x'),
+[['OR',
+['x.id', 'in', [1, 2], 'model_a'],
+['x.id', '!=', None, 'model_b'],
+['x.id', 'in', [3], 'model_c'],
+]])
+
+domain = [['x', 'not in',
+['model_a,1', 'model_b,%', 'model_c,3', 'model_a,2']]]
+self.assertEqual(
+prepare_reference_domain(domain, 'x'),
+[['AND',
+['x.id', 'not in', [1, 2], 'model_a'],
+['x', 'not like', 'model_b,%'],
+['x.id', 'not in', [3], 'model_c'],
+]])
+
+domain = [['x', 'in', ['model_a,1', 'foo']]]
+self.assertEqual(
+prepare_reference_domain(domain, 'x'),
+[[]])
 
 def test_extract_models(self):
 domain = [['x', 'like', 'A%']]
diff -r dd0bb6655e6d -r ee23aca64b26 trytond/tools/domain_inversion.py
--- a/trytond/tools/domain_inversion.py Mon Apr 12 20:39:23 2021 +0200
+++ b/trytond/tools/domain_inversion.py Mon Apr 12 20:54:03 2021 +0200
@@ -170,15 +170,60 @@
 
 def prepare_reference_domain(domain, reference):
 "convert domain to replace reference fields by their local part"
+
+def value2reference(value):
+model, ref_id = None, None
+if isinstance(value, str) and ',' in value:
+model, ref_id = value.split(',', 1)
+if ref_id != '%':
+try:
+ref_id = int(ref_id)
+except ValueError:
+model, ref_id = None, value
+elif (isinstance(value, (list, tuple))
+and len(value) == 2
+and isinstance(value[0], str)
+and (isinstance(value[1], int) or value[1] == '%')):
+model, ref_id = value
+else:
+ref_id = value
+return model, ref_id
+
 if domain in ('AND', 'OR'):
 return domain
 elif is_leaf(domain):
-# When a Reference field is using the dotted notation the model
-# specified must be removed from the clause
-if domain[0].count('.') and len(domain) > 3:
-local_name, target_name = domain[0].split('.', 1)
-if local_name == reference:
-return [target_name] + list(domain[1:3] + domain[4:])
+if domain[0] == reference:
+if domain[1] in {'=', '!='}:
+model, ref_id = value2reference(domain[2])
+if model is not None:
+if ref_id == '%':
+if domain[1] == '=':
+return 

[tryton-commits] changeset in sao:default Use specific search domain for referenc...

2021-04-12 Thread Nicolas Évrard
changeset eab088332684 in sao:default
details: https://hg.tryton.org/sao?cmd=changeset=eab088332684
description:
Use specific search domain for reference field searches

issue9997
review324781002
diffstat:

 src/common.js |  105 +++--
 src/model.js  |6 ++-
 tests/sao.js  |   58 +--
 3 files changed, 151 insertions(+), 18 deletions(-)

diffs (217 lines):

diff -r 8058db617181 -r eab088332684 src/common.js
--- a/src/common.js Sun Apr 11 18:32:18 2021 +0200
+++ b/src/common.js Mon Apr 12 20:54:03 2021 +0200
@@ -2371,23 +2371,104 @@
 }
 },
 prepare_reference_domain: function(domain, reference) {
+
+var value2reference = function(value) {
+var model = null;
+var ref_id = null;
+if ((typeof(value) == 'string') && value.contains(',')) {
+var split = value.split(',');
+var result = split.splice(0, 1);
+result.push(split.join(','));
+model = result[0];
+ref_id = result[1];
+if (ref_id != '%') {
+ref_id = parseInt(ref_id, 10);
+if (isNaN(ref_id)) {
+model = null;
+ref_id = value;
+}
+}
+} else if ((value instanceof Array) &&
+(value.length == 2) &&
+(typeof(value[0]) == 'string') &&
+((typeof(value[1]) == 'number') ||
+(value[1] == '%'))) {
+model = value[0];
+ref_id = value[1];
+} else {
+ref_id = value;
+}
+return [model, ref_id];
+};
+
 if (~['AND', 'OR'].indexOf(domain)) {
 return domain;
 } else if (this.is_leaf(domain)) {
-if ((domain[0].split('.').length > 1) &&
-(domain.length > 3)) {
-var parts = domain[0].split('.');
-var local_name = parts[0];
-var target_name = parts.slice(1).join('.');
+if (domain[0] == reference) {
+var model, ref_id, splitted;
+if ((domain[1] == '=') || (domain[1] ==  '!=')) {
+splitted = value2reference(domain[2]);
+model = splitted[0];
+ref_id = splitted[1];
+if (model) {
+if (ref_id == '%') {
+if (domain[1] == '=') {
+return [
+reference + '.id', '!=', null, model];
+} else {
+return [reference, 'not like', domain[2]];
+}
+}
+return [
+reference + '.id', domain[1], ref_id, model];
+}
+} else if ((domain[1] == 'in') || (domain[1] == 'not in')) 
{
+var model_values = {};
+var break_p = false;
+for (var i=0; i < domain[2].length; i++) {
+splitted = value2reference(domain[2][i]);
+model = splitted[0];
+ref_id = splitted[1];
+if (!model) {
+break_p = true;
+break;
+}
+if (!(model in model_values)) {
+model_values[model] = [];
+}
+model_values[model].push(ref_id);
+}
 
-if (local_name == reference) {
-var where = [];
-where.push(target_name);
-where = where.concat(
-domain.slice(1, 3), domain.slice(4));
-return where;
+if (!break_p) {
+var ref_ids;
+var new_domain;
+if (domain[1] == 'in') {
+new_domain = ['OR'];
+} else {
+new_domain = ['AND'];
+}
+for (model in model_values) {
+ref_ids = model_values[model];
+if (~ref_ids.indexOf('%')) {
+if 

[tryton-commits] changeset in trytond:default Add support for database connection...

2021-04-12 Thread Nicolas Évrard
changeset dd0bb6655e6d in trytond:default
details: https://hg.tryton.org/trytond?cmd=changeset=dd0bb6655e6d
description:
Add support for database connection parameters to configuration URI

issue10101
review339891002
diffstat:

 CHANGELOG  |   1 +
 doc/topics/configuration.rst   |  21 --
 trytond/backend/postgresql/database.py |  14 +++-
 trytond/backend/sqlite/database.py |  37 +
 4 files changed, 51 insertions(+), 22 deletions(-)

diffs (159 lines):

diff -r 36ce819e336a -r dd0bb6655e6d CHANGELOG
--- a/CHANGELOG Mon Apr 12 20:16:41 2021 +0200
+++ b/CHANGELOG Mon Apr 12 20:39:23 2021 +0200
@@ -1,3 +1,4 @@
+* Add support for database connection parameters to configuration URI
 * Use immutable datastructures for Dict and MultiSelection fields
 * Skip warnings for non-interactive operations
 * Check rule only if _check_access is set
diff -r 36ce819e336a -r dd0bb6655e6d doc/topics/configuration.rst
--- a/doc/topics/configuration.rst  Mon Apr 12 20:16:41 2021 +0200
+++ b/doc/topics/configuration.rst  Mon Apr 12 20:39:23 2021 +0200
@@ -106,7 +106,10 @@
 Contains the URI to connect to the SQL database. The URI follows the RFC-3986_.
 The typical form is:
 
-database://username:password@host:port/
+database://username:password@host:port/?param1=value1=value2
+
+The parameters are database dependent, check the database documentation for a
+list of valid parameters.
 
 Default: The value of the environment variable ``TRYTOND_DATABASE_URI`` or
 ``sqlite://`` if not set.
@@ -116,15 +119,27 @@
 PostgreSQL
 **
 
-``pyscopg2`` supports two type of connections:
+``psycopg2`` supports two type of connections:
 
 - TCP/IP connection: ``postgresql://user:password@localhost:5432/``
 - Unix domain connection: ``postgresql://username:password@/``
 
+Please refer to `psycopg2 for the complete specification of the URI
+`_.
+
+A list of parameters supported by PostgreSQL can be found in the
+`documentation 
`_.
+
 SQLite
 **
 
-The only possible URI is: ``sqlite://``
+The URI is defined as ``sqlite://``
+
+If the name of the database is ``:memory:``, the parameter ``mode`` will be set
+to ``memory`` thus using a pure in-memory database.
+
+The recognized query parameters can be found in SQLite's
+`documentation `_.
 
 path
 
diff -r 36ce819e336a -r dd0bb6655e6d trytond/backend/postgresql/database.py
--- a/trytond/backend/postgresql/database.pyMon Apr 12 20:16:41 2021 +0200
+++ b/trytond/backend/postgresql/database.pyMon Apr 12 20:39:23 2021 +0200
@@ -4,8 +4,8 @@
 import time
 import logging
 import os
-import urllib.parse
 import json
+import warnings
 from datetime import datetime
 from decimal import Decimal
 from itertools import chain, repeat
@@ -243,17 +243,11 @@
 @classmethod
 def _connection_params(cls, name):
 uri = parse_uri(config.get('database', 'uri'))
+if uri.path:
+warnings.warn("The path specified in the URI will be overridden")
 params = {
-'dbname': name,
+'dsn': uri._replace(path=name).geturl(),
 }
-if uri.username:
-params['user'] = uri.username
-if uri.password:
-params['password'] = urllib.parse.unquote_plus(uri.password)
-if uri.hostname:
-params['host'] = uri.hostname
-if uri.port:
-params['port'] = uri.port
 return params
 
 def connect(self):
diff -r 36ce819e336a -r dd0bb6655e6d trytond/backend/sqlite/database.py
--- a/trytond/backend/sqlite/database.pyMon Apr 12 20:16:41 2021 +0200
+++ b/trytond/backend/sqlite/database.pyMon Apr 12 20:39:23 2021 +0200
@@ -7,8 +7,11 @@
 import random
 import threading
 import time
+import urllib.parse
+import warnings
 from decimal import Decimal
 from weakref import WeakKeyDictionary
+from werkzeug.security import safe_join
 
 try:
 from pysqlite2 import dbapi2 as sqlite
@@ -24,7 +27,7 @@
 Overlay, CharLength, CurrentTimestamp, Trim)
 
 from trytond.backend.database import DatabaseInterface, SQLType
-from trytond.config import config
+from trytond.config import config, parse_uri
 from trytond.transaction import Transaction
 
 __all__ = ['Database', 'DatabaseIntegrityError', 'DatabaseOperationalError']
@@ -336,16 +339,10 @@
 Database._local.memory_database = self
 
 def connect(self):
-if self.name == ':memory:':
-path = ':memory:'
-else:
-db_filename = self.name + '.sqlite'
-path = os.path.join(config.get('database', 'path'), db_filename)
-if not os.path.isfile(path):
-raise IOError('Database "%s" doesn\'t exist!' % path)
 

[tryton-commits] changeset in modules/account_invoice:default Allow column sql ty...

2021-04-12 Thread Nicolas Évrard
changeset 6675d0caa210 in modules/account_invoice:default
details: 
https://hg.tryton.org/modules/account_invoice?cmd=changeset=6675d0caa210
description:
Allow column sql types to be tested from the table handler

issue9645
review312421002
diffstat:

 payment_term.py |  17 -
 1 files changed, 4 insertions(+), 13 deletions(-)

diffs (35 lines):

diff -r b33b6e913b27 -r 6675d0caa210 payment_term.py
--- a/payment_term.py   Sun Apr 11 20:52:21 2021 +0200
+++ b/payment_term.py   Mon Apr 12 20:56:08 2021 +0200
@@ -267,6 +267,7 @@
 line = Line.__table__()
 month = Month.__table__()
 day = Day.__table__()
+table_h = cls.__table_handler__(module_name)
 
 # Migration from 4.0: rename long table
 old_model_name = 'account.invoice.payment_term.line.relativedelta'
@@ -278,20 +279,10 @@
 # Migration from 5.0: use ir.calendar
 migrate_calendar = False
 if backend.TableHandler.table_exist(cls._table):
-cursor.execute(*sql_table.select(
-sql_table.month, sql_table.weekday,
-where=(sql_table.month != Null)
-| (sql_table.weekday != Null),
-limit=1))
-try:
-row, = cursor.fetchall()
-migrate_calendar = any(isinstance(v, str) for v in row)
-except ValueError:
-# As we cannot know the column type
-# we migrate any way as no data need to be migrated
-migrate_calendar = True
+migrate_calendar = (
+(table_h.column_is_type('month', 'VARCHAR')
+or (table_h.column_is_type('weekday', 'VARCHAR'))
 if migrate_calendar:
-table_h = cls.__table_handler__(module_name)
 table_h.column_rename('month', '_temp_month')
 table_h.column_rename('weekday', '_temp_weekday')
 



[tryton-commits] changeset in trytond:default Allow column sql types to be tested...

2021-04-12 Thread Nicolas Évrard
changeset e775cf82255f in trytond:default
details: https://hg.tryton.org/trytond?cmd=changeset=e775cf82255f
description:
Allow column sql types to be tested from the table handler

issue9645
review312421002
diffstat:

 CHANGELOG   |   1 +
 trytond/backend/postgresql/table.py |  12 
 trytond/backend/sqlite/table.py |  14 +-
 trytond/backend/table.py|  12 
 4 files changed, 38 insertions(+), 1 deletions(-)

diffs (83 lines):

diff -r ee23aca64b26 -r e775cf82255f CHANGELOG
--- a/CHANGELOG Mon Apr 12 20:54:03 2021 +0200
+++ b/CHANGELOG Mon Apr 12 20:56:08 2021 +0200
@@ -1,3 +1,4 @@
+* Allow column sql types to be tested from the table handler
 * Add support for database connection parameters to configuration URI
 * Use immutable datastructures for Dict and MultiSelection fields
 * Skip warnings for non-interactive operations
diff -r ee23aca64b26 -r e775cf82255f trytond/backend/postgresql/table.py
--- a/trytond/backend/postgresql/table.py   Mon Apr 12 20:54:03 2021 +0200
+++ b/trytond/backend/postgresql/table.py   Mon Apr 12 20:56:08 2021 +0200
@@ -221,6 +221,18 @@
 'ALTER "' + column_name + '" TYPE ' + column_type)
 self._update_definitions(columns=True)
 
+def column_is_type(self, column_name, type_, *, size=-1):
+db_type = self._columns[column_name]['typname'].upper()
+
+database = Transaction().database
+base_type = database.sql_type(type_).base.upper()
+if base_type == 'VARCHAR':
+same_size = self._columns[column_name]['size'] == size
+else:
+same_size = True
+
+return base_type == db_type and same_size
+
 def db_default(self, column_name, value):
 if value in [True, False]:
 test = str(value).lower()
diff -r ee23aca64b26 -r e775cf82255f trytond/backend/sqlite/table.py
--- a/trytond/backend/sqlite/table.py   Mon Apr 12 20:54:03 2021 +0200
+++ b/trytond/backend/sqlite/table.py   Mon Apr 12 20:56:08 2021 +0200
@@ -138,7 +138,7 @@
 size = match.group(3) and int(match.group(3)) or 0
 else:
 typname = type_.upper()
-size = -1
+size = None
 self._columns[column] = {
 'notnull': notnull,
 'hasdef': hasdef,
@@ -170,6 +170,18 @@
 def alter_type(self, column_name, column_type):
 self._recreate_table({column_name: {'typname': column_type}})
 
+def column_is_type(self, column_name, type_, *, size=-1):
+db_type = self._columns[column_name]['typname'].upper()
+
+database = Transaction().database
+base_type = database.sql_type(type_).base.upper()
+if base_type == 'VARCHAR':
+same_size = self._columns[column_name]['size'] == size
+else:
+same_size = True
+
+return base_type == db_type and same_size
+
 def db_default(self, column_name, value):
 warnings.warn('Unable to set default on column with SQLite backend')
 
diff -r ee23aca64b26 -r e775cf82255f trytond/backend/table.py
--- a/trytond/backend/table.py  Mon Apr 12 20:54:03 2021 +0200
+++ b/trytond/backend/table.py  Mon Apr 12 20:56:08 2021 +0200
@@ -84,6 +84,18 @@
 '''
 raise NotImplementedError
 
+def column_is_type(self, column_name, type_, *, size=-1):
+'''
+Return True if the column is of type type_
+
+:param column_name: the column name
+:param type_: the generic name of the type
+:param size: if `type` is VARCHAR you can specify its size.
+ Defaults to -1 which will won't match any size
+:return: a boolean
+'''
+raise NotImplementedError
+
 def db_default(self, column_name, value):
 '''
 Set a default on a column



[tryton-commits] changeset in trytond:default Use immutable datastructures for Di...

2021-04-12 Thread Nicolas Évrard
changeset e7526f0686ec in trytond:default
details: https://hg.tryton.org/trytond?cmd=changeset=e7526f0686ec
description:
Use immutable datastructures for Dict and MultiSelection fields

In case a Dict field is sent during an on_change, modification of its 
value
won't be detected because a shallow copy will keep its reference in the
pseudo-record

issue10105
review347541002
diffstat:

 CHANGELOG  |   1 +
 trytond/model/fields/dict.py   |  26 +-
 trytond/model/fields/multiselection.py |   7 ++-
 trytond/tests/modelview.py |  13 -
 trytond/tests/test_field_dict.py   |  13 +
 trytond/tests/test_field_multiselection.py |  14 +++---
 trytond/tests/test_modelstorage.py |   2 +-
 trytond/tests/test_modelview.py|  18 +-
 8 files changed, 82 insertions(+), 12 deletions(-)

diffs (267 lines):

diff -r 7a4a537f0f87 -r e7526f0686ec CHANGELOG
--- a/CHANGELOG Mon Apr 12 17:44:16 2021 +0200
+++ b/CHANGELOG Mon Apr 12 19:39:25 2021 +0200
@@ -1,3 +1,4 @@
+* Use immutable datastructures for Dict and MultiSelection fields
 * Skip warnings for non-interactive operations
 * Check rule only if _check_access is set
 * Add statistics to Cache
diff -r 7a4a537f0f87 -r e7526f0686ec trytond/model/fields/dict.py
--- a/trytond/model/fields/dict.py  Mon Apr 12 17:44:16 2021 +0200
+++ b/trytond/model/fields/dict.py  Mon Apr 12 19:39:25 2021 +0200
@@ -17,6 +17,25 @@
 json.dumps, cls=JSONEncoder, separators=(',', ':'), sort_keys=True)
 
 
+class ImmutableDict(dict):
+
+__slots__ = ()
+
+def _not_allowed(cls, *args, **kwargs):
+raise TypeError("Operation not allowed on ImmutableDict")
+
+__setitem__ = _not_allowed
+__delitem__ = _not_allowed
+__ior__ = _not_allowed
+clear = _not_allowed
+pop = _not_allowed
+popitem = _not_allowed
+setdefault = _not_allowed
+update = _not_allowed
+
+del _not_allowed
+
+
 class Dict(Field):
 'Define dict field.'
 _type = 'dict'
@@ -41,7 +60,7 @@
 # If stored as JSON conversion is done on backend
 if isinstance(data, str):
 data = json.loads(data, object_hook=JSONDecoder())
-dicts[value['id']] = data
+dicts[value['id']] = ImmutableDict(data)
 return dicts
 
 def sql_format(self, value):
@@ -57,6 +76,11 @@
 value = dumps(d)
 return value
 
+def __set__(self, inst, value):
+if value:
+value = ImmutableDict(value)
+super().__set__(inst, value)
+
 def translated(self, name=None, type_='values'):
 "Return a descriptor for the translated value of the field"
 if name is None:
diff -r 7a4a537f0f87 -r e7526f0686ec trytond/model/fields/multiselection.py
--- a/trytond/model/fields/multiselection.pyMon Apr 12 17:44:16 2021 +0200
+++ b/trytond/model/fields/multiselection.pyMon Apr 12 19:39:25 2021 +0200
@@ -61,7 +61,7 @@
 # If stored as JSON conversion is done on backend
 if isinstance(data, str):
 data = json.loads(data)
-lists[value['id']] = data
+lists[value['id']] = tuple(data)
 return lists
 
 def sql_format(self, value):
@@ -70,6 +70,11 @@
 value = dumps(sorted(set(value)))
 return value
 
+def __set__(self, inst, value):
+if value:
+value = tuple(value)
+super().__set__(inst, value)
+
 def _domain_column(self, operator, column):
 database = Transaction().database
 return database.json_get(super()._domain_column(operator, column))
diff -r 7a4a537f0f87 -r e7526f0686ec trytond/tests/modelview.py
--- a/trytond/tests/modelview.pyMon Apr 12 17:44:16 2021 +0200
+++ b/trytond/tests/modelview.pyMon Apr 12 19:39:25 2021 +0200
@@ -1,7 +1,7 @@
 # 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 trytond.model import ModelView, ModelSQL, fields
+from trytond.model import ModelView, ModelSQL, DictSchemaMixin, fields
 from trytond.pool import Pool
 from trytond.pyson import If, Eval
 
@@ -20,6 +20,16 @@
 'Targets')
 m2m_targets = fields.Many2Many('test.modelview.changed_values.target',
 None, None, 'Targets')
+multiselection = fields.MultiSelection([
+('a', 'A'), ('b', 'B'),
+], "MultiSelection")
+dictionary = fields.Dict(
+'test.modelview.changed_values.dictionary', "Dictionary")
+
+
+class ModelViewChangedValuesDictSchema(DictSchemaMixin, ModelSQL):
+'ModelView Changed Values Dict Schema'
+__name__ = 'test.modelview.changed_values.dictionary'
 
 
 class ModelViewChangedValuesTarget(ModelView):
@@ -253,6 +263,7 @@
 def 

[tryton-commits] changeset in trytond:default Escape identifiers in backend

2021-04-12 Thread Nicolas Évrard
changeset 892807681516 in trytond:default
details: https://hg.tryton.org/trytond?cmd=changeset=892807681516
description:
Escape identifiers in backend

issue10245
review317021002
diffstat:

 trytond/backend/postgresql/database.py |   66 +
 trytond/backend/postgresql/table.py|  232 +---
 trytond/backend/sqlite/table.py|  101 -
 3 files changed, 243 insertions(+), 156 deletions(-)

diffs (738 lines):

diff -r 41723b243136 -r 892807681516 trytond/backend/postgresql/database.py
--- a/trytond/backend/postgresql/database.pyMon Apr 12 22:08:36 2021 +0200
+++ b/trytond/backend/postgresql/database.pyMon Apr 12 23:07:55 2021 +0200
@@ -17,6 +17,7 @@
 except ImportError:
 pass
 from psycopg2 import connect, Binary
+from psycopg2.sql import SQL, Identifier
 from psycopg2.pool import ThreadedConnectionPool, PoolError
 from psycopg2.extensions import cursor
 from psycopg2.extensions import ISOLATION_LEVEL_REPEATABLE_READ
@@ -295,14 +296,19 @@
 @classmethod
 def create(cls, connection, database_name, template='template0'):
 cursor = connection.cursor()
-cursor.execute('CREATE DATABASE "' + database_name + '" '
-'TEMPLATE "' + template + '" ENCODING \'unicode\'')
+cursor.execute(
+SQL(
+"CREATE DATABASE {} TEMPLATE {} ENCODING 'unicode'")
+.format(
+Identifier(database_name),
+Identifier(template)))
 connection.commit()
 cls._list_cache.clear()
 
 def drop(self, connection, database_name):
 cursor = connection.cursor()
-cursor.execute('DROP DATABASE "' + database_name + '"')
+cursor.execute(SQL("DROP DATABASE {}")
+.format(Identifier(database_name)))
 self.__class__._list_cache.clear()
 
 def get_version(self, connection):
@@ -412,21 +418,23 @@
 
 def nextid(self, connection, table):
 cursor = connection.cursor()
-cursor.execute("SELECT NEXTVAL('" + table + "_id_seq')")
+cursor.execute("SELECT NEXTVAL(%s)", (table + '_id_seq',))
 return cursor.fetchone()[0]
 
 def setnextid(self, connection, table, value):
 cursor = connection.cursor()
-cursor.execute("SELECT SETVAL('" + table + "_id_seq', %d)" % value)
+cursor.execute("SELECT SETVAL(%s, %s)", (table + '_id_seq', value))
 
 def currid(self, connection, table):
 cursor = connection.cursor()
-cursor.execute('SELECT last_value FROM "' + table + '_id_seq"')
+cursor.execute(SQL("SELECT last_value FROM {}").format(
+Identifier(table + '_id_seq')))
 return cursor.fetchone()[0]
 
 def lock(self, connection, table):
 cursor = connection.cursor()
-cursor.execute('LOCK "%s" IN EXCLUSIVE MODE NOWAIT' % table)
+cursor.execute(SQL('LOCK {} IN EXCLUSIVE MODE NOWAIT').format(
+Identifier(table)))
 
 def lock_id(self, id, timeout=None):
 if not timeout:
@@ -660,35 +668,32 @@
 self, connection, name, number_increment=1, start_value=1):
 cursor = connection.cursor()
 
-param = self.flavor.param
 cursor.execute(
-'CREATE SEQUENCE "%s" '
-'INCREMENT BY %s '
-'START WITH %s'
-% (name, param, param),
+SQL("CREATE SEQUENCE {} INCREMENT BY %s START WITH %s").format(
+Identifier(name)),
 (number_increment, start_value))
 
 def sequence_update(
 self, connection, name, number_increment=1, start_value=1):
 cursor = connection.cursor()
-param = self.flavor.param
 cursor.execute(
-'ALTER SEQUENCE "%s" '
-'INCREMENT BY %s '
-'RESTART WITH %s'
-% (name, param, param),
+SQL("ALTER SEQUENCE {} INCREMENT BY %s RESTART WITH %s").format(
+Identifier(name)),
 (number_increment, start_value))
 
 def sequence_rename(self, connection, old_name, new_name):
 cursor = connection.cursor()
 if (self.sequence_exist(connection, old_name)
 and not self.sequence_exist(connection, new_name)):
-cursor.execute('ALTER TABLE "%s" RENAME TO "%s"'
-% (old_name, new_name))
+cursor.execute(
+SQL("ALTER TABLE {} to {}").format(
+Identifier(old_name),
+Identifier(new_name)))
 
 def sequence_delete(self, connection, name):
 cursor = connection.cursor()
-cursor.execute('DROP SEQUENCE "%s"' % name)
+cursor.execute(SQL("DROP SEQUENCE {}").format(
+Identifier(name)))
 
 def sequence_next_number(self, connection, name):
 cursor = connection.cursor()
@@ -697,22 +702,23 @@
 cursor.execute(
 'SELECT increment_by '
 'FROM 

[tryton-commits] changeset in tryton:5.0 Use specific search domain for reference...

2021-04-19 Thread Nicolas Évrard
changeset af77eea40b5b in tryton:5.0
details: https://hg.tryton.org/tryton?cmd=changeset=af77eea40b5b
description:
Use specific search domain for reference field searches

issue9997
review324781002
(grafted from 558aeef898cd410c74d51a2e5ced9cbe8baaa899)
diffstat:

 tryton/common/domain_inversion.py  |  57 ++---
 tryton/gui/window/view_form/model/field.py |   3 +-
 2 files changed, 53 insertions(+), 7 deletions(-)

diffs (86 lines):

diff -r 5cbb6bc8aa76 -r af77eea40b5b tryton/common/domain_inversion.py
--- a/tryton/common/domain_inversion.py Sun Apr 11 18:28:37 2021 +0200
+++ b/tryton/common/domain_inversion.py Mon Apr 12 20:54:03 2021 +0200
@@ -164,15 +164,60 @@
 
 def prepare_reference_domain(domain, reference):
 "convert domain to replace reference fields by their local part"
+
+def value2reference(value):
+model, ref_id = None, None
+if isinstance(value, str) and ',' in value:
+model, ref_id = value.split(',', 1)
+if ref_id != '%':
+try:
+ref_id = int(ref_id)
+except ValueError:
+model, ref_id = None, value
+elif (isinstance(value, (list, tuple))
+and len(value) == 2
+and isinstance(value[0], str)
+and (isinstance(value[1], int) or value[1] == '%')):
+model, ref_id = value
+else:
+ref_id = value
+return model, ref_id
+
 if domain in ('AND', 'OR'):
 return domain
 elif is_leaf(domain):
-# When a Reference field is using the dotted notation the model
-# specified must be removed from the clause
-if domain[0].count('.') and len(domain) > 3:
-local_name, target_name = domain[0].split('.', 1)
-if local_name == reference:
-return [target_name] + list(domain[1:3] + domain[4:])
+if domain[0] == reference:
+if domain[1] in {'=', '!='}:
+model, ref_id = value2reference(domain[2])
+if model is not None:
+if ref_id == '%':
+if domain[1] == '=':
+return [reference + '.id', '!=', None, model]
+else:
+return [reference, 'not like', domain[2]]
+return [reference + '.id', domain[1], ref_id, model]
+elif domain[1] in {'in', 'not in'}:
+model_values = {}
+for value in domain[2]:
+model, ref_id = value2reference(value)
+if model is None:
+break
+model_values.setdefault(model, []).append(ref_id)
+else:
+new_domain = ['OR'] if domain[1] == 'in' else ['AND']
+for model, ref_ids in model_values.items():
+if '%' in ref_ids:
+if domain[1] == 'in':
+new_domain.append(
+[reference + '.id', '!=', None, model])
+else:
+new_domain.append(
+[reference, 'not like', model + ',%'])
+else:
+new_domain.append(
+[reference + '.id', domain[1], ref_ids, model])
+return new_domain
+return []
 return domain
 else:
 return [prepare_reference_domain(d, reference) for d in domain]
diff -r 5cbb6bc8aa76 -r af77eea40b5b tryton/gui/window/view_form/model/field.py
--- a/tryton/gui/window/view_form/model/field.pySun Apr 11 18:28:37 
2021 +0200
+++ b/tryton/gui/window/view_form/model/field.pyMon Apr 12 20:54:03 
2021 +0200
@@ -908,10 +908,11 @@
 screen_domain = filter_leaf(screen_domain, self.name, model)
 screen_domain = prepare_reference_domain(screen_domain, self.name)
 return concat(localize_domain(
-screen_domain, strip_target=True), attr_domain)
+screen_domain, self.name, strip_target=True), attr_domain)
 
 def get_models(self, record):
 screen_domain, attr_domain = self.domains_get(record)
+screen_domain = prepare_reference_domain(screen_domain, self.name)
 return extract_reference_models(
 concat(screen_domain, attr_domain), self.name)
 



[tryton-commits] changeset in trytond:5.8 Use specific search domain for referenc...

2021-04-19 Thread Nicolas Évrard
changeset 95a67741ae49 in trytond:5.8
details: https://hg.tryton.org/trytond?cmd=changeset=95a67741ae49
description:
Use specific search domain for reference field searches

issue9997
review324781002
(grafted from ee23aca64b26f0bc15419382936bdc54ce3800f0)
diffstat:

 trytond/tests/test_tools.py   |  59 --
 trytond/tools/domain_inversion.py |  57 +---
 2 files changed, 106 insertions(+), 10 deletions(-)

diffs (145 lines):

diff -r 4b90e7d2441d -r 95a67741ae49 trytond/tests/test_tools.py
--- a/trytond/tests/test_tools.py   Wed Apr 14 00:39:56 2021 +0200
+++ b/trytond/tests/test_tools.py   Mon Apr 12 20:54:03 2021 +0200
@@ -721,16 +721,67 @@
 domain = [['x', 'like', 'A%']]
 self.assertEqual(
 prepare_reference_domain(domain, 'x'),
-[['x', 'like', 'A%']])
+[[]])
 
-domain = [['x.y', 'like', 'A%', 'model']]
+domain = [['x', '=', 'A']]
 self.assertEqual(
-prepare_reference_domain(domain, 'x'), [['y', 'like', 'A%']])
+prepare_reference_domain(domain, 'x'),
+[[]])
 
 domain = [['x.y', 'child_of', [1], 'model', 'parent']]
 self.assertEqual(
 prepare_reference_domain(domain, 'x'),
-[['y', 'child_of', [1], 'parent']])
+[['x.y', 'child_of', [1], 'model', 'parent']])
+
+domain = [['x.y', 'like', 'A%', 'model']]
+self.assertEqual(
+prepare_reference_domain(domain, 'x'),
+[['x.y', 'like', 'A%', 'model']])
+
+domain = [['x', '=', 'model,1']]
+self.assertEqual(
+prepare_reference_domain(domain, 'x'),
+[['x.id', '=', 1, 'model']])
+
+domain = [['x', '!=', 'model,1']]
+self.assertEqual(
+prepare_reference_domain(domain, 'x'),
+[['x.id', '!=', 1, 'model']])
+
+domain = [['x', '=', 'model,%']]
+self.assertEqual(
+prepare_reference_domain(domain, 'x'),
+[['x.id', '!=', None, 'model']])
+
+domain = [['x', '!=', 'model,%']]
+self.assertEqual(
+prepare_reference_domain(domain, 'x'),
+[['x', 'not like', 'model,%']])
+
+domain = [['x', 'in',
+['model_a,1', 'model_b,%', 'model_c,3', 'model_a,2']]]
+self.assertEqual(
+prepare_reference_domain(domain, 'x'),
+[['OR',
+['x.id', 'in', [1, 2], 'model_a'],
+['x.id', '!=', None, 'model_b'],
+['x.id', 'in', [3], 'model_c'],
+]])
+
+domain = [['x', 'not in',
+['model_a,1', 'model_b,%', 'model_c,3', 'model_a,2']]]
+self.assertEqual(
+prepare_reference_domain(domain, 'x'),
+[['AND',
+['x.id', 'not in', [1, 2], 'model_a'],
+['x', 'not like', 'model_b,%'],
+['x.id', 'not in', [3], 'model_c'],
+]])
+
+domain = [['x', 'in', ['model_a,1', 'foo']]]
+self.assertEqual(
+prepare_reference_domain(domain, 'x'),
+[[]])
 
 def test_extract_models(self):
 domain = [['x', 'like', 'A%']]
diff -r 4b90e7d2441d -r 95a67741ae49 trytond/tools/domain_inversion.py
--- a/trytond/tools/domain_inversion.py Wed Apr 14 00:39:56 2021 +0200
+++ b/trytond/tools/domain_inversion.py Mon Apr 12 20:54:03 2021 +0200
@@ -170,15 +170,60 @@
 
 def prepare_reference_domain(domain, reference):
 "convert domain to replace reference fields by their local part"
+
+def value2reference(value):
+model, ref_id = None, None
+if isinstance(value, str) and ',' in value:
+model, ref_id = value.split(',', 1)
+if ref_id != '%':
+try:
+ref_id = int(ref_id)
+except ValueError:
+model, ref_id = None, value
+elif (isinstance(value, (list, tuple))
+and len(value) == 2
+and isinstance(value[0], str)
+and (isinstance(value[1], int) or value[1] == '%')):
+model, ref_id = value
+else:
+ref_id = value
+return model, ref_id
+
 if domain in ('AND', 'OR'):
 return domain
 elif is_leaf(domain):
-# When a Reference field is using the dotted notation the model
-# specified must be removed from the clause
-if domain[0].count('.') and len(domain) > 3:
-local_name, target_name = domain[0].split('.', 1)
-if local_name == reference:
-return [target_name] + list(domain[1:3] + domain[4:])
+if domain[0] == reference:
+if domain[1] in {'=', '!='}:
+model, ref_id = value2reference(domain[2])
+if model is not None:
+if ref_id == '%':
+

[tryton-commits] changeset in trytond:5.6 Use specific search domain for referenc...

2021-04-19 Thread Nicolas Évrard
changeset 801d153ef88d in trytond:5.6
details: https://hg.tryton.org/trytond?cmd=changeset=801d153ef88d
description:
Use specific search domain for reference field searches

issue9997
review324781002
(grafted from ee23aca64b26f0bc15419382936bdc54ce3800f0)
diffstat:

 trytond/tests/test_tools.py   |  59 --
 trytond/tools/domain_inversion.py |  57 +---
 2 files changed, 106 insertions(+), 10 deletions(-)

diffs (145 lines):

diff -r 2f9144334712 -r 801d153ef88d trytond/tests/test_tools.py
--- a/trytond/tests/test_tools.py   Fri Apr 02 21:52:26 2021 +0200
+++ b/trytond/tests/test_tools.py   Mon Apr 12 20:54:03 2021 +0200
@@ -705,16 +705,67 @@
 domain = [['x', 'like', 'A%']]
 self.assertEqual(
 prepare_reference_domain(domain, 'x'),
-[['x', 'like', 'A%']])
+[[]])
 
-domain = [['x.y', 'like', 'A%', 'model']]
+domain = [['x', '=', 'A']]
 self.assertEqual(
-prepare_reference_domain(domain, 'x'), [['y', 'like', 'A%']])
+prepare_reference_domain(domain, 'x'),
+[[]])
 
 domain = [['x.y', 'child_of', [1], 'model', 'parent']]
 self.assertEqual(
 prepare_reference_domain(domain, 'x'),
-[['y', 'child_of', [1], 'parent']])
+[['x.y', 'child_of', [1], 'model', 'parent']])
+
+domain = [['x.y', 'like', 'A%', 'model']]
+self.assertEqual(
+prepare_reference_domain(domain, 'x'),
+[['x.y', 'like', 'A%', 'model']])
+
+domain = [['x', '=', 'model,1']]
+self.assertEqual(
+prepare_reference_domain(domain, 'x'),
+[['x.id', '=', 1, 'model']])
+
+domain = [['x', '!=', 'model,1']]
+self.assertEqual(
+prepare_reference_domain(domain, 'x'),
+[['x.id', '!=', 1, 'model']])
+
+domain = [['x', '=', 'model,%']]
+self.assertEqual(
+prepare_reference_domain(domain, 'x'),
+[['x.id', '!=', None, 'model']])
+
+domain = [['x', '!=', 'model,%']]
+self.assertEqual(
+prepare_reference_domain(domain, 'x'),
+[['x', 'not like', 'model,%']])
+
+domain = [['x', 'in',
+['model_a,1', 'model_b,%', 'model_c,3', 'model_a,2']]]
+self.assertEqual(
+prepare_reference_domain(domain, 'x'),
+[['OR',
+['x.id', 'in', [1, 2], 'model_a'],
+['x.id', '!=', None, 'model_b'],
+['x.id', 'in', [3], 'model_c'],
+]])
+
+domain = [['x', 'not in',
+['model_a,1', 'model_b,%', 'model_c,3', 'model_a,2']]]
+self.assertEqual(
+prepare_reference_domain(domain, 'x'),
+[['AND',
+['x.id', 'not in', [1, 2], 'model_a'],
+['x', 'not like', 'model_b,%'],
+['x.id', 'not in', [3], 'model_c'],
+]])
+
+domain = [['x', 'in', ['model_a,1', 'foo']]]
+self.assertEqual(
+prepare_reference_domain(domain, 'x'),
+[[]])
 
 def test_extract_models(self):
 domain = [['x', 'like', 'A%']]
diff -r 2f9144334712 -r 801d153ef88d trytond/tools/domain_inversion.py
--- a/trytond/tools/domain_inversion.py Fri Apr 02 21:52:26 2021 +0200
+++ b/trytond/tools/domain_inversion.py Mon Apr 12 20:54:03 2021 +0200
@@ -164,15 +164,60 @@
 
 def prepare_reference_domain(domain, reference):
 "convert domain to replace reference fields by their local part"
+
+def value2reference(value):
+model, ref_id = None, None
+if isinstance(value, str) and ',' in value:
+model, ref_id = value.split(',', 1)
+if ref_id != '%':
+try:
+ref_id = int(ref_id)
+except ValueError:
+model, ref_id = None, value
+elif (isinstance(value, (list, tuple))
+and len(value) == 2
+and isinstance(value[0], str)
+and (isinstance(value[1], int) or value[1] == '%')):
+model, ref_id = value
+else:
+ref_id = value
+return model, ref_id
+
 if domain in ('AND', 'OR'):
 return domain
 elif is_leaf(domain):
-# When a Reference field is using the dotted notation the model
-# specified must be removed from the clause
-if domain[0].count('.') and len(domain) > 3:
-local_name, target_name = domain[0].split('.', 1)
-if local_name == reference:
-return [target_name] + list(domain[1:3] + domain[4:])
+if domain[0] == reference:
+if domain[1] in {'=', '!='}:
+model, ref_id = value2reference(domain[2])
+if model is not None:
+if ref_id == '%':
+

[tryton-commits] changeset in sao:5.6 Use specific search domain for reference fi...

2021-04-19 Thread Nicolas Évrard
changeset 31c8626e1723 in sao:5.6
details: https://hg.tryton.org/sao?cmd=changeset=31c8626e1723
description:
Use specific search domain for reference field searches

issue9997
review324781002
(grafted from eab0883326841dcaa1770e3c7d2aaeda27edb28b)
diffstat:

 src/common.js |  105 +++--
 src/model.js  |6 ++-
 tests/sao.js  |   58 +--
 3 files changed, 151 insertions(+), 18 deletions(-)

diffs (217 lines):

diff -r 06018a5aa718 -r 31c8626e1723 src/common.js
--- a/src/common.js Sun Apr 11 18:32:18 2021 +0200
+++ b/src/common.js Mon Apr 12 20:54:03 2021 +0200
@@ -2323,23 +2323,104 @@
 }
 },
 prepare_reference_domain: function(domain, reference) {
+
+var value2reference = function(value) {
+var model = null;
+var ref_id = null;
+if ((typeof(value) == 'string') && value.contains(',')) {
+var split = value.split(',');
+var result = split.splice(0, 1);
+result.push(split.join(','));
+model = result[0];
+ref_id = result[1];
+if (ref_id != '%') {
+ref_id = parseInt(ref_id, 10);
+if (isNaN(ref_id)) {
+model = null;
+ref_id = value;
+}
+}
+} else if ((value instanceof Array) &&
+(value.length == 2) &&
+(typeof(value[0]) == 'string') &&
+((typeof(value[1]) == 'number') ||
+(value[1] == '%'))) {
+model = value[0];
+ref_id = value[1];
+} else {
+ref_id = value;
+}
+return [model, ref_id];
+};
+
 if (~['AND', 'OR'].indexOf(domain)) {
 return domain;
 } else if (this.is_leaf(domain)) {
-if ((domain[0].split('.').length > 1) &&
-(domain.length > 3)) {
-var parts = domain[0].split('.');
-var local_name = parts[0];
-var target_name = parts.slice(1).join('.');
+if (domain[0] == reference) {
+var model, ref_id, splitted;
+if ((domain[1] == '=') || (domain[1] ==  '!=')) {
+splitted = value2reference(domain[2]);
+model = splitted[0];
+ref_id = splitted[1];
+if (model) {
+if (ref_id == '%') {
+if (domain[1] == '=') {
+return [
+reference + '.id', '!=', null, model];
+} else {
+return [reference, 'not like', domain[2]];
+}
+}
+return [
+reference + '.id', domain[1], ref_id, model];
+}
+} else if ((domain[1] == 'in') || (domain[1] == 'not in')) 
{
+var model_values = {};
+var break_p = false;
+for (var i=0; i < domain[2].length; i++) {
+splitted = value2reference(domain[2][i]);
+model = splitted[0];
+ref_id = splitted[1];
+if (!model) {
+break_p = true;
+break;
+}
+if (!(model in model_values)) {
+model_values[model] = [];
+}
+model_values[model].push(ref_id);
+}
 
-if (local_name == reference) {
-var where = [];
-where.push(target_name);
-where = where.concat(
-domain.slice(1, 3), domain.slice(4));
-return where;
+if (!break_p) {
+var ref_ids;
+var new_domain;
+if (domain[1] == 'in') {
+new_domain = ['OR'];
+} else {
+new_domain = ['AND'];
+}
+for (model in model_values) {
+ref_ids = model_values[model];
+if 

[tryton-commits] changeset in sao:5.0 Use specific search domain for reference fi...

2021-04-19 Thread Nicolas Évrard
changeset 96c7d07a3eed in sao:5.0
details: https://hg.tryton.org/sao?cmd=changeset=96c7d07a3eed
description:
Use specific search domain for reference field searches

issue9997
review324781002
(grafted from eab0883326841dcaa1770e3c7d2aaeda27edb28b)
diffstat:

 src/common.js |  105 +++--
 src/model.js  |6 ++-
 tests/sao.js  |   58 +--
 3 files changed, 151 insertions(+), 18 deletions(-)

diffs (217 lines):

diff -r ca080990835a -r 96c7d07a3eed src/common.js
--- a/src/common.js Fri Apr 16 19:07:33 2021 +0200
+++ b/src/common.js Mon Apr 12 20:54:03 2021 +0200
@@ -2269,23 +2269,104 @@
 }
 },
 prepare_reference_domain: function(domain, reference) {
+
+var value2reference = function(value) {
+var model = null;
+var ref_id = null;
+if ((typeof(value) == 'string') && value.contains(',')) {
+var split = value.split(',');
+var result = split.splice(0, 1);
+result.push(split.join(','));
+model = result[0];
+ref_id = result[1];
+if (ref_id != '%') {
+ref_id = parseInt(ref_id, 10);
+if (isNaN(ref_id)) {
+model = null;
+ref_id = value;
+}
+}
+} else if ((value instanceof Array) &&
+(value.length == 2) &&
+(typeof(value[0]) == 'string') &&
+((typeof(value[1]) == 'number') ||
+(value[1] == '%'))) {
+model = value[0];
+ref_id = value[1];
+} else {
+ref_id = value;
+}
+return [model, ref_id];
+};
+
 if (~['AND', 'OR'].indexOf(domain)) {
 return domain;
 } else if (this.is_leaf(domain)) {
-if ((domain[0].split('.').length > 1) &&
-(domain.length > 3)) {
-var parts = domain[0].split('.');
-var local_name = parts[0];
-var target_name = parts.slice(1).join('.');
+if (domain[0] == reference) {
+var model, ref_id, splitted;
+if ((domain[1] == '=') || (domain[1] ==  '!=')) {
+splitted = value2reference(domain[2]);
+model = splitted[0];
+ref_id = splitted[1];
+if (model) {
+if (ref_id == '%') {
+if (domain[1] == '=') {
+return [
+reference + '.id', '!=', null, model];
+} else {
+return [reference, 'not like', domain[2]];
+}
+}
+return [
+reference + '.id', domain[1], ref_id, model];
+}
+} else if ((domain[1] == 'in') || (domain[1] == 'not in')) 
{
+var model_values = {};
+var break_p = false;
+for (var i=0; i < domain[2].length; i++) {
+splitted = value2reference(domain[2][i]);
+model = splitted[0];
+ref_id = splitted[1];
+if (!model) {
+break_p = true;
+break;
+}
+if (!(model in model_values)) {
+model_values[model] = [];
+}
+model_values[model].push(ref_id);
+}
 
-if (local_name == reference) {
-var where = [];
-where.push(target_name);
-where = where.concat(
-domain.slice(1, 3), domain.slice(4));
-return where;
+if (!break_p) {
+var ref_ids;
+var new_domain;
+if (domain[1] == 'in') {
+new_domain = ['OR'];
+} else {
+new_domain = ['AND'];
+}
+for (model in model_values) {
+ref_ids = model_values[model];
+if 

[tryton-commits] changeset in sao:5.8 Use specific search domain for reference fi...

2021-04-19 Thread Nicolas Évrard
changeset edfe33c6a4f2 in sao:5.8
details: https://hg.tryton.org/sao?cmd=changeset=edfe33c6a4f2
description:
Use specific search domain for reference field searches

issue9997
review324781002
(grafted from eab0883326841dcaa1770e3c7d2aaeda27edb28b)
diffstat:

 src/common.js |  105 +++--
 src/model.js  |6 ++-
 tests/sao.js  |   58 +--
 3 files changed, 151 insertions(+), 18 deletions(-)

diffs (217 lines):

diff -r 81b0890b9062 -r edfe33c6a4f2 src/common.js
--- a/src/common.js Sun Apr 11 18:32:18 2021 +0200
+++ b/src/common.js Mon Apr 12 20:54:03 2021 +0200
@@ -2358,23 +2358,104 @@
 }
 },
 prepare_reference_domain: function(domain, reference) {
+
+var value2reference = function(value) {
+var model = null;
+var ref_id = null;
+if ((typeof(value) == 'string') && value.contains(',')) {
+var split = value.split(',');
+var result = split.splice(0, 1);
+result.push(split.join(','));
+model = result[0];
+ref_id = result[1];
+if (ref_id != '%') {
+ref_id = parseInt(ref_id, 10);
+if (isNaN(ref_id)) {
+model = null;
+ref_id = value;
+}
+}
+} else if ((value instanceof Array) &&
+(value.length == 2) &&
+(typeof(value[0]) == 'string') &&
+((typeof(value[1]) == 'number') ||
+(value[1] == '%'))) {
+model = value[0];
+ref_id = value[1];
+} else {
+ref_id = value;
+}
+return [model, ref_id];
+};
+
 if (~['AND', 'OR'].indexOf(domain)) {
 return domain;
 } else if (this.is_leaf(domain)) {
-if ((domain[0].split('.').length > 1) &&
-(domain.length > 3)) {
-var parts = domain[0].split('.');
-var local_name = parts[0];
-var target_name = parts.slice(1).join('.');
+if (domain[0] == reference) {
+var model, ref_id, splitted;
+if ((domain[1] == '=') || (domain[1] ==  '!=')) {
+splitted = value2reference(domain[2]);
+model = splitted[0];
+ref_id = splitted[1];
+if (model) {
+if (ref_id == '%') {
+if (domain[1] == '=') {
+return [
+reference + '.id', '!=', null, model];
+} else {
+return [reference, 'not like', domain[2]];
+}
+}
+return [
+reference + '.id', domain[1], ref_id, model];
+}
+} else if ((domain[1] == 'in') || (domain[1] == 'not in')) 
{
+var model_values = {};
+var break_p = false;
+for (var i=0; i < domain[2].length; i++) {
+splitted = value2reference(domain[2][i]);
+model = splitted[0];
+ref_id = splitted[1];
+if (!model) {
+break_p = true;
+break;
+}
+if (!(model in model_values)) {
+model_values[model] = [];
+}
+model_values[model].push(ref_id);
+}
 
-if (local_name == reference) {
-var where = [];
-where.push(target_name);
-where = where.concat(
-domain.slice(1, 3), domain.slice(4));
-return where;
+if (!break_p) {
+var ref_ids;
+var new_domain;
+if (domain[1] == 'in') {
+new_domain = ['OR'];
+} else {
+new_domain = ['AND'];
+}
+for (model in model_values) {
+ref_ids = model_values[model];
+if 

[tryton-commits] changeset in tryton:5.8 Use specific search domain for reference...

2021-04-19 Thread Nicolas Évrard
changeset d8984ce5a8e6 in tryton:5.8
details: https://hg.tryton.org/tryton?cmd=changeset=d8984ce5a8e6
description:
Use specific search domain for reference field searches

issue9997
review324781002
(grafted from 558aeef898cd410c74d51a2e5ced9cbe8baaa899)
diffstat:

 tryton/common/domain_inversion.py  |  57 ++---
 tryton/gui/window/view_form/model/field.py |   3 +-
 2 files changed, 53 insertions(+), 7 deletions(-)

diffs (86 lines):

diff -r cee6239e02c3 -r d8984ce5a8e6 tryton/common/domain_inversion.py
--- a/tryton/common/domain_inversion.py Sun Apr 11 18:28:37 2021 +0200
+++ b/tryton/common/domain_inversion.py Mon Apr 12 20:54:03 2021 +0200
@@ -170,15 +170,60 @@
 
 def prepare_reference_domain(domain, reference):
 "convert domain to replace reference fields by their local part"
+
+def value2reference(value):
+model, ref_id = None, None
+if isinstance(value, str) and ',' in value:
+model, ref_id = value.split(',', 1)
+if ref_id != '%':
+try:
+ref_id = int(ref_id)
+except ValueError:
+model, ref_id = None, value
+elif (isinstance(value, (list, tuple))
+and len(value) == 2
+and isinstance(value[0], str)
+and (isinstance(value[1], int) or value[1] == '%')):
+model, ref_id = value
+else:
+ref_id = value
+return model, ref_id
+
 if domain in ('AND', 'OR'):
 return domain
 elif is_leaf(domain):
-# When a Reference field is using the dotted notation the model
-# specified must be removed from the clause
-if domain[0].count('.') and len(domain) > 3:
-local_name, target_name = domain[0].split('.', 1)
-if local_name == reference:
-return [target_name] + list(domain[1:3] + domain[4:])
+if domain[0] == reference:
+if domain[1] in {'=', '!='}:
+model, ref_id = value2reference(domain[2])
+if model is not None:
+if ref_id == '%':
+if domain[1] == '=':
+return [reference + '.id', '!=', None, model]
+else:
+return [reference, 'not like', domain[2]]
+return [reference + '.id', domain[1], ref_id, model]
+elif domain[1] in {'in', 'not in'}:
+model_values = {}
+for value in domain[2]:
+model, ref_id = value2reference(value)
+if model is None:
+break
+model_values.setdefault(model, []).append(ref_id)
+else:
+new_domain = ['OR'] if domain[1] == 'in' else ['AND']
+for model, ref_ids in model_values.items():
+if '%' in ref_ids:
+if domain[1] == 'in':
+new_domain.append(
+[reference + '.id', '!=', None, model])
+else:
+new_domain.append(
+[reference, 'not like', model + ',%'])
+else:
+new_domain.append(
+[reference + '.id', domain[1], ref_ids, model])
+return new_domain
+return []
 return domain
 else:
 return [prepare_reference_domain(d, reference) for d in domain]
diff -r cee6239e02c3 -r d8984ce5a8e6 tryton/gui/window/view_form/model/field.py
--- a/tryton/gui/window/view_form/model/field.pySun Apr 11 18:28:37 
2021 +0200
+++ b/tryton/gui/window/view_form/model/field.pyMon Apr 12 20:54:03 
2021 +0200
@@ -971,10 +971,11 @@
 screen_domain = filter_leaf(screen_domain, self.name, model)
 screen_domain = prepare_reference_domain(screen_domain, self.name)
 return concat(localize_domain(
-screen_domain, strip_target=True), attr_domain)
+screen_domain, self.name, strip_target=True), attr_domain)
 
 def get_models(self, record):
 screen_domain, attr_domain = self.domains_get(record)
+screen_domain = prepare_reference_domain(screen_domain, self.name)
 return extract_reference_models(
 concat(screen_domain, attr_domain), self.name)
 



[tryton-commits] changeset in tryton:5.6 Use specific search domain for reference...

2021-04-19 Thread Nicolas Évrard
changeset 72ccc8121d55 in tryton:5.6
details: https://hg.tryton.org/tryton?cmd=changeset=72ccc8121d55
description:
Use specific search domain for reference field searches

issue9997
review324781002
(grafted from 558aeef898cd410c74d51a2e5ced9cbe8baaa899)
diffstat:

 tryton/common/domain_inversion.py  |  57 ++---
 tryton/gui/window/view_form/model/field.py |   3 +-
 2 files changed, 53 insertions(+), 7 deletions(-)

diffs (86 lines):

diff -r d0256c1d535c -r 72ccc8121d55 tryton/common/domain_inversion.py
--- a/tryton/common/domain_inversion.py Sun Apr 11 18:28:37 2021 +0200
+++ b/tryton/common/domain_inversion.py Mon Apr 12 20:54:03 2021 +0200
@@ -164,15 +164,60 @@
 
 def prepare_reference_domain(domain, reference):
 "convert domain to replace reference fields by their local part"
+
+def value2reference(value):
+model, ref_id = None, None
+if isinstance(value, str) and ',' in value:
+model, ref_id = value.split(',', 1)
+if ref_id != '%':
+try:
+ref_id = int(ref_id)
+except ValueError:
+model, ref_id = None, value
+elif (isinstance(value, (list, tuple))
+and len(value) == 2
+and isinstance(value[0], str)
+and (isinstance(value[1], int) or value[1] == '%')):
+model, ref_id = value
+else:
+ref_id = value
+return model, ref_id
+
 if domain in ('AND', 'OR'):
 return domain
 elif is_leaf(domain):
-# When a Reference field is using the dotted notation the model
-# specified must be removed from the clause
-if domain[0].count('.') and len(domain) > 3:
-local_name, target_name = domain[0].split('.', 1)
-if local_name == reference:
-return [target_name] + list(domain[1:3] + domain[4:])
+if domain[0] == reference:
+if domain[1] in {'=', '!='}:
+model, ref_id = value2reference(domain[2])
+if model is not None:
+if ref_id == '%':
+if domain[1] == '=':
+return [reference + '.id', '!=', None, model]
+else:
+return [reference, 'not like', domain[2]]
+return [reference + '.id', domain[1], ref_id, model]
+elif domain[1] in {'in', 'not in'}:
+model_values = {}
+for value in domain[2]:
+model, ref_id = value2reference(value)
+if model is None:
+break
+model_values.setdefault(model, []).append(ref_id)
+else:
+new_domain = ['OR'] if domain[1] == 'in' else ['AND']
+for model, ref_ids in model_values.items():
+if '%' in ref_ids:
+if domain[1] == 'in':
+new_domain.append(
+[reference + '.id', '!=', None, model])
+else:
+new_domain.append(
+[reference, 'not like', model + ',%'])
+else:
+new_domain.append(
+[reference + '.id', domain[1], ref_ids, model])
+return new_domain
+return []
 return domain
 else:
 return [prepare_reference_domain(d, reference) for d in domain]
diff -r d0256c1d535c -r 72ccc8121d55 tryton/gui/window/view_form/model/field.py
--- a/tryton/gui/window/view_form/model/field.pySun Apr 11 18:28:37 
2021 +0200
+++ b/tryton/gui/window/view_form/model/field.pyMon Apr 12 20:54:03 
2021 +0200
@@ -949,10 +949,11 @@
 screen_domain = filter_leaf(screen_domain, self.name, model)
 screen_domain = prepare_reference_domain(screen_domain, self.name)
 return concat(localize_domain(
-screen_domain, strip_target=True), attr_domain)
+screen_domain, self.name, strip_target=True), attr_domain)
 
 def get_models(self, record):
 screen_domain, attr_domain = self.domains_get(record)
+screen_domain = prepare_reference_domain(screen_domain, self.name)
 return extract_reference_models(
 concat(screen_domain, attr_domain), self.name)
 



[tryton-commits] changeset in sao:default Protect trusted devices against brute f...

2021-02-21 Thread Nicolas Évrard
changeset d10e0a87299d in sao:default
details: https://hg.tryton.org/sao?cmd=changeset;node=d10e0a87299d
description:
Protect trusted devices against brute force attack

issue9386
review321511002
diffstat:

 CHANGELOG  |   1 +
 src/session.js |  35 +++
 2 files changed, 36 insertions(+), 0 deletions(-)

diffs (68 lines):

diff -r 90fb8b703fe7 -r d10e0a87299d CHANGELOG
--- a/CHANGELOG Sat Feb 20 00:54:38 2021 +0100
+++ b/CHANGELOG Sun Feb 21 16:23:11 2021 +0100
@@ -1,3 +1,4 @@
+* Handle device cookie
 * Add breadcrumb as title of window form
 * Manage help for each selection
 * Display revision on dialog
diff -r 90fb8b703fe7 -r d10e0a87299d src/session.js
--- a/src/session.jsSat Feb 20 00:54:38 2021 +0100
+++ b/src/session.jsSun Feb 21 16:23:11 2021 +0100
@@ -29,7 +29,14 @@
 do_login: function(parameters) {
 var dfd = jQuery.Deferred();
 var login = this.login;
+var device_cookies = JSON.parse(
+localStorage.getItem('sao_device_cookies'));
+var device_cookie = null;
+if (device_cookies) {
+device_cookie = device_cookies[this.database][this.login];
+}
 var func = function(parameters) {
+parameters.device_cookie = device_cookie;
 return {
 'method': 'common.db.login',
 'params': [login, parameters, Sao.i18n.getlang()]
@@ -40,6 +47,7 @@
 this.user_id = result[0];
 this.session = result[1];
 this.store();
+this.renew_device_cookie();
 dfd.resolve();
 }.bind(this), function() {
 this.user_id = null;
@@ -135,6 +143,33 @@
 unstore: function() {
 localStorage.removeItem('sao_session_' + this.database);
 },
+renew_device_cookie: function() {
+var device_cookie;
+var device_cookies = JSON.parse(
+localStorage.getItem('sao_device_cookies'));
+if (!device_cookies || !(this.database in device_cookies)) {
+device_cookie = null;
+} else {
+device_cookie = device_cookies[this.database][this.login];
+}
+var renew_prm = Sao.rpc({
+method: 'model.res.user.device.renew',
+params: [device_cookie, {}],
+}, this);
+renew_prm.done(function(result) {
+device_cookies = JSON.parse(
+localStorage.getItem('sao_device_cookies'));
+if (!device_cookies) {
+device_cookies = {};
+}
+if (!(this.database in device_cookies)) {
+device_cookies[this.database] = {};
+}
+device_cookies[this.database][this.login] = result;
+localStorage.setItem(
+'sao_device_cookies', JSON.stringify(device_cookies));
+}.bind(this));
+}
 });
 
 Sao.Session.login_dialog = function() {



[tryton-commits] changeset in tryton:default Protect trusted devices against brut...

2021-02-21 Thread Nicolas Évrard
changeset e4bfb7718b16 in tryton:default
details: https://hg.tryton.org/tryton?cmd=changeset;node=e4bfb7718b16
description:
Protect trusted devices against brute force attack

issue9386
review321511002
diffstat:

 CHANGELOG   |   1 +
 tryton/device_cookie.py |  76 +
 tryton/rpc.py   |   4 +-
 3 files changed, 80 insertions(+), 1 deletions(-)

diffs (116 lines):

diff -r 6bca8daa4d9c -r e4bfb7718b16 CHANGELOG
--- a/CHANGELOG Sat Feb 20 00:53:18 2021 +0100
+++ b/CHANGELOG Sun Feb 21 16:23:11 2021 +0100
@@ -1,3 +1,4 @@
+* Handle device cookie
 * Add breadcrumb as title of window form
 * Display revision on dialog
 * Execute report asynchronously
diff -r 6bca8daa4d9c -r e4bfb7718b16 tryton/device_cookie.py
--- /dev/null   Thu Jan 01 00:00:00 1970 +
+++ b/tryton/device_cookie.py   Sun Feb 21 16:23:11 2021 +0100
@@ -0,0 +1,76 @@
+# 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 os
+import json
+import logging
+from threading import Lock
+
+from tryton.config import get_config_dir, CONFIG
+
+logger = logging.getLogger(__name__)
+COOKIES_PATH = os.path.join(get_config_dir(), 'device_cookies')
+
+
+_lock = Lock()
+
+
+def renew():
+from tryton.common import common
+
+def set_cookie(new_cookie):
+try:
+new_cookie = new_cookie()
+except Exception:
+logger.error("Cannot renew device cookie", exc_info=True)
+else:
+_set(new_cookie)
+
+current_cookie = get()
+common.RPCExecute(
+'model', 'res.user.device', 'renew', current_cookie,
+process_exception=False, callback=set_cookie)
+
+
+def get():
+cookies = _load()
+return cookies.get(_key())
+
+
+def _key():
+from tryton import common
+
+host = CONFIG['login.host']
+hostname = common.get_hostname(host)
+port = common.get_port(host)
+database = CONFIG['login.db']
+username = CONFIG['login.login']
+
+return '%(username)s@%(hostname)s:%(port)s/%(database)s' % {
+'username': username,
+'hostname': hostname,
+'port': port,
+'database': database,
+}
+
+
+def _set(cookie):
+cookies = _load()
+cookies[_key()] = cookie
+try:
+with _lock:
+with open(COOKIES_PATH, 'w') as cookies_file:
+json.dump(cookies, cookies_file)
+except Exception:
+logger.error('Unable to save cookies file')
+
+
+def _load():
+if not os.path.isfile(COOKIES_PATH):
+return {}
+try:
+with open(COOKIES_PATH) as cookies:
+cookies = json.load(cookies)
+except Exception:
+logger.error("Unable to load device cookies file", exc_info=True)
+cookies = {}
+return cookies
diff -r 6bca8daa4d9c -r e4bfb7718b16 tryton/rpc.py
--- a/tryton/rpc.py Sat Feb 20 00:53:18 2021 +0100
+++ b/tryton/rpc.py Sun Feb 21 16:23:11 2021 +0100
@@ -11,7 +11,7 @@
 
 from functools import partial
 
-from tryton import bus
+from tryton import bus, device_cookies
 from tryton.jsonrpc import ServerProxy, ServerPool, Fault
 from tryton.fingerprints import Fingerprints
 from tryton.config import get_config_dir
@@ -80,6 +80,7 @@
 database = CONFIG['login.db']
 username = CONFIG['login.login']
 language = CONFIG['client.lang']
+parameters['device_cookie'] = device_cookies.get()
 connection = ServerProxy(hostname, port, database)
 logging.getLogger(__name__).info('common.db.login(%s, %s, %s)'
 % (username, 'x' * 10, language))
@@ -91,6 +92,7 @@
 CONNECTION.close()
 CONNECTION = ServerPool(
 hostname, port, database, session=session, cache=not CONFIG['dev'])
+device_cookies.renew()
 bus.listen(CONNECTION)
 
 



[tryton-commits] changeset in trytond:default Protect trusted devices against bru...

2021-02-21 Thread Nicolas Évrard
changeset d3a8ed75e4c0 in trytond:default
details: https://hg.tryton.org/trytond?cmd=changeset;node=d3a8ed75e4c0
description:
Protect trusted devices against brute force attack

issue9386
review321511002
diffstat:

 CHANGELOG  |   1 +
 trytond/res/__init__.py|   1 +
 trytond/res/user.py|  88 +
 trytond/tests/test_user.py |  45 +++
 4 files changed, 126 insertions(+), 9 deletions(-)

diffs (251 lines):

diff -r da66215e2990 -r d3a8ed75e4c0 CHANGELOG
--- a/CHANGELOG Sat Feb 20 00:58:39 2021 +0100
+++ b/CHANGELOG Sun Feb 21 16:23:11 2021 +0100
@@ -1,3 +1,4 @@
+* Protect trusted devices against brute force attack
 * Make ModelView.parse_view public
 * Add help for each value to selection and multiselection
 * Use safe_join in SharedDataMiddlewareIndex (issue10068)
diff -r da66215e2990 -r d3a8ed75e4c0 trytond/res/__init__.py
--- a/trytond/res/__init__.py   Sat Feb 20 00:58:39 2021 +0100
+++ b/trytond/res/__init__.py   Sun Feb 21 16:23:11 2021 +0100
@@ -15,6 +15,7 @@
 group.Group,
 user.User,
 user.LoginAttempt,
+user.UserDevice,
 user.UserAction,
 user.UserGroup,
 user.Warning_,
diff -r da66215e2990 -r d3a8ed75e4c0 trytond/res/user.py
--- a/trytond/res/user.py   Sat Feb 20 00:58:39 2021 +0100
+++ b/trytond/res/user.py   Sun Feb 21 16:23:11 2021 +0100
@@ -368,10 +368,12 @@
 def write(cls, users, values, *args):
 pool = Pool()
 Session = pool.get('ir.session')
+UserDevice = pool.get('res.user.device')
 
 actions = iter((users, values) + args)
 all_users = []
 session_to_clear = []
+users_to_clear = []
 args = []
 for users, values in zip(actions, actions):
 all_users += users
@@ -379,10 +381,12 @@
 
 if 'password' in values:
 session_to_clear += users
+users_to_clear += [u.login for u in users]
 
 super(User, cls).write(*args)
 
 Session.clear(session_to_clear)
+UserDevice.clear(users_to_clear)
 
 # Clean cursor cache as it could be filled by domain_get
 for cache in Transaction().cache.values():
@@ -618,15 +622,20 @@
 '''
 Return user id if password matches
 '''
-LoginAttempt = Pool().get('res.user.login.attempt')
+pool = Pool()
+LoginAttempt = pool.get('res.user.login.attempt')
+UserDevice = pool.get('res.user.device')
+
 count_ip = LoginAttempt.count_ip()
 if count_ip > config.getint(
 'session', 'max_attempt_ip_network', default=300):
 # Do not add attempt as the goal is to prevent flooding
 raise RateLimitException()
-count = LoginAttempt.count(login)
+device_cookie = UserDevice.get_valid_cookie(
+login, parameters.get('device_cookie'))
+count = LoginAttempt.count(login, device_cookie)
 if count > config.getint('session', 'max_attempt', default=5):
-LoginAttempt.add(login)
+LoginAttempt.add(login, device_cookie)
 raise RateLimitException()
 Transaction().atexit(time.sleep, random.randint(0, 2 ** count - 1))
 for methods in config.get(
@@ -642,9 +651,9 @@
 if len(user_ids) != 1 or not all(user_ids):
 break
 if len(user_ids) == 1 and all(user_ids):
-LoginAttempt.remove(login)
+LoginAttempt.remove(login, device_cookie)
 return user_ids.pop()
-LoginAttempt.add(login)
+LoginAttempt.add(login, device_cookie)
 
 @classmethod
 def _login_password(cls, login, parameters):
@@ -740,6 +749,7 @@
 """
 __name__ = 'res.user.login.attempt'
 login = fields.Char('Login', size=512)
+device_cookie = fields.Char("Device Cookie")
 ip_address = fields.Char("IP Address")
 ip_network = fields.Char("IP Network")
 
@@ -771,7 +781,7 @@
 
 @classmethod
 @_login_size
-def add(cls, login):
+def add(cls, login, device_cookie=None):
 cursor = Transaction().connection.cursor()
 table = cls.__table__()
 cursor.execute(*table.delete(where=table.create_date < cls.delay()))
@@ -779,24 +789,28 @@
 ip_address, ip_network = cls.ipaddress()
 cls.create([{
 'login': login,
+'device_cookie': device_cookie,
 'ip_address': str(ip_address),
 'ip_network': str(ip_network),
 }])
 
 @classmethod
 @_login_size
-def remove(cls, login):
+def remove(cls, login, cookie):
 cursor = Transaction().connection.cursor()
 table = cls.__table__()
-cursor.execute(*table.delete(where=table.login == login))
+cursor.execute(*table.delete(
+where=(table.login == 

[tryton-commits] changeset in tryton:default Fix wrong import name introduced in ...

2021-02-21 Thread Nicolas Évrard
changeset 1d89fd9b64b3 in tryton:default
details: https://hg.tryton.org/tryton?cmd=changeset;node=1d89fd9b64b3
description:
Fix wrong import name introduced in changeset e4bfb7718b16
diffstat:

 tryton/rpc.py |  6 +++---
 1 files changed, 3 insertions(+), 3 deletions(-)

diffs (30 lines):

diff -r e4bfb7718b16 -r 1d89fd9b64b3 tryton/rpc.py
--- a/tryton/rpc.py Sun Feb 21 16:23:11 2021 +0100
+++ b/tryton/rpc.py Sun Feb 21 17:37:08 2021 +0100
@@ -11,7 +11,7 @@
 
 from functools import partial
 
-from tryton import bus, device_cookies
+from tryton import bus, device_cookie
 from tryton.jsonrpc import ServerProxy, ServerPool, Fault
 from tryton.fingerprints import Fingerprints
 from tryton.config import get_config_dir
@@ -80,7 +80,7 @@
 database = CONFIG['login.db']
 username = CONFIG['login.login']
 language = CONFIG['client.lang']
-parameters['device_cookie'] = device_cookies.get()
+parameters['device_cookie'] = device_cookie.get()
 connection = ServerProxy(hostname, port, database)
 logging.getLogger(__name__).info('common.db.login(%s, %s, %s)'
 % (username, 'x' * 10, language))
@@ -92,7 +92,7 @@
 CONNECTION.close()
 CONNECTION = ServerPool(
 hostname, port, database, session=session, cache=not CONFIG['dev'])
-device_cookies.renew()
+device_cookie.renew()
 bus.listen(CONNECTION)
 
 



[tryton-commits] changeset in sao:5.6 Set loaded fields first after O2M on_change...

2021-02-11 Thread Nicolas Évrard
changeset 2b26d52977fc in sao:5.6
details: https://hg.tryton.org/sao?cmd=changeset;node=2b26d52977fc
description:
Set loaded fields first after O2M on_change calls

issue9930
review302811002
(grafted from 10156791f1edd6b90c87f21bc8ee3f69b75fe7d3)
diffstat:

 src/model.js |  34 +++---
 1 files changed, 27 insertions(+), 7 deletions(-)

diffs (72 lines):

diff -r 034ffea72122 -r 2b26d52977fc src/model.js
--- a/src/model.js  Wed Feb 03 19:30:13 2021 +0100
+++ b/src/model.js  Thu Feb 04 18:05:18 2021 +0100
@@ -2226,7 +2226,7 @@
 if (value.add || value.update) {
 var context = this.get_context(record);
 fields = record._values[this.name].model.fields;
-var field_names = {};
+var new_field_names = {};
 var adding_values = [];
 if (value.add) {
 for (var i=0; i < value.add.length; i++) {
@@ -2240,25 +2240,25 @@
 if (!(f in fields) &&
 (f != 'id') &&
 (!~f.indexOf('.'))) {
-field_names[f] = true;
+new_field_names[f] = true;
 }
 });
 });
 }
 });
-if (!jQuery.isEmptyObject(field_names)) {
+if (!jQuery.isEmptyObject(new_field_names)) {
 var args = {
 'method': 'model.' + this.description.relation +
 '.fields_get',
-'params': [Object.keys(field_names), context]
+'params': [Object.keys(new_field_names), context]
 };
 try {
-fields = Sao.rpc(args, record.model.session, false);
+new_fields = Sao.rpc(args, record.model.session, 
false);
 } catch (e) {
 return;
 }
 } else {
-fields = {};
+new_fields = {};
 }
 }
 
@@ -2281,7 +2281,27 @@
 }
 
 if (value.add || value.update) {
-group.add_fields(fields);
+// First set already added fields to prevent triggering a
+// second on_change call
+if (value.update) {
+value.update.forEach(function(vals) {
+if (!vals.id) {
+return;
+}
+var record2 = group.get(vals.id);
+if (record2) {
+var vals_to_set = {};
+for (var key in vals) {
+if (!(key in new_field_names)) {
+vals_to_set[key] = vals[key];
+}
+}
+record2.set_on_change(vals_to_set);
+}
+});
+}
+
+group.add_fields(new_fields);
 if (value.add) {
 value.add.forEach(function(vals) {
 var new_record;



[tryton-commits] changeset in sao:5.8 Set loaded fields first after O2M on_change...

2021-02-11 Thread Nicolas Évrard
changeset 660c38b94d52 in sao:5.8
details: https://hg.tryton.org/sao?cmd=changeset;node=660c38b94d52
description:
Set loaded fields first after O2M on_change calls

issue9930
review302811002
(grafted from 10156791f1edd6b90c87f21bc8ee3f69b75fe7d3)
diffstat:

 src/model.js |  34 +++---
 1 files changed, 27 insertions(+), 7 deletions(-)

diffs (72 lines):

diff -r b6626deecdc9 -r 660c38b94d52 src/model.js
--- a/src/model.js  Wed Feb 03 19:30:13 2021 +0100
+++ b/src/model.js  Thu Feb 04 18:05:18 2021 +0100
@@ -2277,7 +2277,7 @@
 if (value.add || value.update) {
 var context = this.get_context(record);
 fields = record._values[this.name].model.fields;
-var field_names = {};
+var new_field_names = {};
 var adding_values = [];
 if (value.add) {
 for (var i=0; i < value.add.length; i++) {
@@ -2291,25 +2291,25 @@
 if (!(f in fields) &&
 (f != 'id') &&
 (!~f.indexOf('.'))) {
-field_names[f] = true;
+new_field_names[f] = true;
 }
 });
 });
 }
 });
-if (!jQuery.isEmptyObject(field_names)) {
+if (!jQuery.isEmptyObject(new_field_names)) {
 var args = {
 'method': 'model.' + this.description.relation +
 '.fields_get',
-'params': [Object.keys(field_names), context]
+'params': [Object.keys(new_field_names), context]
 };
 try {
-fields = Sao.rpc(args, record.model.session, false);
+new_fields = Sao.rpc(args, record.model.session, 
false);
 } catch (e) {
 return;
 }
 } else {
-fields = {};
+new_fields = {};
 }
 }
 
@@ -2332,7 +2332,27 @@
 }
 
 if (value.add || value.update) {
-group.add_fields(fields);
+// First set already added fields to prevent triggering a
+// second on_change call
+if (value.update) {
+value.update.forEach(function(vals) {
+if (!vals.id) {
+return;
+}
+var record2 = group.get(vals.id);
+if (record2) {
+var vals_to_set = {};
+for (var key in vals) {
+if (!(key in new_field_names)) {
+vals_to_set[key] = vals[key];
+}
+}
+record2.set_on_change(vals_to_set);
+}
+});
+}
+
+group.add_fields(new_fields);
 if (value.add) {
 value.add.forEach(function(vals) {
 var new_record;



[tryton-commits] changeset in tryton:5.8 Set loaded fields first after O2M on_cha...

2021-02-11 Thread Nicolas Évrard
changeset fbd06865965b in tryton:5.8
details: https://hg.tryton.org/tryton?cmd=changeset;node=fbd06865965b
description:
Set loaded fields first after O2M on_change calls

issue9930
review302811002
(grafted from 65002d9965d4e90f160baa20dc4edcf16c32a6eb)
diffstat:

 tryton/gui/window/view_form/model/field.py |  17 ++---
 1 files changed, 14 insertions(+), 3 deletions(-)

diffs (38 lines):

diff -r bf5ba8d1bcda -r fbd06865965b tryton/gui/window/view_form/model/field.py
--- a/tryton/gui/window/view_form/model/field.pyWed Feb 03 19:34:15 
2021 +0100
+++ b/tryton/gui/window/view_form/model/field.pyThu Feb 04 18:05:18 
2021 +0100
@@ -763,12 +763,12 @@
 for f in v if f not in fields and f != 'id' and '.' not in f)
 if field_names:
 try:
-fields = RPCExecute('model', self.attrs['relation'],
+new_fields = RPCExecute('model', self.attrs['relation'],
 'fields_get', list(field_names), context=context)
 except RPCException:
 return
 else:
-fields = {}
+new_fields = {}
 
 group = record.value[self.name]
 if value and value.get('delete'):
@@ -787,7 +787,18 @@
 force_remove=False)
 
 if value and (value.get('add') or value.get('update', [])):
-record.value[self.name].add_fields(fields)
+# First set already added fields to prevent triggering a
+# second on_change call
+for vals in value.get('update', []):
+if 'id' not in vals:
+continue
+record2 = group.get(vals['id'])
+if record2 is not None:
+vals_to_set = {
+k: v for k, v in vals.items() if k not in new_fields}
+record2.set_on_change(vals_to_set)
+
+record.value[self.name].add_fields(new_fields)
 for index, vals in value.get('add', []):
 new_record = None
 id_ = vals.pop('id', None)



[tryton-commits] changeset in tryton:5.6 Set loaded fields first after O2M on_cha...

2021-02-11 Thread Nicolas Évrard
changeset 53774043c622 in tryton:5.6
details: https://hg.tryton.org/tryton?cmd=changeset;node=53774043c622
description:
Set loaded fields first after O2M on_change calls

issue9930
review302811002
(grafted from 65002d9965d4e90f160baa20dc4edcf16c32a6eb)
diffstat:

 tryton/gui/window/view_form/model/field.py |  17 ++---
 1 files changed, 14 insertions(+), 3 deletions(-)

diffs (38 lines):

diff -r bae6994b4b0b -r 53774043c622 tryton/gui/window/view_form/model/field.py
--- a/tryton/gui/window/view_form/model/field.pyWed Feb 03 19:34:15 
2021 +0100
+++ b/tryton/gui/window/view_form/model/field.pyThu Feb 04 18:05:18 
2021 +0100
@@ -741,12 +741,12 @@
 for f in v if f not in fields and f != 'id' and '.' not in f)
 if field_names:
 try:
-fields = RPCExecute('model', self.attrs['relation'],
+new_fields = RPCExecute('model', self.attrs['relation'],
 'fields_get', list(field_names), context=context)
 except RPCException:
 return
 else:
-fields = {}
+new_fields = {}
 
 group = record.value[self.name]
 if value and value.get('delete'):
@@ -765,7 +765,18 @@
 force_remove=False)
 
 if value and (value.get('add') or value.get('update', [])):
-record.value[self.name].add_fields(fields)
+# First set already added fields to prevent triggering a
+# second on_change call
+for vals in value.get('update', []):
+if 'id' not in vals:
+continue
+record2 = group.get(vals['id'])
+if record2 is not None:
+vals_to_set = {
+k: v for k, v in vals.items() if k not in new_fields}
+record2.set_on_change(vals_to_set)
+
+record.value[self.name].add_fields(new_fields)
 for index, vals in value.get('add', []):
 new_record = None
 id_ = vals.pop('id', None)



[tryton-commits] changeset in tryton:default Recursively destroy records before p...

2021-09-02 Thread Nicolas Évrard
changeset b1b613ba6059 in tryton:default
details: https://hg.tryton.org/tryton?cmd=changeset=b1b613ba6059
description:
Recursively destroy records before propagating the signal

issue10686
review371591003
diffstat:

 tryton/gui/window/view_form/model/group.py |  4 +++-
 1 files changed, 3 insertions(+), 1 deletions(-)

diffs (16 lines):

diff -r 3b129cae8778 -r b1b613ba6059 tryton/gui/window/view_form/model/group.py
--- a/tryton/gui/window/view_form/model/group.pySat Aug 21 09:09:56 
2021 +0200
+++ b/tryton/gui/window/view_form/model/group.pyThu Sep 02 14:50:23 
2021 +0200
@@ -133,9 +133,11 @@
 # has more chances to be on top of the list.
 length = self.__len__()
 for record in reversed(self[:]):
+# Destroy record before propagating the signal to recursively
+# destroy also the underlying records
+record.destroy()
 self.signal(
 'group-list-changed', ('record-removed', record, length - 1))
-record.destroy()
 self.pop()
 length -= 1
 self.__id2record = {}



[tryton-commits] changeset in tryton:5.0 Recursively destroy records before propa...

2021-09-10 Thread Nicolas Évrard
changeset 15d0f7fceefb in tryton:5.0
details: https://hg.tryton.org/tryton?cmd=changeset=15d0f7fceefb
description:
Recursively destroy records before propagating the signal

issue10686
review371591003
(grafted from b1b613ba6059248938777afd16e58eb64b6d18de)
diffstat:

 tryton/gui/window/view_form/model/group.py |  4 +++-
 1 files changed, 3 insertions(+), 1 deletions(-)

diffs (15 lines):

diff -r ee43feb8166c -r 15d0f7fceefb tryton/gui/window/view_form/model/group.py
--- a/tryton/gui/window/view_form/model/group.pyWed Sep 08 10:12:38 
2021 +0200
+++ b/tryton/gui/window/view_form/model/group.pyThu Sep 02 14:50:23 
2021 +0200
@@ -129,8 +129,10 @@
 # Use reversed order to minimize the cursor reposition as the cursor
 # has more chances to be on top of the list.
 for record in reversed(self[:]):
+# Destroy record before propagating the signal to recursively
+# destroy also the underlying records
+record.destroy()
 self.signal('group-list-changed', ('record-removed', record))
-record.destroy()
 self.pop()
 self.__id2record = {}
 self.record_removed, self.record_deleted = [], []



[tryton-commits] changeset in tryton:6.0 Recursively destroy records before propa...

2021-09-10 Thread Nicolas Évrard
changeset eec33f5ae46d in tryton:6.0
details: https://hg.tryton.org/tryton?cmd=changeset=eec33f5ae46d
description:
Recursively destroy records before propagating the signal

issue10686
review371591003
(grafted from b1b613ba6059248938777afd16e58eb64b6d18de)
diffstat:

 tryton/gui/window/view_form/model/group.py |  4 +++-
 1 files changed, 3 insertions(+), 1 deletions(-)

diffs (16 lines):

diff -r 0050fc1a2d60 -r eec33f5ae46d tryton/gui/window/view_form/model/group.py
--- a/tryton/gui/window/view_form/model/group.pyWed Sep 01 22:47:02 
2021 +0200
+++ b/tryton/gui/window/view_form/model/group.pyThu Sep 02 14:50:23 
2021 +0200
@@ -133,9 +133,11 @@
 # has more chances to be on top of the list.
 length = self.__len__()
 for record in reversed(self[:]):
+# Destroy record before propagating the signal to recursively
+# destroy also the underlying records
+record.destroy()
 self.signal(
 'group-list-changed', ('record-removed', record, length - 1))
-record.destroy()
 self.pop()
 length -= 1
 self.__id2record = {}



[tryton-commits] changeset in tryton:5.8 Recursively destroy records before propa...

2021-09-10 Thread Nicolas Évrard
changeset f7682f7f12cc in tryton:5.8
details: https://hg.tryton.org/tryton?cmd=changeset=f7682f7f12cc
description:
Recursively destroy records before propagating the signal

issue10686
review371591003
(grafted from b1b613ba6059248938777afd16e58eb64b6d18de)
diffstat:

 tryton/gui/window/view_form/model/group.py |  4 +++-
 1 files changed, 3 insertions(+), 1 deletions(-)

diffs (16 lines):

diff -r a55c46e8b832 -r f7682f7f12cc tryton/gui/window/view_form/model/group.py
--- a/tryton/gui/window/view_form/model/group.pyWed Sep 01 22:47:38 
2021 +0200
+++ b/tryton/gui/window/view_form/model/group.pyThu Sep 02 14:50:23 
2021 +0200
@@ -131,9 +131,11 @@
 # has more chances to be on top of the list.
 length = self.__len__()
 for record in reversed(self[:]):
+# Destroy record before propagating the signal to recursively
+# destroy also the underlying records
+record.destroy()
 self.signal(
 'group-list-changed', ('record-removed', record, length - 1))
-record.destroy()
 self.pop()
 length -= 1
 self.__id2record = {}



[tryton-commits] changeset in trytond:default Use UNION for 'Or'-ed domain with s...

2021-09-15 Thread Nicolas Évrard
changeset 47c55a8f9db6 in trytond:default
details: https://hg.tryton.org/trytond?cmd=changeset=47c55a8f9db6
description:
Use UNION for 'Or'-ed domain with subqueries

issue10658
review365781002
diffstat:

 CHANGELOG  |1 +
 trytond/model/modelsql.py  |  156 
 trytond/tests/test_modelsql.py |   92 
 3 files changed, 203 insertions(+), 46 deletions(-)

diffs (319 lines):

diff -r 7a211140f570 -r 47c55a8f9db6 CHANGELOG
--- a/CHANGELOG Tue Sep 14 21:49:26 2021 +0200
+++ b/CHANGELOG Wed Sep 15 15:16:40 2021 +0200
@@ -1,3 +1,4 @@
+* Use UNION for 'Or'-ed domain with subqueries
 * Add remove_forbidden_chars in tools
 * Manage errors during non-interactive operations
 * Add estimation count to ModelStorage
diff -r 7a211140f570 -r 47c55a8f9db6 trytond/model/modelsql.py
--- a/trytond/model/modelsql.py Tue Sep 14 21:49:26 2021 +0200
+++ b/trytond/model/modelsql.py Wed Sep 15 15:16:40 2021 +0200
@@ -6,7 +6,7 @@
 from functools import wraps
 
 from sql import (Table, Column, Literal, Desc, Asc, Expression, Null,
-NullsFirst, NullsLast, For)
+NullsFirst, NullsLast, For, Union)
 from sql.functions import CurrentTimestamp, Extract
 from sql.conditionals import Coalesce
 from sql.operators import Or, And, Operator, Equal
@@ -1257,20 +1257,70 @@
 raise AccessError(msg)
 
 @classmethod
-def search(cls, domain, offset=0, limit=None, order=None, count=False,
-query=False):
+def __search_query(cls, domain, count, query):
 pool = Pool()
 Rule = pool.get('ir.rule')
-transaction = Transaction()
-cursor = transaction.connection.cursor()
+
+rule_domain = Rule.domain_get(cls.__name__, mode='read')
+if domain and domain[0] == 'OR':
+local_domains, subquery_domains = split_subquery_domain(domain)
+else:
+local_domains, subquery_domains = None, None
 
-super(ModelSQL, cls).search(
-domain, offset=offset, limit=limit, order=order, count=count)
+# In case the search uses subqueries it's more efficient to use a UNION
+# of queries than using clauses with some JOIN because databases can
+# used indexes
+if subquery_domains:
+union_tables = []
+for sub_domain in [['OR'] + local_domains] + subquery_domains:
+tables, expression = cls.search_domain(sub_domain)
+if rule_domain:
+tables, domain_exp = cls.search_domain(
+rule_domain, active_test=False, tables=tables)
+expression &= domain_exp
+main_table, _ = tables[None]
+table = convert_from(None, tables)
+columns = cls.__searched_columns(
+main_table, not count and not query)
+union_tables.append(table.select(
+*columns, where=expression))
+expression = None
+tables = {
+None: (Union(*union_tables, all_=False), None),
+}
+else:
+tables, expression = cls.search_domain(domain)
+if rule_domain:
+tables, domain_exp = cls.search_domain(
+rule_domain, active_test=False, tables=tables)
+expression &= domain_exp
 
-# Get domain clauses
-tables, expression = cls.search_domain(domain)
+return tables, expression
 
-# Get order by
+@classmethod
+def __searched_columns(cls, table, eager=False, history=False):
+columns = [table.id.as_('id')]
+if (cls._history and Transaction().context.get('_datetime')
+and (eager or history)):
+columns.append(
+Coalesce(table.write_date, table.create_date).as_('_datetime'))
+columns.append(Column(table, '__id').as_('__id'))
+if eager:
+columns += [f.sql_column(table).as_(n)
+for n, f in cls._fields.items()
+if not hasattr(f, 'get')
+and n != 'id'
+and not getattr(f, 'translate', False)
+and f.loading == 'eager']
+if not callable(cls.table_query):
+sql_type = fields.Char('timestamp').sql_type().base
+columns += [Extract('EPOCH',
+Coalesce(table.write_date, table.create_date)
+).cast(sql_type).as_('_timestamp')]
+return columns
+
+@classmethod
+def __search_order(cls, order, tables):
 order_by = []
 order_types = {
 'DESC': Desc,
@@ -1299,42 +1349,34 @@
 forder = field.convert_order(oexpr, tables, cls)
 order_by.extend((NullOrdering(Order(o)) for o in forder))
 
-# construct a clause for the rules :
-domain = Rule.domain_get(cls.__name__, 

[tryton-commits] changeset in modules/account:default Computes GeneralLedger time...

2021-10-12 Thread Nicolas Évrard
changeset 1b3d9d7a758f in modules/account:default
details: https://hg.tryton.org/modules/account?cmd=changeset=1b3d9d7a758f
description:
Computes GeneralLedger timespan on either dates or period

issue10249
review351991002
diffstat:

 account.py |  49 ++---
 1 files changed, 42 insertions(+), 7 deletions(-)

diffs (97 lines):

diff -r 9f4fabb78e30 -r 1b3d9d7a758f account.py
--- a/account.pyMon Oct 11 23:21:55 2021 +0200
+++ b/account.pyTue Oct 12 11:29:34 2021 +0200
@@ -1059,7 +1059,7 @@
 values[name][account.id] += getattr(deferral, name)
 else:
 with Transaction().set_context(fiscalyear=fiscalyear.id,
-date=None, periods=None):
+date=None, periods=None, from_date=None, to_date=None):
 previous_result = func(accounts, names)
 for name in names:
 vals = values[name]
@@ -1680,8 +1680,16 @@
 def get_account(cls, records, name):
 Account = cls._get_account()
 
-period_ids = cls.get_period_ids(name)
-from_date, to_date = cls.get_dates(name)
+period_ids, from_date, to_date = None, None, None
+context_keys = Transaction().context.keys()
+if context_keys & {'start_period', 'end_period'}:
+period_ids = cls.get_period_ids(name)
+elif context_keys & {'from_date', 'end_date'}:
+from_date, to_date = cls.get_dates(name)
+else:
+if name.startswith('start_'):
+period_ids = []
+
 with Transaction().set_context(
 periods=period_ids,
 from_date=from_date, to_date=to_date):
@@ -1797,27 +1805,42 @@
 domain=[
 ('fiscalyear', '=', Eval('fiscalyear')),
 ('start_date', '<=', (Eval('end_period'), 'start_date')),
-], depends=['fiscalyear', 'end_period'])
+],
+states={
+'invisible': Eval('from_date', False) | Eval('to_date', False),
+},
+depends=['fiscalyear', 'end_period', 'from_date', 'to_date'])
 end_period = fields.Many2One('account.period', 'End Period',
 domain=[
 ('fiscalyear', '=', Eval('fiscalyear')),
 ('start_date', '>=', (Eval('start_period'), 'start_date'))
 ],
-depends=['fiscalyear', 'start_period'])
+states={
+'invisible': Eval('from_date', False) | Eval('to_date', False),
+},
+depends=['fiscalyear', 'start_period', 'from_date', 'to_date'])
 from_date = fields.Date("From Date",
 domain=[
 If(Eval('to_date') & Eval('from_date'),
 ('from_date', '<=', Eval('to_date')),
 ()),
 ],
-depends=['to_date'])
+states={
+'invisible': (Eval('start_period', 'False')
+| Eval('end_period', False)),
+},
+depends=['to_date', 'start_period', 'end_period'])
 to_date = fields.Date("To Date",
 domain=[
 If(Eval('from_date') & Eval('to_date'),
 ('to_date', '>=', Eval('from_date')),
 ()),
 ],
-depends=['from_date'])
+states={
+'invisible': (Eval('start_period', 'False')
+| Eval('end_period', False)),
+},
+depends=['from_date', 'start_period', 'end_period'])
 company = fields.Many2One('company.company', 'Company', required=True)
 posted = fields.Boolean('Posted Move', help="Only include posted moves.")
 journal = fields.Many2One(
@@ -1874,6 +1897,18 @@
 and self.end_period.fiscalyear != self.fiscalyear):
 self.end_period = None
 
+def on_change_start_period(self):
+self.from_date = self.to_date = None
+
+def on_change_end_period(self):
+self.from_date = self.to_date = None
+
+def on_change_from_date(self):
+self.start_period = self.end_period = None
+
+def on_change_to_date(self):
+self.start_period = self.end_period = None
+
 
 class GeneralLedgerAccountParty(_GeneralLedgerAccount):
 "General Ledger Account Party"



[tryton-commits] changeset in modules/account:default Computes GeneralLedger time...

2021-10-12 Thread Nicolas Évrard
changeset 9f6bf9d17129 in modules/account:default
details: https://hg.tryton.org/modules/account?cmd=changeset=9f6bf9d17129
description:
Computes GeneralLedger timespan on either dates or period (scenario 
patch)

issue10249
review351991002
diffstat:

 tests/scenario_reports.rst |  77 +++--
 1 files changed, 66 insertions(+), 11 deletions(-)

diffs (132 lines):

diff -r 1b3d9d7a758f -r 9f6bf9d17129 tests/scenario_reports.rst
--- a/tests/scenario_reports.rstTue Oct 12 11:29:34 2021 +0200
+++ b/tests/scenario_reports.rstTue Oct 12 11:32:48 2021 +0200
@@ -28,7 +28,8 @@
 
 >>> fiscalyear = create_fiscalyear(company)
 >>> fiscalyear.click('create_period')
->>> period = fiscalyear.periods[0]
+>>> periods = fiscalyear.periods
+>>> period_1, period_3, period_5 = periods[0], periods[2], periods[4]
 
 Create chart of accounts::
 
@@ -56,9 +57,9 @@
 ... ('code', '=', 'CASH'),
 ... ])
 >>> move = Move()
->>> move.period = period
+>>> move.period = period_3
 >>> move.journal = journal_revenue
->>> move.date = period.start_date
+>>> move.date = period_3.start_date
 >>> line = move.lines.new()
 >>> line.account = revenue
 >>> line.credit = Decimal(10)
@@ -69,9 +70,9 @@
 >>> move.save()
 
 >>> move = Move()
->>> move.period = period
+>>> move.period = period_5
 >>> move.journal = journal_cash
->>> move.date = period.start_date
+>>> move.date = period_5.start_date
 >>> line = move.lines.new()
 >>> line.account = cash
 >>> line.debit = Decimal(10)
@@ -128,8 +129,8 @@
 >>> context = {
 ... 'company': company.id,
 ... 'fiscalyear': fiscalyear.id,
-... 'from_date': fiscalyear.periods[0].start_date,
-... 'to_date': fiscalyear.periods[1].end_date,
+... 'from_date': period_1.start_date,
+... 'to_date': period_3.end_date,
 ... }
 >>> with config.set_context(context):
 ... gl_revenue, = GeneralLedgerAccount.find([
@@ -150,16 +151,70 @@
 >>> glp_receivable.start_balance
 Decimal('0.00')
 >>> glp_receivable.credit
-Decimal('10.00')
+Decimal('0.00')
 >>> glp_receivable.debit
 Decimal('10.00')
 >>> glp_receivable.end_balance
+Decimal('10.00')
+
+>>> context = {
+... 'company': company.id,
+... 'fiscalyear': fiscalyear.id,
+... 'start_period': period_3.id,
+... }
+>>> with config.set_context(context):
+... gl_revenue, = GeneralLedgerAccount.find([
+...   ('account', '=', revenue.id),
+...   ])
+>>> gl_revenue.start_balance
 Decimal('0.00')
+>>> gl_revenue.credit
+Decimal('10.00')
+>>> gl_revenue.debit
+Decimal('0.00')
+>>> gl_revenue.end_balance
+Decimal('-10.00')
 
 >>> context = {
 ... 'company': company.id,
 ... 'fiscalyear': fiscalyear.id,
-... 'start_period': fiscalyear.periods[1].id,
+... 'start_period': period_5.id,
+... }
+>>> with config.set_context(context):
+... gl_revenue, = GeneralLedgerAccount.find([
+...   ('account', '=', revenue.id),
+...   ])
+>>> gl_revenue.start_balance
+Decimal('-10.00')
+>>> gl_revenue.credit
+Decimal('0.00')
+>>> gl_revenue.debit
+Decimal('0.00')
+>>> gl_revenue.end_balance
+Decimal('-10.00')
+
+>>> context = {
+... 'company': company.id,
+... 'fiscalyear': fiscalyear.id,
+... 'from_date': period_3.start_date,
+... }
+>>> with config.set_context(context):
+... gl_revenue, = GeneralLedgerAccount.find([
+...   ('account', '=', revenue.id),
+...   ])
+>>> gl_revenue.start_balance
+Decimal('0.00')
+>>> gl_revenue.credit
+Decimal('10.00')
+>>> gl_revenue.debit
+Decimal('0.00')
+>>> gl_revenue.end_balance
+Decimal('-10.00')
+
+>>> context = {
+... 'company': company.id,
+... 'fiscalyear': fiscalyear.id,
+... 'from_date': period_5.start_date,
 ... }
 >>> with config.set_context(context):
 ... gl_revenue, = GeneralLedgerAccount.find([
@@ -204,8 +259,8 @@
 >>> _ = general_journal.execute(Move.find([]))
 
 >>> with config.set_context(
-... start_date=period.start_date,
-... end_date=period.end_date):
+... start_date=period_5.start_date,
+... end_date=period_5.end_date):
 ... journal_cash = Journal(journal_cash.id)
 >>> journal_cash.credit
 Decimal('0.00')



[tryton-commits] changeset in tryton:default Fix typo in Gdk identifier

2021-10-20 Thread Nicolas Évrard
changeset ad169d67663a in tryton:default
details: https://hg.tryton.org/tryton?cmd=changeset=ad169d67663a
description:
Fix typo in Gdk identifier

issue10882
review354241002
diffstat:

 tryton/gui/window/win_export.py |  2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)

diffs (12 lines):

diff -r 795501d0c8d1 -r ad169d67663a tryton/gui/window/win_export.py
--- a/tryton/gui/window/win_export.py   Tue Oct 19 00:23:30 2021 +0200
+++ b/tryton/gui/window/win_export.py   Wed Oct 20 15:18:58 2021 +0200
@@ -447,7 +447,7 @@
 return True
 
 def export_keypress(self, treeview, event):
-if event.keyval not in [Gdk.KEY_Return, Gdk.KEY_.space]:
+if event.keyval not in [Gdk.KEY_Return, Gdk.KEY_space]:
 return
 model, selected = treeview.get_selection().get_selected()
 if not selected:



[tryton-commits] changeset in trytond:default Use global cache for Function field...

2021-10-11 Thread Nicolas Évrard
changeset 05235e67b280 in trytond:default
details: https://hg.tryton.org/trytond?cmd=changeset=05235e67b280
description:
Use global cache for Function fields in readonly transactions

issue10491
review352081002
diffstat:

 CHANGELOG |   1 +
 trytond/model/modelsql.py |  17 ++---
 trytond/model/modelstorage.py |  14 ++
 3 files changed, 25 insertions(+), 7 deletions(-)

diffs (93 lines):

diff -r f04093042cc4 -r 05235e67b280 CHANGELOG
--- a/CHANGELOG Mon Oct 11 11:38:37 2021 +0200
+++ b/CHANGELOG Mon Oct 11 11:52:42 2021 +0200
@@ -1,3 +1,4 @@
+* Use global cache for Function fields in readonly transactions
 * Add format method to DictSchemaMixin
 * Allow modify record name on the reports
 * Add methods to format number and symbol on Lang
diff -r f04093042cc4 -r 05235e67b280 trytond/model/modelsql.py
--- a/trytond/model/modelsql.py Mon Oct 11 11:38:37 2021 +0200
+++ b/trytond/model/modelsql.py Mon Oct 11 11:52:42 2021 +0200
@@ -835,8 +835,8 @@
 getter_fields = [f for f in all_fields
 if f in cls._fields and hasattr(cls._fields[f], 'get')]
 
+cache = transaction.get_cache()[cls.__name__]
 if getter_fields and cachable_fields:
-cache = transaction.get_cache()[cls.__name__]
 for row in result:
 for fname in cachable_fields:
 cache[row['id']][fname] = row[fname]
@@ -879,13 +879,24 @@
 for sub_results in grouped_slice(
 result, record_cache_size(transaction)):
 sub_results = list(sub_results)
-sub_ids = [r['id'] for r in sub_results]
+sub_ids = []
+for row in sub_results:
+if (row['id'] not in cache
+or any(f not in cache[row['id']]
+for f in field_list)):
+sub_ids.append(row['id'])
+else:
+for fname in field_list:
+row[fname] = cache[row['id']][fname]
 getter_results = field.get(
 sub_ids, cls, field_list, values=sub_results)
 for fname in field_list:
 getter_result = getter_results[fname]
 for row in sub_results:
-row[fname] = getter_result[row['id']]
+if row['id'] in sub_ids:
+row[fname] = getter_result[row['id']]
+if transaction.readonly:
+cache[row['id']][fname] = row[fname]
 
 def read_related(field, Target, rows, fields):
 name = field.name
diff -r f04093042cc4 -r 05235e67b280 trytond/model/modelstorage.py
--- a/trytond/model/modelstorage.py Mon Oct 11 11:38:37 2021 +0200
+++ b/trytond/model/modelstorage.py Mon Oct 11 11:52:42 2021 +0200
@@ -1515,7 +1515,10 @@
 raise AttributeError('"%s" has no attribute "%s"' % (self, name))
 
 try:
-if field._type not in ('many2one', 'reference'):
+if field._type not in (
+'many2one', 'reference',
+'one2many', 'many2many', 'one2one',
+):
 # fill local cache for quicker access later
 value \
 = self._local_cache[self.id][name] \
@@ -1667,7 +1670,7 @@
 self._transaction.set_user(self._user), \
 self._transaction.reset_context(), \
 self._transaction.set_context(
-self._context, _check_access=False):
+self._context, _check_access=False) as transaction:
 if (self.id in self._cache and name in self._cache[self.id]
 and not require_context_field):
 # Use values from cache
@@ -1684,7 +1687,9 @@
 read_data = self.read(list(index.keys()), list(ffields.keys()))
 read_data.sort(key=lambda r: index[r['id']])
 # create browse records for 'remote' models
-no_local_cache = {'one2one', 'one2many', 'many2many', 'binary'}
+no_local_cache = {'binary'}
+if not transaction.readonly:
+no_local_cache |= {'one2one', 'one2many', 'many2many'}
 for data in read_data:
 id_ = data['id']
 to_delete = set()
@@ -1705,7 +1710,8 @@
 if (field._type in no_local_cache
 or field.context
 or getattr(field, 'datetime_field', None)
-or isinstance(field, fields.Function)):
+or (isinstance(field, fields.Function)
+and not transaction.readonly)):
 

[tryton-commits] changeset in trytond:default Pass default class order to __searc...

2021-10-11 Thread Nicolas Évrard
changeset f04093042cc4 in trytond:default
details: https://hg.tryton.org/trytond?cmd=changeset=f04093042cc4
description:
Pass default class order to __search_query

issue10838
review383251002
diffstat:

 trytond/model/modelsql.py  |   4 ++--
 trytond/tests/modelsql.py  |  28 
 trytond/tests/test_modelsql.py |  34 ++
 3 files changed, 64 insertions(+), 2 deletions(-)

diffs (110 lines):

diff -r c4e9e56d8759 -r f04093042cc4 trytond/model/modelsql.py
--- a/trytond/model/modelsql.py Mon Oct 11 08:53:21 2021 +0200
+++ b/trytond/model/modelsql.py Mon Oct 11 11:38:37 2021 +0200
@@ -1399,8 +1399,6 @@
 'NULLS LAST': NullsLast,
 None: lambda _: _
 }
-if order is None or order is False:
-order = cls._order
 for oexpr, otype in order:
 fname, _, extra_expr = oexpr.partition('.')
 field = cls._fields[fname]
@@ -1428,6 +1426,8 @@
 super(ModelSQL, cls).search(
 domain, offset=offset, limit=limit, order=order, count=count)
 
+if order is None or order is False:
+order = cls._order
 tables, expression = cls.__search_query(domain, count, query, order)
 
 main_table, _ = tables[None]
diff -r c4e9e56d8759 -r f04093042cc4 trytond/tests/modelsql.py
--- a/trytond/tests/modelsql.py Mon Oct 11 08:53:21 2021 +0200
+++ b/trytond/tests/modelsql.py Mon Oct 11 11:38:37 2021 +0200
@@ -111,6 +111,32 @@
 parent = fields.Many2One('test.modelsql.search.or2union', "Parent")
 
 
+class ModelSQLSearchOR2UnionOrder(ModelSQL):
+"ModelSQL Search OR to UNION optimization with class order"
+__name__ = 'test.modelsql.search.or2union.class_order'
+name = fields.Char("Name")
+reference = fields.Reference("Reference", [
+(None, ''),
+('test.modelsql.search.or2union.class_order.target', "Target"),
+])
+targets = fields.One2Many(
+'test.modelsql.search.or2union.class_order.target', 'parent',
+"Targets")
+
+@classmethod
+def __setup__(cls):
+super().__setup__()
+cls._order = [('reference', 'DESC')]
+
+
+class ModelSQLSearchOR2UnionOrderTarget(ModelSQL):
+"ModelSQL Target to test read"
+__name__ = 'test.modelsql.search.or2union.class_order.target'
+name = fields.Char("Name")
+parent = fields.Many2One(
+'test.modelsql.search.or2union.class_order', "Parent")
+
+
 class ModelSQLForeignKey(DeactivableMixin, ModelSQL):
 "ModelSQL Foreign Key"
 __name__ = 'test.modelsql.fk'
@@ -208,6 +234,8 @@
 ModelSQLSearch,
 ModelSQLSearchOR2Union,
 ModelSQLSearchOR2UnionTarget,
+ModelSQLSearchOR2UnionOrder,
+ModelSQLSearchOR2UnionOrderTarget,
 ModelSQLForeignKey,
 ModelSQLForeignKeyTarget,
 NullOrder,
diff -r c4e9e56d8759 -r f04093042cc4 trytond/tests/test_modelsql.py
--- a/trytond/tests/test_modelsql.pyMon Oct 11 08:53:21 2021 +0200
+++ b/trytond/tests/test_modelsql.pyMon Oct 11 11:38:37 2021 +0200
@@ -882,6 +882,40 @@
 self.assertIn('UNION', str(Model.search(domain, query=True)))
 self.assertNotIn('UNION', str(query_without_split))
 
+@with_transaction()
+def test_search_or_to_union_class_order(self):
+"""
+Test searching for 'OR'-ed domain when the class defines _order
+"""
+pool = Pool()
+Model = pool.get('test.modelsql.search.or2union.class_order')
+Target = pool.get('test.modelsql.search.or2union.class_order.target')
+
+target_a, target_b, target_c = Target.create([
+{'name': 'A'}, {'name': 'B'}, {'name': 'C'},
+])
+model_a, model_b, model_c = Model.create([{
+'name': 'A',
+'reference': str(target_a),
+}, {
+'name': 'B',
+'reference': str(target_b),
+}, {
+'name': 'C',
+'reference': str(target_c),
+'targets': [('create', [{
+'name': 'C.A',
+}]),
+],
+}])
+
+domain = ['OR',
+('name', 'ilike', '%A%'),
+('targets.name', 'ilike', '%A'),
+]
+self.assertEqual(Model.search(domain), [model_c, model_a])
+self.assertIn('UNION', str(Model.search(domain, query=True)))
+
 def test_split_subquery_domain_empty(self):
 """
 Test the split of domains in local and relation parts (empty domain)



[tryton-commits] changeset in trytond:default Move tests related to OR-ed domain ...

2021-09-28 Thread Nicolas Évrard
changeset 100cdd3d479a in trytond:default
details: https://hg.tryton.org/trytond?cmd=changeset=100cdd3d479a
description:
Move tests related to OR-ed domain to ModelSQL test case
diffstat:

 trytond/tests/test_modelsql.py |  182 
 1 files changed, 91 insertions(+), 91 deletions(-)

diffs (199 lines):

diff -r 6c65322c5fd4 -r 100cdd3d479a trytond/tests/test_modelsql.py
--- a/trytond/tests/test_modelsql.pySun Sep 26 23:16:53 2021 +0200
+++ b/trytond/tests/test_modelsql.pyTue Sep 28 10:59:56 2021 +0200
@@ -541,6 +541,97 @@
 with self.assertRaises(backend.DatabaseOperationalError):
 record.lock()
 
+@with_transaction()
+def test_search_or_to_union(self):
+"""
+Test searching for 'OR'-ed domain
+"""
+pool = Pool()
+Model = pool.get('test.modelsql.read')
+
+Model.create([{
+'name': 'A',
+}, {
+'name': 'B',
+}, {
+'name': 'C',
+'targets': [('create', [{
+'name': 'C.A',
+}]),
+],
+}])
+
+domain = ['OR',
+('name', 'ilike', '%A%'),
+('targets.name', 'ilike', '%A'),
+]
+with patch('trytond.model.modelsql.split_subquery_domain') as no_split:
+# Mocking in order not to trigger the split
+no_split.side_effect = lambda d: (d, [])
+result_without_split = Model.search(domain)
+self.assertEqual(
+Model.search(domain),
+result_without_split)
+
+def test_split_subquery_domain_empty(self):
+"""
+Test the split of domains in local and relation parts (empty domain)
+"""
+local, related = split_subquery_domain([])
+self.assertEqual(local, [])
+self.assertEqual(related, [])
+
+def test_split_subquery_domain_simple(self):
+"""
+Test the split of domains in local and relation parts (simple domain)
+"""
+local, related = split_subquery_domain([('a', '=', 1)])
+self.assertEqual(local, [('a', '=', 1)])
+self.assertEqual(related, [])
+
+def test_split_subquery_domain_dotter(self):
+"""
+Test the split of domains in local and relation parts (dotted domain)
+"""
+local, related = split_subquery_domain([('a.b', '=', 1)])
+self.assertEqual(local, [])
+self.assertEqual(related, [('a.b', '=', 1)])
+
+def test_split_subquery_domain_mixed(self):
+"""
+Test the split of domains in local and relation parts (mixed domains)
+"""
+local, related = split_subquery_domain(
+[('a', '=', 1), ('b.c', '=', 2)])
+self.assertEqual(local, [('a', '=', 1)])
+self.assertEqual(related, [('b.c', '=', 2)])
+
+def test_split_subquery_domain_operator(self):
+"""
+Test the split of domains in local and relation parts (with operator)
+"""
+local, related = split_subquery_domain(
+['OR', ('a', '=', 1), ('b.c', '=', 2)])
+self.assertEqual(local, [('a', '=', 1)])
+self.assertEqual(related, [('b.c', '=', 2)])
+
+def test_split_subquery_domain_nested(self):
+"""
+Test the split of domains in local and relation parts (nested domains)
+"""
+local, related = split_subquery_domain(
+[
+['AND', ('a', '=', 1), ('b', '=', 2)],
+['AND',
+('b', '=', 2),
+['OR', ('c', '=', 3), ('d.e', '=', 4)]]])
+self.assertEqual(local, [['AND', ('a', '=', 1), ('b', '=', 2)]])
+self.assertEqual(related, [
+['AND',
+('b', '=', 2),
+['OR', ('c', '=', 3), ('d.e', '=', 4)]]
+])
+
 
 class TranslationTestCase(unittest.TestCase):
 default_language = 'fr'
@@ -893,97 +984,6 @@
 self.assertEqual(cache[record.id]['name'], "Foo")
 self.assertNotIn('_timestamp', cache[record.id])
 
-@with_transaction()
-def test_search_or_to_union(self):
-"""
-Test searching for 'OR'-ed domain
-"""
-pool = Pool()
-Model = pool.get('test.modelsql.read')
-
-Model.create([{
-'name': 'A',
-}, {
-'name': 'B',
-}, {
-'name': 'C',
-'targets': [('create', [{
-'name': 'C.A',
-}]),
-],
-}])
-
-domain = ['OR',
-('name', 'ilike', '%A%'),
-('targets.name', 'ilike', '%A'),
-]
-with 

[tryton-commits] changeset in trytond:default Reconstruct local domain after spli...

2021-09-28 Thread Nicolas Évrard
changeset 08914e8bb9b1 in trytond:default
details: https://hg.tryton.org/trytond?cmd=changeset=08914e8bb9b1
description:
Reconstruct local domain after split_subquery_domain

issue10761
review338921002
diffstat:

 trytond/model/modelsql.py  |  12 
 trytond/tests/test_modelsql.py |  31 +++
 2 files changed, 39 insertions(+), 4 deletions(-)

diffs (70 lines):

diff -r 100cdd3d479a -r 08914e8bb9b1 trytond/model/modelsql.py
--- a/trytond/model/modelsql.py Tue Sep 28 10:59:56 2021 +0200
+++ b/trytond/model/modelsql.py Tue Sep 28 16:44:12 2021 +0200
@@ -1264,17 +1264,21 @@
 Rule = pool.get('ir.rule')
 
 rule_domain = Rule.domain_get(cls.__name__, mode='read')
+joined_domains = None
 if domain and domain[0] == 'OR':
 local_domains, subquery_domains = split_subquery_domain(domain)
-else:
-local_domains, subquery_domains = None, None
+if subquery_domains:
+joined_domains = subquery_domains
+if local_domains:
+local_domains.insert(0, 'OR')
+joined_domains.append(local_domains)
 
 # In case the search uses subqueries it's more efficient to use a UNION
 # of queries than using clauses with some JOIN because databases can
 # used indexes
-if subquery_domains:
+if joined_domains is not None:
 union_tables = []
-for sub_domain in [['OR'] + local_domains] + subquery_domains:
+for sub_domain in joined_domains:
 tables, expression = cls.search_domain(sub_domain)
 if rule_domain:
 tables, domain_exp = cls.search_domain(
diff -r 100cdd3d479a -r 08914e8bb9b1 trytond/tests/test_modelsql.py
--- a/trytond/tests/test_modelsql.pyTue Sep 28 10:59:56 2021 +0200
+++ b/trytond/tests/test_modelsql.pyTue Sep 28 16:44:12 2021 +0200
@@ -573,6 +573,37 @@
 Model.search(domain),
 result_without_split)
 
+@with_transaction()
+def test_search_or_to_union_no_local_clauses(self):
+"""
+Test searching for 'OR'-ed domain without local clauses
+"""
+pool = Pool()
+Model = pool.get('test.modelsql.read')
+
+Model.create([{
+'name': 'A',
+}, {
+'name': 'B',
+}, {
+'name': 'C',
+'targets': [('create', [{
+'name': 'C.A',
+}]),
+],
+}])
+
+domain = ['OR',
+('targets.name', 'ilike', '%A'),
+]
+with patch('trytond.model.modelsql.split_subquery_domain') as no_split:
+# Mocking in order not to trigger the split
+no_split.side_effect = lambda d: (d, [])
+result_without_split = Model.search(domain)
+self.assertEqual(
+Model.search(domain),
+result_without_split)
+
 def test_split_subquery_domain_empty(self):
 """
 Test the split of domains in local and relation parts (empty domain)



[tryton-commits] changeset in trytond:default Include order columns in UNION-ed s...

2021-10-06 Thread Nicolas Évrard
changeset 428bf1c84a3d in trytond:default
details: https://hg.tryton.org/trytond?cmd=changeset=428bf1c84a3d
description:
Include order columns in UNION-ed search queries

issue10790
review365991002
diffstat:

 trytond/model/modelsql.py  |   59 --
 trytond/tests/modelsql.py  |   17 
 trytond/tests/test_modelsql.py |  167 +
 3 files changed, 233 insertions(+), 10 deletions(-)

diffs (324 lines):

diff -r 9ca2feb6cc84 -r 428bf1c84a3d trytond/model/modelsql.py
--- a/trytond/model/modelsql.py Mon Oct 04 18:52:01 2021 +0200
+++ b/trytond/model/modelsql.py Wed Oct 06 17:20:40 2021 +0200
@@ -1282,7 +1282,7 @@
 raise AccessError(msg)
 
 @classmethod
-def __search_query(cls, domain, count, query):
+def __search_query(cls, domain, count, query, order):
 pool = Pool()
 Rule = pool.get('ir.rule')
 
@@ -1296,6 +1296,34 @@
 local_domains.insert(0, 'OR')
 joined_domains.append(local_domains)
 
+def get_local_columns(order_exprs):
+local_columns = []
+for order_expr in order_exprs:
+if (isinstance(order_expr, Column)
+and isinstance(order_expr._from, Table)
+and order_expr._from._name == cls._table):
+local_columns.append(order_expr._name)
+else:
+raise NotImplementedError
+return local_columns
+
+# The UNION optimization needs the columns used to order the query
+extra_columns = set()
+if order and joined_domains:
+tables = {
+None: (cls.__table__(), None),
+}
+for oexpr, otype in order:
+fname = oexpr.partition('.')[0]
+field = cls._fields[fname]
+field_orders = field.convert_order(oexpr, tables, cls)
+try:
+order_columns = get_local_columns(field_orders)
+extra_columns.update(order_columns)
+except NotImplementedError:
+joined_domains = None
+break
+
 # In case the search uses subqueries it's more efficient to use a UNION
 # of queries than using clauses with some JOIN because databases can
 # used indexes
@@ -1309,8 +1337,9 @@
 expression &= domain_exp
 main_table, _ = tables[None]
 table = convert_from(None, tables)
-columns = cls.__searched_columns(
-main_table, not count and not query)
+columns = cls.__searched_columns(main_table,
+eager=not count and not query,
+extra_columns=extra_columns)
 union_tables.append(table.select(
 *columns, where=expression))
 expression = None
@@ -1327,20 +1356,30 @@
 return tables, expression
 
 @classmethod
-def __searched_columns(cls, table, eager=False, history=False):
+def __searched_columns(
+cls, table, *, eager=False, history=False, extra_columns=None):
+if extra_columns is None:
+extra_columns = []
+else:
+extra_columns = sorted(extra_columns - {'id', '__id', '_datetime'})
 columns = [table.id.as_('id')]
 if (cls._history and Transaction().context.get('_datetime')
 and (eager or history)):
 columns.append(
 Coalesce(table.write_date, table.create_date).as_('_datetime'))
 columns.append(Column(table, '__id').as_('__id'))
+for column_name in extra_columns:
+field = cls._fields[column_name]
+sql_column = field.sql_column(table).as_(column_name)
+columns.append(sql_column)
 if eager:
 columns += [f.sql_column(table).as_(n)
-for n, f in cls._fields.items()
+for n, f in sorted(cls._fields.items())
 if not hasattr(f, 'get')
-and n != 'id'
-and not getattr(f, 'translate', False)
-and f.loading == 'eager']
+and n not in extra_columns
+and n != 'id'
+and not getattr(f, 'translate', False)
+and f.loading == 'eager']
 if not callable(cls.table_query):
 sql_type = fields.Char('timestamp').sql_type().base
 columns += [Extract('EPOCH',
@@ -1389,7 +1428,7 @@
 super(ModelSQL, cls).search(
 domain, offset=offset, limit=limit, order=order, count=count)
 
-tables, expression = cls.__search_query(domain, count, query)
+tables, expression = cls.__search_query(domain, count, query, order)
 
 main_table, _ = tables[None]
 if count:
@@ -1401,7 +1440,7 

[tryton-commits] changeset in trytond:default Use specific model for OR2UNION tests

2021-10-07 Thread Nicolas Évrard
changeset 4eb15d8c9f95 in trytond:default
details: https://hg.tryton.org/trytond?cmd=changeset=4eb15d8c9f95
description:
Use specific model for OR2UNION tests

issue10832
review362881002
diffstat:

 trytond/tests/modelsql.py  |  19 ---
 trytond/tests/test_modelsql.py |  19 +--
 2 files changed, 29 insertions(+), 9 deletions(-)

diffs (133 lines):

diff -r 428bf1c84a3d -r 4eb15d8c9f95 trytond/tests/modelsql.py
--- a/trytond/tests/modelsql.py Wed Oct 06 17:20:40 2021 +0200
+++ b/trytond/tests/modelsql.py Thu Oct 07 17:46:14 2021 +0200
@@ -87,10 +87,15 @@
 "ModelSQL Search OR to UNION optimization"
 __name__ = 'test.modelsql.search.or2union'
 name = fields.Char("Name")
-target = fields.Many2One('test.modelsql.read.target', "Target")
-targets = fields.One2Many('test.modelsql.read.target', 'parent', "Targets")
+target = fields.Many2One('test.modelsql.search.or2union.target', "Target")
+targets = fields.One2Many(
+'test.modelsql.search.or2union.target', 'parent', "Targets")
 reference = fields.Reference(
-"Reference", [(None, ""), ('test.modelsql.read.target', "Target")])
+"Reference",
+[
+(None, ""),
+('test.modelsql.search.or2union.target', "Target"),
+])
 integer = fields.Integer("Integer")
 
 @classmethod
@@ -99,6 +104,13 @@
 return [table.integer + 1]
 
 
+class ModelSQLSearchOR2UnionTarget(ModelSQL):
+"ModelSQL Target to test read"
+__name__ = 'test.modelsql.search.or2union.target'
+name = fields.Char("Name")
+parent = fields.Many2One('test.modelsql.search.or2union', "Parent")
+
+
 class ModelSQLForeignKey(DeactivableMixin, ModelSQL):
 "ModelSQL Foreign Key"
 __name__ = 'test.modelsql.fk'
@@ -195,6 +207,7 @@
 ModelSQLOne2ManyTarget,
 ModelSQLSearch,
 ModelSQLSearchOR2Union,
+ModelSQLSearchOR2UnionTarget,
 ModelSQLForeignKey,
 ModelSQLForeignKeyTarget,
 NullOrder,
diff -r 428bf1c84a3d -r 4eb15d8c9f95 trytond/tests/test_modelsql.py
--- a/trytond/tests/test_modelsql.pyWed Oct 06 17:20:40 2021 +0200
+++ b/trytond/tests/test_modelsql.pyThu Oct 07 17:46:14 2021 +0200
@@ -651,7 +651,7 @@
 Test searching for 'OR'-ed domain
 """
 pool = Pool()
-Model = pool.get('test.modelsql.read')
+Model = pool.get('test.modelsql.search.or2union')
 
 Model.create([{
 'name': 'A',
@@ -673,9 +673,12 @@
 # Mocking in order not to trigger the split
 no_split.side_effect = lambda d: (d, [])
 result_without_split = Model.search(domain)
+query_without_split = Model.search(domain, query=True)
 self.assertEqual(
 Model.search(domain),
 result_without_split)
+self.assertIn('UNION', str(Model.search(domain, query=True)))
+self.assertNotIn('UNION', str(query_without_split))
 
 @with_transaction()
 def test_search_or_to_union_order_eager_field(self):
@@ -684,7 +687,7 @@
 """
 pool = Pool()
 Model = pool.get('test.modelsql.search.or2union')
-Target = pool.get('test.modelsql.read.target')
+Target = pool.get('test.modelsql.search.or2union.target')
 
 target_a, target_b, target_c = Target.create([
 {'name': 'A'}, {'name': 'B'}, {'name': 'C'},
@@ -725,7 +728,7 @@
 """
 pool = Pool()
 Model = pool.get('test.modelsql.search.or2union')
-Target = pool.get('test.modelsql.read.target')
+Target = pool.get('test.modelsql.search.or2union.target')
 
 target_a, target_b, target_c = Target.create([
 {'name': 'A'}, {'name': 'B'}, {'name': 'C'},
@@ -766,7 +769,7 @@
 """
 pool = Pool()
 Model = pool.get('test.modelsql.search.or2union')
-Target = pool.get('test.modelsql.read.target')
+Target = pool.get('test.modelsql.search.or2union.target')
 
 target_a, target_b, target_c = Target.create([
 {'name': 'A'}, {'name': 'B'}, {'name': 'C'},
@@ -807,7 +810,7 @@
 """
 pool = Pool()
 Model = pool.get('test.modelsql.search.or2union')
-Target = pool.get('test.modelsql.read.target')
+Target = pool.get('test.modelsql.search.or2union.target')
 
 target_a, target_b, target_c = Target.create([
 {'name': 'A'}, {'name': 'B'}, {'name': 'C'},
@@ -850,7 +853,7 @@
 Test searching for 'OR'-ed domain without local clauses
 """
 pool = Pool()
-Model = pool.get('test.modelsql.read')
+Model = pool.get('test.modelsql.search.or2union')
 
 Model.create([{
 'name': 'A',
@@ -866,14 +869,18 @@
 
 domain = ['OR',
 ('targets.name', 'ilike', '%A'),
+('targets.name', 'ilike', '%B'),
 ]
 with 

[tryton-commits] changeset in sao:default Consider Dict Char entries set to None ...

2021-09-21 Thread Nicolas Évrard
changeset fb81af61fc6c in sao:default
details: https://hg.tryton.org/sao?cmd=changeset=fb81af61fc6c
description:
Consider Dict Char entries set to None as set to ""

issue10485
review346431002
diffstat:

 src/view/form.js |  9 -
 1 files changed, 8 insertions(+), 1 deletions(-)

diffs (26 lines):

diff -r 7f0a7f10f094 -r fb81af61fc6c src/view/form.js
--- a/src/view/form.js  Mon Sep 20 17:23:43 2021 +0200
+++ b/src/view/form.js  Tue Sep 21 16:00:11 2021 +0200
@@ -4778,7 +4778,6 @@
 });
 
 Sao.View.Form.Dict.Entry = Sao.class_(Object, {
-class_: 'dict-char',
 init: function(name, parent_widget) {
 this.name = name;
 this.definition = parent_widget.field.keys[name];
@@ -4828,6 +4827,14 @@
 }
 });
 
+Sao.View.Form.Dict.Char = Sao.class_(Sao.View.Form.Dict.Entry, {
+class_: 'dict-char',
+modified: function(value) {
+return (JSON.stringify(this.get_value()) !=
+JSON.stringify(value[this.name] || ""));
+}
+});
+
 Sao.View.Form.Dict.Boolean = Sao.class_(Sao.View.Form.Dict.Entry, {
 class_: 'dict-boolean',
 create_widget: function() {



[tryton-commits] changeset in tryton:default Consider Dict Char entries set to No...

2021-09-21 Thread Nicolas Évrard
changeset 456afb21b221 in tryton:default
details: https://hg.tryton.org/tryton?cmd=changeset=456afb21b221
description:
Consider Dict Char entries set to None as set to ""

issue10485
review346431002
diffstat:

 tryton/gui/window/view_form/view/form_gtk/dictionary.py |  8 +++-
 1 files changed, 7 insertions(+), 1 deletions(-)

diffs (25 lines):

diff -r f7d8d964d69d -r 456afb21b221 
tryton/gui/window/view_form/view/form_gtk/dictionary.py
--- a/tryton/gui/window/view_form/view/form_gtk/dictionary.py   Sun Sep 19 
00:06:54 2021 +0200
+++ b/tryton/gui/window/view_form/view/form_gtk/dictionary.py   Tue Sep 21 
16:00:11 2021 +0200
@@ -65,6 +65,12 @@
 self.widget.set_editable(not readonly)
 
 
+class DictCharEntry(DictEntry):
+
+def modified(self, value):
+return self.get_value() != (value.get(self.name, '') or '')
+
+
 class DictBooleanEntry(DictEntry):
 
 def create_widget(self):
@@ -365,7 +371,7 @@
 
 
 DICT_ENTRIES = {
-'char': DictEntry,
+'char': DictCharEntry,
 'boolean': DictBooleanEntry,
 'selection': DictSelectionEntry,
 'multiselection': DictMultiSelectionEntry,



[tryton-commits] changeset in tryton:default Replace deepcopy by our own implemen...

2022-01-04 Thread Nicolas Évrard
changeset 3486a6152b50 in tryton:default
details: https://hg.tryton.org/tryton?cmd=changeset=3486a6152b50
description:
Replace deepcopy by our own implementation

issue11054
review387551002
diffstat:

 tryton/gui/window/view_form/screen/screen.py |  10 +-
 tryton/jsonrpc.py|  15 ---
 2 files changed, 17 insertions(+), 8 deletions(-)

diffs (75 lines):

diff -r 4e57efeca146 -r 3486a6152b50 
tryton/gui/window/view_form/screen/screen.py
--- a/tryton/gui/window/view_form/screen/screen.py  Mon Jan 03 22:48:18 
2022 +0100
+++ b/tryton/gui/window/view_form/screen/screen.py  Wed Jan 05 08:58:54 
2022 +0100
@@ -3,7 +3,6 @@
 "Screen"
 import calendar
 import collections
-import copy
 import datetime
 import functools
 import gettext
@@ -184,12 +183,13 @@
 else:
 view_tree = self.fields_view_tree[view_id]
 
-fields = copy.deepcopy(view_tree['fields'])
-for name, props in fields.items():
-if props['type'] not in ('selection', 'reference'):
+fields = view_tree['fields'].copy()
+for name in fields:
+if fields[name]['type'] not in ('selection', 'reference'):
 continue
-if isinstance(props['selection'], (tuple, list)):
+if isinstance(fields[name]['selection'], (tuple, list)):
 continue
+props = fields[name] = fields[name].copy()
 props['selection'] = self.get_selection(props)
 
 if 'arch' in view_tree:
diff -r 4e57efeca146 -r 3486a6152b50 tryton/jsonrpc.py
--- a/tryton/jsonrpc.py Mon Jan 03 22:48:18 2022 +0100
+++ b/tryton/jsonrpc.py Wed Jan 05 08:58:54 2022 +0100
@@ -1,7 +1,6 @@
 # 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 base64
-import copy
 import datetime
 import errno
 import hashlib
@@ -25,6 +24,16 @@
 logger = logging.getLogger(__name__)
 
 
+def deepcopy(obj):
+"""Recursively copy python mutable datastructures"""
+if isinstance(obj, (list, tuple)):
+return [deepcopy(o) for o in obj]
+elif isinstance(obj, dict):
+return {k: deepcopy(v) for k, v in obj.items()}
+else:
+return obj
+
+
 class ResponseError(xmlrpc.client.ResponseError):
 pass
 
@@ -392,7 +401,7 @@
 expire = datetime.timedelta(seconds=expire)
 if isinstance(expire, datetime.timedelta):
 expire = datetime.datetime.now() + expire
-self.store[prefix][key] = (expire, copy.deepcopy(value))
+self.store[prefix][key] = (expire, deepcopy(value))
 
 def get(self, prefix, key):
 now = datetime.datetime.now()
@@ -404,7 +413,7 @@
 self.store.pop(key)
 raise KeyError
 logger.info('(cached) %s %s', prefix, key)
-return copy.deepcopy(value)
+return deepcopy(value)
 
 def clear(self, prefix=None):
 if prefix:



[tryton-commits] changeset in modules/account_payment:default Add the payment amo...

2021-11-28 Thread Nicolas Évrard
changeset 7bf36298ce19 in modules/account_payment:default
details: 
https://hg.tryton.org/modules/account_payment?cmd=changeset=7bf36298ce19
description:
Add the payment amount to invoice's amount_to_pay when the payment line 
is unreconciled

issue10757
review371951002
diffstat:

 CHANGELOG  |   2 ++
 account.py |   9 -
 payment.py |  10 ++
 3 files changed, 16 insertions(+), 5 deletions(-)

diffs (47 lines):

diff -r fb95a592c17b -r 7bf36298ce19 CHANGELOG
--- a/CHANGELOG Mon Nov 01 17:17:56 2021 +0100
+++ b/CHANGELOG Sun Nov 28 18:56:16 2021 +0100
@@ -1,3 +1,5 @@
+* Add amount_line_paid property on payments
+
 Version 6.2.0 - 2021-11-01
 * Bug fixes (see mercurial logs for details)
 * Add wizard to create direct debit for configured parties
diff -r fb95a592c17b -r 7bf36298ce19 account.py
--- a/account.pyMon Nov 01 17:17:56 2021 +0100
+++ b/account.pySun Nov 28 18:56:16 2021 +0100
@@ -557,10 +557,9 @@
 continue
 payment_amount = Decimal(0)
 for payment in line.payments:
-if payment.state != 'failed':
-with Transaction().set_context(date=payment.date):
-payment_amount += Currency.compute(
-payment.currency, payment.amount,
-invoice.currency)
+with Transaction().set_context(date=payment.date):
+payment_amount += Currency.compute(
+payment.currency, payment.amount_line_paid,
+invoice.currency)
 amounts[invoice.id] -= payment_amount
 return amounts
diff -r fb95a592c17b -r 7bf36298ce19 payment.py
--- a/payment.pyMon Nov 01 17:17:56 2021 +0100
+++ b/payment.pySun Nov 28 18:56:16 2021 +0100
@@ -363,6 +363,16 @@
 ('failed', 'Failed'),
 ], 'State', readonly=True, select=True)
 
+@property
+def amount_line_paid(self):
+if self.state != 'failed':
+if self.line.second_currency:
+payment_amount = abs(self.line.amount_second_currency)
+else:
+payment_amount = abs(self.line.credit - self.line.debit)
+return max(min(self.amount, payment_amount), 0)
+return Decimal(0)
+
 @classmethod
 def __setup__(cls):
 super(Payment, cls).__setup__()



[tryton-commits] changeset in modules/project_invoice:default Take into account u...

2021-11-22 Thread Nicolas Évrard
changeset 36f5c90fef99 in modules/project_invoice:default
details: 
https://hg.tryton.org/modules/project_invoice?cmd=changeset=36f5c90fef99
description:
Take into account up to date when computing invoice quantity

issue10836
review373811003
diffstat:

 project.py   |  37 ++-
 tests/scenario_project_invoice_timesheet.rst |  26 ++-
 2 files changed, 49 insertions(+), 14 deletions(-)

diffs (104 lines):

diff -r eda64cc40746 -r 36f5c90fef99 project.py
--- a/project.pyMon Nov 01 17:27:34 2021 +0100
+++ b/project.pyMon Nov 22 10:12:44 2021 +0100
@@ -250,19 +250,32 @@
 cursor = Transaction().connection.cursor()
 line = TimesheetLine.__table__()
 
+upto2tworks = defaultdict(list)
+twork2work = {}
+for work in works:
+upto = work.invoice_timesheet_up_to
+for timesheet_work in work.timesheet_works:
+twork2work[timesheet_work.id] = work.id
+upto2tworks[upto].append(timesheet_work.id)
+
 durations = defaultdict(datetime.timedelta)
-twork2work = {tw.id: w.id for w in works for tw in w.timesheet_works}
-for sub_ids in grouped_slice(twork2work.keys()):
-red_sql = reduce_ids(line.work, sub_ids)
-cursor.execute(*line.select(line.work, Sum(line.duration),
-where=red_sql & (line.invoice_line == Null),
-group_by=line.work))
-for twork_id, duration in cursor:
-if duration:
-# SQLite uses float for SUM
-if not isinstance(duration, datetime.timedelta):
-duration = datetime.timedelta(seconds=duration)
-durations[twork2work[twork_id]] += duration
+query = line.select(
+line.work, Sum(line.duration),
+group_by=line.work)
+for upto, tworks in upto2tworks.items():
+for sub_ids in grouped_slice(tworks):
+query.where = (reduce_ids(line.work, sub_ids)
+& (line.invoice_line == Null))
+if upto:
+query.where &= (line.date <= upto)
+cursor.execute(*query)
+
+for twork_id, duration in cursor:
+if duration:
+# SQLite uses float for SUM
+if not isinstance(duration, datetime.timedelta):
+duration = datetime.timedelta(seconds=duration)
+durations[twork2work[twork_id]] += duration
 
 quantities = {}
 for work in works:
diff -r eda64cc40746 -r 36f5c90fef99 
tests/scenario_project_invoice_timesheet.rst
--- a/tests/scenario_project_invoice_timesheet.rst  Mon Nov 01 17:27:34 
2021 +0100
+++ b/tests/scenario_project_invoice_timesheet.rst  Mon Nov 22 10:12:44 
2021 +0100
@@ -46,8 +46,9 @@
 >>> project_invoice_user.login = 'project_invoice'
 >>> project_invoice_group, = Group.find([('name', '=', 'Project Invoice')])
 >>> project_group, = Group.find([('name', '=', 'Project Administration')])
+>>> invoice_group, = Group.find([('name', '=', 'Account')])
 >>> project_invoice_user.groups.extend(
-... [project_invoice_group, project_group])
+... [project_invoice_group, project_group, invoice_group])
 >>> project_invoice_user.save()
 
 Create chart of accounts::
@@ -160,8 +161,19 @@
 >>> set_user(project_invoice_user)
 >>> project.click('invoice')
 >>> project.amount_to_invoice
+Decimal('0.00')
+>>> project.invoiced_amount
+Decimal('60.00')
+
+>>> project.project_invoice_timesheet_up_to = today
+>>> project.save()
+>>> project.amount_to_invoice
 Decimal('40.00')
->>> project.invoiced_amount
+
+>>> set_user(project_invoice_user)
+>>> Invoice = Model.get('account.invoice')
+>>> invoice, = Invoice.find([])
+>>> invoice.total_amount
 Decimal('60.00')
 
 Invoice all project::
@@ -176,6 +188,11 @@
 >>> project.invoiced_amount
 Decimal('100.00')
 
+>>> set_user(project_invoice_user)
+>>> _, invoice = Invoice.find([], order=[('id', 'ASC')])
+>>> invoice.total_amount
+Decimal('40.00')
+
 Create more timesheets::
 
 >>> set_user(project_user)
@@ -202,3 +219,8 @@
 Decimal('0.00')
 >>> project.invoiced_amount
 Decimal('180.00')
+
+>>> set_user(project_invoice_user)
+>>> _, _, invoice = Invoice.find([], order=[('id', 'ASC')])
+>>> invoice.total_amount
+Decimal('80.00')



[tryton-commits] changeset in modules/account_invoice:5.8 Do not copy invoice pay...

2021-11-02 Thread Nicolas Évrard
changeset fa41151c466c in modules/account_invoice:5.8
details: 
https://hg.tryton.org/modules/account_invoice?cmd=changeset=fa41151c466c
description:
Do not copy invoice payments when copying move lines

issue10502
review367121002
(grafted from 24ecdc76c60aedbd4f62501e46338371b9fabce1)
diffstat:

 account.py |  6 ++
 1 files changed, 6 insertions(+), 0 deletions(-)

diffs (16 lines):

diff -r 824cb7eede7c -r fa41151c466c account.py
--- a/account.pyWed Sep 01 22:59:22 2021 +0200
+++ b/account.pyFri Oct 29 17:03:20 2021 +0200
@@ -221,6 +221,12 @@
 'account.invoice.line', 'account.invoice.tax']
 
 @classmethod
+def copy(cls, lines, default=None):
+default = {} if default is None else default.copy()
+default.setdefault('invoice_payments', None)
+return super().copy(lines, default=default)
+
+@classmethod
 def get_invoice_payment(cls, lines, name):
 pool = Pool()
 InvoicePaymentLine = pool.get('account.invoice-account.move.line')



[tryton-commits] changeset in modules/account_invoice:6.0 Do not copy invoice pay...

2021-11-02 Thread Nicolas Évrard
changeset de52945e5a1f in modules/account_invoice:6.0
details: 
https://hg.tryton.org/modules/account_invoice?cmd=changeset=de52945e5a1f
description:
Do not copy invoice payments when copying move lines

issue10502
review367121002
(grafted from 24ecdc76c60aedbd4f62501e46338371b9fabce1)
diffstat:

 account.py |  6 ++
 1 files changed, 6 insertions(+), 0 deletions(-)

diffs (16 lines):

diff -r 8fdde22c6a2d -r de52945e5a1f account.py
--- a/account.pyWed Sep 01 22:59:02 2021 +0200
+++ b/account.pyFri Oct 29 17:03:20 2021 +0200
@@ -261,6 +261,12 @@
 'account.invoice.line', 'account.invoice.tax']
 
 @classmethod
+def copy(cls, lines, default=None):
+default = {} if default is None else default.copy()
+default.setdefault('invoice_payments', None)
+return super().copy(lines, default=default)
+
+@classmethod
 def get_invoice_payment(cls, lines, name):
 pool = Pool()
 InvoicePaymentLine = pool.get('account.invoice-account.move.line')



[tryton-commits] changeset in modules/account_invoice:5.0 Do not copy invoice pay...

2021-11-02 Thread Nicolas Évrard
changeset 0a1b68ee851f in modules/account_invoice:5.0
details: 
https://hg.tryton.org/modules/account_invoice?cmd=changeset=0a1b68ee851f
description:
Do not copy invoice payments when copying move lines

issue10502
review367121002
(grafted from 24ecdc76c60aedbd4f62501e46338371b9fabce1)
diffstat:

 account.py |  6 ++
 1 files changed, 6 insertions(+), 0 deletions(-)

diffs (16 lines):

diff -r 713ffeaf3450 -r 0a1b68ee851f account.py
--- a/account.pyWed Sep 01 22:59:39 2021 +0200
+++ b/account.pyFri Oct 29 17:03:20 2021 +0200
@@ -220,6 +220,12 @@
 cls._check_modify_exclude.add('invoice_payment')
 
 @classmethod
+def copy(cls, lines, default=None):
+default = {} if default is None else default.copy()
+default.setdefault('invoice_payments', None)
+return super().copy(lines, default=default)
+
+@classmethod
 def get_invoice_payment(cls, lines, name):
 pool = Pool()
 InvoicePaymentLine = pool.get('account.invoice-account.move.line')



[tryton-commits] changeset in modules/account:6.0 Test GL context values against ...

2021-11-02 Thread Nicolas Évrard
changeset b49cef68eccf in modules/account:6.0
details: https://hg.tryton.org/modules/account?cmd=changeset=b49cef68eccf
description:
Test GL context values against None instead of using their presence

issue10896
review385271002
(grafted from 1e8fd17cfc05205c24ec565f96b6fe14bf9bf39c)
diffstat:

 account.py |  6 +++---
 1 files changed, 3 insertions(+), 3 deletions(-)

diffs (17 lines):

diff -r b8e1dbe63fe9 -r b49cef68eccf account.py
--- a/account.pyTue Oct 12 11:29:34 2021 +0200
+++ b/account.pyWed Oct 27 17:01:59 2021 +0200
@@ -1724,10 +1724,10 @@
 Account = cls._get_account()
 
 period_ids, from_date, to_date = None, None, None
-context_keys = Transaction().context.keys()
-if context_keys & {'start_period', 'end_period'}:
+context = Transaction().context
+if context.get('start_period') or context.get('end_period'):
 period_ids = cls.get_period_ids(name)
-elif context_keys & {'from_date', 'end_date'}:
+elif context.get('from_date') or context.get('end_date'):
 from_date, to_date = cls.get_dates(name)
 else:
 if name.startswith('start_'):



[tryton-commits] changeset in modules/account:5.8 Test GL context values against ...

2021-11-02 Thread Nicolas Évrard
changeset b815f9d87027 in modules/account:5.8
details: https://hg.tryton.org/modules/account?cmd=changeset=b815f9d87027
description:
Test GL context values against None instead of using their presence

issue10896
review385271002
(grafted from 1e8fd17cfc05205c24ec565f96b6fe14bf9bf39c)
diffstat:

 account.py |  6 +++---
 1 files changed, 3 insertions(+), 3 deletions(-)

diffs (17 lines):

diff -r 457a43244a31 -r b815f9d87027 account.py
--- a/account.pyTue Oct 12 11:29:34 2021 +0200
+++ b/account.pyWed Oct 27 17:01:59 2021 +0200
@@ -1689,10 +1689,10 @@
 Account = cls._get_account()
 
 period_ids, from_date, to_date = None, None, None
-context_keys = Transaction().context.keys()
-if context_keys & {'start_period', 'end_period'}:
+context = Transaction().context
+if context.get('start_period') or context.get('end_period'):
 period_ids = cls.get_period_ids(name)
-elif context_keys & {'from_date', 'end_date'}:
+elif context.get('from_date') or context.get('end_date'):
 from_date, to_date = cls.get_dates(name)
 else:
 if name.startswith('start_'):



[tryton-commits] changeset in modules/account:5.0 Test GL context values against ...

2021-11-02 Thread Nicolas Évrard
changeset 9e62f657bd4c in modules/account:5.0
details: https://hg.tryton.org/modules/account?cmd=changeset=9e62f657bd4c
description:
Test GL context values against None instead of using their presence

issue10896
review385271002
(grafted from 1e8fd17cfc05205c24ec565f96b6fe14bf9bf39c)
diffstat:

 account.py |  6 +++---
 1 files changed, 3 insertions(+), 3 deletions(-)

diffs (17 lines):

diff -r 7e4d170b3f7c -r 9e62f657bd4c account.py
--- a/account.pyTue Oct 12 11:29:34 2021 +0200
+++ b/account.pyWed Oct 27 17:01:59 2021 +0200
@@ -1385,10 +1385,10 @@
 Account = pool.get('account.account')
 
 period_ids, from_date, to_date = None, None, None
-context_keys = Transaction().context.keys()
-if context_keys & {'start_period', 'end_period'}:
+context = Transaction().context
+if context.get('start_period') or context.get('end_period'):
 period_ids = cls.get_period_ids(name)
-elif context_keys & {'from_date', 'end_date'}:
+elif context.get('from_date') or context.get('end_date'):
 from_date, to_date = cls.get_dates(name)
 else:
 if name.startswith('start_'):



[tryton-commits] changeset in modules/account_invoice:default Do not copy invoice...

2021-10-29 Thread Nicolas Évrard
changeset 24ecdc76c60a in modules/account_invoice:default
details: 
https://hg.tryton.org/modules/account_invoice?cmd=changeset=24ecdc76c60a
description:
Do not copy invoice payments when copying move lines

issue10502
review367121002
diffstat:

 account.py |  6 ++
 1 files changed, 6 insertions(+), 0 deletions(-)

diffs (16 lines):

diff -r d653206ef0ca -r 24ecdc76c60a account.py
--- a/account.pyThu Oct 21 00:03:36 2021 +0200
+++ b/account.pyFri Oct 29 17:03:20 2021 +0200
@@ -261,6 +261,12 @@
 'account.invoice.line', 'account.invoice.tax']
 
 @classmethod
+def copy(cls, lines, default=None):
+default = {} if default is None else default.copy()
+default.setdefault('invoice_payments', None)
+return super().copy(lines, default=default)
+
+@classmethod
 def get_invoice_payment(cls, lines, name):
 pool = Pool()
 InvoicePaymentLine = pool.get('account.invoice-account.move.line')



[tryton-commits] changeset in tryton:6.0 Fix typo in Gdk identifier

2021-10-22 Thread Nicolas Évrard
changeset 310c54d59854 in tryton:6.0
details: https://hg.tryton.org/tryton?cmd=changeset=310c54d59854
description:
Fix typo in Gdk identifier

issue10882
review354241002
(grafted from ad169d67663af50ba14d203d891bdf040a975c56)
diffstat:

 tryton/gui/window/win_export.py |  2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)

diffs (12 lines):

diff -r ffe200f85eb7 -r 310c54d59854 tryton/gui/window/win_export.py
--- a/tryton/gui/window/win_export.py   Tue Oct 19 00:23:30 2021 +0200
+++ b/tryton/gui/window/win_export.py   Wed Oct 20 15:18:58 2021 +0200
@@ -444,7 +444,7 @@
 return True
 
 def export_keypress(self, treeview, event):
-if event.keyval not in [Gdk.KEY_Return, Gdk.KEY_.space]:
+if event.keyval not in [Gdk.KEY_Return, Gdk.KEY_space]:
 return
 model, selected = treeview.get_selection().get_selected()
 if not selected:



[tryton-commits] changeset in tryton:5.8 Fix typo in Gdk identifier

2021-10-22 Thread Nicolas Évrard
changeset 80b122d3d328 in tryton:5.8
details: https://hg.tryton.org/tryton?cmd=changeset=80b122d3d328
description:
Fix typo in Gdk identifier

issue10882
review354241002
(grafted from ad169d67663af50ba14d203d891bdf040a975c56)
diffstat:

 tryton/gui/window/win_export.py |  2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)

diffs (12 lines):

diff -r e8a99916f407 -r 80b122d3d328 tryton/gui/window/win_export.py
--- a/tryton/gui/window/win_export.py   Tue Oct 19 00:23:30 2021 +0200
+++ b/tryton/gui/window/win_export.py   Wed Oct 20 15:18:58 2021 +0200
@@ -415,7 +415,7 @@
 return True
 
 def export_keypress(self, treeview, event):
-if event.keyval not in [Gdk.KEY_Return, Gdk.KEY_.space]:
+if event.keyval not in [Gdk.KEY_Return, Gdk.KEY_space]:
 return
 model, selected = treeview.get_selection().get_selected()
 if not selected:



[tryton-commits] changeset in modules/account:6.0 Computes GeneralLedger timespan...

2021-10-28 Thread Nicolas Évrard
changeset b8e1dbe63fe9 in modules/account:6.0
details: https://hg.tryton.org/modules/account?cmd=changeset=b8e1dbe63fe9
description:
Computes GeneralLedger timespan on either dates or period

issue10249
review351991002
(grafted from 1b3d9d7a758f7db21e171103b17f27442583cea8)
diffstat:

 account.py |  49 ++---
 1 files changed, 42 insertions(+), 7 deletions(-)

diffs (97 lines):

diff -r b81bb4d92123 -r b8e1dbe63fe9 account.py
--- a/account.pyWed Sep 01 23:00:23 2021 +0200
+++ b/account.pyTue Oct 12 11:29:34 2021 +0200
@@ -1073,7 +1073,7 @@
 values[name][account.id] += getattr(deferral, name)
 else:
 with Transaction().set_context(fiscalyear=fiscalyear.id,
-date=None, periods=None):
+date=None, periods=None, from_date=None, to_date=None):
 previous_result = func(accounts, names)
 for name in names:
 vals = values[name]
@@ -1723,8 +1723,16 @@
 def get_account(cls, records, name):
 Account = cls._get_account()
 
-period_ids = cls.get_period_ids(name)
-from_date, to_date = cls.get_dates(name)
+period_ids, from_date, to_date = None, None, None
+context_keys = Transaction().context.keys()
+if context_keys & {'start_period', 'end_period'}:
+period_ids = cls.get_period_ids(name)
+elif context_keys & {'from_date', 'end_date'}:
+from_date, to_date = cls.get_dates(name)
+else:
+if name.startswith('start_'):
+period_ids = []
+
 with Transaction().set_context(
 periods=period_ids,
 from_date=from_date, to_date=to_date):
@@ -1843,27 +1851,42 @@
 domain=[
 ('fiscalyear', '=', Eval('fiscalyear')),
 ('start_date', '<=', (Eval('end_period'), 'start_date')),
-], depends=['fiscalyear', 'end_period'])
+],
+states={
+'invisible': Eval('from_date', False) | Eval('to_date', False),
+},
+depends=['fiscalyear', 'end_period', 'from_date', 'to_date'])
 end_period = fields.Many2One('account.period', 'End Period',
 domain=[
 ('fiscalyear', '=', Eval('fiscalyear')),
 ('start_date', '>=', (Eval('start_period'), 'start_date'))
 ],
-depends=['fiscalyear', 'start_period'])
+states={
+'invisible': Eval('from_date', False) | Eval('to_date', False),
+},
+depends=['fiscalyear', 'start_period', 'from_date', 'to_date'])
 from_date = fields.Date("From Date",
 domain=[
 If(Eval('to_date') & Eval('from_date'),
 ('from_date', '<=', Eval('to_date')),
 ()),
 ],
-depends=['to_date'])
+states={
+'invisible': (Eval('start_period', 'False')
+| Eval('end_period', False)),
+},
+depends=['to_date', 'start_period', 'end_period'])
 to_date = fields.Date("To Date",
 domain=[
 If(Eval('from_date') & Eval('to_date'),
 ('to_date', '>=', Eval('from_date')),
 ()),
 ],
-depends=['from_date'])
+states={
+'invisible': (Eval('start_period', 'False')
+| Eval('end_period', False)),
+},
+depends=['from_date', 'start_period', 'end_period'])
 company = fields.Many2One('company.company', 'Company', required=True)
 posted = fields.Boolean('Posted Move', help="Only included posted moves.")
 journal = fields.Many2One(
@@ -1920,6 +1943,18 @@
 and self.end_period.fiscalyear != self.fiscalyear):
 self.end_period = None
 
+def on_change_start_period(self):
+self.from_date = self.to_date = None
+
+def on_change_end_period(self):
+self.from_date = self.to_date = None
+
+def on_change_from_date(self):
+self.start_period = self.end_period = None
+
+def on_change_to_date(self):
+self.start_period = self.end_period = None
+
 
 class GeneralLedgerAccountParty(_GeneralLedgerAccount):
 "General Ledger Account Party"



[tryton-commits] changeset in modules/account:5.8 Computes GeneralLedger timespan...

2021-10-28 Thread Nicolas Évrard
changeset 457a43244a31 in modules/account:5.8
details: https://hg.tryton.org/modules/account?cmd=changeset=457a43244a31
description:
Computes GeneralLedger timespan on either dates or period

issue10249
review351991002
(grafted from 1b3d9d7a758f7db21e171103b17f27442583cea8)
diffstat:

 account.py |  49 ++---
 1 files changed, 42 insertions(+), 7 deletions(-)

diffs (97 lines):

diff -r 9b2ae6b16182 -r 457a43244a31 account.py
--- a/account.pyThu Jul 01 21:47:58 2021 +0200
+++ b/account.pyTue Oct 12 11:29:34 2021 +0200
@@ -1030,7 +1030,7 @@
 values[name][account.id] += getattr(deferral, name)
 else:
 with Transaction().set_context(fiscalyear=fiscalyear.id,
-date=None, periods=None):
+date=None, periods=None, from_date=None, to_date=None):
 previous_result = func(accounts, names)
 for name in names:
 vals = values[name]
@@ -1688,8 +1688,16 @@
 def get_account(cls, records, name):
 Account = cls._get_account()
 
-period_ids = cls.get_period_ids(name)
-from_date, to_date = cls.get_dates(name)
+period_ids, from_date, to_date = None, None, None
+context_keys = Transaction().context.keys()
+if context_keys & {'start_period', 'end_period'}:
+period_ids = cls.get_period_ids(name)
+elif context_keys & {'from_date', 'end_date'}:
+from_date, to_date = cls.get_dates(name)
+else:
+if name.startswith('start_'):
+period_ids = []
+
 with Transaction().set_context(
 periods=period_ids,
 from_date=from_date, to_date=to_date):
@@ -1808,27 +1816,42 @@
 domain=[
 ('fiscalyear', '=', Eval('fiscalyear')),
 ('start_date', '<=', (Eval('end_period'), 'start_date')),
-], depends=['fiscalyear', 'end_period'])
+],
+states={
+'invisible': Eval('from_date', False) | Eval('to_date', False),
+},
+depends=['fiscalyear', 'end_period', 'from_date', 'to_date'])
 end_period = fields.Many2One('account.period', 'End Period',
 domain=[
 ('fiscalyear', '=', Eval('fiscalyear')),
 ('start_date', '>=', (Eval('start_period'), 'start_date'))
 ],
-depends=['fiscalyear', 'start_period'])
+states={
+'invisible': Eval('from_date', False) | Eval('to_date', False),
+},
+depends=['fiscalyear', 'start_period', 'from_date', 'to_date'])
 from_date = fields.Date("From Date",
 domain=[
 If(Eval('to_date') & Eval('from_date'),
 ('from_date', '<=', Eval('to_date')),
 ()),
 ],
-depends=['to_date'])
+states={
+'invisible': (Eval('start_period', 'False')
+| Eval('end_period', False)),
+},
+depends=['to_date', 'start_period', 'end_period'])
 to_date = fields.Date("To Date",
 domain=[
 If(Eval('from_date') & Eval('to_date'),
 ('to_date', '>=', Eval('from_date')),
 ()),
 ],
-depends=['from_date'])
+states={
+'invisible': (Eval('start_period', 'False')
+| Eval('end_period', False)),
+},
+depends=['from_date', 'start_period', 'end_period'])
 company = fields.Many2One('company.company', 'Company', required=True)
 posted = fields.Boolean('Posted Move', help="Only included posted moves.")
 
@@ -1874,6 +1897,18 @@
 and self.end_period.fiscalyear != self.fiscalyear):
 self.end_period = None
 
+def on_change_start_period(self):
+self.from_date = self.to_date = None
+
+def on_change_end_period(self):
+self.from_date = self.to_date = None
+
+def on_change_from_date(self):
+self.start_period = self.end_period = None
+
+def on_change_to_date(self):
+self.start_period = self.end_period = None
+
 
 class GeneralLedgerAccountParty(_GeneralLedgerAccount):
 "General Ledger Account Party"



[tryton-commits] changeset in modules/account:5.0 Computes GeneralLedger timespan...

2021-10-28 Thread Nicolas Évrard
changeset 7e4d170b3f7c in modules/account:5.0
details: https://hg.tryton.org/modules/account?cmd=changeset=7e4d170b3f7c
description:
Computes GeneralLedger timespan on either dates or period

issue10249
review351991002
(grafted from 1b3d9d7a758f7db21e171103b17f27442583cea8)
diffstat:

 account.py |  49 ++---
 1 files changed, 42 insertions(+), 7 deletions(-)

diffs (97 lines):

diff -r 1b9e3e358d94 -r 7e4d170b3f7c account.py
--- a/account.pyThu Jun 17 21:42:39 2021 +0200
+++ b/account.pyTue Oct 12 11:29:34 2021 +0200
@@ -932,7 +932,7 @@
 values[name][account.id] += getattr(deferral, name)
 else:
 with Transaction().set_context(fiscalyear=fiscalyear.id,
-date=None, periods=None):
+date=None, periods=None, from_date=None, to_date=None):
 previous_result = func(accounts, names)
 for name in names:
 vals = values[name]
@@ -1384,8 +1384,16 @@
 pool = Pool()
 Account = pool.get('account.account')
 
-period_ids = cls.get_period_ids(name)
-from_date, to_date = cls.get_dates(name)
+period_ids, from_date, to_date = None, None, None
+context_keys = Transaction().context.keys()
+if context_keys & {'start_period', 'end_period'}:
+period_ids = cls.get_period_ids(name)
+elif context_keys & {'from_date', 'end_date'}:
+from_date, to_date = cls.get_dates(name)
+else:
+if name.startswith('start_'):
+period_ids = []
+
 with Transaction().set_context(
 periods=period_ids,
 from_date=from_date, to_date=to_date):
@@ -1485,27 +1493,42 @@
 domain=[
 ('fiscalyear', '=', Eval('fiscalyear')),
 ('start_date', '<=', (Eval('end_period'), 'start_date')),
-], depends=['fiscalyear', 'end_period'])
+],
+states={
+'invisible': Eval('from_date', False) | Eval('to_date', False),
+},
+depends=['fiscalyear', 'end_period', 'from_date', 'to_date'])
 end_period = fields.Many2One('account.period', 'End Period',
 domain=[
 ('fiscalyear', '=', Eval('fiscalyear')),
 ('start_date', '>=', (Eval('start_period'), 'start_date'))
 ],
-depends=['fiscalyear', 'start_period'])
+states={
+'invisible': Eval('from_date', False) | Eval('to_date', False),
+},
+depends=['fiscalyear', 'start_period', 'from_date', 'to_date'])
 from_date = fields.Date("From Date",
 domain=[
 If(Eval('to_date') & Eval('from_date'),
 ('from_date', '<=', Eval('to_date')),
 ()),
 ],
-depends=['to_date'])
+states={
+'invisible': (Eval('start_period', 'False')
+| Eval('end_period', False)),
+},
+depends=['to_date', 'start_period', 'end_period'])
 to_date = fields.Date("To Date",
 domain=[
 If(Eval('from_date') & Eval('to_date'),
 ('to_date', '>=', Eval('from_date')),
 ()),
 ],
-depends=['from_date'])
+states={
+'invisible': (Eval('start_period', 'False')
+| Eval('end_period', False)),
+},
+depends=['from_date', 'start_period', 'end_period'])
 company = fields.Many2One('company.company', 'Company', required=True)
 posted = fields.Boolean('Posted Move', help='Show only posted move')
 
@@ -1551,6 +1574,18 @@
 and self.end_period.fiscalyear != self.fiscalyear):
 self.end_period = None
 
+def on_change_start_period(self):
+self.from_date = self.to_date = None
+
+def on_change_end_period(self):
+self.from_date = self.to_date = None
+
+def on_change_from_date(self):
+self.start_period = self.end_period = None
+
+def on_change_to_date(self):
+self.start_period = self.end_period = None
+
 
 class GeneralLedgerLine(ModelSQL, ModelView):
 'General Ledger Line'



[tryton-commits] changeset in modules/account:default Test GL context values agai...

2021-10-27 Thread Nicolas Évrard
changeset 1e8fd17cfc05 in modules/account:default
details: https://hg.tryton.org/modules/account?cmd=changeset=1e8fd17cfc05
description:
Test GL context values against None instead of using their presence

issue10896
review385271002
diffstat:

 account.py |  6 +++---
 1 files changed, 3 insertions(+), 3 deletions(-)

diffs (17 lines):

diff -r 6af390be96cf -r 1e8fd17cfc05 account.py
--- a/account.pyFri Oct 22 19:28:00 2021 +0200
+++ b/account.pyWed Oct 27 17:01:59 2021 +0200
@@ -1681,10 +1681,10 @@
 Account = cls._get_account()
 
 period_ids, from_date, to_date = None, None, None
-context_keys = Transaction().context.keys()
-if context_keys & {'start_period', 'end_period'}:
+context = Transaction().context
+if context.get('start_period') or context.get('end_period'):
 period_ids = cls.get_period_ids(name)
-elif context_keys & {'from_date', 'end_date'}:
+elif context.get('from_date') or context.get('end_date'):
 from_date, to_date = cls.get_dates(name)
 else:
 if name.startswith('start_'):



[tryton-commits] changeset in modules/project_invoice:6.0 Take into account up to...

2021-11-29 Thread Nicolas Évrard
changeset 01cc29f7f793 in modules/project_invoice:6.0
details: 
https://hg.tryton.org/modules/project_invoice?cmd=changeset=01cc29f7f793
description:
Take into account up to date when computing invoice quantity

issue10836
review373811003
(grafted from 36f5c90fef9919986ea5d3a9d4fd1e9c60f3174b)
diffstat:

 project.py   |  37 ++-
 tests/scenario_project_invoice_timesheet.rst |  26 ++-
 2 files changed, 49 insertions(+), 14 deletions(-)

diffs (104 lines):

diff -r f9af646b52db -r 01cc29f7f793 project.py
--- a/project.pyMon May 03 16:01:11 2021 +0200
+++ b/project.pyMon Nov 22 10:12:44 2021 +0100
@@ -247,19 +247,32 @@
 cursor = Transaction().connection.cursor()
 line = TimesheetLine.__table__()
 
+upto2tworks = defaultdict(list)
+twork2work = {}
+for work in works:
+upto = work.invoice_timesheet_up_to
+for timesheet_work in work.timesheet_works:
+twork2work[timesheet_work.id] = work.id
+upto2tworks[upto].append(timesheet_work.id)
+
 durations = defaultdict(datetime.timedelta)
-twork2work = {tw.id: w.id for w in works for tw in w.timesheet_works}
-for sub_ids in grouped_slice(twork2work.keys()):
-red_sql = reduce_ids(line.work, sub_ids)
-cursor.execute(*line.select(line.work, Sum(line.duration),
-where=red_sql & (line.invoice_line == Null),
-group_by=line.work))
-for twork_id, duration in cursor:
-if duration:
-# SQLite uses float for SUM
-if not isinstance(duration, datetime.timedelta):
-duration = datetime.timedelta(seconds=duration)
-durations[twork2work[twork_id]] += duration
+query = line.select(
+line.work, Sum(line.duration),
+group_by=line.work)
+for upto, tworks in upto2tworks.items():
+for sub_ids in grouped_slice(tworks):
+query.where = (reduce_ids(line.work, sub_ids)
+& (line.invoice_line == Null))
+if upto:
+query.where &= (line.date <= upto)
+cursor.execute(*query)
+
+for twork_id, duration in cursor:
+if duration:
+# SQLite uses float for SUM
+if not isinstance(duration, datetime.timedelta):
+duration = datetime.timedelta(seconds=duration)
+durations[twork2work[twork_id]] += duration
 
 quantities = {}
 for work in works:
diff -r f9af646b52db -r 01cc29f7f793 
tests/scenario_project_invoice_timesheet.rst
--- a/tests/scenario_project_invoice_timesheet.rst  Mon May 03 16:01:11 
2021 +0200
+++ b/tests/scenario_project_invoice_timesheet.rst  Mon Nov 22 10:12:44 
2021 +0100
@@ -46,8 +46,9 @@
 >>> project_invoice_user.login = 'project_invoice'
 >>> project_invoice_group, = Group.find([('name', '=', 'Project Invoice')])
 >>> project_group, = Group.find([('name', '=', 'Project Administration')])
+>>> invoice_group, = Group.find([('name', '=', 'Account')])
 >>> project_invoice_user.groups.extend(
-... [project_invoice_group, project_group])
+... [project_invoice_group, project_group, invoice_group])
 >>> project_invoice_user.save()
 
 Create chart of accounts::
@@ -160,8 +161,19 @@
 >>> set_user(project_invoice_user)
 >>> project.click('invoice')
 >>> project.amount_to_invoice
+Decimal('0.00')
+>>> project.invoiced_amount
+Decimal('60.00')
+
+>>> project.project_invoice_timesheet_up_to = today
+>>> project.save()
+>>> project.amount_to_invoice
 Decimal('40.00')
->>> project.invoiced_amount
+
+>>> set_user(project_invoice_user)
+>>> Invoice = Model.get('account.invoice')
+>>> invoice, = Invoice.find([])
+>>> invoice.total_amount
 Decimal('60.00')
 
 Invoice all project::
@@ -176,6 +188,11 @@
 >>> project.invoiced_amount
 Decimal('100.00')
 
+>>> set_user(project_invoice_user)
+>>> _, invoice = Invoice.find([], order=[('id', 'ASC')])
+>>> invoice.total_amount
+Decimal('40.00')
+
 Create more timesheets::
 
 >>> set_user(project_user)
@@ -202,3 +219,8 @@
 Decimal('0.00')
 >>> project.invoiced_amount
 Decimal('180.00')
+
+>>> set_user(project_invoice_user)
+>>> _, _, invoice = Invoice.find([], order=[('id', 'ASC')])
+>>> invoice.total_amount
+Decimal('80.00')



[tryton-commits] changeset in modules/project_invoice:6.2 Take into account up to...

2021-11-29 Thread Nicolas Évrard
changeset b861d455d5b7 in modules/project_invoice:6.2
details: 
https://hg.tryton.org/modules/project_invoice?cmd=changeset=b861d455d5b7
description:
Take into account up to date when computing invoice quantity

issue10836
review373811003
(grafted from 36f5c90fef9919986ea5d3a9d4fd1e9c60f3174b)
diffstat:

 project.py   |  37 ++-
 tests/scenario_project_invoice_timesheet.rst |  26 ++-
 2 files changed, 49 insertions(+), 14 deletions(-)

diffs (104 lines):

diff -r 8a5a3e2f10d0 -r b861d455d5b7 project.py
--- a/project.pyMon Nov 01 17:27:33 2021 +0100
+++ b/project.pyMon Nov 22 10:12:44 2021 +0100
@@ -250,19 +250,32 @@
 cursor = Transaction().connection.cursor()
 line = TimesheetLine.__table__()
 
+upto2tworks = defaultdict(list)
+twork2work = {}
+for work in works:
+upto = work.invoice_timesheet_up_to
+for timesheet_work in work.timesheet_works:
+twork2work[timesheet_work.id] = work.id
+upto2tworks[upto].append(timesheet_work.id)
+
 durations = defaultdict(datetime.timedelta)
-twork2work = {tw.id: w.id for w in works for tw in w.timesheet_works}
-for sub_ids in grouped_slice(twork2work.keys()):
-red_sql = reduce_ids(line.work, sub_ids)
-cursor.execute(*line.select(line.work, Sum(line.duration),
-where=red_sql & (line.invoice_line == Null),
-group_by=line.work))
-for twork_id, duration in cursor:
-if duration:
-# SQLite uses float for SUM
-if not isinstance(duration, datetime.timedelta):
-duration = datetime.timedelta(seconds=duration)
-durations[twork2work[twork_id]] += duration
+query = line.select(
+line.work, Sum(line.duration),
+group_by=line.work)
+for upto, tworks in upto2tworks.items():
+for sub_ids in grouped_slice(tworks):
+query.where = (reduce_ids(line.work, sub_ids)
+& (line.invoice_line == Null))
+if upto:
+query.where &= (line.date <= upto)
+cursor.execute(*query)
+
+for twork_id, duration in cursor:
+if duration:
+# SQLite uses float for SUM
+if not isinstance(duration, datetime.timedelta):
+duration = datetime.timedelta(seconds=duration)
+durations[twork2work[twork_id]] += duration
 
 quantities = {}
 for work in works:
diff -r 8a5a3e2f10d0 -r b861d455d5b7 
tests/scenario_project_invoice_timesheet.rst
--- a/tests/scenario_project_invoice_timesheet.rst  Mon Nov 01 17:27:33 
2021 +0100
+++ b/tests/scenario_project_invoice_timesheet.rst  Mon Nov 22 10:12:44 
2021 +0100
@@ -46,8 +46,9 @@
 >>> project_invoice_user.login = 'project_invoice'
 >>> project_invoice_group, = Group.find([('name', '=', 'Project Invoice')])
 >>> project_group, = Group.find([('name', '=', 'Project Administration')])
+>>> invoice_group, = Group.find([('name', '=', 'Account')])
 >>> project_invoice_user.groups.extend(
-... [project_invoice_group, project_group])
+... [project_invoice_group, project_group, invoice_group])
 >>> project_invoice_user.save()
 
 Create chart of accounts::
@@ -160,8 +161,19 @@
 >>> set_user(project_invoice_user)
 >>> project.click('invoice')
 >>> project.amount_to_invoice
+Decimal('0.00')
+>>> project.invoiced_amount
+Decimal('60.00')
+
+>>> project.project_invoice_timesheet_up_to = today
+>>> project.save()
+>>> project.amount_to_invoice
 Decimal('40.00')
->>> project.invoiced_amount
+
+>>> set_user(project_invoice_user)
+>>> Invoice = Model.get('account.invoice')
+>>> invoice, = Invoice.find([])
+>>> invoice.total_amount
 Decimal('60.00')
 
 Invoice all project::
@@ -176,6 +188,11 @@
 >>> project.invoiced_amount
 Decimal('100.00')
 
+>>> set_user(project_invoice_user)
+>>> _, invoice = Invoice.find([], order=[('id', 'ASC')])
+>>> invoice.total_amount
+Decimal('40.00')
+
 Create more timesheets::
 
 >>> set_user(project_user)
@@ -202,3 +219,8 @@
 Decimal('0.00')
 >>> project.invoiced_amount
 Decimal('180.00')
+
+>>> set_user(project_invoice_user)
+>>> _, _, invoice = Invoice.find([], order=[('id', 'ASC')])
+>>> invoice.total_amount
+Decimal('80.00')



[tryton-commits] changeset in www.tryton.org:default Remove bitpay donation form

2022-02-15 Thread Nicolas Évrard
changeset e5bf688bb6f9 in www.tryton.org:default
details: https://hg.tryton.org/www.tryton.org?cmd=changeset=e5bf688bb6f9
description:
Remove bitpay donation form

review385711005
diffstat:

 templates/donate.html |  58 ---
 1 files changed, 0 insertions(+), 58 deletions(-)

diffs (68 lines):

diff -r f92c122dbaa8 -r e5bf688bb6f9 templates/donate.html
--- a/templates/donate.html Tue Feb 08 10:14:24 2022 +0100
+++ b/templates/donate.html Tue Feb 15 15:53:28 2022 +0100
@@ -92,64 +92,6 @@
 
 
 
-Bitcoin
-
-
-You can easily donate in bitcoin:
-https://bitpay.com/checkout; method="post">
-
-
-
-
-
-
-
-USD
-BTC
-EUR
-GBP
-AUD
-BGN
-BRL
-CAD
-CHF
-CNY
-CZK
-DKK
-HKD
-HRK
-HUF
-IDR
-ILS
-INR
-JPY
-KRW
-LTL
-LVL
-MXN
-MYR
-NOK
-NZD
-PHP
-PLN
-RON
-RUB
-SEK
-SGD
-THB
-TRY
-ZAR
-
-
-
-{{ heart | safe }}Donate
-
-
-
-
-
-
-
 Contribute
 
 



[tryton-commits] changeset in modules/account:default Add missing alias on migrat...

2022-02-10 Thread Nicolas Évrard
changeset 0ab649829988 in modules/account:default
details: https://hg.tryton.org/modules/account?cmd=changeset=0ab649829988
description:
Add missing alias on migration query of Deferrals

issue11072
review360881005
diffstat:

 account.py |  3 ++-
 1 files changed, 2 insertions(+), 1 deletions(-)

diffs (13 lines):

diff -r 3bb05ae28b9d -r 0ab649829988 account.py
--- a/account.pyFri Feb 04 15:39:41 2022 +0100
+++ b/account.pyThu Feb 10 13:35:19 2022 +0100
@@ -1652,7 +1652,8 @@
 .join(move, condition=move_line.move == move.id)
 .join(period, condition=move.period == period.id)
 .select(
-move_line.account, period.fiscalyear, Count(Literal('*')),
+move_line.account, period.fiscalyear,
+Count(Literal('*')).as_('line_count'),
 group_by=[move_line.account, period.fiscalyear]))
 cursor.execute(*deferral.update(
 [deferral.line_count], [counting_query.line_count],



[tryton-commits] changeset in sao:6.2 Return rec_name RPC call promise when avail...

2022-02-26 Thread Nicolas Évrard
changeset 1fa900154756 in sao:6.2
details: https://hg.tryton.org/sao?cmd=changeset=1fa900154756
description:
Return rec_name RPC call promise when available in M2O field set and 
set_default

issue11030
review374421002
(grafted from 4585dcb48a1a96f78c776ac9612d4c8fb4e6d265)
diffstat:

 src/model.js |  15 +++
 1 files changed, 7 insertions(+), 8 deletions(-)

diffs (27 lines):

diff -r ea8f5d48d503 -r 1fa900154756 src/model.js
--- a/src/model.js  Wed Feb 16 21:59:03 2022 +0100
+++ b/src/model.js  Tue Feb 08 18:55:25 2022 +0100
@@ -2075,16 +2075,15 @@
 if (!rec_name && (value >= 0) && (value !== null)) {
 var model_name = record.model.fields[this.name].description
 .relation;
-Sao.rpc({
+var remote_rec_name = Sao.rpc({
 'method': 'model.' + model_name + '.read',
 'params': [[value], ['rec_name'], record.get_context()]
-}, record.model.session).done(store_rec_name.bind(this)).done(
-function() {
-record.group.root_group.screens.forEach(
-function(screen) {
-screen.display();
-});
-   });
+}, record.model.session, false);
+store_rec_name(remote_rec_name);
+record.group.root_group.screens.forEach(
+function (screen) {
+screen.display();
+});
 } else {
 store_rec_name.call(this, [{'rec_name': rec_name}]);
 }



[tryton-commits] changeset in sao:5.0 Return rec_name RPC call promise when avail...

2022-02-26 Thread Nicolas Évrard
changeset 5c064d99853e in sao:5.0
details: https://hg.tryton.org/sao?cmd=changeset=5c064d99853e
description:
Return rec_name RPC call promise when available in M2O field set and 
set_default

issue11030
review374421002
(grafted from 4585dcb48a1a96f78c776ac9612d4c8fb4e6d265)
diffstat:

 src/model.js |  15 +++
 1 files changed, 7 insertions(+), 8 deletions(-)

diffs (27 lines):

diff -r af9424826d00 -r 5c064d99853e src/model.js
--- a/src/model.js  Wed Feb 16 22:00:07 2022 +0100
+++ b/src/model.js  Tue Feb 08 18:55:25 2022 +0100
@@ -1898,16 +1898,15 @@
 if (!rec_name && (value >= 0) && (value !== null)) {
 var model_name = record.model.fields[this.name].description
 .relation;
-Sao.rpc({
+var remote_rec_name = Sao.rpc({
 'method': 'model.' + model_name + '.read',
 'params': [[value], ['rec_name'], record.get_context()]
-}, record.model.session).done(store_rec_name.bind(this)).done(
-function() {
-record.group.root_group().screens.forEach(
-function(screen) {
-screen.display();
-});
-   });
+}, record.model.session, false);
+store_rec_name(remote_rec_name);
+record.group.root_group().screens.forEach(
+function (screen) {
+screen.display();
+});
 } else {
 store_rec_name.call(this, [{'rec_name': rec_name}]);
 }



<    1   2   3   4   5   >