diff --git a/addons/crm/models/res_partner.py b/addons/crm/models/res_partner.py index 55747d8d9b4..b6b52b95c11 100644 --- a/addons/crm/models/res_partner.py +++ b/addons/crm/models/res_partner.py @@ -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')]) diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index e1cfa920742..2baffa50168 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -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: diff --git a/addons/point_of_sale/static/lib/sha1.js b/addons/point_of_sale/static/lib/sha1.js new file mode 100644 index 00000000000..0fc324cfd33 --- /dev/null +++ b/addons/point_of_sale/static/lib/sha1.js @@ -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>> 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>>(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 diff --git a/addons/point_of_sale/views/point_of_sale.xml b/addons/point_of_sale/views/point_of_sale.xml index b476328700a..1d8cc009793 100644 --- a/addons/point_of_sale/views/point_of_sale.xml +++ b/addons/point_of_sale/views/point_of_sale.xml @@ -10,6 +10,7 @@ + diff --git a/addons/pos_hr/models/__init__.py b/addons/pos_hr/models/__init__.py index 5e5c6fc9b07..784da4cda9e 100644 --- a/addons/pos_hr/models/__init__.py +++ b/addons/pos_hr/models/__init__.py @@ -2,3 +2,4 @@ from . import pos_config from . import pos_order +from . import hr_employee diff --git a/addons/pos_hr/models/hr_employee.py b/addons/pos_hr/models/hr_employee.py new file mode 100644 index 00000000000..fc5a3f9b154 --- /dev/null +++ b/addons/pos_hr/models/hr_employee.py @@ -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 diff --git a/addons/pos_hr/static/src/js/gui.js b/addons/pos_hr/static/src/js/gui.js index b0faa322924..45c20b7551e 100644 --- a/addons/pos_hr/static/src/js/gui.js +++ b/addons/pos_hr/static/src/js/gui.js @@ -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 { diff --git a/addons/pos_hr/static/src/js/models.js b/addons/pos_hr/static/src/js/models.js index 54a6b1d31d1..4d9e0e20e23 100644 --- a/addons/pos_hr/static/src/js/models.js +++ b/addons/pos_hr/static/src/js/models.js @@ -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; + } + }); + }); + }); + }, +}); + }); diff --git a/addons/pos_hr/static/src/js/screens.js b/addons/pos_hr/static/src/js/screens.js index 9b7fe1c4a9a..bef83516f0f 100644 --- a/addons/pos_hr/static/src/js/screens.js +++ b/addons/pos_hr/static/src/js/screens.js @@ -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]); diff --git a/addons/sale/models/sales_team.py b/addons/sale/models/sales_team.py index 280b6d0a3ad..d0a16ecc1b7 100644 --- a/addons/sale/models/sales_team.py +++ b/addons/sale/models/sales_team.py @@ -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))] diff --git a/addons/uom/data/uom_data.xml b/addons/uom/data/uom_data.xml index bece7ca77d9..e46a7de9c35 100644 --- a/addons/uom/data/uom_data.xml +++ b/addons/uom/data/uom_data.xml @@ -16,10 +16,6 @@ Working Time working_time - - Time - time - Length / Distance length @@ -56,25 +52,6 @@ smaller - - - Day(s) - - - - - Week(s) - - - bigger - - - Hour(s) - - - smaller - - Day(s) diff --git a/addons/uom/models/uom_uom.py b/addons/uom/models/uom_uom.py index ae8e22b1c10..3ba5b970fec 100644 --- a/addons/uom/models/uom_uom.py +++ b/addons/uom/models/uom_uom.py @@ -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() diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js index 0cddc887f18..60c2f6a3314 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -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]; diff --git a/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js b/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js index f1f5f2a5e7e..508ce38a961 100644 --- a/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js +++ b/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js @@ -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: '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '
', + }); + + // 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(); + }); + }); }); }); diff --git a/addons/website_sale/models/product.py b/addons/website_sale/models/product.py index 65b8db17882..24e30a271f7 100644 --- a/addons/website_sale/models/product.py +++ b/addons/website_sale/models/product.py @@ -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 diff --git a/odoo/models.py b/odoo/models.py index 5af3855ea75..0ffac157bc6 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -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 diff --git a/setup.py b/setup.py index c9531cac4b3..00d90191525 100644 --- a/setup.py +++ b/setup.py @@ -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',