[MERGE] forward port from saas-12.4 to master up to b27c145cf2

closes odoo/odoo#34697

Signed-off-by: Martin Trigaux (mat) <mat@odoo.com>
This commit is contained in:
Martin Trigaux
2019-07-09 12:28:03 +00:00
17 changed files with 321 additions and 54 deletions
+2 -2
View File
@@ -5,8 +5,8 @@ from odoo import api, fields, models
class Partner(models.Model):
_inherit = 'res.partner'
_name = 'res.partner'
_inherit = ['res.partner', 'phone.validation.mixin']
team_id = fields.Many2one('crm.team', string='Sales Team', oldname='section_id')
opportunity_ids = fields.One2many('crm.lead', 'partner_id', string='Opportunities', domain=[('type', '=', 'opportunity')])
+10 -9
View File
@@ -1999,15 +1999,15 @@ class MailThread(models.AbstractModel):
if rdata['channels']:
message_values['channel_ids'] = [(6, 0, [r['id'] for r in rdata['channels']])]
if rdata['partners']:
message_values['needaction_partner_ids'] = [(6, 0, [r['id'] for r in rdata['partners'] if rdata['partners'] != 'channel_email'])]
# change of behaviour to check: since email_cids partner are added in _notify_compute_recipients,
# they will be added to needaction_partner_ids to.
message_values['needaction_partner_ids'] = [(6, 0, [r['id'] for r in rdata['partners'] if r['type'] != 'channel_email'])]
# change of behavior to check: since email_cids partner are added in _notify_compute_recipients,
# they will be added to needaction_partner_ids to.
# we may want to filter them (example with channel_email, a cleaner solution may be great)
# -> instead of using _notify_customize_recipients, we could add a flag on rdata
# -> instead of using _notify_customize_recipients, we could add a flag on rdata
# (would work for needactions, not if we want to erase partner_ids, ids)
# (could also be interresting for, we could add partners with r['notif'] = 'ocn_client' and r['needaction']=False)
# then overide a notify_recipients (as it was before) to effectively send ocn notifications.
# enveloppe will contain more needaction, those for the member of a email channel.
# (could also be interesting for, we could add partners with r['notif'] = 'ocn_client' and r['needaction']=False)
# then override a notify_recipients (as it was before) to effectively send ocn notifications.
# envelope will contain more needaction, those for the member of a email channel.
if message_values and self:
message_values.update(self._notify_customize_recipients(message, msg_vals))
if message_values:
@@ -2032,7 +2032,7 @@ class MailThread(models.AbstractModel):
self.env['bus.bus'].sudo().sendmany(notifications)
return True
def _notify_record_by_email(self, message, partners_data, msg_vals=False, model_description=False, mail_auto_delete=True, send_after_commit=False):
def _notify_record_by_email(self, message, partners_data, msg_vals=False, model_description=False, mail_auto_delete=True, send_after_commit=True):
""" Method to send email linked to notified messages.
:param message: mail.message record to notify;
:param partners_data: partner to notify by email coming from _notify_compute_recipients
@@ -2255,9 +2255,10 @@ class MailThread(models.AbstractModel):
email_cids = [r['id'] for r in recipient_data['channels'] if r['notif'] == 'email']
if email_cids:
# we are doing a similar search in ocn_client
# Could be interresting to make everything in a single query.
# Could be interesting to make everything in a single query.
# ocn_client: (searching all partners linked to channels of type chat).
# here : (searching all partners linked to channels with notif email if email is not the author one)
# TDE FIXME: use email_sanitized
email_from = msg_vals.get('email_from') or message.email_from
exept_partner = [r['id'] for r in recipient_data['partners']]
if author_id:
+159
View File
@@ -0,0 +1,159 @@
/* from http://www.movable-type.co.uk/scripts/sha1.html */
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/* SHA-1 implementation in JavaScript (c) Chris Veness 2002-2014 / MIT Licence */
/* */
/* - see http://csrc.nist.gov/groups/ST/toolkit/secure_hashing.html */
/* http://csrc.nist.gov/groups/ST/toolkit/examples.html */
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/* jshint node:true *//* global define, escape, unescape */
'use strict';
/**
* SHA-1 hash function reference implementation.
*
* @namespace
*/
var Sha1 = {};
/**
* Generates SHA-1 hash of string.
*
* @param {string} msg - (Unicode) string to be hashed.
* @returns {string} Hash of msg as hex character string.
*/
Sha1.hash = function(msg) {
// convert string to UTF-8, as SHA only deals with byte-streams
msg = msg.utf8Encode();
// constants [§4.2.1]
var K = [ 0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xca62c1d6 ];
// PREPROCESSING
msg += String.fromCharCode(0x80); // add trailing '1' bit (+ 0's padding) to string [§5.1.1]
// convert string msg into 512-bit/16-integer blocks arrays of ints [§5.2.1]
var l = msg.length/4 + 2; // length (in 32-bit integers) of msg + ‘1’ + appended length
var N = Math.ceil(l/16); // number of 16-integer-blocks required to hold 'l' ints
var M = new Array(N);
for (var i=0; i<N; i++) {
M[i] = new Array(16);
for (var j=0; j<16; j++) { // encode 4 chars per integer, big-endian encoding
M[i][j] = (msg.charCodeAt(i*64+j*4)<<24) | (msg.charCodeAt(i*64+j*4+1)<<16) |
(msg.charCodeAt(i*64+j*4+2)<<8) | (msg.charCodeAt(i*64+j*4+3));
} // note running off the end of msg is ok 'cos bitwise ops on NaN return 0
}
// add length (in bits) into final pair of 32-bit integers (big-endian) [§5.1.1]
// note: most significant word would be (len-1)*8 >>> 32, but since JS converts
// bitwise-op args to 32 bits, we need to simulate this by arithmetic operators
M[N-1][14] = ((msg.length-1)*8) / Math.pow(2, 32); M[N-1][14] = Math.floor(M[N-1][14]);
M[N-1][15] = ((msg.length-1)*8) & 0xffffffff;
// set initial hash value [§5.3.1]
var H0 = 0x67452301;
var H1 = 0xefcdab89;
var H2 = 0x98badcfe;
var H3 = 0x10325476;
var H4 = 0xc3d2e1f0;
// HASH COMPUTATION [§6.1.2]
var W = new Array(80); var a, b, c, d, e;
for (var i=0; i<N; i++) {
// 1 - prepare message schedule 'W'
for (var t=0; t<16; t++) W[t] = M[i][t];
for (var t=16; t<80; t++) W[t] = Sha1.ROTL(W[t-3] ^ W[t-8] ^ W[t-14] ^ W[t-16], 1);
// 2 - initialise five working variables a, b, c, d, e with previous hash value
a = H0; b = H1; c = H2; d = H3; e = H4;
// 3 - main loop
for (var t=0; t<80; t++) {
var s = Math.floor(t/20); // seq for blocks of 'f' functions and 'K' constants
var T = (Sha1.ROTL(a,5) + Sha1.f(s,b,c,d) + e + K[s] + W[t]) & 0xffffffff;
e = d;
d = c;
c = Sha1.ROTL(b, 30);
b = a;
a = T;
}
// 4 - compute the new intermediate hash value (note 'addition modulo 2^32')
H0 = (H0+a) & 0xffffffff;
H1 = (H1+b) & 0xffffffff;
H2 = (H2+c) & 0xffffffff;
H3 = (H3+d) & 0xffffffff;
H4 = (H4+e) & 0xffffffff;
}
return Sha1.toHexStr(H0) + Sha1.toHexStr(H1) + Sha1.toHexStr(H2) +
Sha1.toHexStr(H3) + Sha1.toHexStr(H4);
};
/**
* Function 'f' [§4.1.1].
* @private
*/
Sha1.f = function(s, x, y, z) {
switch (s) {
case 0: return (x & y) ^ (~x & z); // Ch()
case 1: return x ^ y ^ z; // Parity()
case 2: return (x & y) ^ (x & z) ^ (y & z); // Maj()
case 3: return x ^ y ^ z; // Parity()
}
};
/**
* Rotates left (circular left shift) value x by n positions [§3.2.5].
* @private
*/
Sha1.ROTL = function(x, n) {
return (x<<n) | (x>>>(32-n));
};
/**
* Hexadecimal representation of a number.
* @private
*/
Sha1.toHexStr = function(n) {
// note can't use toString(16) as it is implementation-dependant,
// and in IE returns signed numbers when used on full words
var s="", v;
for (var i=7; i>=0; i--) { v = (n>>>(i*4)) & 0xf; s += v.toString(16); }
return s;
};
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/** Extend String object with method to encode multi-byte string to utf8
* - monsur.hossa.in/2012/07/20/utf-8-in-javascript.html */
if (typeof String.prototype.utf8Encode == 'undefined') {
String.prototype.utf8Encode = function() {
return unescape( encodeURIComponent( this ) );
};
}
/** Extend String object with method to decode utf8 string to multi-byte */
if (typeof String.prototype.utf8Decode == 'undefined') {
String.prototype.utf8Decode = function() {
try {
return decodeURIComponent( escape( this ) );
} catch (e) {
return this; // invalid UTF-8? return as-is
}
};
}
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
if (typeof module != 'undefined' && module.exports) module.exports = Sha1; // CommonJs export
if (typeof define == 'function' && define.amd) define([], function() { return Sha1; }); // AMD
@@ -10,6 +10,7 @@
<script type="text/javascript" src="/point_of_sale/static/lib/backbone/backbone.js"></script>
<script type="text/javascript" src="/point_of_sale/static/lib/waitfont.js"></script>
<script type="text/javascript" src="/point_of_sale/static/lib/sha1.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/main.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/db.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/models.js"></script>
+1
View File
@@ -2,3 +2,4 @@
from . import pos_config
from . import pos_order
from . import hr_employee
+22
View File
@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import hashlib
from odoo import api, models
class HrEmployee(models.Model):
_inherit = 'hr.employee'
def get_barcodes_and_pin_hashed(self):
if not self.env.user.has_group('point_of_sale.group_pos_user'):
return []
# Apply visibility filters (record rules)
visible_emp_ids = self.search([('id', 'in', self.ids)])
employees_data = self.sudo().search_read([('id', 'in', visible_emp_ids.ids)], ['barcode', 'pin'])
for e in employees_data:
e['barcode'] = hashlib.sha1(e['barcode'].encode('utf8')).hexdigest() if e['barcode'] else False
e['pin'] = hashlib.sha1(e['pin'].encode('utf8')).hexdigest() if e['pin'] else False
return employees_data
+1 -1
View File
@@ -56,7 +56,7 @@ gui.Gui.include({
self.show_popup('password',{
'title': _t('Password ?'),
confirm: function (pw) {
if (pw !== password) {
if (Sha1.hash(pw) !== password) {
self.show_popup('error', _t('Incorrect Password'));
reject();
} else {
+27 -1
View File
@@ -2,6 +2,7 @@ odoo.define('pos_hr.employees', function (require) {
"use strict";
var models = require('point_of_sale.models');
var rpc = require('web.rpc');
models.load_models([{
model: 'res.users',
@@ -27,7 +28,7 @@ models.load_models([{
},
},{
model: 'hr.employee',
fields: ['name', 'id', 'barcode', 'pin', 'user_id'],
fields: ['name', 'id', 'user_id'],
domain: function(self){ return [['company_id', '=', self.config.company_id[0]]]; },
loaded: function(self, employees) {
if (self.config.module_pos_hr) {
@@ -53,4 +54,29 @@ models.load_models([{
}
}
}]);
var posmodel_super = models.PosModel.prototype;
models.PosModel = models.PosModel.extend({
load_server_data: function () {
var self = this;
return posmodel_super.load_server_data.apply(this, arguments).then(function () {
var employee_ids = _.map(self.employees, function(employee){return employee.id;});
var records = rpc.query({
model: 'hr.employee',
method: 'get_barcodes_and_pin_hashed',
args: [employee_ids],
});
return records.then(function (employee_data) {
self.employees.forEach(function (employee) {
var data = _.findWhere(employee_data, {'id': employee.id});
if (data !== undefined){
employee.barcode = data.barcode;
employee.pin = data.pin;
}
});
});
});
},
});
});
+1 -1
View File
@@ -17,7 +17,7 @@ ScreenWidget.include({
var self = this;
var employees = this.pos.employees;
for(var i = 0, len = employees.length; i < len; i++){
if(employees[i].barcode === code.code){
if(employees[i].barcode === Sha1.hash(code.code)){
if (employees[i].id !== this.pos.get_cashier().id && employees[i].pin) {
return this.gui.ask_password(employees[i].pin).then(function(){
self.pos.set_cashier(employees[i]);
+2 -2
View File
@@ -74,7 +74,6 @@ class CrmTeam(models.Model):
SUM(line.balance) AS amount_untaxed_signed
FROM account_move move
LEFT JOIN account_move_line line ON line.move_id = move.id
INNER JOIN account_account_type account_type ON account_type.id = line.user_type_id
WHERE move.type IN ('out_invoice', 'out_refund', 'in_invoice', 'in_refund')
AND move.invoice_payment_state IN ('in_payment', 'paid')
AND move.state = 'posted'
@@ -82,7 +81,8 @@ class CrmTeam(models.Model):
AND move.date BETWEEN %s AND %s
AND line.tax_line_id IS NULL
AND line.display_type IS NULL
AND account_type.type NOT IN ('receivable', 'payable')
AND line.account_internal_type NOT IN ('receivable', 'payable')
GROUP BY move.team_id
'''
today = fields.Date.today()
params = [tuple(self.ids), fields.Date.to_string(today), fields.Date.to_string(today.replace(day=1))]
-23
View File
@@ -16,10 +16,6 @@
<field name="name">Working Time</field>
<field name="measure_type">working_time</field>
</record>
<record id="uom_categ_time" model="uom.category">
<field name="name">Time</field>
<field name="measure_type">time</field>
</record>
<record id="uom_categ_length" model="uom.category">
<field name="name">Length / Distance</field>
<field name="measure_type">length</field>
@@ -56,25 +52,6 @@
<field name="factor" eval="1000"/>
<field name="uom_type">smaller</field>
</record>
<record id="uom_time_day" model="uom.uom">
<field name="name">Day(s)</field>
<field name="category_id" ref="uom_categ_time"/>
<field name="factor" eval="1.0"/>
</record>
<record id="uom_time_week" model="uom.uom">
<field name="name">Week(s)</field>
<field name="category_id" ref="uom_categ_time"/>
<field name="factor" eval="7.0"/>
<field name="uom_type">bigger</field>
</record>
<record id="uom_time_hour" model="uom.uom">
<field name="name">Hour(s)</field>
<field name="category_id" ref="uom_categ_time"/>
<field name="factor" eval="24.0"/>
<field name="uom_type">smaller</field>
</record>
<record id="product_uom_day" model="uom.uom">
<field name="name">Day(s)</field>
<field eval="uom_categ_wtime" name="category_id"/>
+1 -2
View File
@@ -13,7 +13,6 @@ class UoMCategory(models.Model):
measure_type = fields.Selection([
('unit', 'Units'),
('weight', 'Weight'),
('time', 'Time (Duration)'),
('working_time', 'Working Time'),
('length', 'Length'),
('volume', 'Volume'),
@@ -25,7 +24,7 @@ class UoMCategory(models.Model):
@api.multi
def unlink(self):
if self.filtered(lambda categ: categ.measure_type in ['working_time', 'time']):
if self.filtered(lambda categ: categ.measure_type == 'working_time'):
raise UserError(_("You cannot delete this UoM Category as it is used by the system."))
return super(UoMCategory, self).unlink()
@@ -1597,7 +1597,7 @@ var BasicModel = AbstractModel.extend({
// updating an existing (virtual) record
var previousChange = _.find(oldChanges, function (operation) {
var child = self.localData[operation.id];
return child && (child.res_id === command[1]);
return child && (child.ref === command[1]);
});
recID = previousChange && previousChange.id;
rec = self.localData[recID];
@@ -8421,6 +8421,75 @@ QUnit.module('fields', {}, function () {
form.destroy();
});
QUnit.test('two one2many fields with same relation and onchanges', async function (assert) {
// this test simulates the presence of two one2many fields with onchanges, such that
// changes to the first o2m are repercuted on the second one
assert.expect(6);
this.data.partner.fields.turtles2 = {
string: "Turtles 2",
type: "one2many",
relation: 'turtle',
relation_field: 'turtle_trululu',
};
this.data.partner.onchanges = {
turtles: function (obj) {
// when we add a line to turtles, add same line to turtles2
if (obj.turtles.length) {
obj.turtles = [[5]].concat(obj.turtles);
obj.turtles2 = obj.turtles;
}
},
turtles2: function (obj) {
// simulate an onchange on turtles2 as well
if (obj.turtles2.length) {
obj.turtles2 = [[5]].concat(obj.turtles2);
}
}
};
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<field name="turtles">' +
'<tree editable="bottom"><field name="name" required="1"/></tree>' +
'</field>' +
'<field name="turtles2">' +
'<tree editable="bottom"><field name="name" required="1"/></tree>' +
'</field>' +
'</form>',
});
// trigger first onchange by adding a line in turtles field (should add a line in turtles2)
await testUtils.dom.click(form.$('.o_field_widget[name="turtles"] .o_field_x2many_list_row_add a'));
await testUtils.fields.editInput(form.$('.o_field_widget[name="turtles"] .o_field_widget[name="name"]'), 'ABC');
assert.containsOnce(form, '.o_field_widget[name="turtles"] .o_data_row',
'line of first o2m should have been created');
assert.containsOnce(form, '.o_field_widget[name="turtles2"] .o_data_row',
'line of second o2m should have been created');
// add a line in turtles2
await testUtils.dom.click(form.$('.o_field_widget[name="turtles2"] .o_field_x2many_list_row_add a'));
await testUtils.fields.editInput(form.$('.o_field_widget[name="turtles2"] .o_field_widget[name="name"]'), 'DEF');
assert.containsOnce(form, '.o_field_widget[name="turtles"] .o_data_row',
'we should still have 1 line in turtles');
assert.containsN(form, '.o_field_widget[name="turtles2"] .o_data_row', 2,
'we should have 2 lines in turtles2');
assert.hasClass(form.$('.o_field_widget[name="turtles2"] .o_data_row:nth(1)'), 'o_selected_row',
'second row should be in edition');
await testUtils.form.clickSave(form);
assert.strictEqual(form.$('.o_field_widget[name="turtles2"] .o_data_row').text(), 'ABCDEF');
form.destroy();
});
});
});
});
+7
View File
@@ -448,6 +448,13 @@ class Product(models.Model):
res['url'] = self.website_url
return res
@api.multi
def open_website_url(self):
self.ensure_one()
res = self.product_tmpl_id.open_website_url()
res['url'] = self.website_url
return res
@api.multi
def _get_images(self):
"""Return a list of records implementing `image.mixin` to
+16 -12
View File
@@ -5591,8 +5591,8 @@ Fields:
for subname in subnames:
new_lines.mapped(subname)
# Isolate changed x2many values, to handle inconsistent data sent from
# the client side: when a form view contains two one2many fields that
# Isolate changed values, to handle inconsistent data sent from the
# client side: when a form view contains two one2many fields that
# overlap, the lines that appear in both fields may be sent with
# different data. Consider, for instance:
#
@@ -5607,22 +5607,26 @@ Fields:
# The idea is to put 'foo_ids' in cache first, so that the snapshot
# contains value=1 for line in 'foo_ids'. The snapshot is then updated
# with the value of `bar_ids`, which will contain value=2 on line.
values = dict(values)
changed_x2many = {
name: values.pop(name, [])
for name in names
if self._fields[name].type in ('one2many', 'many2many')
}
#
# The issue also occurs with other fields. For instance, an onchange on
# a move line has a value for the field 'move_id' that contains the
# values of the move, among which the one2many that contains the line
# itself, with old values!
#
changed_values = {name: values[name] for name in names}
# set changed values to null in initial_values; not setting them
# triggers default_get() on the new record when creating snapshot0
initial_values = dict(values, **dict.fromkeys(names, False))
# create a new record with values, and attach ``self`` to it
record = self.new(values, origin=self)
# create a new record with values
record = self.new(initial_values, origin=self)
# make a snapshot based on the initial values of record
snapshot0 = Snapshot(record, nametree)
# store changed values in cache, and update snapshot0
for name, value in changed_x2many.items():
record._update_cache({name: value}, validate=False)
record._update_cache(changed_values, validate=False)
for name in names:
snapshot0.fetch(name)
# determine which field(s) should be triggered an onchange
+1
View File
@@ -37,6 +37,7 @@ setup(
'ofxparse',
'passlib',
'pillow', # windows binary http://www.lfd.uci.edu/~gohlke/pythonlibs/
'polib',
'psutil', # windows binary code.google.com/p/psutil/downloads/list
'psycopg2 >= 2.2',
'pydot',