[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:
@@ -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')])
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
|
||||
from . import pos_config
|
||||
from . import pos_order
|
||||
from . import hr_employee
|
||||
|
||||
@@ -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
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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))]
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user