3011 lines
		
	
	
		
			115 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			3011 lines
		
	
	
		
			115 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* This file is part of Tryton.  The COPYRIGHT file at the top level of
 | |
|    this repository contains the full copyright notices and license terms. */
 | |
| (function() {
 | |
|     'use strict';
 | |
| 
 | |
|     function get_x2m_sub_fields(f_attrs, prefix) {
 | |
|         if (f_attrs.visible && !jQuery.isEmptyObject(f_attrs.views)) {
 | |
|             // There's only one key but we don't know its value
 | |
|             const [[, view],] = Object.entries(f_attrs.views);
 | |
| 
 | |
|             const sub_fields = view.fields || {};
 | |
|             const x2m_sub_fields = [];
 | |
| 
 | |
|             for (const [s_field, f_def] of Object.entries(sub_fields)) {
 | |
|                 x2m_sub_fields.push(`${prefix}.${s_field}`);
 | |
| 
 | |
|                 var type_ = f_def.type;
 | |
|                 if (['many2one', 'one2one', 'reference'].includes(type_)) {
 | |
|                     x2m_sub_fields.push(`${prefix}.${s_field}.rec_name`);
 | |
|                 } else if (['selection', 'multiselection'].includes(type_)) {
 | |
|                     x2m_sub_fields.push(`${prefix}.${s_field}:string`);
 | |
|                 } else if (['one2many', 'many2many'].includes(type_)) {
 | |
|                     x2m_sub_fields.push(
 | |
|                         ...get_x2m_sub_fields(f_def, `${prefix}.${s_field}`)
 | |
|                     );
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             x2m_sub_fields.push(
 | |
|                 `${prefix}._timestamp`,
 | |
|                 `${prefix}._write`,
 | |
|                 `${prefix}._delete`);
 | |
|             return x2m_sub_fields;
 | |
|         } else {
 | |
|             return [];
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     Sao.Model = Sao.class_(Object, {
 | |
|         init: function(name, attributes) {
 | |
|             attributes = attributes || {};
 | |
|             this.name = name;
 | |
|             this.session = Sao.Session.current_session;
 | |
|             this.fields = {};
 | |
|         },
 | |
|         add_fields: function(descriptions) {
 | |
|             var added = [];
 | |
|             for (var name in descriptions) {
 | |
|                 var desc = descriptions[name];
 | |
|                 if (!(name in this.fields)) {
 | |
|                     var Field = Sao.field.get(desc.type);
 | |
|                     this.fields[name] = new Field(desc);
 | |
|                     added.push(name);
 | |
|                 } else {
 | |
|                     jQuery.extend(this.fields[name].description, desc);
 | |
|                 }
 | |
|             }
 | |
|             return added;
 | |
|         },
 | |
|         execute: function(
 | |
|                 method, params, context={}, async=true,
 | |
|                 process_exception=true) {
 | |
|             var args = {
 | |
|                 'method': 'model.' + this.name + '.' + method,
 | |
|                 'params': params.concat(context)
 | |
|             };
 | |
|             return Sao.rpc(args, this.session, async, process_exception);
 | |
|         },
 | |
|         copy: function(records, context) {
 | |
|             if (jQuery.isEmptyObject(records)) {
 | |
|                 return jQuery.when();
 | |
|             }
 | |
|             var record_ids = records.map(function(record) {
 | |
|                 return record.id;
 | |
|             });
 | |
|             return this.execute('copy', [record_ids, {}], context);
 | |
|         }
 | |
|     });
 | |
| 
 | |
|     Sao.Group = function(model, context, array) {
 | |
|         array.prm = jQuery.when();
 | |
|         array.model = model;
 | |
|         array._context = context;
 | |
|         array.on_write = [];
 | |
|         array.parent = undefined;
 | |
|         array.screens = [];
 | |
|         array.parent_name = '';
 | |
|         array.children = [];
 | |
|         array.child_name = '';
 | |
|         array.parent_datetime_field = undefined;
 | |
|         array.record_removed = [];
 | |
|         array.record_deleted = [];
 | |
|         array.__readonly = false;
 | |
|         array.exclude_field = null;
 | |
|         array.skip_model_access = false;
 | |
|         array.forEach(function(e, i, a) {
 | |
|             e.group = a;
 | |
|         });
 | |
|         Object.defineProperty(array, 'readonly', {
 | |
|             get: function() {
 | |
|                 // Must skip res.user for Preference windows
 | |
|                 var access = Sao.common.MODELACCESS.get(this.model.name);
 | |
|                 if (this.context._datetime ||
 | |
|                     (!(access.write || access.create) &&
 | |
|                         !this.skip_model_access)) {
 | |
|                     return true;
 | |
|                 }
 | |
|                 return this.__readonly;
 | |
|             },
 | |
|             set: function(value) {
 | |
|                 this.__readonly = value;
 | |
|             }
 | |
|         });
 | |
|         array.load = function(ids, modified=false, position=-1, preloaded=null) {
 | |
|             if (position == -1) {
 | |
|                 position = this.length;
 | |
|             }
 | |
|             var new_records = [];
 | |
|             for (const id of ids) {
 | |
|                 let new_record = this.get(id);
 | |
|                 if (!new_record) {
 | |
|                     new_record = new Sao.Record(this.model, id);
 | |
|                     new_record.group = this;
 | |
|                     this.splice(position, 0, new_record);
 | |
|                     position += 1;
 | |
|                 }
 | |
|                 if (preloaded && (id in preloaded)) {
 | |
|                     new_record.set(preloaded[id], false, false);
 | |
|                 }
 | |
|                 new_records.push(new_record);
 | |
|             }
 | |
|             // Remove previously removed or deleted records
 | |
|             var record_removed = [];
 | |
|             for (const record of this.record_removed) {
 | |
|                 if (!~ids.indexOf(record.id)) {
 | |
|                     record_removed.push(record);
 | |
|                 }
 | |
|             }
 | |
|             this.record_removed = record_removed;
 | |
|             var record_deleted = [];
 | |
|             for (const record of this.record_deleted) {
 | |
|                 if (!~ids.indexOf(record.id)) {
 | |
|                     record_deleted.push(record);
 | |
|                 }
 | |
|             }
 | |
|             this.record_deleted = record_deleted;
 | |
|             if (new_records.length && modified) {
 | |
|                 for (const record of new_records) {
 | |
|                     record.modified_fields.id = true;
 | |
|                 }
 | |
|                 this.record_modified();
 | |
|             }
 | |
|         };
 | |
|         array.get = function(id) {
 | |
|             // TODO optimize
 | |
|             for (const record of this) {
 | |
|                 if (record.id == id) {
 | |
|                     return record;
 | |
|                 }
 | |
|             }
 | |
|         };
 | |
|         array.new_ = function(default_, id, defaults=null) {
 | |
|             var record = new Sao.Record(this.model, id);
 | |
|             record.group = this;
 | |
|             if (default_) {
 | |
|                 record.default_get(defaults);
 | |
|             }
 | |
|             return record;
 | |
|         };
 | |
|         array.add = function(record, position=-1, modified=true) {
 | |
|             if (position == -1) {
 | |
|                 position = this.length;
 | |
|             }
 | |
|             position = Math.min(position, this.length);
 | |
|             if (record.group != this) {
 | |
|                 record.group = this;
 | |
|             }
 | |
|             if (this.indexOf(record) < 0) {
 | |
|                 this.splice(position, 0, record);
 | |
|             }
 | |
|             for (var record_rm of this.record_removed) {
 | |
|                 if (record_rm.id == record.id) {
 | |
|                     this.record_removed.splice(
 | |
|                             this.record_removed.indexOf(record_rm), 1);
 | |
|                 }
 | |
|             }
 | |
|             for (var record_del of this.record_deleted) {
 | |
|                 if (record_del.id == record.id) {
 | |
|                     this.record_deleted.splice(
 | |
|                             this.record_deleted.indexOf(record_del), 1);
 | |
|                 }
 | |
|             }
 | |
|             record.modified_fields.id = true;
 | |
|             if (modified) {
 | |
|                 // Set parent field to trigger on_change
 | |
|                 if (this.parent && this.model.fields[this.parent_name]) {
 | |
|                     var field = this.model.fields[this.parent_name];
 | |
|                     if ((field instanceof Sao.field.Many2One) ||
 | |
|                             field instanceof Sao.field.Reference) {
 | |
|                         var value = [this.parent.id, ''];
 | |
|                         if (field instanceof Sao.field.Reference) {
 | |
|                             value = [this.parent.model.name, value];
 | |
|                         }
 | |
|                         field.set_client(record, value);
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|             return record;
 | |
|         };
 | |
|         array.remove = function(
 | |
|             record, remove, force_remove=false, modified=true) {
 | |
|             if (record.id >= 0) {
 | |
|                 if (remove) {
 | |
|                     if (~this.record_deleted.indexOf(record)) {
 | |
|                         this.record_deleted.splice(
 | |
|                                 this.record_deleted.indexOf(record), 1);
 | |
|                     }
 | |
|                     if (!~this.record_removed.indexOf(record)) {
 | |
|                         this.record_removed.push(record);
 | |
|                     }
 | |
|                 } else {
 | |
|                     if (~this.record_removed.indexOf(record)) {
 | |
|                         this.record_removed.splice(
 | |
|                                 this.record_removed.indexOf(record), 1);
 | |
|                     }
 | |
|                     if (!~this.record_deleted.indexOf(record)) {
 | |
|                         this.record_deleted.push(record);
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|             record.modified_fields.id = true;
 | |
|             if ((record.id < 0) ||
 | |
|                 (this.parent && this.parent.id < 0) ||
 | |
|                 force_remove) {
 | |
|                 this._remove(record);
 | |
|             }
 | |
|             if (modified) {
 | |
|                 this.record_modified();
 | |
|             }
 | |
|         };
 | |
|         array._remove = function(record) {
 | |
|             var idx = this.indexOf(record);
 | |
|             this.splice(idx, 1);
 | |
|             record.destroy();
 | |
|         };
 | |
|         array.unremove = function(record) {
 | |
|             this.record_removed.splice(this.record_removed.indexOf(record), 1);
 | |
|             this.record_deleted.splice(this.record_deleted.indexOf(record), 1);
 | |
|             record.group.record_modified();
 | |
|         };
 | |
|         array.clear = function() {
 | |
|             this.splice(0, this.length);
 | |
|             this.record_removed = [];
 | |
|             this.record_deleted = [];
 | |
|         };
 | |
|         array.record_modified = function() {
 | |
|             if (!this.parent) {
 | |
|                 for (const screen of this.screens) {
 | |
|                     screen.record_modified();
 | |
|                 }
 | |
|             } else {
 | |
|                 this.parent.modified_fields[this.child_name] = true;
 | |
|                 this.parent.model.fields[this.child_name].changed(this.parent);
 | |
|                 this.parent.validate(null, true, false);
 | |
|                 this.parent.group.record_modified();
 | |
|             }
 | |
|         };
 | |
|         array.record_notify = function(notifications) {
 | |
|             for (const screen of this.screens) {
 | |
|                 screen.record_notify(notifications);
 | |
|             }
 | |
|         };
 | |
|         array.delete_ = function(records) {
 | |
|             if (jQuery.isEmptyObject(records)) {
 | |
|                 return jQuery.when();
 | |
|             }
 | |
|             var root_group = this.root_group;
 | |
|             Sao.Logger.assert(records.every(
 | |
|                 r => r.model.name == this.model.name),
 | |
|                 'records not from the same model');
 | |
|             Sao.Logger.assert(records.every(
 | |
|                 r =>  r.group.root_group == root_group),
 | |
|                 'records not from the same root group');
 | |
|             records = records.filter(record => record.id >= 0);
 | |
|             var context = this.context;
 | |
|             context._timestamp = {};
 | |
|             for (const record of records) {
 | |
|                 jQuery.extend(context._timestamp, record.get_timestamp());
 | |
|             }
 | |
|             var record_ids = records.map(function(record) {
 | |
|                 return record.id;
 | |
|             });
 | |
|             return root_group.on_write_ids(record_ids).then(reload_ids => {
 | |
|                 for (const record of records) {
 | |
|                     record.destroy();
 | |
|                 }
 | |
|                 reload_ids = reload_ids.filter(e => !~record_ids.indexOf(e));
 | |
|                 return this.model.execute('delete', [record_ids], context)
 | |
|                 .then(() => {
 | |
|                     root_group.reload(reload_ids);
 | |
|                 });
 | |
|             });
 | |
|         };
 | |
|         Object.defineProperty(array, 'root_group', {
 | |
|             get: function() {
 | |
|                 var root = this;
 | |
|                 var parent = this.parent;
 | |
|                 while (parent) {
 | |
|                     root = parent.group;
 | |
|                     parent = parent.parent;
 | |
|                 }
 | |
|                 return root;
 | |
|             }
 | |
|         });
 | |
|         array.save = function() {
 | |
|             var deferreds = [];
 | |
|             this.forEach(record => {
 | |
|                 deferreds.push(record.save());
 | |
|             });
 | |
|             if (!jQuery.isEmptyObject(this.record_deleted)) {
 | |
|                 for (const record of this.record_deleted) {
 | |
|                     this._remove(record);
 | |
|                 }
 | |
|                 deferreds.push(this.delete_(this.record_deleted));
 | |
|                 this.record_deleted.splice(0, this.record_deleted.length);
 | |
|             }
 | |
|             return jQuery.when.apply(jQuery, deferreds);
 | |
|         };
 | |
|         array.written = function(ids) {
 | |
|             if (typeof(ids) == 'number') {
 | |
|                 ids = [ids];
 | |
|             }
 | |
|             return this.on_write_ids(ids).then(to_reload => {
 | |
|                 to_reload = to_reload.filter(e => !~ids.indexOf(e));
 | |
|                 this.root_group.reload(to_reload);
 | |
|             });
 | |
|         };
 | |
|         array.reload = function(ids) {
 | |
|             for (const child of this.children) {
 | |
|                 child.reload(ids);
 | |
|             }
 | |
|             for (const id of ids) {
 | |
|                 const record = this.get(id);
 | |
|                 if (record && jQuery.isEmptyObject(record.modified_fields)) {
 | |
|                     record.cancel();
 | |
|                 }
 | |
|             }
 | |
|         };
 | |
|         array.on_write_ids = function(ids) {
 | |
|             var deferreds = [];
 | |
|             var result = [];
 | |
|             this.on_write.forEach(fnct => {
 | |
|                 var prm = this.model.execute(fnct, [ids], this._context)
 | |
|                 .then(res => {
 | |
|                     jQuery.extend(result, res);
 | |
|                 });
 | |
|                 deferreds.push(prm);
 | |
|             });
 | |
|             return jQuery.when.apply(jQuery, deferreds).then(
 | |
|                 () => result.filter((e, i, a) =>  i == a.indexOf(e)));
 | |
|         };
 | |
|         array.set_parent = function(parent) {
 | |
|             this.parent = parent;
 | |
|             if (parent && parent.model.name == this.model.name) {
 | |
|                 this.parent.group.children.push(this);
 | |
|             }
 | |
|         };
 | |
|         array.add_fields = function(fields) {
 | |
|             var added = this.model.add_fields(fields);
 | |
|             if (jQuery.isEmptyObject(this)) {
 | |
|                 return;
 | |
|             }
 | |
|             var new_ = [];
 | |
|             for (const record of this) {
 | |
|                 if (record.id < 0) {
 | |
|                     new_.push(record);
 | |
|                 }
 | |
|             }
 | |
|             if (new_.length && added.length) {
 | |
|                 this.model.execute('default_get', [added, this.context])
 | |
|                     .then(values => {
 | |
|                         for (const record of new_) {
 | |
|                             record.set_default(values, true, false);
 | |
|                         }
 | |
|                         this.record_modified();
 | |
|                     });
 | |
|             }
 | |
|         };
 | |
|         array.destroy = function() {
 | |
|             if (this.parent) {
 | |
|                 var i = this.parent.group.children.indexOf(this);
 | |
|                 if (~i) {
 | |
|                     this.parent.group.children.splice(i, 1);
 | |
|                 }
 | |
|             }
 | |
|             this.parent = null;
 | |
|         };
 | |
|         Object.defineProperty(array, 'domain', {
 | |
|             get: function() {
 | |
|                 var domain = [];
 | |
|                 for (const screen of this.screens) {
 | |
|                     if (screen.attributes.domain) {
 | |
|                         domain.push(screen.attributes.domain);
 | |
|                     }
 | |
|                 }
 | |
|                 if (this.parent && this.child_name) {
 | |
|                     var field = this.parent.model.fields[this.child_name];
 | |
|                     return [domain, field.get_domain(this.parent)];
 | |
|                 } else {
 | |
|                     return domain;
 | |
|                 }
 | |
|             }
 | |
|         });
 | |
|         Object.defineProperty(array, 'context', {
 | |
|             get: function() {
 | |
|                 return this._get_context();
 | |
|             },
 | |
|             set: function(context) {
 | |
|                 this._context = jQuery.extend({}, context);
 | |
|             }
 | |
|         });
 | |
|         Object.defineProperty(array, 'local_context', {
 | |
|             get: function() {
 | |
|                 return this._get_context(true);
 | |
|             }
 | |
|         });
 | |
|         array._get_context = function(local) {
 | |
|             var context;
 | |
|             if (!local) {
 | |
|                 context = jQuery.extend({}, this.model.session.context);
 | |
|             } else {
 | |
|                 context = {};
 | |
|             }
 | |
|             if (this.parent) {
 | |
|                 var parent_context = this.parent.get_context(local);
 | |
|                 jQuery.extend(context, parent_context);
 | |
|                 if (this.child_name in this.parent.model.fields) {
 | |
|                     var field = this.parent.model.fields[this.child_name];
 | |
|                     jQuery.extend(context, field.get_context(
 | |
|                         this.parent, parent_context, local));
 | |
|                 }
 | |
|             }
 | |
|             jQuery.extend(context, this._context);
 | |
|             if (this.parent_datetime_field) {
 | |
|                 context._datetime = this.parent.get_eval()[
 | |
|                     this.parent_datetime_field];
 | |
|             }
 | |
|             return context;
 | |
|         };
 | |
|         array.clean4inversion = function(domain) {
 | |
|             if (jQuery.isEmptyObject(domain)) {
 | |
|                 return [];
 | |
|             }
 | |
|             var inversion = new Sao.common.DomainInversion();
 | |
|             var head = domain[0];
 | |
|             var tail = domain.slice(1);
 | |
|             if (~['AND', 'OR'].indexOf(head)) {
 | |
|                 // pass
 | |
|             } else if (inversion.is_leaf(head)) {
 | |
|                 var field = head[0];
 | |
|                 if ((field in this.model.fields) &&
 | |
|                         (this.model.fields[field].description.readonly)) {
 | |
|                     head = [];
 | |
|                 }
 | |
|             } else {
 | |
|                 head = this.clean4inversion(head);
 | |
|             }
 | |
|             return [head].concat(this.clean4inversion(tail));
 | |
|         };
 | |
|         array.domain4inversion = function() {
 | |
|             var domain = this.domain;
 | |
|             if (!this.__domain4inversion ||
 | |
|                     !Sao.common.compare(this.__domain4inversion[0], domain)) {
 | |
|                 this.__domain4inversion = [domain, this.clean4inversion(domain)];
 | |
|             }
 | |
|             return this.__domain4inversion[1];
 | |
|         };
 | |
|         array.get_by_path = function(path) {
 | |
|             path = jQuery.extend([], path);
 | |
|             var record = null;
 | |
|             var group = this;
 | |
| 
 | |
|             var browse_child = function() {
 | |
|                 if (jQuery.isEmptyObject(path)) {
 | |
|                     return record;
 | |
|                 }
 | |
|                 var child_name = path[0][0];
 | |
|                 var id = path[0][1];
 | |
|                 path.splice(0, 1);
 | |
|                 record = group.get(id);
 | |
|                 if (!record) {
 | |
|                     return null;
 | |
|                 }
 | |
|                 if (!child_name) {
 | |
|                     return browse_child();
 | |
|                 }
 | |
|                 return record.load(child_name).then(function() {
 | |
|                     group = record._values[child_name];
 | |
|                     if (!group) {
 | |
|                         return null;
 | |
|                     }
 | |
|                     return browse_child();
 | |
|                 });
 | |
|             };
 | |
|             return jQuery.when().then(browse_child);
 | |
|         };
 | |
|         array.set_sequence = function(field, position) {
 | |
|             var changed = false;
 | |
|             var prev = null;
 | |
|             var index, update, value, cmp;
 | |
|             if (position === 0) {
 | |
|                 cmp = function(a, b) { return a > b; };
 | |
|             } else {
 | |
|                 cmp = function(a, b) { return a < b; };
 | |
|             }
 | |
|             for (const record of this) {
 | |
|                 if (record.get_loaded([field]) || changed || record.id < 0) {
 | |
|                     if (prev) {
 | |
|                         prev.load(field, false);
 | |
|                         index = prev.field_get(field);
 | |
|                     } else {
 | |
|                         index = null;
 | |
|                     }
 | |
|                     update = false;
 | |
|                     value = record.field_get(field);
 | |
|                     if (value === null) {
 | |
|                         if (index) {
 | |
|                             update = true;
 | |
|                         } else if (prev) {
 | |
|                             if (record.id >= 0) {
 | |
|                                 update = cmp(record.id, prev.id);
 | |
|                             } else if (position === 0) {
 | |
|                                 update = true;
 | |
|                             }
 | |
|                         }
 | |
|                     } else if (value === index) {
 | |
|                         if (prev) {
 | |
|                             if (record.id >= 0) {
 | |
|                                 update = cmp(record.id, prev.id);
 | |
|                             } else if (position === 0) {
 | |
|                                 update = true;
 | |
|                             }
 | |
|                         }
 | |
|                     } else if (value <= (index || 0)) {
 | |
|                         update = true;
 | |
|                     }
 | |
|                     if (update) {
 | |
|                         if (index === null) {
 | |
|                             index = 0;
 | |
|                         }
 | |
|                         index += 1;
 | |
|                         record.field_set_client(field, index);
 | |
|                         changed = record;
 | |
|                     }
 | |
|                 }
 | |
|                 prev = record;
 | |
|             }
 | |
|         };
 | |
|         return array;
 | |
|     };
 | |
| 
 | |
|     Sao.Record = Sao.class_(Object, {
 | |
|         id_counter: -1,
 | |
|         init: function(model, id=null) {
 | |
|             this.model = model;
 | |
|             this.group = Sao.Group(model, {}, []);
 | |
|             if (id === null) {
 | |
|                 this.id = Sao.Record.prototype.id_counter;
 | |
|             } else {
 | |
|                 this.id = id;
 | |
|             }
 | |
|             if (this.id < 0) {
 | |
|                 Sao.Record.prototype.id_counter--;
 | |
|             }
 | |
|             this._values = {};
 | |
|             this.modified_fields = {};
 | |
|             this._loaded = {};
 | |
|             this.fields = {};
 | |
|             this._timestamp = null;
 | |
|             this._write = true;
 | |
|             this._delete = true;
 | |
|             this.resources = null;
 | |
|             this.button_clicks = {};
 | |
|             this.links_counts = {};
 | |
|             this.state_attrs = {};
 | |
|             this.autocompletion = {};
 | |
|             this.exception = false;
 | |
|             this.destroyed = false;
 | |
|             this._save_prm = jQuery.when();
 | |
|         },
 | |
|         get modified() {
 | |
|             if (!jQuery.isEmptyObject(this.modified_fields)) {
 | |
|                 Sao.Logger.info(
 | |
|                     "Modified fields of %s@%s", this.id, this.model.name,
 | |
|                     Object.keys(this.modified_fields));
 | |
|                 return true;
 | |
|             } else {
 | |
|                 return false;
 | |
|             }
 | |
|         },
 | |
|         save: function(force_reload=false) {
 | |
|             var context = this.get_context();
 | |
|             if (this._save_prm.state() == 'pending') {
 | |
|                 return this._save_prm.then(() => this.save(force_reload));
 | |
|             }
 | |
|             var prm = jQuery.when();
 | |
|             if ((this.id < 0) || this.modified) {
 | |
|                 var values = this.get();
 | |
|                 if (this.id < 0) {
 | |
|                     prm = this.model.execute('create', [[values]], context)
 | |
|                         .then(ids => this.id = ids[0]);
 | |
|                 } else {
 | |
|                     if (!jQuery.isEmptyObject(values)) {
 | |
|                         context._timestamp = this.get_timestamp();
 | |
|                         prm = this.model.execute(
 | |
|                             'write', [[this.id], values], context);
 | |
|                     }
 | |
|                 }
 | |
|                 prm = prm.then(() => {
 | |
|                     this.cancel();
 | |
|                     if (force_reload) {
 | |
|                         return this.reload();
 | |
|                     }
 | |
|                 });
 | |
|                 if (this.group) {
 | |
|                     prm = prm.then(() => this.group.written(this.id));
 | |
|                 }
 | |
|             }
 | |
|             if (this.group.parent) {
 | |
|                 delete this.group.parent.modified_fields[this.group.child_name];
 | |
|                 prm = prm.then(() => this.group.parent.save(force_reload));
 | |
|             }
 | |
|             this._save_prm = prm;
 | |
|             return prm;
 | |
|         },
 | |
|         reload: function(fields, async=true) {
 | |
|             if (this.id < 0) {
 | |
|                 return async? jQuery.when() : null;
 | |
|             }
 | |
|             if (!fields) {
 | |
|                 return this.load('*', async);
 | |
|             } else if (!async) {
 | |
|                 for (let field of fields) {
 | |
|                     this.load(field, async);
 | |
|                 }
 | |
|             } else {
 | |
|                 var prms = fields.map(field => this.load(field));
 | |
|                 return jQuery.when.apply(jQuery, prms);
 | |
|             }
 | |
|         },
 | |
|         is_loaded: function(name) {
 | |
|             return ((this.id < 0) || (name in this._loaded));
 | |
|         },
 | |
|         load: function(name, async=true, process_exception=true) {
 | |
|             var fname;
 | |
|             if (this.destroyed || this.is_loaded(name)) {
 | |
|                 if (async) {
 | |
|                     return jQuery.when();
 | |
|                 } else if (name !== '*') {
 | |
|                     return this.model.fields[name];
 | |
|                 } else {
 | |
|                     return;
 | |
|                 }
 | |
|             }
 | |
|             if (async && this.group.prm.state() == 'pending') {
 | |
|                 return this.group.prm.then(() => this.load(name));
 | |
|             }
 | |
|             var id2record = {};
 | |
|             id2record[this.id] = this;
 | |
|             var loading, views, field;
 | |
|             if (name == '*') {
 | |
|                 loading = 'eager';
 | |
|                 views = new Set();
 | |
|                 for (fname in this.model.fields) {
 | |
|                     field = this.model.fields[fname];
 | |
|                     if ((field.description.loading || 'eager') == 'lazy') {
 | |
|                         loading = 'lazy';
 | |
|                     }
 | |
|                     for (const view of field.views) {
 | |
|                         views.add(view);
 | |
|                     }
 | |
|                 }
 | |
|             } else {
 | |
|                 loading = this.model.fields[name].description.loading || 'eager';
 | |
|                 views = this.model.fields[name].views;
 | |
|             }
 | |
|             var fields = {};
 | |
|             var views_operator;
 | |
|             if (loading == 'eager') {
 | |
|                 for (fname in this.model.fields) {
 | |
|                     field = this.model.fields[fname];
 | |
|                     if ((field.description.loading || 'eager') == 'eager') {
 | |
|                         fields[fname] = field;
 | |
|                     }
 | |
|                 }
 | |
|                 views_operator = views.isSubsetOf.bind(views);
 | |
|             } else {
 | |
|                 fields = this.model.fields;
 | |
|                 views_operator = function(view) {
 | |
|                     return Boolean(this.intersection(view).size);
 | |
|                 }.bind(views);
 | |
|             }
 | |
|             var fnames = [];
 | |
|             for (fname in fields) {
 | |
|                 field = fields[fname];
 | |
|                 if (!(fname in this._loaded) &&
 | |
|                     (!views.size ||
 | |
|                         views_operator(new Set(field.views)))) {
 | |
|                     fnames.push(fname);
 | |
|                 }
 | |
|             }
 | |
|             var related_read_limit = null;
 | |
|             var fnames_to_fetch = fnames.slice();
 | |
|             var rec_named_fields = ['many2one', 'one2one', 'reference'];
 | |
|             const selection_fields = ['selection', 'multiselection'];
 | |
|             for (const fname of fnames) {
 | |
|                 var fdescription = this.model.fields[fname].description;
 | |
|                 if (~rec_named_fields.indexOf(fdescription.type))
 | |
|                     fnames_to_fetch.push(fname + '.rec_name');
 | |
|                 else if (~selection_fields.indexOf(fdescription.type) &&
 | |
|                     ((fdescription.loading || 'lazy') == 'eager')) {
 | |
|                     fnames_to_fetch.push(fname + ':string');
 | |
|                 } else if (
 | |
|                     ['many2many', 'one2many'].includes(fdescription.type)) {
 | |
|                     var sub_fields = get_x2m_sub_fields(fdescription, fname);
 | |
|                     fnames_to_fetch = [ ...fnames_to_fetch, ...sub_fields];
 | |
|                     if (sub_fields.length > 0) {
 | |
|                         related_read_limit = Sao.config.display_size;
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|             if (!~fnames.indexOf('rec_name')) {
 | |
|                 fnames_to_fetch.push('rec_name');
 | |
|             }
 | |
|             fnames_to_fetch.push('_timestamp');
 | |
|             fnames_to_fetch.push('_write');
 | |
|             fnames_to_fetch.push('_delete');
 | |
| 
 | |
|             var context = jQuery.extend({}, this.get_context());
 | |
|             if (related_read_limit) {
 | |
|                 context.related_read_limit = related_read_limit;
 | |
|             }
 | |
|             if (loading == 'eager') {
 | |
|                 var limit = Math.trunc(Sao.config.limit /
 | |
|                     Math.min(fnames_to_fetch.length, 10));
 | |
| 
 | |
|                 const filter_group = record => {
 | |
|                     return (!record.destroyed &&
 | |
|                         (record.id >= 0) &&
 | |
|                         !(name in record._loaded));
 | |
|                 };
 | |
|                 const filter_parent_group = record => {
 | |
|                     return (filter_group(record) &&
 | |
|                             (id2record[record.id] === undefined) &&
 | |
|                             ((record.group === this.group) ||
 | |
|                              // Don't compute context for same group
 | |
|                              (JSON.stringify(record.get_context()) ===
 | |
|                               JSON.stringify(context))));
 | |
|                 };
 | |
|                 var group, filter;
 | |
|                 if (this.group.parent &&
 | |
|                         (this.group.parent.model.name == this.model.name)) {
 | |
|                     group = [];
 | |
|                     group = group.concat.apply(
 | |
|                             group, this.group.parent.group.children);
 | |
|                     filter = filter_parent_group;
 | |
|                 } else {
 | |
|                     group = this.group;
 | |
|                     filter = filter_group;
 | |
|                 }
 | |
|                 var idx = group.indexOf(this);
 | |
|                 if (~idx) {
 | |
|                     var length = group.length;
 | |
|                     var n = 1;
 | |
|                     while ((Object.keys(id2record).length < limit) &&
 | |
|                         ((idx - n >= 0) || (idx + n < length)) &&
 | |
|                         (n < 2 * limit)) {
 | |
|                             var record;
 | |
|                             if (idx - n >= 0) {
 | |
|                                 record = group[idx - n];
 | |
|                                 if (filter(record)) {
 | |
|                                     id2record[record.id] = record;
 | |
|                                 }
 | |
|                             }
 | |
|                             if (idx + n < length) {
 | |
|                                 record = group[idx + n];
 | |
|                                 if (filter(record)) {
 | |
|                                     id2record[record.id] = record;
 | |
|                                 }
 | |
|                             }
 | |
|                             n++;
 | |
|                         }
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             for (fname in this.model.fields) {
 | |
|                 if ((this.model.fields[fname].description.type == 'binary') &&
 | |
|                         ~fnames_to_fetch.indexOf(fname, fnames_to_fetch)) {
 | |
|                     context[this.model.name + '.' + fname] = 'size';
 | |
|                 }
 | |
|             }
 | |
|             var result = this.model.execute('read', [
 | |
|                 Object.keys(id2record).map( e => parseInt(e, 10)),
 | |
|                 fnames_to_fetch], context, async, process_exception);
 | |
|             const succeed = (values, exception=false) => {
 | |
|                 var id2value = {};
 | |
|                 for (const e of values) {
 | |
|                     id2value[e.id] = e;
 | |
|                 }
 | |
|                 for (var id in id2record) {
 | |
|                     var record = id2record[id];
 | |
|                     if (!record.exception) {
 | |
|                         record.exception = exception;
 | |
|                     }
 | |
|                     var value = id2value[id];
 | |
|                     if (record && value) {
 | |
|                         for (var key in this.modified_fields) {
 | |
|                             delete value[key];
 | |
|                         }
 | |
|                         record.set(value, false);
 | |
|                     }
 | |
|                 }
 | |
|             };
 | |
|             const failed = () => {
 | |
|                 var failed_values = [];
 | |
|                 var default_values = {};
 | |
|                 for (let fname of fnames_to_fetch) {
 | |
|                     if (fname != 'id') {
 | |
|                         default_values[fname] = null;
 | |
|                     }
 | |
|                 }
 | |
| 
 | |
|                 for (let id in id2record) {
 | |
|                     failed_values.push(Object.assign({'id': id}, default_values));
 | |
|                 }
 | |
|                 return succeed(failed_values, true);
 | |
|             };
 | |
|             if (async) {
 | |
|                 this.group.prm = result.then(succeed, failed);
 | |
|                 return this.group.prm;
 | |
|             } else {
 | |
|                 if (result) {
 | |
|                     succeed(result);
 | |
|                 } else {
 | |
|                     failed();
 | |
|                 }
 | |
|                 if (name !== '*') {
 | |
|                     return this.model.fields[name];
 | |
|                 } else {
 | |
|                     return;
 | |
|                 }
 | |
|             }
 | |
|         },
 | |
|         set: function(values, modified=true, validate=true) {
 | |
|             var name, value;
 | |
|             var later = {};
 | |
|             var fieldnames = [];
 | |
|             for (name in values) {
 | |
|                 value = values[name];
 | |
|                 if (name == '_timestamp') {
 | |
|                     // Always keep the older timestamp
 | |
|                     if (!this._timestamp) {
 | |
|                         this._timestamp = value;
 | |
|                     }
 | |
|                     continue;
 | |
|                 }
 | |
|                 if (name == '_write' || name == '_delete') {
 | |
|                     this[name] = value;
 | |
|                     continue;
 | |
|                 }
 | |
|                 if (!(name in this.model.fields)) {
 | |
|                     if (name == 'rec_name') {
 | |
|                         this._values[name] = value;
 | |
|                     }
 | |
|                     continue;
 | |
|                 }
 | |
|                 if (this.model.fields[name] instanceof Sao.field.One2Many) {
 | |
|                     later[name] = value;
 | |
|                     continue;
 | |
|                 }
 | |
|                 const field = this.model.fields[name];
 | |
|                 var related;
 | |
|                 if ((field instanceof Sao.field.Many2One) ||
 | |
|                         (field instanceof Sao.field.Reference)) {
 | |
|                     related = name + '.';
 | |
|                     this._values[related] = values[related] || {};
 | |
|                 } else if ((field instanceof Sao.field.Selection) ||
 | |
|                     (field instanceof Sao.field.MultiSelection)) {
 | |
|                     related = name + ':string';
 | |
|                     if (name + ':string' in values) {
 | |
|                         this._values[related] = values[related];
 | |
|                     } else {
 | |
|                         delete this._values[related];
 | |
|                     }
 | |
|                 }
 | |
|                 this.model.fields[name].set(this, value);
 | |
|                 this._loaded[name] = true;
 | |
|                 fieldnames.push(name);
 | |
|             }
 | |
|             for (name in later) {
 | |
|                 value = later[name];
 | |
|                 this.model.fields[name].set(this, value, false, values[`${name}.`]);
 | |
|                 this._loaded[name] = true;
 | |
|                 fieldnames.push(name);
 | |
|             }
 | |
|             if (validate) {
 | |
|                 this.validate(fieldnames, true, false);
 | |
|             }
 | |
|             if (modified) {
 | |
|                 this.set_modified();
 | |
|             }
 | |
|         },
 | |
|         get: function() {
 | |
|             var value = {};
 | |
|             for (var name in this.model.fields) {
 | |
|                 var field = this.model.fields[name];
 | |
|                 if (field.description.readonly &&
 | |
|                         !((field instanceof Sao.field.One2Many) &&
 | |
|                             !(field instanceof Sao.field.Many2Many))) {
 | |
|                     continue;
 | |
|                 }
 | |
|                 if ((this.modified_fields[name] === undefined) && this.id >= 0) {
 | |
|                     continue;
 | |
|                 }
 | |
|                 value[name] = field.get(this);
 | |
|                 // Sending an empty x2MField breaks ModelFieldAccess.check
 | |
|                 if ((field instanceof Sao.field.One2Many) &&
 | |
|                         (value[name].length === 0)) {
 | |
|                     delete value[name];
 | |
|                 }
 | |
|             }
 | |
|             return value;
 | |
|         },
 | |
|         invalid_fields: function() {
 | |
|             var fields = {};
 | |
|             for (var fname in this.model.fields) {
 | |
|                 var field = this.model.fields[fname];
 | |
|                 var invalid = field.get_state_attrs(this).invalid;
 | |
|                 if (invalid) {
 | |
|                     fields[fname] = invalid;
 | |
|                 }
 | |
|             }
 | |
|             return fields;
 | |
|         },
 | |
|         get_context: function(local) {
 | |
|             if (!local) {
 | |
|                 return this.group.context;
 | |
|             } else {
 | |
|                 return this.group.local_context;
 | |
|             }
 | |
|         },
 | |
|         field_get: function(name) {
 | |
|             return this.model.fields[name].get(this);
 | |
|         },
 | |
|         field_set: function(name, value) {
 | |
|             this.model.fields[name].set(this, value);
 | |
|         },
 | |
|         field_get_client: function(name) {
 | |
|             return this.model.fields[name].get_client(this);
 | |
|         },
 | |
|         field_set_client: function(name, value, force_change) {
 | |
|             this.model.fields[name].set_client(this, value, force_change);
 | |
|         },
 | |
|         default_get: function(defaults=null) {
 | |
|             if (!jQuery.isEmptyObject(this.model.fields)) {
 | |
|                 var context = this.get_context();
 | |
|                 if (defaults) {
 | |
|                     for (const name in defaults) {
 | |
|                         Sao.setdefault(context, `default_${name}` ,defaults[name]);
 | |
|                     }
 | |
|                 }
 | |
|                 var prm = this.model.execute('default_get',
 | |
|                         [Object.keys(this.model.fields)], context);
 | |
|                 return prm.then(values => {
 | |
|                     if (this.group.parent &&
 | |
|                             this.group.parent_name in this.group.model.fields) {
 | |
|                         var parent_field =
 | |
|                             this.group.model.fields[this.group.parent_name];
 | |
|                         if (parent_field instanceof Sao.field.Reference) {
 | |
|                             values[this.group.parent_name] = [
 | |
|                                 this.group.parent.model.name,
 | |
|                                 this.group.parent.id];
 | |
|                         } else if (parent_field.description.relation ==
 | |
|                                 this.group.parent.model.name) {
 | |
|                             values[this.group.parent_name] =
 | |
|                                 this.group.parent.id;
 | |
|                         }
 | |
|                     }
 | |
|                     return this.set_default(values);
 | |
|                 });
 | |
|             }
 | |
|             return jQuery.when();
 | |
|         },
 | |
|         set_default: function(values, validate=true, modified=true) {
 | |
|             var promises = [];
 | |
|             var fieldnames = [];
 | |
|             for (var fname in values) {
 | |
|                 if ((fname == '_write') ||
 | |
|                     (fname == '_delete') ||
 | |
|                     (fname == '_timestamp')) {
 | |
|                     this[fname] = values[fname];
 | |
|                     continue;
 | |
|                 }
 | |
|                 var value = values[fname];
 | |
|                 if (!(fname in this.model.fields)) {
 | |
|                     continue;
 | |
|                 }
 | |
|                 if (fname == this.group.exclude_field) {
 | |
|                     continue;
 | |
|                 }
 | |
|                 if ((this.model.fields[fname] instanceof Sao.field.Many2One) ||
 | |
|                         (this.model.fields[fname] instanceof Sao.field.Reference)) {
 | |
|                     var related = fname + '.';
 | |
|                     this._values[related] = values[related] || {};
 | |
|                 }
 | |
|                 promises.push(this.model.fields[fname].set_default(this, value));
 | |
|                 this._loaded[fname] = true;
 | |
|                 fieldnames.push(fname);
 | |
|             }
 | |
|             return jQuery.when.apply(jQuery, promises).then(() => {
 | |
|                 this.on_change(fieldnames);
 | |
|                 this.on_change_with(fieldnames);
 | |
|                 if (validate) {
 | |
|                     return this.validate(null, true);
 | |
|                 }
 | |
|                 if (modified) {
 | |
|                     this.set_modified();
 | |
|                     return jQuery.when.apply(
 | |
|                         jQuery, this.group.root_group.screens
 | |
|                         .map(screen => screen.display()));
 | |
|                 }
 | |
|             });
 | |
|         },
 | |
|         get_timestamp: function() {
 | |
|             var timestamps = {};
 | |
|             timestamps[this.model.name + ',' + this.id] = this._timestamp;
 | |
|             for (var fname in this.model.fields) {
 | |
|                 if (!(fname in this._loaded)) {
 | |
|                     continue;
 | |
|                 }
 | |
|                 jQuery.extend(timestamps,
 | |
|                     this.model.fields[fname].get_timestamp(this));
 | |
|             }
 | |
|             return timestamps;
 | |
|         },
 | |
|         get_eval: function() {
 | |
|             var value = {};
 | |
|             for (var key in this.model.fields) {
 | |
|                 if (!(key in this._loaded) && this.id >= 0)
 | |
|                     continue;
 | |
|                 value[key] = this.model.fields[key].get_eval(this);
 | |
|             }
 | |
|             value.id = this.id;
 | |
|             return value;
 | |
|         },
 | |
|         get_on_change_value: function(skip) {
 | |
|             var value = {};
 | |
|             for (var key in this.model.fields) {
 | |
|                 if (skip && ~skip.indexOf(key)) {
 | |
|                     continue;
 | |
|                 }
 | |
|                 if ((this.id >= 0) &&
 | |
|                         (!this._loaded[key] || !this.modified_fields[key])) {
 | |
|                     continue;
 | |
|                 }
 | |
|                 value[key] = this.model.fields[key].get_on_change_value(this);
 | |
|             }
 | |
|             value.id = this.id;
 | |
|             return value;
 | |
|         },
 | |
|         _get_on_change_args: function(args) {
 | |
|             var result = {};
 | |
|             var values = Sao.common.EvalEnvironment(this, 'on_change');
 | |
|             for (const arg of args) {
 | |
|                 var scope = values;
 | |
|                 for (const e of arg.split('.')) {
 | |
|                     if (scope !== undefined) {
 | |
|                         scope = scope[e];
 | |
|                     }
 | |
|                 }
 | |
|                 result[arg] = scope;
 | |
|             }
 | |
|             return result;
 | |
|         },
 | |
|         on_change: function(fieldnames) {
 | |
|             var values = {};
 | |
|             for (const fieldname of fieldnames) {
 | |
|                 var on_change = this.model.fields[fieldname]
 | |
|                 .description.on_change;
 | |
|                 if (!jQuery.isEmptyObject(on_change)) {
 | |
|                     values = jQuery.extend(values,
 | |
|                         this._get_on_change_args(on_change));
 | |
|                 }
 | |
|             }
 | |
|             let modified = new Set(fieldnames);
 | |
|             if (!jQuery.isEmptyObject(values)) {
 | |
|                 values.id = this.id;
 | |
|                 var changes;
 | |
|                 try {
 | |
|                     if ((fieldnames.length == 1) ||
 | |
|                         (values.id === undefined)) {
 | |
|                         changes = [];
 | |
|                         for (const fieldname of fieldnames) {
 | |
|                             changes.push(this.model.execute(
 | |
|                                 'on_change_' + fieldname,
 | |
|                                 [values], this.get_context(), false));
 | |
|                         }
 | |
|                     } else {
 | |
|                         changes = [this.model.execute(
 | |
|                             'on_change',
 | |
|                             [values, fieldnames], this.get_context(), false)];
 | |
|                     }
 | |
|                 } catch (e) {
 | |
|                     return;
 | |
|                 }
 | |
|                 changes.forEach((values) => {
 | |
|                     this.set_on_change(values);
 | |
|                     for (let fieldname in values) {
 | |
|                         modified.add(fieldname);
 | |
|                     }
 | |
|                 });
 | |
|             }
 | |
| 
 | |
|             var notification_fields = Sao.common.MODELNOTIFICATION.get(
 | |
|                 this.model.name);
 | |
|             if (modified.intersection(new Set(notification_fields)).size) {
 | |
|                 values = this._get_on_change_args(notification_fields);
 | |
|                 this.model.execute(
 | |
|                     'on_change_notify', [values], this.get_context())
 | |
|                     .then(this.group.record_notify.bind(this.group));
 | |
|             }
 | |
|         },
 | |
|         on_change_with: function(field_names) {
 | |
|             var fieldnames = {};
 | |
|             var values = {};
 | |
|             var later = {};
 | |
|             var fieldname, on_change_with;
 | |
|             for (fieldname in this.model.fields) {
 | |
|                 on_change_with = this.model.fields[fieldname]
 | |
|                     .description.on_change_with;
 | |
|                 if (jQuery.isEmptyObject(on_change_with)) {
 | |
|                     continue;
 | |
|                 }
 | |
|                 for (var i = 0; i < field_names.length; i++) {
 | |
|                     if (~on_change_with.indexOf(field_names[i])) {
 | |
|                         break;
 | |
|                     }
 | |
|                 }
 | |
|                 if (i >= field_names.length) {
 | |
|                     continue;
 | |
|                 }
 | |
|                 if (!jQuery.isEmptyObject(Sao.common.intersect(
 | |
|                                 Object.keys(fieldnames).sort(),
 | |
|                                 on_change_with.sort()))) {
 | |
|                     later[fieldname] = true;
 | |
|                     continue;
 | |
|                 }
 | |
|                 fieldnames[fieldname] = true;
 | |
|                 values = jQuery.extend(values,
 | |
|                     this._get_on_change_args(
 | |
|                         on_change_with.concat([fieldname])));
 | |
|                 if ((this.model.fields[fieldname] instanceof
 | |
|                             Sao.field.Many2One) ||
 | |
|                         (this.model.fields[fieldname] instanceof
 | |
|                          Sao.field.Reference)) {
 | |
|                     delete this._values[fieldname + '.'];
 | |
|                 }
 | |
|             }
 | |
|             var changed;
 | |
|             fieldnames = Object.keys(fieldnames);
 | |
|             if (fieldnames.length) {
 | |
|                 try {
 | |
|                     if ((fieldnames.length == 1) ||
 | |
|                         (values.id === undefined)) {
 | |
|                         changed = {};
 | |
|                         for (const fieldname of fieldnames) {
 | |
|                             changed = jQuery.extend(
 | |
|                                 changed,
 | |
|                                 this.model.execute(
 | |
|                                     'on_change_with_' + fieldname,
 | |
|                                     [values], this.get_context(), false));
 | |
|                         }
 | |
|                     } else {
 | |
|                         values.id = this.id;
 | |
|                         changed = this.model.execute(
 | |
|                             'on_change_with',
 | |
|                             [values, fieldnames], this.get_context(), false);
 | |
|                     }
 | |
|                 } catch (e) {
 | |
|                     return;
 | |
|                 }
 | |
|                 this.set_on_change(changed);
 | |
|             }
 | |
|             if (!jQuery.isEmptyObject(later)) {
 | |
|                 values = {};
 | |
|                 for (const fieldname in later) {
 | |
|                     on_change_with = this.model.fields[fieldname]
 | |
|                         .description.on_change_with;
 | |
|                     values = jQuery.extend(
 | |
|                         values,
 | |
|                         this._get_on_change_args(
 | |
|                             on_change_with.concat([fieldname])));
 | |
|                 }
 | |
|                 fieldnames = Object.keys(later);
 | |
|                 try {
 | |
|                     if ((fieldnames.length == 1) ||
 | |
|                         (values.id === undefined)) {
 | |
|                         changed = {};
 | |
|                         for (const fieldname of fieldnames) {
 | |
|                             changed = jQuery.extend(
 | |
|                                 changed,
 | |
|                                 this.model.execute(
 | |
|                                     'on_change_with_' + fieldname,
 | |
|                                     [values], this.get_context(), false));
 | |
|                         }
 | |
|                     } else {
 | |
|                         values.id = this.id;
 | |
|                         changed = this.model.execute(
 | |
|                             'on_change_with',
 | |
|                             [values, fieldnames], this.get_context(), false);
 | |
|                     }
 | |
|                 } catch (e) {
 | |
|                     return;
 | |
|                 }
 | |
|                 this.set_on_change(changed);
 | |
|             }
 | |
|             let notification_fields = Sao.common.MODELNOTIFICATION.get(
 | |
|                 this.model.name);
 | |
|             if (new Set(field_names).intersection(new Set(notification_fields)).size) {
 | |
|                 values = this._get_on_change_args(notification_fields);
 | |
|                 this.model.execute(
 | |
|                     'on_change_notify', [values], this.get_context())
 | |
|                     .then(this.group.record_notify.bind(this.group));
 | |
|             }
 | |
|         },
 | |
|         set_on_change: function(values) {
 | |
|             var fieldname, value;
 | |
|             for (fieldname in values) {
 | |
|                 value = values[fieldname];
 | |
|                 if (!(fieldname in this.model.fields)) {
 | |
|                     continue;
 | |
|                 }
 | |
|                 if ((this.model.fields[fieldname] instanceof
 | |
|                             Sao.field.Many2One) ||
 | |
|                         (this.model.fields[fieldname] instanceof
 | |
|                          Sao.field.Reference)) {
 | |
|                     var related = fieldname + '.';
 | |
|                     this._values[related] = values[related] || {};
 | |
|                 }
 | |
|                 this.load(fieldname, false).set_on_change(this, value);
 | |
|             }
 | |
|         },
 | |
|         autocomplete_with: function(fieldname) {
 | |
|             for (var fname in this.model.fields) {
 | |
|                 var field = this.model.fields[fname];
 | |
|                 var autocomplete = field.description.autocomplete || [];
 | |
|                 if (!~autocomplete.indexOf(fieldname)) {
 | |
|                     continue;
 | |
|                 }
 | |
|                 this.do_autocomplete(fname);
 | |
|             }
 | |
|         },
 | |
|         do_autocomplete: function(fieldname) {
 | |
|             this.autocompletion[fieldname] = [];
 | |
|             var field = this.model.fields[fieldname];
 | |
|             var autocomplete = field.description.autocomplete;
 | |
|             var values = this._get_on_change_args(autocomplete);
 | |
|             var result;
 | |
|             try {
 | |
|                 result = this.model.execute(
 | |
|                     'autocomplete_' + fieldname, [values], this.get_context(),
 | |
|                     false, false);
 | |
|             } catch (e) {
 | |
|                 result = [];
 | |
|             }
 | |
|             this.autocompletion[fieldname] = result;
 | |
|         },
 | |
|         on_scan_code: function(code, depends) {
 | |
|             depends = this.expr_eval(depends);
 | |
|             var values = this._get_on_change_args(depends);
 | |
|             values.id = this.id;
 | |
|             return this.model.execute(
 | |
|                 'on_scan_code', [values, code], this.get_context(),
 | |
|                 true, false).then((changes) => {
 | |
|                     this.set_on_change(changes);
 | |
|                     this.set_modified();
 | |
|                     return !jQuery.isEmptyObject(changes);
 | |
|                 });
 | |
|         },
 | |
|         reset: function(value) {
 | |
|             this.cancel();
 | |
|             this.set(value, true);
 | |
|             if (this.group.parent) {
 | |
|                 this.group.parent.on_change([this.group.child_name]);
 | |
|                 this.group.parent.on_change_with([this.group.child_name]);
 | |
|             }
 | |
|         },
 | |
|         expr_eval: function(expr) {
 | |
|             if (typeof(expr) != 'string') return expr;
 | |
|             if (!expr) {
 | |
|                 return;
 | |
|             } else if (expr == '[]') {
 | |
|                 return [];
 | |
|             } else if (expr == '{}') {
 | |
|                 return {};
 | |
|             }
 | |
|             var ctx = this.get_eval();
 | |
|             ctx.context = this.get_context();
 | |
|             ctx.active_model = this.model.name;
 | |
|             ctx.active_id = this.id;
 | |
|             if (this.group.parent && this.group.parent_name) {
 | |
|                 var parent_env = Sao.common.EvalEnvironment(this.group.parent);
 | |
|                 ctx['_parent_' + this.group.parent_name] = parent_env;
 | |
|             }
 | |
|             return new Sao.PYSON.Decoder(ctx).decode(expr);
 | |
|         },
 | |
|         rec_name: function() {
 | |
|             var prm = this.model.execute('read', [[this.id], ['rec_name']],
 | |
|                     this.get_context());
 | |
|             return prm.then(function(values) {
 | |
|                 return values[0].rec_name;
 | |
|             });
 | |
|         },
 | |
|         validate: function(fields, softvalidation, pre_validate) {
 | |
|             var result = true;
 | |
|             for (var fname in this.model.fields) {
 | |
|                 var field = this.model.fields[fname];
 | |
|                 if (fields && !~fields.indexOf(fname)) {
 | |
|                     continue;
 | |
|                 }
 | |
|                 if (!this.get_loaded([fname])) {
 | |
|                     continue;
 | |
|                 }
 | |
|                 if (field.description.readonly) {
 | |
|                     continue;
 | |
|                 }
 | |
|                 if (fname == this.group.exclude_field) {
 | |
|                     continue;
 | |
|                 }
 | |
|                 if (!field.validate(this, softvalidation, pre_validate)) {
 | |
|                     result = false;
 | |
|                 }
 | |
|             }
 | |
|             return result;
 | |
|         },
 | |
|         pre_validate: function() {
 | |
|             if (jQuery.isEmptyObject(this.modified_fields)) {
 | |
|                 return jQuery.Deferred().resolve(true);
 | |
|             }
 | |
|             var values = this._get_on_change_args(
 | |
|                 Object.keys(this.modified_fields).concat(['id']));
 | |
|             return this.model.execute('pre_validate',
 | |
|                     [values], this.get_context());
 | |
|         },
 | |
|         cancel: function() {
 | |
|             this._loaded = {};
 | |
|             this._values = {};
 | |
|             this.modified_fields = {};
 | |
|             this._timestamp = null;
 | |
|             this.button_clicks = {};
 | |
|             this.links_counts = {};
 | |
|             this.exception = false;
 | |
|         },
 | |
|         _check_load: function(fields) {
 | |
|             if (!this.get_loaded(fields)) {
 | |
|                 this.reload(fields, false);
 | |
|             }
 | |
|         },
 | |
|         get_loaded: function(fields) {
 | |
|             if (this.id < 0) {
 | |
|                 return true;
 | |
|             }
 | |
|             if (!fields) {
 | |
|                 fields = Object.keys(this.model.fields);
 | |
|             }
 | |
|             fields = new Set(fields);
 | |
|             var loaded = new Set(Object.keys(this._loaded));
 | |
|             loaded = loaded.union(new Set(Object.keys(this.modified_fields)));
 | |
|             return fields.isSubsetOf(loaded);
 | |
|         },
 | |
|         get root_parent() {
 | |
|             var parent = this;
 | |
|             while (parent.group.parent) {
 | |
|                 parent = parent.group.parent;
 | |
|             }
 | |
|             return parent;
 | |
|         },
 | |
|         get_path: function(group) {
 | |
|             var path = [];
 | |
|             var i = this;
 | |
|             var child_name = '';
 | |
|             while (i) {
 | |
|                 path.push([child_name, i.id]);
 | |
|                 if (i.group === group) {
 | |
|                     break;
 | |
|                 }
 | |
|                 child_name = i.group.child_name;
 | |
|                 i = i.group.parent;
 | |
|             }
 | |
|             path.reverse();
 | |
|             return path;
 | |
|         },
 | |
|         get_index_path: function(group) {
 | |
|             var path = [],
 | |
|                 record = this;
 | |
|             while (record) {
 | |
|                 path.push(record.group.indexOf(record));
 | |
|                 if (record.group === group) {
 | |
|                     break;
 | |
|                 }
 | |
|                 record = record.group.parent;
 | |
|             }
 | |
|             path.reverse();
 | |
|             return path;
 | |
|         },
 | |
|         children_group: function(field_name) {
 | |
|             if (!field_name) {
 | |
|                 return [];
 | |
|             }
 | |
|             this._check_load([field_name]);
 | |
|             var group = this._values[field_name];
 | |
|             if (group === undefined) {
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             if (group.model.fields !== this.group.model.fields) {
 | |
|                 jQuery.extend(this.group.model.fields, group.model.fields);
 | |
|                 group.model.fields = this.group.model.fields;
 | |
|             }
 | |
|             group.on_write = this.group.on_write;
 | |
|             group.readonly = this.group.readonly;
 | |
|             jQuery.extend(group._context, this.group._context);
 | |
|             return group;
 | |
|         },
 | |
|         get deleted() {
 | |
|             return Boolean(~this.group.record_deleted.indexOf(this));
 | |
|         },
 | |
|         get removed() {
 | |
|             return Boolean(~this.group.record_removed.indexOf(this));
 | |
|         },
 | |
|         get readonly() {
 | |
|             return (this.deleted ||
 | |
|                 this.removed ||
 | |
|                 this.exception ||
 | |
|                 this.group.readonly ||
 | |
|                 !this._write);
 | |
|         },
 | |
|         get deletable() {
 | |
|             return this._delete;
 | |
|         },
 | |
|         get identity() {
 | |
|             return JSON.stringify(
 | |
|                 Object.keys(this._values).reduce((values, name) => {
 | |
|                     var field = this.model.fields[name];
 | |
|                     if (field) {
 | |
|                         if (field instanceof Sao.field.Binary) {
 | |
|                             values[name] = field.get_size(this);
 | |
|                         } else {
 | |
|                             values[name] = field.get(this);
 | |
|                         }
 | |
|                     }
 | |
|                     return values;
 | |
|                 }, {}));
 | |
|         },
 | |
|         set_field_context: function() {
 | |
|             for (var name in this.model.fields) {
 | |
|                 var field = this.model.fields[name];
 | |
|                 var value = this._values[name];
 | |
|                 if (!(value instanceof Array)) {
 | |
|                     continue;
 | |
|                 }
 | |
|                 var context_descriptor = Object.getOwnPropertyDescriptor(
 | |
|                     value, 'context');
 | |
|                 if (!context_descriptor || !context_descriptor.set) {
 | |
|                     continue;
 | |
|                 }
 | |
|                 var context = field.description.context;
 | |
|                 if (context) {
 | |
|                     value.context = this.expr_eval(context);
 | |
|                 }
 | |
|             }
 | |
|         },
 | |
|         get_resources: function(reload) {
 | |
|             var prm;
 | |
|             if ((this.id >= 0) && (!this.resources || reload)) {
 | |
|                 prm = this.model.execute(
 | |
|                     'resources', [this.id], this.get_context())
 | |
|                     .then(resources => {
 | |
|                         this.resources = resources;
 | |
|                         return resources;
 | |
|                     });
 | |
|             } else {
 | |
|                 prm = jQuery.when(this.resources);
 | |
|             }
 | |
|             return prm;
 | |
|         },
 | |
|         get_button_clicks: function(name) {
 | |
|             if (this.id < 0) {
 | |
|                 return jQuery.when();
 | |
|             }
 | |
|             var clicks = this.button_clicks[name];
 | |
|             if (clicks !== undefined) {
 | |
|                 return jQuery.when(clicks);
 | |
|             }
 | |
|             return Sao.rpc({
 | |
|                 'method': 'model.ir.model.button.click.get_click',
 | |
|                 'params': [this.model.name, name, this.id, {}],
 | |
|             }, this.model.session).then(clicks => {
 | |
|                 this.button_clicks[name] = clicks;
 | |
|                 return clicks;
 | |
|             });
 | |
|         },
 | |
|         set_modified: function(field) {
 | |
|             if (field) {
 | |
|                 this.modified_fields[field] = true;
 | |
|             }
 | |
|             this.group.record_modified();
 | |
|         },
 | |
|         destroy: function() {
 | |
|             var vals = Object.values(this._values);
 | |
|             for (const val of vals) {
 | |
|                 if (val &&
 | |
|                     Object.prototype.hasOwnProperty.call(val, 'destroy')) {
 | |
|                     val.destroy();
 | |
|                 }
 | |
|             }
 | |
|             this.destroyed = true;
 | |
|         }
 | |
|     });
 | |
| 
 | |
| 
 | |
|     Sao.field = {};
 | |
| 
 | |
|     Sao.field.get = function(type) {
 | |
|         switch (type) {
 | |
|             case 'char':
 | |
|                 return Sao.field.Char;
 | |
|             case 'selection':
 | |
|                 return Sao.field.Selection;
 | |
|             case 'multiselection':
 | |
|                 return Sao.field.MultiSelection;
 | |
|             case 'datetime':
 | |
|             case 'timestamp':
 | |
|                 return Sao.field.DateTime;
 | |
|             case 'date':
 | |
|                 return Sao.field.Date;
 | |
|             case 'time':
 | |
|                 return Sao.field.Time;
 | |
|             case 'timedelta':
 | |
|                 return Sao.field.TimeDelta;
 | |
|             case 'float':
 | |
|                 return Sao.field.Float;
 | |
|             case 'numeric':
 | |
|                 return Sao.field.Numeric;
 | |
|             case 'integer':
 | |
|                 return Sao.field.Integer;
 | |
|             case 'boolean':
 | |
|                 return Sao.field.Boolean;
 | |
|             case 'many2one':
 | |
|                 return Sao.field.Many2One;
 | |
|             case 'one2one':
 | |
|                 return Sao.field.One2One;
 | |
|             case 'one2many':
 | |
|                 return Sao.field.One2Many;
 | |
|             case 'many2many':
 | |
|                 return Sao.field.Many2Many;
 | |
|             case 'reference':
 | |
|                 return Sao.field.Reference;
 | |
|             case 'binary':
 | |
|                 return Sao.field.Binary;
 | |
|             case 'dict':
 | |
|                 return Sao.field.Dict;
 | |
|             default:
 | |
|                 return Sao.field.Char;
 | |
|         }
 | |
|     };
 | |
| 
 | |
|     Sao.field.Field = Sao.class_(Object, {
 | |
|         _default: null,
 | |
|         _single_value: true,
 | |
|         init: function(description) {
 | |
|             this.description = description;
 | |
|             this.name = description.name;
 | |
|             this.views = new Set();
 | |
|         },
 | |
|         set: function(record, value) {
 | |
|             record._values[this.name] = value;
 | |
|         },
 | |
|         get: function(record) {
 | |
|             var value = record._values[this.name];
 | |
|             if (value === undefined) {
 | |
|                 value = this._default;
 | |
|             }
 | |
|             return value;
 | |
|         },
 | |
|         _has_changed: function(previous, value) {
 | |
|             // Use stringify to compare object instance like Number for Decimal
 | |
|             return JSON.stringify(previous) != JSON.stringify(value);
 | |
|         },
 | |
|         set_client: function(record, value, force_change) {
 | |
|             var previous_value = this.get(record);
 | |
|             this.set(record, value);
 | |
|             if (this._has_changed(previous_value, this.get(record))) {
 | |
|                 this.changed(record);
 | |
|                 record.validate(null, true, false);
 | |
|                 record.set_modified(this.name);
 | |
|             } else if (force_change) {
 | |
|                 this.changed(record);
 | |
|                 record.validate(null, true, false);
 | |
|                 record.set_modified();
 | |
|             }
 | |
|         },
 | |
|         get_client: function(record) {
 | |
|             return this.get(record);
 | |
|         },
 | |
|         set_default: function(record, value) {
 | |
|             this.set(record, value);
 | |
|             record.modified_fields[this.name] = true;
 | |
|         },
 | |
|         set_on_change: function(record, value) {
 | |
|             this.set(record, value);
 | |
|             record.modified_fields[this.name] = true;
 | |
|         },
 | |
|         changed: function(record) {
 | |
|             record.on_change([this.name]);
 | |
|             record.on_change_with([this.name]);
 | |
|             record.autocomplete_with(this.name);
 | |
|             record.set_field_context();
 | |
|         },
 | |
|         get_timestamp: function(record) {
 | |
|             return {};
 | |
|         },
 | |
|         get_context: function(record, record_context, local) {
 | |
|             var context;
 | |
|             if (record_context) {
 | |
|                 context = jQuery.extend({}, record_context);
 | |
|             } else {
 | |
|                 context = record.get_context(local);
 | |
|             }
 | |
|             jQuery.extend(context,
 | |
|                 record.expr_eval(this.description.context || {}));
 | |
|             return context;
 | |
|         },
 | |
|         get_search_context: function(record) {
 | |
|             var context = this.get_context(record);
 | |
|             jQuery.extend(context,
 | |
|                 record.expr_eval(this.description.search_context || {}));
 | |
|             return context;
 | |
|         },
 | |
|         get_search_order: function(record) {
 | |
|             return record.expr_eval(this.description.search_order || null);
 | |
|         },
 | |
|         get_domains: function(record, pre_validate) {
 | |
|             var inversion = new Sao.common.DomainInversion();
 | |
|             var screen_domain = inversion.domain_inversion(
 | |
|                     [record.group.domain4inversion(), pre_validate || []],
 | |
|                     this.name, Sao.common.EvalEnvironment(record));
 | |
|             if ((typeof screen_domain == 'boolean') && !screen_domain) {
 | |
|                 screen_domain = [['id', '=', null]];
 | |
|             } else if ((typeof screen_domain == 'boolean') && screen_domain) {
 | |
|                 screen_domain = [];
 | |
|             }
 | |
|             var attr_domain = record.expr_eval(this.description.domain || []);
 | |
|             return [screen_domain, attr_domain];
 | |
|         },
 | |
|         get_domain: function(record) {
 | |
|             var domains = this.get_domains(record);
 | |
|             var screen_domain = domains[0];
 | |
|             var attr_domain = domains[1];
 | |
|             var inversion = new Sao.common.DomainInversion();
 | |
|             return inversion.concat(
 | |
|                     [inversion.localize_domain(screen_domain), attr_domain]);
 | |
|         },
 | |
|         validation_domains: function(record, pre_validate) {
 | |
|             var inversion = new Sao.common.DomainInversion();
 | |
|             return inversion.concat(this.get_domains(record, pre_validate));
 | |
|         },
 | |
|         get_eval: function(record) {
 | |
|             return this.get(record);
 | |
|         },
 | |
|         get_on_change_value: function(record) {
 | |
|             return this.get_eval(record);
 | |
|         },
 | |
|         set_state: function(
 | |
|             record, states=['readonly', 'required', 'invisible']) {
 | |
|             var state_changes = record.expr_eval(
 | |
|                     this.description.states || {});
 | |
|             for (const state of states) {
 | |
|                 if ((state == 'readonly') && this.description.readonly) {
 | |
|                     continue;
 | |
|                 }
 | |
|                 if (state_changes[state] !== undefined) {
 | |
|                     this.get_state_attrs(record)[state] = state_changes[state];
 | |
|                 } else if (this.description[state] !== undefined) {
 | |
|                     this.get_state_attrs(record)[state] =
 | |
|                         this.description[state];
 | |
|                 }
 | |
|             }
 | |
|             if (record.group.readonly ||
 | |
|                 this.get_state_attrs(record).domain_readonly ||
 | |
|                 (record.parent_name == this.name)) {
 | |
|                 this.get_state_attrs(record).readonly = true;
 | |
|             }
 | |
|         },
 | |
|         get_state_attrs: function(record) {
 | |
|             if (!(this.name in record.state_attrs)) {
 | |
|                 record.state_attrs[this.name] = jQuery.extend(
 | |
|                         {}, this.description);
 | |
|             }
 | |
|             if (record.group.readonly || record.readonly) {
 | |
|                 record.state_attrs[this.name].readonly = true;
 | |
|             }
 | |
|             return record.state_attrs[this.name];
 | |
|         },
 | |
|         _is_empty: function(record) {
 | |
|             return !this.get_eval(record);
 | |
|         },
 | |
|         check_required: function(record) {
 | |
|             var state_attrs = this.get_state_attrs(record);
 | |
|             if (state_attrs.required == 1) {
 | |
|                 if (this._is_empty(record) && (state_attrs.readonly != 1)) {
 | |
|                     return false;
 | |
|                 }
 | |
|             }
 | |
|             return true;
 | |
|         },
 | |
|         validate: function(record, softvalidation, pre_validate) {
 | |
|             if (this.description.readonly) {
 | |
|                 return true;
 | |
|             }
 | |
|             var invalid = false;
 | |
|             var state_attrs = this.get_state_attrs(record);
 | |
|             var is_required = Boolean(parseInt(state_attrs.required, 10));
 | |
|             var is_invisible = Boolean(parseInt(state_attrs.invisible, 10));
 | |
|             state_attrs.domain_readonly = false;
 | |
|             var inversion = new Sao.common.DomainInversion();
 | |
|             var domain = inversion.simplify(this.validation_domains(record,
 | |
|                         pre_validate));
 | |
|             if (!softvalidation) {
 | |
|                 if (!this.check_required(record)) {
 | |
|                     invalid = 'required';
 | |
|                 }
 | |
|             }
 | |
|             if (typeof domain == 'boolean') {
 | |
|                 if (!domain) {
 | |
|                     invalid = 'domain';
 | |
|                 }
 | |
|             } else if (Sao.common.compare(domain, [['id', '=', null]])) {
 | |
|                 invalid = 'domain';
 | |
|             } else {
 | |
|                 let [screen_domain] = this.get_domains(record, pre_validate);
 | |
|                 var uniques = inversion.unique_value(
 | |
|                     domain, this._single_value);
 | |
|                 var unique = uniques[0];
 | |
|                 var leftpart = uniques[1];
 | |
|                 var value = uniques[2];
 | |
|                 let unique_from_screen = inversion.unique_value(
 | |
|                     screen_domain, this._single_value)[0];
 | |
|                 if (this._is_empty(record) &&
 | |
|                     !is_required &&
 | |
|                     !is_invisible &&
 | |
|                     !unique_from_screen) {
 | |
|                     // Do nothing
 | |
|                 } else if (unique) {
 | |
|                     // If the inverted domain is so constraint that only one
 | |
|                     // value is possible we should use it. But we must also pay
 | |
|                     // attention to the fact that the original domain might be
 | |
|                     // a 'OR' domain and thus not preventing the modification
 | |
|                     // of fields.
 | |
|                     if (value === false) {
 | |
|                         // XXX to remove once server domains are fixed
 | |
|                         value = null;
 | |
|                     }
 | |
|                     var setdefault = true;
 | |
|                     var original_domain;
 | |
|                     if (!jQuery.isEmptyObject(record.group.domain)) {
 | |
|                         original_domain = inversion.merge(record.group.domain);
 | |
|                     } else {
 | |
|                         original_domain = inversion.merge(domain);
 | |
|                     }
 | |
|                     var domain_readonly = original_domain[0] == 'AND';
 | |
|                     if (leftpart.contains('.')) {
 | |
|                         var recordpart = leftpart.split('.', 1)[0];
 | |
|                         var localpart = leftpart.split('.', 1)[1];
 | |
|                         var constraintfields = [];
 | |
|                         if (domain_readonly) {
 | |
|                             for (const leaf of inversion.localize_domain(
 | |
|                                 original_domain.slice(1))) {
 | |
|                                 constraintfields.push(leaf);
 | |
|                             }
 | |
|                         }
 | |
|                         if ((localpart != 'id') ||
 | |
|                                 !~constraintfields.indexOf(recordpart)) {
 | |
|                             setdefault = false;
 | |
|                         }
 | |
|                     }
 | |
|                     if (setdefault && jQuery.isEmptyObject(pre_validate)) {
 | |
|                         this.set_client(record, value);
 | |
|                         state_attrs.domain_readonly = domain_readonly;
 | |
|                     }
 | |
|                 }
 | |
|                 if (!inversion.eval_domain(domain,
 | |
|                             Sao.common.EvalEnvironment(record))) {
 | |
|                     invalid = domain;
 | |
|                 }
 | |
|             }
 | |
|             state_attrs.invalid = invalid;
 | |
|             return !invalid;
 | |
|         }
 | |
|     });
 | |
| 
 | |
|     Sao.field.Char = Sao.class_(Sao.field.Field, {
 | |
|         _default: '',
 | |
|         set: function(record, value) {
 | |
|             if (this.description.strip && value) {
 | |
|                 switch (this.description.strip) {
 | |
|                     case 'leading':
 | |
|                         value = value.trimStart();
 | |
|                         break;
 | |
|                     case 'trailing':
 | |
|                         value = value.trimEnd();
 | |
|                         break;
 | |
|                     default:
 | |
|                         value = value.trim();
 | |
|                 }
 | |
|             }
 | |
|             Sao.field.Char._super.set.call(this, record, value);
 | |
|         },
 | |
|         get: function(record) {
 | |
|             return Sao.field.Char._super.get.call(this, record) || this._default;
 | |
|         }
 | |
|     });
 | |
| 
 | |
|     Sao.field.Selection = Sao.class_(Sao.field.Field, {
 | |
|         _default: null,
 | |
|         set_client: function(record, value, force_change) {
 | |
|             // delete before trigger the display
 | |
|             delete record._values[this.name + ':string'];
 | |
|             Sao.field.Selection._super.set_client.call(
 | |
|                 this, record, value, force_change);
 | |
|         }
 | |
|     });
 | |
| 
 | |
|     Sao.field.MultiSelection = Sao.class_(Sao.field.Selection, {
 | |
|         _default: null,
 | |
|         _single_value: false,
 | |
|         get: function(record) {
 | |
|             var value = Sao.field.MultiSelection._super.get.call(this, record);
 | |
|             if (jQuery.isEmptyObject(value)) {
 | |
|                 value = this._default;
 | |
|             } else {
 | |
|                 value.sort();
 | |
|             }
 | |
|             return value;
 | |
|         },
 | |
|         get_eval: function(record) {
 | |
|             var value = Sao.field.MultiSelection._super.get_eval.call(
 | |
|                 this, record);
 | |
|             if (value === null) {
 | |
|                 value = [];
 | |
|             }
 | |
|             return value;
 | |
|         },
 | |
|         set_client: function(record, value, force_change) {
 | |
|             if (value === null) {
 | |
|                 value = [];
 | |
|             }
 | |
|             if (typeof(value) == 'string') {
 | |
|                 value = [value];
 | |
|             }
 | |
|             if (value) {
 | |
|                 value = value.slice().sort();
 | |
|             }
 | |
|             Sao.field.MultiSelection._super.set_client.call(
 | |
|                 this, record, value, force_change);
 | |
|         }
 | |
|     });
 | |
| 
 | |
|     Sao.field.DateTime = Sao.class_(Sao.field.Field, {
 | |
|         _default: null,
 | |
|         time_format: function(record) {
 | |
|             return record.expr_eval(this.description.format);
 | |
|         },
 | |
|         set_client: function(record, value, force_change) {
 | |
|             var current_value;
 | |
|             if (value) {
 | |
|                 if (value.isTime) {
 | |
|                     current_value = this.get(record);
 | |
|                     if (current_value) {
 | |
|                         value = Sao.DateTime.combine(current_value, value);
 | |
|                     } else {
 | |
|                         value = null;
 | |
|                     }
 | |
|                 } else if (value.isDate) {
 | |
|                     current_value = this.get(record) || Sao.Time();
 | |
|                     value = Sao.DateTime.combine(value, current_value);
 | |
|                 }
 | |
|             }
 | |
|             Sao.field.DateTime._super.set_client.call(this, record, value,
 | |
|                 force_change);
 | |
|         },
 | |
|         date_format: function(record) {
 | |
|             var context = this.get_context(record);
 | |
|             return Sao.common.date_format(context.date_format);
 | |
|         }
 | |
|     });
 | |
| 
 | |
|     Sao.field.Date = Sao.class_(Sao.field.Field, {
 | |
|         _default: null,
 | |
|         set_client: function(record, value, force_change) {
 | |
|             if (value && !value.isDate) {
 | |
|                 value.isDate = true;
 | |
|                 value.isDateTime = false;
 | |
|             }
 | |
|             Sao.field.Date._super.set_client.call(this, record, value,
 | |
|                 force_change);
 | |
|         },
 | |
|         date_format: function(record) {
 | |
|             var context = this.get_context(record);
 | |
|             return Sao.common.date_format(context.date_format);
 | |
|         }
 | |
|     });
 | |
| 
 | |
|     Sao.field.Time = Sao.class_(Sao.field.Field, {
 | |
|         _default: null,
 | |
|         time_format: function(record) {
 | |
|             return record.expr_eval(this.description.format);
 | |
|         },
 | |
|         set_client: function(record, value, force_change) {
 | |
|             if (value && (value.isDate || value.isDateTime)) {
 | |
|                 value = Sao.Time(value.hour(), value.minute(),
 | |
|                     value.second(), value.millisecond());
 | |
|             }
 | |
|             Sao.field.Time._super.set_client.call(this, record, value,
 | |
|                 force_change);
 | |
|         }
 | |
|     });
 | |
| 
 | |
|     Sao.field.TimeDelta = Sao.class_(Sao.field.Field, {
 | |
|         _default: null,
 | |
|         converter: function(group) {
 | |
|             return group.context[this.description.converter];
 | |
|         },
 | |
|         set_client: function(record, value, force_change) {
 | |
|             if (typeof(value) == 'string') {
 | |
|                 value = Sao.common.timedelta.parse(
 | |
|                     value, this.converter(record.group));
 | |
|             }
 | |
|             Sao.field.TimeDelta._super.set_client.call(
 | |
|                 this, record, value, force_change);
 | |
|         },
 | |
|         get_client: function(record) {
 | |
|             var value = Sao.field.TimeDelta._super.get_client.call(
 | |
|                 this, record);
 | |
|             return Sao.common.timedelta.format(
 | |
|                 value, this.converter(record.group));
 | |
|         }
 | |
|     });
 | |
| 
 | |
|     Sao.field.Float = Sao.class_(Sao.field.Field, {
 | |
|         _default: null,
 | |
|         init: function(description) {
 | |
|             Sao.field.Float._super.init.call(this, description);
 | |
|             this._digits = {};
 | |
|             this._symbol = {};
 | |
|         },
 | |
|         digits: function(record, factor=1) {
 | |
|             var digits = record.expr_eval(this.description.digits);
 | |
|             if (typeof(digits) == 'string') {
 | |
|                 if (!(digits in record.model.fields)) {
 | |
|                     return;
 | |
|                 }
 | |
|                 var digits_field = record.model.fields[digits];
 | |
|                 var digits_name = digits_field.description.relation;
 | |
|                 var digits_id = digits_field.get(record);
 | |
|                 if (digits_name && (digits_id !== null) && (digits_id >= 0)) {
 | |
|                     if (digits_id in this._digits) {
 | |
|                         digits = this._digits[digits_id];
 | |
|                     } else {
 | |
|                         try {
 | |
|                             digits = Sao.rpc({
 | |
|                                 'method': 'model.' + digits_name + '.get_digits',
 | |
|                                 'params': [digits_id, {}],
 | |
|                             }, record.model.session, false);
 | |
|                         } catch(e) {
 | |
|                             Sao.Logger.warn(
 | |
|                                 "Fail to fetch digits for %s,%s",
 | |
|                                 digits_name, digits_id);
 | |
|                             return;
 | |
|                         }
 | |
|                         this._digits[digits_id] = digits;
 | |
|                     }
 | |
|                 } else {
 | |
|                     return;
 | |
|                 }
 | |
|             }
 | |
|             var shift = Math.round(Math.log(Math.abs(factor)) / Math.LN10);
 | |
|             if (!digits) {
 | |
|                 return;
 | |
|             }
 | |
|             var int_size = digits[0];
 | |
|             if (int_size !== null) {
 | |
|                 int_size += shift;
 | |
|             }
 | |
|             var dec_size = digits[1];
 | |
|             if (dec_size !== null) {
 | |
|                 dec_size -= shift;
 | |
|             }
 | |
|             return [int_size, dec_size];
 | |
|         },
 | |
|         get_symbol: function(record, symbol) {
 | |
|             if (record && (symbol in record.model.fields)) {
 | |
|                 var value = this.get(record) || 0;
 | |
|                 var sign = 1;
 | |
|                 if (value < 0) {
 | |
|                     sign = -1;
 | |
|                 } else if (value === 0) {
 | |
|                     sign = 0;
 | |
|                 }
 | |
|                 var symbol_field = record.model.fields[symbol];
 | |
|                 var symbol_name = symbol_field.description.relation;
 | |
|                 var symbol_id = symbol_field.get(record);
 | |
|                 if (symbol_name && (symbol_id !== null) && (symbol_id >= 0)) {
 | |
|                     if (symbol_id in this._symbol) {
 | |
|                         return this._symbol[symbol_id];
 | |
|                     }
 | |
|                     try {
 | |
|                         var result = Sao.rpc({
 | |
|                             'method': 'model.' + symbol_name + '.get_symbol',
 | |
|                             'params': [symbol_id, sign, record.get_context()],
 | |
|                         }, record.model.session, false) || ['', 1];
 | |
|                         this._symbol[symbol_id] = result;
 | |
|                         return result;
 | |
|                     } catch (e) {
 | |
|                         Sao.Logger.warn(
 | |
|                             "Fail to fetch symbol for %s,%s",
 | |
|                             symbol_name, symbol_id);
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|             return ['', 1];
 | |
|         },
 | |
|         check_required: function(record) {
 | |
|             var state_attrs = this.get_state_attrs(record);
 | |
|             if (state_attrs.required == 1) {
 | |
|                 if ((this.get(record) === null) &&
 | |
|                     (state_attrs.readonly != 1)) {
 | |
|                     return false;
 | |
|                 }
 | |
|             }
 | |
|             return true;
 | |
|         },
 | |
|         convert: function(value) {
 | |
|             if (!value && (value !== 0)) {
 | |
|                 return null;
 | |
|             }
 | |
|             value = Number(value);
 | |
|             if (isNaN(value)) {
 | |
|                 value = this._default;
 | |
|             }
 | |
|             return value;
 | |
|         },
 | |
|         apply_factor: function(record, value, factor) {
 | |
|             if (value !== null) {
 | |
|                 value /= factor;
 | |
|                 var digits = this.digits(record);
 | |
|                 if (digits && (digits[1] !== null)) {
 | |
|                     // Round to avoid float precision error
 | |
|                     // after the division by factor
 | |
|                     value = value.toFixed(digits[1]);
 | |
|                 }
 | |
|                 value = this.convert(value);
 | |
|             }
 | |
|             return value;
 | |
|         },
 | |
|         set_client: function(record, value, force_change, factor=1) {
 | |
|             value = this.apply_factor(record, this.convert(value), factor);
 | |
|             Sao.field.Float._super.set_client.call(this, record, value,
 | |
|                 force_change);
 | |
|         },
 | |
|         get_client: function(record, factor=1, grouping=true) {
 | |
|             var value = this.get(record);
 | |
|             if (value !== null) {
 | |
|                 var options = {
 | |
|                     useGrouping: grouping,
 | |
|                 };
 | |
|                 var digits = this.digits(record, factor);
 | |
|                 if (digits && (digits[1] !== null)) {
 | |
|                     options.minimumFractionDigits = digits[1];
 | |
|                     options.maximumFractionDigits = digits[1];
 | |
|                 }
 | |
|                 return (value * factor).toLocaleString(
 | |
|                     Sao.i18n.BC47(Sao.i18n.getlang()), options);
 | |
|             } else {
 | |
|                 return '';
 | |
|             }
 | |
|         }
 | |
|     });
 | |
| 
 | |
|     Sao.field.Numeric = Sao.class_(Sao.field.Float, {
 | |
|         convert: function(value) {
 | |
|             if (!value && (value !== 0)) {
 | |
|                 return null;
 | |
|             }
 | |
|             value = new Sao.Decimal(value);
 | |
|             if (isNaN(value.valueOf())) {
 | |
|                 value = this._default;
 | |
|             }
 | |
|             return value;
 | |
|         },
 | |
|     });
 | |
| 
 | |
|     Sao.field.Integer = Sao.class_(Sao.field.Float, {
 | |
|         convert: function(value) {
 | |
|             if (!value && (value !== 0)) {
 | |
|                 return null;
 | |
|             }
 | |
|             value = parseInt(value, 10);
 | |
|             if (isNaN(value)) {
 | |
|                 value = this._default;
 | |
|             }
 | |
|             return value;
 | |
|         }
 | |
|     });
 | |
| 
 | |
|     Sao.field.Boolean = Sao.class_(Sao.field.Field, {
 | |
|         _default: false,
 | |
|         set_client: function(record, value, force_change) {
 | |
|             value = Boolean(value);
 | |
|             Sao.field.Boolean._super.set_client.call(this, record, value,
 | |
|                 force_change);
 | |
|         },
 | |
|         get: function(record) {
 | |
|             return Boolean(record._values[this.name]);
 | |
|         },
 | |
|         get_client: function(record) {
 | |
|             return Boolean(record._values[this.name]);
 | |
|         }
 | |
|     });
 | |
| 
 | |
|     Sao.field.Many2One = Sao.class_(Sao.field.Field, {
 | |
|         _default: null,
 | |
|         check_required: function(record) {
 | |
|             var state_attrs = this.get_state_attrs(record);
 | |
|             if (state_attrs.required == 1) {
 | |
|                 if ((this.get(record) === null) &&
 | |
|                     (state_attrs.readonly != 1)) {
 | |
|                     return false;
 | |
|                 }
 | |
|             }
 | |
|             return true;
 | |
|         },
 | |
|         get_client: function(record) {
 | |
|             var rec_name = (record._values[this.name + '.'] || {}).rec_name;
 | |
|             if (rec_name === undefined) {
 | |
|                 this.set(record, this.get(record));
 | |
|                 rec_name = (
 | |
|                     record._values[this.name + '.'] || {}).rec_name || '';
 | |
|             }
 | |
|             return rec_name;
 | |
|         },
 | |
|         set: function(record, value) {
 | |
|             var rec_name = (
 | |
|                 record._values[this.name + '.'] || {}).rec_name || '';
 | |
|             if (!rec_name && (value >= 0) && (value !== null)) {
 | |
|                 var model_name = record.model.fields[this.name].description
 | |
|                     .relation;
 | |
|                 rec_name = Sao.rpc({
 | |
|                     'method': 'model.' + model_name + '.read',
 | |
|                     'params': [[value], ['rec_name'], record.get_context()]
 | |
|                 }, record.model.session, false)[0].rec_name;
 | |
|             }
 | |
|             Sao.setdefault(
 | |
|                 record._values, this.name + '.', {}).rec_name = rec_name;
 | |
|             record._values[this.name] = value;
 | |
|         },
 | |
|         set_client: function(record, value, force_change) {
 | |
|             var rec_name;
 | |
|             if (value instanceof Array) {
 | |
|                 rec_name = value[1];
 | |
|                 value = value[0];
 | |
|             } else {
 | |
|                 if (value == this.get(record)) {
 | |
|                     rec_name = (
 | |
|                         record._values[this.name + '.'] || {}).rec_name || '';
 | |
|                 } else {
 | |
|                     rec_name = '';
 | |
|                 }
 | |
|             }
 | |
|             if ((value < 0) && (this.name != record.group.parent_name)) {
 | |
|                 value = null;
 | |
|                 rec_name = '';
 | |
|             }
 | |
|             Sao.setdefault(
 | |
|                 record._values, this.name + '.', {}).rec_name = rec_name;
 | |
|             Sao.field.Many2One._super.set_client.call(this, record, value,
 | |
|                     force_change);
 | |
|         },
 | |
|         get_context: function(record, record_context, local) {
 | |
|             var context = Sao.field.Many2One._super.get_context.call(
 | |
|                 this, record, record_context, local);
 | |
|             if (this.description.datetime_field) {
 | |
|                 context._datetime = record.get_eval()[
 | |
|                     this.description.datetime_field];
 | |
|             }
 | |
|             return context;
 | |
|         },
 | |
|         validation_domains: function(record, pre_validate) {
 | |
|             return this.get_domains(record, pre_validate)[0];
 | |
|         },
 | |
|         get_domain: function(record) {
 | |
|             var domains = this.get_domains(record);
 | |
|             var screen_domain = domains[0];
 | |
|             var attr_domain = domains[1];
 | |
|             var inversion = new Sao.common.DomainInversion();
 | |
|             return inversion.concat([
 | |
|                     inversion.localize_domain(screen_domain, this.name),
 | |
|                     attr_domain]);
 | |
|         },
 | |
|         get_on_change_value: function(record) {
 | |
|             if ((record.group.parent_name == this.name) &&
 | |
|                     record.group.parent) {
 | |
|                 return record.group.parent.get_on_change_value(
 | |
|                         [record.group.child_name]);
 | |
|             }
 | |
|             return Sao.field.Many2One._super.get_on_change_value.call(
 | |
|                     this, record);
 | |
|         }
 | |
|     });
 | |
| 
 | |
|     Sao.field.One2One = Sao.class_(Sao.field.Many2One, {
 | |
|     });
 | |
| 
 | |
|     Sao.field.One2Many = Sao.class_(Sao.field.Field, {
 | |
|         init: function(description) {
 | |
|             Sao.field.One2Many._super.init.call(this, description);
 | |
|         },
 | |
|         _default: null,
 | |
|         _single_value: false,
 | |
|         _set_value: function(record, value, default_, modified, data) {
 | |
|             this._set_default_value(record);
 | |
|             var group = record._values[this.name];
 | |
|             if (jQuery.isEmptyObject(value)) {
 | |
|                 value = [];
 | |
|             }
 | |
|             var mode;
 | |
|             if (jQuery.isEmptyObject(value) ||
 | |
|                     !isNaN(parseInt(value[0], 10))) {
 | |
|                 mode = 'list ids';
 | |
|             } else {
 | |
|                 mode = 'list values';
 | |
|             }
 | |
|             if ((mode == 'list values') || data) {
 | |
|                 var context = this.get_context(record);
 | |
|                 var value_fields = new Set();
 | |
|                 if (mode == 'list values') {
 | |
|                     for (const v of value) {
 | |
|                         for (const f of Object.keys(v)) {
 | |
|                             value_fields.add(f);
 | |
|                         }
 | |
|                     }
 | |
|                 } else {
 | |
|                     for (const d of data) {
 | |
|                         for (const f in d) {
 | |
|                             value_fields.add(f);
 | |
|                         }
 | |
|                     }
 | |
|                 }
 | |
|                 let field_names = new Set();
 | |
|                 for (const fieldname of value_fields) {
 | |
|                     if (!(fieldname in group.model.fields) &&
 | |
|                             (!~fieldname.indexOf('.')) &&
 | |
|                             (!~fieldname.indexOf(':')) &&
 | |
|                             (!fieldname.startsWith('_'))) {
 | |
|                         field_names.add(fieldname);
 | |
|                     }
 | |
|                 }
 | |
|                 var attr_fields = Object.values(this.description.views || {})
 | |
|                     .map(v => v.fields)
 | |
|                     .reduce((acc, elem) => {
 | |
|                         for (const field in elem) {
 | |
|                             acc[field] = elem[field];
 | |
|                         }
 | |
|                         return acc;
 | |
|                     }, {});
 | |
|                 var fields = {};
 | |
|                 for (const n of field_names) {
 | |
|                     if (n in attr_fields) {
 | |
|                         fields[n] = attr_fields[n];
 | |
|                     }
 | |
|                 }
 | |
| 
 | |
|                 var to_fetch = Array.from(field_names).filter(k => !(k in attr_fields));
 | |
|                 if (to_fetch.length) {
 | |
|                     var args = {
 | |
|                         'method': 'model.' + this.description.relation +
 | |
|                             '.fields_get',
 | |
|                         'params': [to_fetch, context]
 | |
|                     };
 | |
|                     try {
 | |
|                         var rpc_fields = Sao.rpc(
 | |
|                             args, record.model.session, false);
 | |
|                         for (const [key, value] of Object.entries(rpc_fields)) {
 | |
|                             fields[key] = value;
 | |
|                         }
 | |
|                     } catch (e) {
 | |
|                         return;
 | |
|                     }
 | |
|                 }
 | |
|                 if (!jQuery.isEmptyObject(fields)) {
 | |
|                     group.add_fields(fields);
 | |
|                 }
 | |
|             }
 | |
|             if (mode == 'list ids') {
 | |
|                 var records_to_remove = [];
 | |
|                 for (const old_record of group) {
 | |
|                     if (!~value.indexOf(old_record.id)) {
 | |
|                         records_to_remove.push(old_record);
 | |
|                     }
 | |
|                 }
 | |
|                 for (const record_to_remove of records_to_remove) {
 | |
|                     group.remove(record_to_remove, true, false, false);
 | |
|                 }
 | |
|                 var preloaded = {};
 | |
|                 for (const d of (data || [])) {
 | |
|                     preloaded[d.id] = d;
 | |
|                 }
 | |
|                 group.load(value, modified || default_, -1, preloaded);
 | |
|             } else {
 | |
|                 for (const vals of value) {
 | |
|                     var new_record;
 | |
|                     if ('id' in vals) {
 | |
|                         new_record = group.get(vals.id);
 | |
|                         if (!new_record) {
 | |
|                             new_record = group.new_(false, vals.id);
 | |
|                         }
 | |
|                     } else {
 | |
|                         new_record = group.new_(false);
 | |
|                     }
 | |
|                     if (default_) {
 | |
|                         // Don't validate as parent will validate
 | |
|                         new_record.set_default(vals, false, false);
 | |
|                         group.add(new_record, -1, false);
 | |
|                     } else {
 | |
|                         new_record.set(vals, false);
 | |
|                         group.push(new_record);
 | |
|                     }
 | |
|                 }
 | |
|                 // Trigger modified only once
 | |
|                 group.record_modified();
 | |
|             }
 | |
|         },
 | |
|         set: function(record, value, _default=false, data=null) {
 | |
|             var group = record._values[this.name];
 | |
|             var model;
 | |
|             if (group !== undefined) {
 | |
|                 model = group.model;
 | |
|                 group.destroy();
 | |
|             } else if (record.model.name == this.description.relation) {
 | |
|                 model = record.model;
 | |
|             } else {
 | |
|                 model = new Sao.Model(this.description.relation);
 | |
|             }
 | |
|             record._values[this.name] = undefined;
 | |
|             this._set_default_value(record, model);
 | |
|             this._set_value(record, value, _default, undefined, data);
 | |
|         },
 | |
|         get: function(record) {
 | |
|             var group = record._values[this.name];
 | |
|             if (group === undefined) {
 | |
|                 return [];
 | |
|             }
 | |
|             var record_removed = group.record_removed;
 | |
|             var record_deleted = group.record_deleted;
 | |
|             var result = [];
 | |
|             var parent_name = this.description.relation_field || '';
 | |
|             var to_add = [];
 | |
|             var to_create = [];
 | |
|             var to_write = [];
 | |
|             for (const record2 of group) {
 | |
|                 if (~record_removed.indexOf(record2) ||
 | |
|                         ~record_deleted.indexOf(record2)) {
 | |
|                     continue;
 | |
|                 }
 | |
|                 var values;
 | |
|                 if (record2.id >= 0) {
 | |
|                     if (record2.modified) {
 | |
|                         values = record2.get();
 | |
|                         delete values[parent_name];
 | |
|                         if (!jQuery.isEmptyObject(values)) {
 | |
|                             to_write.push([record2.id]);
 | |
|                             to_write.push(values);
 | |
|                         }
 | |
|                         to_add.push(record2.id);
 | |
|                     }
 | |
|                 } else {
 | |
|                     values = record2.get();
 | |
|                     delete values[parent_name];
 | |
|                     to_create.push(values);
 | |
|                 }
 | |
|             }
 | |
|             if (!jQuery.isEmptyObject(to_add)) {
 | |
|                 result.push(['add', to_add]);
 | |
|             }
 | |
|             if (!jQuery.isEmptyObject(to_create)) {
 | |
|                 result.push(['create', to_create]);
 | |
|             }
 | |
|             if (!jQuery.isEmptyObject(to_write)) {
 | |
|                 result.push(['write'].concat(to_write));
 | |
|             }
 | |
|             if (!jQuery.isEmptyObject(record_removed)) {
 | |
|                 result.push(['remove', record_removed.map(function(r) {
 | |
|                     return r.id;
 | |
|                 })]);
 | |
|             }
 | |
|             if (!jQuery.isEmptyObject(record_deleted)) {
 | |
|                 result.push(['delete', record_deleted.map(function(r) {
 | |
|                     return r.id;
 | |
|                 })]);
 | |
|             }
 | |
|             return result;
 | |
|         },
 | |
|         set_client: function(record, value, force_change) {
 | |
|             // domain inversion try to set None as value
 | |
|             if (value === null) {
 | |
|                 value = [];
 | |
|             }
 | |
|             // domain inversion could try to set id as value
 | |
|             if (typeof value == 'number') {
 | |
|                 value = [value];
 | |
|             }
 | |
| 
 | |
|             var previous_ids = this.get_eval(record);
 | |
|             var modified = !Sao.common.compare(
 | |
|                 previous_ids.sort(), value.sort());
 | |
|             this._set_value(record, value, false, modified);
 | |
|             if (modified) {
 | |
|                 this.changed(record);
 | |
|                 record.validate(null, true, false);
 | |
|                 record.set_modified(this.name);
 | |
|             } else if (force_change) {
 | |
|                 this.changed(record);
 | |
|                 record.validate(null, true, false);
 | |
|                 record.set_modified();
 | |
|             }
 | |
|         },
 | |
|         get_client: function(record) {
 | |
|             this._set_default_value(record);
 | |
|             return record._values[this.name];
 | |
|         },
 | |
|         set_default: function(record, value) {
 | |
|             record.modified_fields[this.name] = true;
 | |
|             return this.set(record, value, true);
 | |
|         },
 | |
|         set_on_change: function(record, value) {
 | |
|             var fields, new_fields;
 | |
|             record.modified_fields[this.name] = true;
 | |
|             this._set_default_value(record);
 | |
|             if (value instanceof Array) {
 | |
|                 return this._set_value(record, value, false, true);
 | |
|             }
 | |
|             var new_field_names = {};
 | |
|             if (value && (value.add || value.update)) {
 | |
|                 var context = this.get_context(record);
 | |
|                 fields = record._values[this.name].model.fields;
 | |
|                 var adding_values = [];
 | |
|                 if (value.add) {
 | |
|                     for (const add of value.add) {
 | |
|                         adding_values.push(add[1]);
 | |
|                     }
 | |
|                 }
 | |
|                 for (const l of [adding_values, value.update]) {
 | |
|                     if (!jQuery.isEmptyObject(l)) {
 | |
|                         for (const v of l) {
 | |
|                             for (const f of Object.keys(v)) {
 | |
|                                 if (!(f in fields) &&
 | |
|                                     (f != 'id') &&
 | |
|                                     (!~f.indexOf('.'))) {
 | |
|                                         new_field_names[f] = true;
 | |
|                                     }
 | |
|                             }
 | |
|                         }
 | |
|                     }
 | |
|                 }
 | |
|                 if (!jQuery.isEmptyObject(new_field_names)) {
 | |
|                     var args = {
 | |
|                         'method': 'model.' + this.description.relation +
 | |
|                             '.fields_get',
 | |
|                         'params': [Object.keys(new_field_names), context]
 | |
|                     };
 | |
|                     try {
 | |
|                         new_fields = Sao.rpc(args, record.model.session, false);
 | |
|                     } catch (e) {
 | |
|                         return;
 | |
|                     }
 | |
|                 } else {
 | |
|                     new_fields = {};
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             var group = record._values[this.name];
 | |
|             if (value && value.delete) {
 | |
|                 for (const record_id of value.delete) {
 | |
|                     const record2 = group.get(record_id);
 | |
|                     if (record2) {
 | |
|                         group.remove(record2, false, false, false);
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|             if (value && value.remove) {
 | |
|                 for (const record_id of value.remove) {
 | |
|                     const record2 = group.get(record_id);
 | |
|                     if (record2) {
 | |
|                         group.remove(record2, true, false, false);
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             if (value && (value.add || value.update)) {
 | |
|                 let vals_to_set = {};
 | |
|                 // First set already added fields to prevent triggering a
 | |
|                 // second on_change call
 | |
|                 if (value.update) {
 | |
|                     for (const vals of value.update) {
 | |
|                         if (!vals.id) {
 | |
|                             continue;
 | |
|                         }
 | |
|                         const record2 = group.get(vals.id);
 | |
|                         if (record2) {
 | |
|                             for (var key in vals) {
 | |
|                                 if (!Object.prototype.hasOwnProperty.call(
 | |
|                                     new_field_names, key)) {
 | |
|                                     vals_to_set[key] = vals[key];
 | |
|                                 }
 | |
|                             }
 | |
|                             record2.set_on_change(vals_to_set);
 | |
|                         }
 | |
|                     }
 | |
|                 }
 | |
| 
 | |
|                 group.add_fields(new_fields);
 | |
|                 if (value.add) {
 | |
|                     for (const vals of value.add) {
 | |
|                         let new_record;
 | |
|                         const index = vals[0];
 | |
|                         const data = vals[1];
 | |
|                         const id_ = data.id;
 | |
|                         delete data.id;
 | |
|                         if (id_) {
 | |
|                             new_record = group.get(id_);
 | |
|                         }
 | |
|                         if (!new_record) {
 | |
|                             new_record = group.new_(false, id_);
 | |
|                         }
 | |
|                         group.add(new_record, index, false);
 | |
|                         new_record.set_on_change(data);
 | |
|                     }
 | |
|                 }
 | |
|                 if (value.update) {
 | |
|                     for (const vals of value.update) {
 | |
|                         if (!vals.id) {
 | |
|                             continue;
 | |
|                         }
 | |
|                         const record2 = group.get(vals.id);
 | |
|                         if (record2) {
 | |
|                             let to_update = Object.fromEntries(
 | |
|                                 Object.entries(vals).filter(
 | |
|                                     ([k, v]) => {
 | |
|                                         !Object.prototype.hasOwnProperty.call(
 | |
|                                             vals_to_set, k)
 | |
|                                     }
 | |
|                                 ));
 | |
|                             record2.set_on_change(to_update);
 | |
|                         }
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|         },
 | |
|         _set_default_value: function(record, model) {
 | |
|             if (record._values[this.name] !== undefined) {
 | |
|                 return;
 | |
|             }
 | |
|             if (!model) {
 | |
|                 model = new Sao.Model(this.description.relation);
 | |
|             }
 | |
|             if (record.model.name == this.description.relation) {
 | |
|                 model = record.model;
 | |
|             }
 | |
|             var group = Sao.Group(model, {}, []);
 | |
|             group.set_parent(record);
 | |
|             group.parent_name = this.description.relation_field;
 | |
|             group.child_name = this.name;
 | |
|             group.parent_datetime_field = this.description.datetime_field;
 | |
|             record._values[this.name] = group;
 | |
|         },
 | |
|         get_timestamp: function(record) {
 | |
|             var timestamps = {};
 | |
|             var group = record._values[this.name] || [];
 | |
|             var records = group.filter(function(record) {
 | |
|                 return record.modified;
 | |
|             });
 | |
|             for (const record of jQuery.extend(
 | |
|                 records, group.record_removed, group.record_deleted)) {
 | |
|                 jQuery.extend(timestamps, record.get_timestamp());
 | |
|             }
 | |
|             return timestamps;
 | |
|         },
 | |
|         get_eval: function(record) {
 | |
|             var result = [];
 | |
|             var group = record._values[this.name];
 | |
|             if (group === undefined) return result;
 | |
| 
 | |
|             var record_removed = group.record_removed;
 | |
|             var record_deleted = group.record_deleted;
 | |
|             for (const record2 of group) {
 | |
|                 if (~record_removed.indexOf(record2) ||
 | |
|                         ~record_deleted.indexOf(record2))
 | |
|                     continue;
 | |
|                 result.push(record2.id);
 | |
|             }
 | |
|             return result;
 | |
|         },
 | |
|         get_on_change_value: function(record) {
 | |
|             var result = [];
 | |
|             var group = record._values[this.name];
 | |
|             if (group === undefined) return result;
 | |
|             for (const record2 of group) {
 | |
|                 if (!record2.deleted && !record2.removed)
 | |
|                     result.push(record2.get_on_change_value(
 | |
|                                 [this.description.relation_field || '']));
 | |
|             }
 | |
|             return result;
 | |
|         },
 | |
|         get_removed_ids: function(record) {
 | |
|             return record._values[this.name].record_removed.map(function(r) {
 | |
|                 return r.id;
 | |
|             });
 | |
|         },
 | |
|         get_domain: function(record) {
 | |
|             var domains = this.get_domains(record);
 | |
|             var attr_domain = domains[1];
 | |
|             // Forget screen_domain because it only means at least one record
 | |
|             // and not all records
 | |
|             return attr_domain;
 | |
|         },
 | |
|         validation_domains: function(record, pre_validate) {
 | |
|             return this.get_domains(record, pre_validate)[0];
 | |
|         },
 | |
|         validate: function(record, softvalidation, pre_validate) {
 | |
|             var invalid = false;
 | |
|             var inversion = new Sao.common.DomainInversion();
 | |
|             var ldomain = inversion.localize_domain(inversion.domain_inversion(
 | |
|                         record.group.clean4inversion(pre_validate || []), this.name,
 | |
|                         Sao.common.EvalEnvironment(record)), this.name);
 | |
|             if (typeof ldomain == 'boolean') {
 | |
|                 if (ldomain) {
 | |
|                     ldomain = [];
 | |
|                 } else {
 | |
|                     ldomain = [['id', '=', null]];
 | |
|                 }
 | |
|             }
 | |
|             for (const record2 of (record._values[this.name] || [])) {
 | |
|                 if (!record2.get_loaded() && (record2.id >= 0) &&
 | |
|                         jQuery.isEmptyObject(pre_validate)) {
 | |
|                     continue;
 | |
|                 }
 | |
|                 if (!record2.validate(null, softvalidation, ldomain)) {
 | |
|                     invalid = 'children';
 | |
|                 }
 | |
|             }
 | |
|             var test = Sao.field.One2Many._super.validate.call(this, record,
 | |
|                         softvalidation, pre_validate);
 | |
|             if (test && invalid) {
 | |
|                 this.get_state_attrs(record).invalid = invalid;
 | |
|                 return false;
 | |
|             }
 | |
|             return test;
 | |
|         },
 | |
|         set_state: function(record, states) {
 | |
|             this._set_default_value(record);
 | |
|             Sao.field.One2Many._super.set_state.call(this, record, states);
 | |
|         },
 | |
|         _is_empty: function(record) {
 | |
|             return jQuery.isEmptyObject(this.get_eval(record));
 | |
|         }
 | |
|     });
 | |
| 
 | |
|     Sao.field.Many2Many = Sao.class_(Sao.field.One2Many, {
 | |
|         get_on_change_value: function(record) {
 | |
|             return this.get_eval(record);
 | |
|         }
 | |
|     });
 | |
| 
 | |
|     Sao.field.Reference = Sao.class_(Sao.field.Field, {
 | |
|         _default: null,
 | |
|         get_client: function(record) {
 | |
|             if (record._values[this.name]) {
 | |
|                 var model = record._values[this.name][0];
 | |
|                 var name = (
 | |
|                     record._values[this.name + '.'] || {}).rec_name || '';
 | |
|                 return [model, name];
 | |
|             } else {
 | |
|                 return null;
 | |
|             }
 | |
|         },
 | |
|         get: function(record) {
 | |
|             if (record._values[this.name] &&
 | |
|                 record._values[this.name][0] &&
 | |
|                 record._values[this.name][1] !== null &&
 | |
|                 record._values[this.name][1] >= -1) {
 | |
|                 return record._values[this.name].join(',');
 | |
|             }
 | |
|             return null;
 | |
|         },
 | |
|         set_client: function(record, value, force_change) {
 | |
|             if (value) {
 | |
|                 if (typeof(value) == 'string') {
 | |
|                     value = value.split(',');
 | |
|                 }
 | |
|                 var ref_model = value[0];
 | |
|                 var ref_id = value[1];
 | |
|                 var rec_name;
 | |
|                 if (ref_id instanceof Array) {
 | |
|                     rec_name = ref_id[1];
 | |
|                     ref_id = ref_id[0];
 | |
|                 } else {
 | |
|                     if (ref_id && !isNaN(parseInt(ref_id, 10))) {
 | |
|                         ref_id = parseInt(ref_id, 10);
 | |
|                     }
 | |
|                     if ([ref_model, ref_id].join(',') == this.get(record)) {
 | |
|                         rec_name = (
 | |
|                             record._values[this.name + '.'] || {}).rec_name || '';
 | |
|                     } else {
 | |
|                         rec_name = '';
 | |
|                     }
 | |
|                 }
 | |
|                 Sao.setdefault(
 | |
|                     record._values, this.name + '.', {}).rec_name = rec_name;
 | |
|                 value = [ref_model, ref_id];
 | |
|             }
 | |
|             Sao.field.Reference._super.set_client.call(
 | |
|                     this, record, value, force_change);
 | |
|         },
 | |
|         set: function(record, value) {
 | |
|             if (!value) {
 | |
|                 record._values[this.name] = this._default;
 | |
|                 return;
 | |
|             }
 | |
|             var ref_model, ref_id;
 | |
|             if (typeof(value) == 'string') {
 | |
|                 ref_model = value.split(',')[0];
 | |
|                 ref_id = value.split(',')[1];
 | |
|                 if (!ref_id) {
 | |
|                     ref_id = null;
 | |
|                 } else if (!isNaN(parseInt(ref_id, 10))) {
 | |
|                     ref_id = parseInt(ref_id, 10);
 | |
|                 }
 | |
|             } else {
 | |
|                 ref_model = value[0];
 | |
|                 ref_id = value[1];
 | |
|             }
 | |
|             var rec_name = (
 | |
|                 record._values[this.name + '.'] || {}).rec_name || '';
 | |
|             if (ref_model && ref_id !== null && ref_id >= 0) {
 | |
|                 if (!rec_name && ref_id >= 0) {
 | |
|                     rec_name = Sao.rpc({
 | |
|                         'method': 'model.' + ref_model + '.read',
 | |
|                         'params': [[ref_id], ['rec_name'], record.get_context()]
 | |
|                     }, record.model.session, false)[0].rec_name;
 | |
|                 }
 | |
|             } else if (ref_model) {
 | |
|                 rec_name = '';
 | |
|             } else {
 | |
|                 rec_name = ref_id;
 | |
|             }
 | |
|             Sao.setdefault(
 | |
|                 record._values, this.name + '.', {}).rec_name = rec_name;
 | |
|             record._values[this.name] = [ref_model, ref_id];
 | |
|         },
 | |
|         get_on_change_value: function(record) {
 | |
|             if ((record.group.parent_name == this.name) &&
 | |
|                     record.group.parent) {
 | |
|                 return [record.group.parent.model.name,
 | |
|                     record.group.parent.get_on_change_value(
 | |
|                         [record.group.child_name])];
 | |
|             }
 | |
|             return Sao.field.Reference._super.get_on_change_value.call(
 | |
|                     this, record);
 | |
|         },
 | |
|         get_context: function(record, record_context, local) {
 | |
|             var context = Sao.field.Reference._super.get_context.call(
 | |
|                 this, record, record_context, local);
 | |
|             if (this.description.datetime_field) {
 | |
|                 context._datetime = record.get_eval()[
 | |
|                     this.description.datetime_field];
 | |
|             }
 | |
|             return context;
 | |
|         },
 | |
|         validation_domains: function(record, pre_validate) {
 | |
|             return this.get_domains(record, pre_validate)[0];
 | |
|         },
 | |
|         get_domains: function(record, pre_validate) {
 | |
|             var model = null;
 | |
|             if (record._values[this.name]) {
 | |
|                 model = record._values[this.name][0];
 | |
|             }
 | |
|             var domains = Sao.field.Reference._super.get_domains.call(
 | |
|                 this, record, pre_validate);
 | |
|             domains[1] = domains[1][model] || [];
 | |
|             return domains;
 | |
|         },
 | |
|         get_domain: function(record) {
 | |
|             var model = null;
 | |
|             if (record._values[this.name]) {
 | |
|                 model = record._values[this.name][0];
 | |
|             }
 | |
|             var domains = this.get_domains(record);
 | |
|             var screen_domain = domains[0];
 | |
|             var attr_domain = domains[1];
 | |
|             var inversion = new Sao.common.DomainInversion();
 | |
|             screen_domain = inversion.filter_leaf(
 | |
|                 screen_domain, this.name, model);
 | |
|             screen_domain = inversion.prepare_reference_domain(
 | |
|                 screen_domain, this.name);
 | |
|             return inversion.concat([
 | |
|                 inversion.localize_domain(screen_domain, this.name, true),
 | |
|                 attr_domain]);
 | |
|         },
 | |
|         get_search_order: function(record) {
 | |
|             var order = Sao.field.Reference._super.get_search_order.call(
 | |
|                 this, record);
 | |
|             if (order !== null) {
 | |
|                 var model = null;
 | |
|                 if (record._values[this.name]) {
 | |
|                     model = record._values[this.name][0];
 | |
|                 }
 | |
|                 order = order[model] || null;
 | |
|             }
 | |
|             return order;
 | |
|         },
 | |
|         get_models: function(record) {
 | |
|             var domains = this.get_domains(record);
 | |
|             var inversion = new Sao.common.DomainInversion();
 | |
|             var screen_domain = inversion.prepare_reference_domain(
 | |
|                 domains[0], this.name);
 | |
|             return inversion.extract_reference_models(
 | |
|                 inversion.concat([screen_domain, domains[1]]),
 | |
|                 this.name);
 | |
|         },
 | |
|         _is_empty: function(record) {
 | |
|             var result = Sao.field.Reference._super._is_empty.call(
 | |
|                 this, record);
 | |
|             if (!result && record._values[this.name][1] < 0) {
 | |
|                 result = true;
 | |
|             }
 | |
|             return result;
 | |
|         },
 | |
|     });
 | |
| 
 | |
|     Sao.field.Binary = Sao.class_(Sao.field.Field, {
 | |
|         _default: null,
 | |
|         _has_changed: function(previous, value) {
 | |
|             return previous != value;
 | |
|         },
 | |
|         get_size: function(record) {
 | |
|             var data = record._values[this.name] || 0;
 | |
|             if ((data instanceof Uint8Array) ||
 | |
|                 (typeof(data) == 'string')) {
 | |
|                 return data.length;
 | |
|             }
 | |
|             return data;
 | |
|         },
 | |
|         get_data: function(record) {
 | |
|             var data = record._values[this.name];
 | |
|             var prm = jQuery.when(data);
 | |
|             if (!(data instanceof Uint8Array) &&
 | |
|                 (typeof(data) != 'string') &&
 | |
|                 (data !== null)) {
 | |
|                 if (record.id < 0) {
 | |
|                     return prm;
 | |
|                 }
 | |
|                 var context = record.get_context();
 | |
|                 prm = record.model.execute('read', [[record.id], [this.name]],
 | |
|                     context).then(data => {
 | |
|                         data = data[0][this.name];
 | |
|                         this.set(record, data);
 | |
|                         return data;
 | |
|                     });
 | |
|             }
 | |
|             return prm;
 | |
|         }
 | |
|     });
 | |
| 
 | |
|     Sao.field.Dict = Sao.class_(Sao.field.Field, {
 | |
|         _default: {},
 | |
|         _single_value: false,
 | |
|         init: function(description) {
 | |
|             Sao.field.Dict._super.init.call(this, description);
 | |
|             this.schema_model = new Sao.Model(description.schema_model);
 | |
|             this.keys = {};
 | |
|         },
 | |
|         set: function(record, value) {
 | |
|             if (value) {
 | |
|                 // Order keys to allow comparison with stringify
 | |
|                 var keys = [];
 | |
|                 for (var key in value) {
 | |
|                     keys.push(key);
 | |
|                 }
 | |
|                 keys.sort();
 | |
|                 var new_value = {};
 | |
|                 for (var index in keys) {
 | |
|                     key = keys[index];
 | |
|                     new_value[key] = value[key];
 | |
|                 }
 | |
|                 value = new_value;
 | |
|             }
 | |
|             Sao.field.Dict._super.set.call(this, record, value);
 | |
|         },
 | |
|         get: function(record) {
 | |
|             return jQuery.extend(
 | |
|                 {}, Sao.field.Dict._super.get.call(this, record));
 | |
|         },
 | |
|         get_client: function(record) {
 | |
|             return Sao.field.Dict._super.get_client.call(this, record);
 | |
|         },
 | |
|         validation_domains: function(record, pre_validate) {
 | |
|             return this.get_domains(record, pre_validate)[0];
 | |
|         },
 | |
|         get_domain: function(record) {
 | |
|             var inversion = new Sao.common.DomainInversion();
 | |
|             var domains = this.get_domains(record);
 | |
|             var screen_domain = domains[0];
 | |
|             var attr_domain = domains[1];
 | |
|             return inversion.concat([
 | |
|                     inversion.localize_domain(screen_domain),
 | |
|                     attr_domain]);
 | |
|         },
 | |
|         date_format: function(record) {
 | |
|             var context = this.get_context(record);
 | |
|             return Sao.common.date_format(context.date_format);
 | |
|         },
 | |
|         time_format: function(record) {
 | |
|             return '%X';
 | |
|         },
 | |
|         add_keys: function(keys, record) {
 | |
|             var context = this.get_context(record);
 | |
|             var domain = this.get_domain(record);
 | |
|             var batchlen = Math.min(10, Sao.config.limit);
 | |
| 
 | |
|             keys = jQuery.extend([], keys);
 | |
|             const update_keys = values => {
 | |
|                 for (const k of values) {
 | |
|                     this.keys[k.name] = k;
 | |
|                 }
 | |
|             };
 | |
| 
 | |
|             var prms = [];
 | |
|             while (keys.length > 0) {
 | |
|                 var sub_keys = keys.splice(0, batchlen);
 | |
|                 prms.push(this.schema_model.execute('search_get_keys',
 | |
|                             [[['name', 'in', sub_keys], domain],
 | |
|                                 Sao.config.limit],
 | |
|                             context)
 | |
|                         .then(update_keys));
 | |
|             }
 | |
|             return jQuery.when.apply(jQuery, prms);
 | |
|         },
 | |
|         add_new_keys: function(ids, record) {
 | |
|             var context = this.get_context(record);
 | |
|             return this.schema_model.execute('get_keys', [ids], context)
 | |
|                 .then(new_fields => {
 | |
|                     var names = [];
 | |
|                     for (const new_field of new_fields) {
 | |
|                         this.keys[new_field.name] = new_field;
 | |
|                         names.push(new_field.name);
 | |
|                     }
 | |
|                     return names;
 | |
|                 });
 | |
|         },
 | |
|         validate: function(record, softvalidation, pre_validate) {
 | |
|             var valid = Sao.field.Dict._super.validate.call(
 | |
|                 this, record, softvalidation, pre_validate);
 | |
| 
 | |
|             if (this.description.readonly) {
 | |
|                 return valid;
 | |
|             }
 | |
| 
 | |
|             var decoder = new Sao.PYSON.Decoder();
 | |
|             var field_value = this.get_eval(record);
 | |
|             var domain = [];
 | |
|             for (var key in field_value) {
 | |
|                 if (!(key in this.keys)) {
 | |
|                     continue;
 | |
|                 }
 | |
|                 var key_domain = this.keys[key].domain;
 | |
|                 if (key_domain) {
 | |
|                     domain.push(decoder.decode(key_domain));
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             var inversion = new Sao.common.DomainInversion();
 | |
|             var valid_value = inversion.eval_domain(domain, field_value);
 | |
|             if (!valid_value) {
 | |
|                 this.get_state_attrs(record).invalid = 'domain';
 | |
|             }
 | |
| 
 | |
|             return valid && valid_value;
 | |
|         }
 | |
|     });
 | |
| }());
 |