[ADD] pyeval: date.replace method
[odoo/odoo.git] / addons / web / static / src / js / pyeval.js
1 /*
2  * py.js helpers and setup
3  */
4 (function() {
5
6     var instance = openerp;
7
8     instance.web.pyeval = {};
9
10     var obj = function () {};
11     obj.prototype = py.object;
12     var asJS = function (arg) {
13         if (arg instanceof obj) {
14             return arg.toJSON();
15         }
16         return arg;
17     };
18
19     var datetime = py.PY_call(py.object);
20
21     /**
22      * computes (Math.floor(a/b), a%b and passes that to the callback.
23      *
24      * returns the callback's result
25      */
26     var divmod = function (a, b, fn) {
27         var mod = a%b;
28         // in python, sign(a % b) === sign(b). Not in JS. If wrong side, add a
29         // round of b
30         if (mod > 0 && b < 0 || mod < 0 && b > 0) {
31             mod += b;
32         }
33         return fn(Math.floor(a/b), mod);
34     };
35     /**
36      * Passes the fractional and integer parts of x to the callback, returns
37      * the callback's result
38      */
39     var modf = function (x, fn) {
40         var mod = x%1;
41         if (mod < 0) {
42             mod += 1;
43         }
44         return fn(mod, Math.floor(x));
45     };
46     var zero = py.float.fromJSON(0);
47
48     // Port from pypy/lib_pypy/datetime.py
49     var DAYS_IN_MONTH = [null, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
50     var DAYS_BEFORE_MONTH = [null];
51     var dbm = 0;
52     for (var i=1; i<DAYS_IN_MONTH.length; ++i) {
53         DAYS_BEFORE_MONTH.push(dbm);
54         dbm += DAYS_IN_MONTH[i];
55     }
56     var is_leap = function (year) {
57         return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
58     };
59     var days_before_year = function (year) {
60         var y = year - 1;
61         return y*365 + Math.floor(y/4) - Math.floor(y/100) + Math.floor(y/400);
62     };
63     var days_in_month = function (year, month) {
64         if (month === 2 && is_leap(year)) {
65             return 29;
66         }
67         return DAYS_IN_MONTH[month];
68     };
69     var days_before_month = function (year, month) {
70         var post_leap_feb = month > 2 && is_leap(year);
71         return DAYS_BEFORE_MONTH[month]
72              + (post_leap_feb ? 1 : 0);
73     };
74     var ymd2ord = function (year, month, day) {
75         var dim = days_in_month(year, month);
76         if (!(1 <= day && day <= dim)) {
77             throw new Error("ValueError: day must be in 1.." + dim);
78         }
79         return days_before_year(year)
80              + days_before_month(year, month)
81              + day;
82     };
83     var DI400Y = days_before_year(401);
84     var DI100Y = days_before_year(101);
85     var DI4Y = days_before_year(5);
86     var assert = function (bool) {
87         if (!bool) {
88             throw new Error("AssertionError");
89         }
90     };
91     var ord2ymd = function (n) {
92         --n;
93         var n400, n100, n4, n1, n0;
94         divmod(n, DI400Y, function (_n400, n) {
95             n400 = _n400;
96             divmod(n, DI100Y, function (_n100, n) {
97                 n100 = _n100;
98                 divmod(n, DI4Y, function (_n4, n) {
99                     n4 = _n4;
100                     divmod(n, 365, function (_n1, n) {
101                         n1 = _n1;
102                         n0 = n;
103                     });
104                 });
105             });
106         });
107
108         n = n0;
109         var year = n400 * 400 + 1 + n100 * 100 + n4 * 4 + n1;
110         if (n1 == 4 || n100 == 100) {
111             assert(n0 === 0);
112             return {
113                 year: year - 1,
114                 month: 12,
115                 day: 31
116             };
117         }
118
119         var leapyear = n1 === 3 && (n4 !== 24 || n100 == 3);
120         assert(leapyear == is_leap(year));
121         var month = (n + 50) >> 5;
122         var preceding = DAYS_BEFORE_MONTH[month] + ((month > 2 && leapyear) ? 1 : 0);
123         if (preceding > n) {
124             --month;
125             preceding -= DAYS_IN_MONTH[month] + ((month === 2 && leapyear) ? 1 : 0);
126         }
127         n -= preceding;
128         return {
129             year: year,
130             month: month,
131             day: n+1
132         };
133     };
134
135     /**
136      * Converts the stuff passed in into a valid date, applying overflows as needed
137      */
138     var tmxxx = function (year, month, day, hour, minute, second, microsecond) {
139         hour = hour || 0; minute = minute || 0; second = second || 0;
140         microsecond = microsecond || 0;
141
142         if (microsecond < 0 || microsecond > 999999) {
143             divmod(microsecond, 1000000, function (carry, ms) {
144                 microsecond = ms;
145                 second += carry;
146             });
147         }
148         if (second < 0 || second > 59) {
149             divmod(second, 60, function (carry, s) {
150                 second = s;
151                 minute += carry;
152             });
153         }
154         if (minute < 0 || minute > 59) {
155             divmod(minute, 60, function (carry, m) {
156                 minute = m;
157                 hour += carry;
158             });
159         }
160         if (hour < 0 || hour > 23) {
161             divmod(hour, 24, function (carry, h) {
162                 hour = h;
163                 day += carry;
164             });
165         }
166         // That was easy.  Now it gets muddy:  the proper range for day
167         // can't be determined without knowing the correct month and year,
168         // but if day is, e.g., plus or minus a million, the current month
169         // and year values make no sense (and may also be out of bounds
170         // themselves).
171         // Saying 12 months == 1 year should be non-controversial.
172         if (month < 1 || month > 12) {
173             divmod(month-1, 12, function (carry, m) {
174                 month = m + 1;
175                 year += carry;
176             });
177         }
178         // Now only day can be out of bounds (year may also be out of bounds
179         // for a datetime object, but we don't care about that here).
180         // If day is out of bounds, what to do is arguable, but at least the
181         // method here is principled and explainable.
182         var dim = days_in_month(year, month);
183         if (day < 1 || day > dim) {
184             // Move day-1 days from the first of the month.  First try to
185             // get off cheap if we're only one day out of range (adjustments
186             // for timezone alone can't be worse than that).
187             if (day === 0) {
188                 --month;
189                 if (month > 0) {
190                     day = days_in_month(year, month);
191                 } else {
192                     --year; month=12; day=31;
193                 }
194             } else if (day == dim + 1) {
195                 ++month;
196                 day = 1;
197                 if (month > 12) {
198                     month = 1;
199                     ++year;
200                 }
201             } else {
202                 var r = ord2ymd(ymd2ord(year, month, 1) + (day - 1));
203                 year = r.year;
204                 month = r.month;
205                 day = r.day;
206             }
207         }
208         return {
209             year: year,
210             month: month,
211             day: day,
212             hour: hour,
213             minute: minute,
214             second: second,
215             microsecond: microsecond
216         };
217     };
218     datetime.timedelta = py.type('timedelta', null, {
219         __init__: function () {
220             var args = py.PY_parseArgs(arguments, [
221                 ['days', zero], ['seconds', zero], ['microseconds', zero],
222                 ['milliseconds', zero], ['minutes', zero], ['hours', zero],
223                 ['weeks', zero]
224             ]);
225
226             var d = 0, s = 0, m = 0;
227             var days = args.days.toJSON() + args.weeks.toJSON() * 7;
228             var seconds = args.seconds.toJSON()
229                         + args.minutes.toJSON() * 60
230                         + args.hours.toJSON() * 3600;
231             var microseconds = args.microseconds.toJSON()
232                              + args.milliseconds.toJSON() * 1000;
233
234             // Get rid of all fractions, and normalize s and us.
235             // Take a deep breath <wink>.
236             var daysecondsfrac = modf(days, function (dayfrac, days) {
237                 d = days;
238                 if (dayfrac) {
239                     return modf(dayfrac * 24 * 3600, function (dsf, dsw) {
240                         s = dsw;
241                         return dsf;
242                     });
243                 }
244                 return 0;
245             });
246
247             var secondsfrac = modf(seconds, function (sf, s) {
248                 seconds = s;
249                 return sf + daysecondsfrac;
250             });
251             divmod(seconds, 24*3600, function (days, seconds) {
252                 d += days;
253                 s += seconds;
254             });
255             // seconds isn't referenced again before redefinition
256
257             microseconds += secondsfrac * 1e6;
258             divmod(microseconds, 1000000, function (seconds, microseconds) {
259                 divmod(seconds, 24*3600, function (days, seconds) {
260                     d += days;
261                     s += seconds;
262                     m += Math.round(microseconds);
263                 });
264             });
265
266             // Carrying still possible here?
267
268             this.days = d;
269             this.seconds = s;
270             this.microseconds = m;
271         },
272         __str__: function () {
273             var hh, mm, ss;
274             divmod(this.seconds, 60, function (m, s) {
275                 divmod(m, 60, function (h, m) {
276                     hh = h;
277                     mm = m;
278                     ss = s;
279                 });
280             });
281             var s = _.str.sprintf("%d:%02d:%02d", hh, mm, ss);
282             if (this.days) {
283                 s = _.str.sprintf("%d day%s, %s",
284                     this.days,
285                     (this.days != 1 && this.days != -1) ? 's' : '',
286                     s);
287             }
288             if (this.microseconds) {
289                 s = _.str.sprintf("%s.%06d", s, this.microseconds);
290             }
291             return py.str.fromJSON(s);
292         },
293         __eq__: function (other) {
294             if (!py.PY_isInstance(other, datetime.timedelta)) {
295                 return py.False;
296             }
297             return (this.days === other.days
298                 && this.seconds === other.seconds
299                 && this.microseconds === other.microseconds)
300                     ? py.True : py.False;
301         },
302         __add__: function (other) {
303             if (!py.PY_isInstance(other, datetime.timedelta)) {
304                 return py.NotImplemented;
305             }
306             return py.PY_call(datetime.timedelta, [
307                 py.float.fromJSON(this.days + other.days),
308                 py.float.fromJSON(this.seconds + other.seconds),
309                 py.float.fromJSON(this.microseconds + other.microseconds)
310             ]);
311         },
312         __radd__: function (other) { return this.__add__(other); },
313         __sub__: function (other) {
314             if (!py.PY_isInstance(other, datetime.timedelta)) {
315                 return py.NotImplemented;
316             }
317             return py.PY_call(datetime.timedelta, [
318                 py.float.fromJSON(this.days - other.days),
319                 py.float.fromJSON(this.seconds - other.seconds),
320                 py.float.fromJSON(this.microseconds - other.microseconds)
321             ]);
322         },
323         __rsub__: function (other) {
324             if (!py.PY_isInstance(other, datetime.timedelta)) {
325                 return py.NotImplemented;
326             }
327             return this.__neg__().__add__(other);
328         },
329         __neg__: function () {
330             return py.PY_call(datetime.timedelta, [
331                 py.float.fromJSON(-this.days),
332                 py.float.fromJSON(-this.seconds),
333                 py.float.fromJSON(-this.microseconds)
334             ]);
335         },
336         __pos__: function () { return this; },
337         __mul__: function (other) {
338             if (!py.PY_isInstance(other, py.float)) {
339                 return py.NotImplemented;
340             }
341             var n = other.toJSON();
342             return py.PY_call(datetime.timedelta, [
343                 py.float.fromJSON(this.days * n),
344                 py.float.fromJSON(this.seconds * n),
345                 py.float.fromJSON(this.microseconds * n)
346             ]);
347         },
348         __rmul__: function (other) { return this.__mul__(other); },
349         __div__: function (other) {
350             if (!py.PY_isInstance(other, py.float)) {
351                 return py.NotImplemented;
352             }
353             var usec = ((this.days * 24 * 3600) + this.seconds) * 1000000
354                         + this.microseconds;
355             return py.PY_call(
356                 datetime.timedelta, [
357                     zero, zero, py.float.fromJSON(usec / other.toJSON())]);
358         },
359         __floordiv__: function (other) { return this.__div__(other); },
360         total_seconds: function () {
361             return py.float.fromJSON(
362                 this.days * 86400
363               + this.seconds
364               + this.microseconds / 1000000);
365         },
366         __nonzero__: function () {
367             return (!!this.days || !!this.seconds || !!this.microseconds)
368                 ? py.True
369                 : py.False;
370         }
371     });
372     datetime.datetime = py.type('datetime', null, {
373         __init__: function () {
374             var zero = py.float.fromJSON(0);
375             var args = py.PY_parseArgs(arguments, [
376                 'year', 'month', 'day',
377                 ['hour', zero], ['minute', zero], ['second', zero],
378                 ['microsecond', zero], ['tzinfo', py.None]
379             ]);
380             for(var key in args) {
381                 if (!args.hasOwnProperty(key)) { continue; }
382                 this[key] = asJS(args[key]);
383             }
384         },
385         replace: function () {
386             var args = py.PY_parseArgs(arguments, [
387                 ['year', py.None], ['month', py.None], ['day', py.None],
388                 ['hour', py.None], ['minute', py.None], ['second', py.None],
389                 ['microsecond', py.None] // FIXME: tzinfo, can't use None as valid input
390             ]);
391             var params = {};
392             for(var key in args) {
393                 if (!args.hasOwnProperty(key)) { continue; }
394
395                 var arg = args[key];
396                 params[key] = (arg === py.None ? this[key] : asJS(arg));
397             }
398             return py.PY_call(datetime.datetime, params);
399         },
400         strftime: function () {
401             var self = this;
402             var args = py.PY_parseArgs(arguments, 'format');
403             return py.str.fromJSON(args.format.toJSON()
404                 .replace(/%([A-Za-z])/g, function (m, c) {
405                     switch (c) {
406                     case 'Y': return _.str.sprintf('%04d', self.year);
407                     case 'm': return _.str.sprintf('%02d', self.month);
408                     case 'd': return _.str.sprintf('%02d', self.day);
409                     case 'H': return _.str.sprintf('%02d', self.hour);
410                     case 'M': return _.str.sprintf('%02d', self.minute);
411                     case 'S': return _.str.sprintf('%02d', self.second);
412                     }
413                     throw new Error('ValueError: No known conversion for ' + m);
414                 }));
415         },
416         now: py.classmethod.fromJSON(function () {
417             var d = new Date;
418             return py.PY_call(datetime.datetime, [
419                 d.getFullYear(), d.getMonth() + 1, d.getDate(),
420                 d.getHours(), d.getMinutes(), d.getSeconds(),
421                 d.getMilliseconds() * 1000]);
422         }),
423         today: py.classmethod.fromJSON(function () {
424             var dt_class = py.PY_getAttr(datetime, 'datetime');
425             return py.PY_call(py.PY_getAttr(dt_class, 'now'));
426         }),
427         utcnow: py.classmethod.fromJSON(function () {
428             var d = new Date();
429             return py.PY_call(datetime.datetime,
430                 [d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate(),
431                  d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(),
432                  d.getUTCMilliseconds() * 1000]);
433         }),
434         combine: py.classmethod.fromJSON(function () {
435             var args = py.PY_parseArgs(arguments, 'date time');
436             return py.PY_call(datetime.datetime, [
437                 py.PY_getAttr(args.date, 'year'),
438                 py.PY_getAttr(args.date, 'month'),
439                 py.PY_getAttr(args.date, 'day'),
440                 py.PY_getAttr(args.time, 'hour'),
441                 py.PY_getAttr(args.time, 'minute'),
442                 py.PY_getAttr(args.time, 'second')
443             ]);
444         }),
445         toJSON: function () {
446             return new Date(
447                 this.year,
448                 this.month - 1,
449                 this.day,
450                 this.hour,
451                 this.minute,
452                 this.second,
453                 this.microsecond / 1000);
454         },
455     });
456     datetime.date = py.type('date', null, {
457         __init__: function () {
458             var args = py.PY_parseArgs(arguments, 'year month day');
459             this.year = asJS(args.year);
460             this.month = asJS(args.month);
461             this.day = asJS(args.day);
462         },
463         strftime: function () {
464             var self = this;
465             var args = py.PY_parseArgs(arguments, 'format');
466             return py.str.fromJSON(args.format.toJSON()
467                 .replace(/%([A-Za-z])/g, function (m, c) {
468                     switch (c) {
469                     case 'Y': return self.year;
470                     case 'm': return _.str.sprintf('%02d', self.month);
471                     case 'd': return _.str.sprintf('%02d', self.day);
472                     }
473                     throw new Error('ValueError: No known conversion for ' + m);
474                 }));
475         },
476         __eq__: function (other) {
477             return (this.year === other.year
478                  && this.month === other.month
479                  && this.day === other.day)
480                 ? py.True : py.False;
481         },
482         replace: function () {
483             var args = py.PY_parseArgs(arguments, [
484                 ['year', py.None], ['month', py.None], ['day', py.None]
485             ]);
486             var params = {};
487             for(var key in args) {
488                 if (!args.hasOwnProperty(key)) { continue; }
489
490                 var arg = args[key];
491                 params[key] = (arg === py.None ? this[key] : asJS(arg));
492             }
493             return py.PY_call(datetime.date, params);
494         },
495         __add__: function (other) {
496             if (!py.PY_isInstance(other, datetime.timedelta)) {
497                 return py.NotImplemented;
498             }
499             var s = tmxxx(this.year, this.month, this.day + other.days);
500             return datetime.date.fromJSON(s.year, s.month, s.day);
501         },
502         __radd__: function (other) { return this.__add__(other); },
503         __sub__: function (other) {
504             if (py.PY_isInstance(other, datetime.timedelta)) {
505                 return this.__add__(other.__neg__());
506             }
507             if (py.PY_isInstance(other, datetime.date)) {
508                 // FIXME: getattr and sub API methods
509                 return py.PY_call(datetime.timedelta, [
510                     py.PY_subtract(
511                         py.PY_call(py.PY_getAttr(this, 'toordinal')),
512                         py.PY_call(py.PY_getAttr(other, 'toordinal')))
513                 ]);
514             }
515             return py.NotImplemented;
516         },
517         toordinal: function () {
518             return py.float.fromJSON(ymd2ord(this.year, this.month, this.day));
519         },
520         fromJSON: function (year, month, day) {
521             return py.PY_call(datetime.date, [year, month, day]);
522         },
523         today: py.classmethod.fromJSON(function () {
524             var d = new Date;
525             return py.PY_call(datetime.date, [
526                 d.getFullYear(), d.getMonth() + 1, d.getDate()]);
527         }),
528     });
529     /**
530         Returns the current local date, which means the date on the client (which can be different
531         compared to the date of the server).
532
533         @return {datetime.date}
534     */
535     var context_today = function() {
536         var d = new Date();
537         return py.PY_call(
538             datetime.date, [d.getFullYear(), d.getMonth() + 1, d.getDate()]);
539     };
540     datetime.time = py.type('time', null, {
541         __init__: function () {
542             var zero = py.float.fromJSON(0);
543             var args = py.PY_parseArgs(arguments, [
544                 ['hour', zero], ['minute', zero], ['second', zero], ['microsecond', zero],
545                 ['tzinfo', py.None]
546             ]);
547
548             for(var k in args) {
549                 if (!args.hasOwnProperty(k)) { continue; }
550                 this[k] = asJS(args[k]);
551             }
552         }
553     });
554     var time = py.PY_call(py.object);
555     time.strftime = py.PY_def.fromJSON(function () {
556         var args  = py.PY_parseArgs(arguments, 'format');
557         var dt_class = py.PY_getAttr(datetime, 'datetime');
558         var d = py.PY_call(py.PY_getAttr(dt_class, 'utcnow'));
559         return py.PY_call(py.PY_getAttr(d, 'strftime'), [args.format]);
560     });
561
562     var args = _.map(('year month day hour minute second microsecond '
563                     + 'years months weeks days hours minutes secondes microseconds '
564                     + 'weekday leakdays yearday nlyearday').split(' '), function (arg) {
565         return [arg, null];
566     });
567     args.unshift('*');
568     var relativedelta = py.type('relativedelta', null, {
569         __init__: function () {
570             this.ops = py.PY_parseArgs(arguments, args);
571         },
572         __add__: function (other) {
573             if (!py.PY_isInstance(other, datetime.date)) {
574                 return py.NotImplemented;
575             }
576             // TODO: test this whole mess
577             var year = asJS(this.ops.year) || asJS(other.year);
578             if (asJS(this.ops.years)) {
579                 year += asJS(this.ops.years);
580             }
581
582             var month = asJS(this.ops.month) || asJS(other.month);
583             if (asJS(this.ops.months)) {
584                 month += asJS(this.ops.months);
585                 // FIXME: no divmod in JS?
586                 while (month < 1) {
587                     year -= 1;
588                     month += 12;
589                 }
590                 while (month > 12) {
591                     year += 1;
592                     month -= 12;
593                 }
594             }
595
596             var lastMonthDay = new Date(year, month, 0).getDate();
597             var day = asJS(this.ops.day) || asJS(other.day);
598             if (day > lastMonthDay) { day = lastMonthDay; }
599             var days_offset = ((asJS(this.ops.weeks) || 0) * 7) + (asJS(this.ops.days) || 0);
600             if (days_offset) {
601                 day = new Date(year, month-1, day + days_offset).getDate();
602             }
603             // TODO: leapdays?
604             // TODO: hours, minutes, seconds? Not used in XML domains
605             // TODO: weekday?
606             // FIXME: use date.replace
607             return py.PY_call(datetime.date, [
608                 py.float.fromJSON(year),
609                 py.float.fromJSON(month),
610                 py.float.fromJSON(day)
611             ]);
612         },
613         __radd__: function (other) {
614             return this.__add__(other);
615         },
616
617         __sub__: function (other) {
618             if (!py.PY_isInstance(other, datetime.date)) {
619                 return py.NotImplemented;
620             }
621             // TODO: test this whole mess
622             var year = asJS(this.ops.year) || asJS(other.year);
623             if (asJS(this.ops.years)) {
624                 year -= asJS(this.ops.years);
625             }
626
627             var month = asJS(this.ops.month) || asJS(other.month);
628             if (asJS(this.ops.months)) {
629                 month -= asJS(this.ops.months);
630                 // FIXME: no divmod in JS?
631                 while (month < 1) {
632                     year -= 1;
633                     month += 12;
634                 }
635                 while (month > 12) {
636                     year += 1;
637                     month -= 12;
638                 }
639             }
640
641             var lastMonthDay = new Date(year, month, 0).getDate();
642             var day = asJS(this.ops.day) || asJS(other.day);
643             if (day > lastMonthDay) { day = lastMonthDay; }
644             var days_offset = ((asJS(this.ops.weeks) || 0) * 7) + (asJS(this.ops.days) || 0);
645             if (days_offset) {
646                 day = new Date(year, month-1, day - days_offset).getDate();
647             }
648             // TODO: leapdays?
649             // TODO: hours, minutes, seconds? Not used in XML domains
650             // TODO: weekday?
651             return py.PY_call(datetime.date, [
652                 py.float.fromJSON(year),
653                 py.float.fromJSON(month),
654                 py.float.fromJSON(day)
655             ]);
656         },
657         __rsub__: function (other) {
658             return this.__sub__(other);
659         }
660     });
661
662     // recursively wraps JS objects passed into the context to attributedicts
663     // which jsonify back to JS objects
664     var wrap = function (value) {
665         if (value === null) { return py.None; }
666
667         switch (typeof value) {
668         case 'undefined': throw new Error("No conversion for undefined");
669         case 'boolean': return py.bool.fromJSON(value);
670         case 'number': return py.float.fromJSON(value);
671         case 'string': return py.str.fromJSON(value);
672         }
673
674         switch(value.constructor) {
675         case Object: return wrapping_dict.fromJSON(value);
676         case Array: return wrapping_list.fromJSON(value);
677         }
678
679         throw new Error("ValueError: unable to wrap " + value);
680     };
681     var wrapping_dict = py.type('wrapping_dict', null, {
682         __init__: function () {
683             this._store = {};
684         },
685         __getitem__: function (key) {
686             var k = key.toJSON();
687             if (!(k in this._store)) {
688                 throw new Error("KeyError: '" + k + "'");
689             }
690             return wrap(this._store[k]);
691         },
692         __getattr__: function (key) {
693             return this.__getitem__(py.str.fromJSON(key));
694         },
695         get: function () {
696             var args = py.PY_parseArgs(arguments, ['k', ['d', py.None]]);
697
698             if (!(args.k.toJSON() in this._store)) { return args.d; }
699             return this.__getitem__(args.k);
700         },
701         fromJSON: function (d) {
702             var instance = py.PY_call(wrapping_dict);
703             instance._store = d;
704             return instance;
705         },
706         toJSON: function () {
707             return this._store;
708         },
709     });
710     var wrapping_list = py.type('wrapping_list', null, {
711         __init__: function () {
712             this._store = [];
713         },
714         __getitem__: function (index) {
715             return wrap(this._store[index.toJSON()]);
716         },
717         fromJSON: function (ar) {
718             var instance = py.PY_call(wrapping_list);
719             instance._store = ar;
720             return instance;
721         },
722         toJSON: function () {
723             return this._store;
724         },
725     });
726     var wrap_context = function (context) {
727         for (var k in context) {
728             if (!context.hasOwnProperty(k)) { continue; }
729             var val = context[k];
730
731             if (val === null) { continue; }
732             if (val.constructor === Array) {
733                 context[k] = wrapping_list.fromJSON(val);
734             } else if (val.constructor === Object
735                        && !py.PY_isInstance(val, py.object)) {
736                 context[k] = wrapping_dict.fromJSON(val);
737             }
738         }
739         return context;
740     };
741
742     var eval_contexts = function (contexts, evaluation_context) {
743         evaluation_context = _.extend(instance.web.pyeval.context(), evaluation_context || {});
744         return _(contexts).reduce(function (result_context, ctx) {
745             // __eval_context evaluations can lead to some of `contexts`'s
746             // values being null, skip them as well as empty contexts
747             if (_.isEmpty(ctx)) { return result_context; }
748             if (_.isString(ctx)) {
749                 // wrap raw strings in context
750                 ctx = { __ref: 'context', __debug: ctx };
751             }
752             var evaluated = ctx;
753             switch(ctx.__ref) {
754             case 'context':
755                 evaluation_context.context = evaluation_context;
756                 evaluated = py.eval(ctx.__debug, wrap_context(evaluation_context));
757                 break;
758             case 'compound_context':
759                 var eval_context = eval_contexts([ctx.__eval_context]);
760                 evaluated = eval_contexts(
761                     ctx.__contexts, _.extend({}, evaluation_context, eval_context));
762                 break;
763             }
764             // add newly evaluated context to evaluation context for following
765             // siblings
766             _.extend(evaluation_context, evaluated);
767             return _.extend(result_context, evaluated);
768         }, {});
769     };
770     var eval_domains = function (domains, evaluation_context) {
771         evaluation_context = _.extend(instance.web.pyeval.context(), evaluation_context || {});
772         var result_domain = [];
773         _(domains).each(function (domain) {
774             if (_.isString(domain)) {
775                 // wrap raw strings in domain
776                 domain = { __ref: 'domain', __debug: domain };
777             }
778             switch(domain.__ref) {
779             case 'domain':
780                 evaluation_context.context = evaluation_context;
781                 result_domain.push.apply(
782                     result_domain, py.eval(domain.__debug, wrap_context(evaluation_context)));
783                 break;
784             case 'compound_domain':
785                 var eval_context = eval_contexts([domain.__eval_context]);
786                 result_domain.push.apply(
787                     result_domain, eval_domains(
788                         domain.__domains, _.extend(
789                             {}, evaluation_context, eval_context)));
790                 break;
791             default:
792                 result_domain.push.apply(result_domain, domain);
793             }
794         });
795         return result_domain;
796     };
797     var eval_groupbys = function (contexts, evaluation_context) {
798         evaluation_context = _.extend(instance.web.pyeval.context(), evaluation_context || {});
799         var result_group = [];
800         _(contexts).each(function (ctx) {
801             if (_.isString(ctx)) {
802                 // wrap raw strings in context
803                 ctx = { __ref: 'context', __debug: ctx };
804             }
805             var group;
806             var evaluated = ctx;
807             switch(ctx.__ref) {
808             case 'context':
809                 evaluation_context.context = evaluation_context;
810                 evaluated = py.eval(ctx.__debug, wrap_context(evaluation_context));
811                 break;
812             case 'compound_context':
813                 var eval_context = eval_contexts([ctx.__eval_context]);
814                 evaluated = eval_contexts(
815                     ctx.__contexts, _.extend({}, evaluation_context, eval_context));
816                 break;
817             }
818             group = evaluated.group_by;
819             if (!group) { return; }
820             if (typeof group === 'string') {
821                 result_group.push(group);
822             } else if (group instanceof Array) {
823                 result_group.push.apply(result_group, group);
824             } else {
825                 throw new Error('Got invalid groupby {{'
826                         + JSON.stringify(group) + '}}');
827             }
828             _.extend(evaluation_context, evaluated);
829         });
830         return result_group;
831     };
832
833     instance.web.pyeval.context = function () {
834         return _.extend({
835             datetime: datetime,
836             context_today: context_today,
837             time: time,
838             relativedelta: relativedelta,
839             current_date: py.PY_call(
840                 time.strftime, [py.str.fromJSON('%Y-%m-%d')]),
841         }, instance.session.user_context);
842     };
843
844     /**
845      * @param {String} type "domains", "contexts" or "groupbys"
846      * @param {Array} object domains or contexts to evaluate
847      * @param {Object} [context] evaluation context
848      */
849     instance.web.pyeval.eval = function (type, object, context, options) {
850         options = options || {};
851         context = _.extend(instance.web.pyeval.context(), context || {});
852
853         //noinspection FallthroughInSwitchStatementJS
854         switch(type) {
855         case 'context':
856         case 'contexts':
857             if (type === 'context')
858                 object = [object];
859             return eval_contexts((options.no_user_context ? [] : [instance.session.user_context]).concat(object), context);
860         case 'domain':
861         case 'domains':
862             if (type === 'domain')
863                 object = [object];
864             return eval_domains(object, context);
865         case 'groupbys':
866             return eval_groupbys(object, context);
867         }
868         throw new Error("Unknow evaluation type " + type);
869     };
870
871     var eval_arg = function (arg) {
872         if (typeof arg !== 'object' || !arg.__ref) { return arg; }
873         switch(arg.__ref) {
874         case 'domain': case 'compound_domain':
875             return instance.web.pyeval.eval('domains', [arg]);
876         case 'context': case 'compound_context':
877             return instance.web.pyeval.eval('contexts', [arg]);
878         default:
879             throw new Error(instance.web._t("Unknown nonliteral type " + arg.__ref));
880         }
881     };
882     /**
883      * If args or kwargs are unevaluated contexts or domains (compound or not),
884      * evaluated them in-place.
885      *
886      * Potentially mutates both parameters.
887      *
888      * @param args
889      * @param kwargs
890      */
891     instance.web.pyeval.ensure_evaluated = function (args, kwargs) {
892         for (var i=0; i<args.length; ++i) {
893             args[i] = eval_arg(args[i]);
894         }
895         for (var k in kwargs) {
896             if (!kwargs.hasOwnProperty(k)) { continue; }
897             kwargs[k] = eval_arg(kwargs[k]);
898         }
899     };
900     instance.web.pyeval.eval_domains_and_contexts = function (source) {
901         return new $.Deferred(function (d) {setTimeout(function () {
902             var result;
903             try {
904                 result = instance.web.pyeval.sync_eval_domains_and_contexts(source);
905             }
906             catch (e) {
907                 result = { error: {
908                     code: 400,
909                     message: instance.web._t("Evaluation Error"),
910                     data: {
911                         type: 'local_exception',
912                         debug: _.str.sprintf(
913                                 instance.web._t("Local evaluation failure\n%s\n\n%s"),
914                                 e.message, JSON.stringify(source))
915                     }
916                 }};                
917             }
918             d.resolve(result);
919         }, 0); });
920     };
921     instance.web.pyeval.sync_eval_domains_and_contexts = function (source) {
922         var contexts = ([instance.session.user_context] || []).concat(source.contexts);
923         // see Session.eval_context in Python
924         return {
925             context: instance.web.pyeval.eval('contexts', contexts),
926             domain: instance.web.pyeval.eval('domains', source.domains),
927             group_by: instance.web.pyeval.eval('groupbys', source.group_by_seq || [])
928         };
929     };
930 })();