diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 0f95c697e60..7a9662d2937 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -406,7 +406,7 @@ class AccountMove(models.Model): aml = ac_move.line_ids.filtered(lambda x: x.account_id.reconcile or x.account_id.internal_type == 'liquidity') aml.remove_move_reconcile() #reconcile together the reconcilable (or the liquidity aml) and their newly created counterpart - for account in list(set([x.account_id for x in aml])): + for account in set([x.account_id for x in aml]): to_rec = aml.filtered(lambda y: y.account_id == account) to_rec |= reversed_move.line_ids.filtered(lambda y: y.account_id == account) #reconciliation will be full, so speed up the computation by using skip_full_reconcile_check in the context @@ -1061,10 +1061,8 @@ class AccountMoveLine(models.Model): # the provided values were not already multi-currency if account.currency_id and 'amount_currency' not in vals and account.currency_id.id != account.company_id.currency_id.id: vals['currency_id'] = account.currency_id.id - ctx = {} - if 'date' in vals: - ctx['date'] = vals['date'] - vals['amount_currency'] = account.company_id.currency_id._convert(amount, account.currency_id, account.company_id, vals.get('date', fields.Date.today())) + date = vals.get('date') or vals.get('date_maturity') or fields.Date.today() + vals['amount_currency'] = account.company_id.currency_id._convert(amount, account.currency_id, account.company_id, date) if not ok: raise UserError(_('You cannot use this general account in this journal, check the tab \'Entry Controls\' on the related journal.')) diff --git a/addons/hr_attendance/models/hr_employee.py b/addons/hr_attendance/models/hr_employee.py index e0cec9681bc..83e44105c00 100644 --- a/addons/hr_attendance/models/hr_employee.py +++ b/addons/hr_attendance/models/hr_employee.py @@ -90,6 +90,7 @@ class HrEmployee(models.Model): action_message = self.env.ref('hr_attendance.hr_attendance_action_greeting_message').read()[0] action_message['previous_attendance_change_date'] = self.last_attendance_id and (self.last_attendance_id.check_out or self.last_attendance_id.check_in) or False action_message['employee_name'] = self.name + action_message['barcode'] = self.barcode action_message['next_action'] = next_action if self.user_id: diff --git a/addons/hr_attendance/static/src/js/greeting_message.js b/addons/hr_attendance/static/src/js/greeting_message.js index a97564ae563..ac5762d7788 100644 --- a/addons/hr_attendance/static/src/js/greeting_message.js +++ b/addons/hr_attendance/static/src/js/greeting_message.js @@ -51,6 +51,7 @@ var GreetingMessage = AbstractAction.extend({ this.attendance.check_in_time = this.attendance.check_in && this.attendance.check_in.format(this.format_time); this.attendance.check_out_time = this.attendance.check_out && this.attendance.check_out.format(this.format_time); this.employee_name = action.employee_name; + this.attendanceBarcode = action.barcode; }, start: function() { @@ -136,21 +137,27 @@ var GreetingMessage = AbstractAction.extend({ _onBarcodeScanned: function(barcode) { var self = this; - if (this.return_to_main_menu) { // in case of multiple scans in the greeting message view, delete the timer, a new one will be created. - clearTimeout(this.return_to_main_menu); + if (this.attendanceBarcode !== barcode){ + if (this.return_to_main_menu) { // in case of multiple scans in the greeting message view, delete the timer, a new one will be created. + clearTimeout(this.return_to_main_menu); + } + core.bus.off('barcode_scanned', this, this._onBarcodeScanned); + this._rpc({ + model: 'hr.employee', + method: 'attendance_scan', + args: [barcode, ], + }) + .then(function (result) { + if (result.action) { + self.do_action(result.action); + } else if (result.warning) { + self.do_warn(result.warning); + setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, 5000); + } + }, function () { + setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, 5000); + }); } - this._rpc({ - model: 'hr.employee', - method: 'attendance_scan', - args: [barcode, ], - }) - .then(function (result) { - if (result.action) { - self.do_action(result.action); - } else if (result.warning) { - self.do_warn(result.warning); - } - }); }, destroy: function () { diff --git a/addons/hr_attendance/static/src/js/kiosk_mode.js b/addons/hr_attendance/static/src/js/kiosk_mode.js index 750fa43df47..07b03aa8b97 100644 --- a/addons/hr_attendance/static/src/js/kiosk_mode.js +++ b/addons/hr_attendance/static/src/js/kiosk_mode.js @@ -36,6 +36,7 @@ var KioskMode = AbstractAction.extend({ _onBarcodeScanned: function(barcode) { var self = this; + core.bus.off('barcode_scanned', this, this._onBarcodeScanned); this._rpc({ model: 'hr.employee', method: 'attendance_scan', @@ -46,7 +47,10 @@ var KioskMode = AbstractAction.extend({ self.do_action(result.action); } else if (result.warning) { self.do_warn(result.warning); + core.bus.on('barcode_scanned', self, self._onBarcodeScanned); } + }, function () { + core.bus.on('barcode_scanned', self, self._onBarcodeScanned); }); }, diff --git a/addons/hr_attendance/static/tests/hr_attendance_tests.js b/addons/hr_attendance/static/tests/hr_attendance_tests.js index c8e7c298162..ee582a5d7ad 100644 --- a/addons/hr_attendance/static/tests/hr_attendance_tests.js +++ b/addons/hr_attendance/static/tests/hr_attendance_tests.js @@ -2,8 +2,11 @@ odoo.define('hr_attendance.tests', function (require) { "use strict"; var testUtils = require('web.test_utils'); +var core = require('web.core'); var MyAttendances = require('hr_attendance.my_attendances'); +var KioskMode = require('hr_attendance.kiosk_mode'); +var GreetingMessage = require('hr_attendance.greeting_message'); QUnit.module('HR Attendance', { @@ -19,12 +22,30 @@ QUnit.module('HR Attendance', { default: 1, }, user_id: {string: 'user ID', type: 'integer'}, + barcode: {string:'barcode', type: 'integer'}, }, records: [{ id: 1, name: "Employee A", attendance_state: 'checked_out', user_id: 1, + barcode: 1, + }, + { + id: 2, + name: "Employee B", + attendance_state: 'checked_out', + user_id: 2, + barcode: 2, + }], + }, + 'res.company': { + fields: { + name: {string: 'Name', type: 'char'}, + }, + records: [{ + id: 1, + name: "Company A", }], }, }; @@ -51,6 +72,112 @@ QUnit.module('HR Attendance', { clientAction.destroy(); }); + QUnit.test('Attendance Kiosk Mode Test', function (assert) { + assert.expect(2); + + var $target = $('#qunit-fixture'); + var self = this; + var rpcCount = 0; + var clientAction = new KioskMode(null); + testUtils.addMockEnvironment(clientAction, { + data: this.data, + session: { + uid: 1, + company_id: 1, + }, + mockRPC: function(route, args) { + if (args.method === 'attendance_scan' && args.model === 'hr.employee') { + + rpcCount++; + return $.when(self.data['hr.employee'].records[0]); + } + return this._super(route, args); + }, + }); + clientAction.appendTo($target); + core.bus.trigger('barcode_scanned', 1); + core.bus.trigger('barcode_scanned', 1); + assert.strictEqual(rpcCount, 1, 'RPC call should have been done only once.'); + + core.bus.trigger('barcode_scanned', 2); + assert.strictEqual(rpcCount, 1, 'RPC call should have been done only once.'); + + clientAction.destroy(); + }); + + QUnit.test('Attendance Greeting Message Test', function (assert) { + assert.expect(10); + + var $target = $('#qunit-fixture'); + var self = this; + var rpcCount = 0; + + var clientActions = []; + function createGreetingMessage (target, barcode){ + var action = { + attendance: { + check_in: "2018-09-20 13:41:13", + employee_id: [barcode], + }, + next_action: "hr_attendance.hr_attendance_action_kiosk_mode", + barcode: barcode, + } + var clientAction = new GreetingMessage(null, action); + testUtils.addMockEnvironment(clientAction, { + data: self.data, + session: { + uid: 1, + company_id: 1, + }, + mockRPC: function(route, args) { + if (args.method === 'attendance_scan' && args.model === 'hr.employee') { + rpcCount++; + action.attendance.employee_id = [args.args[0], 'Employee']; + /* + if rpc have been made, a new instance is created to simulate the same behaviour + as functional flow. + */ + createGreetingMessage (target, args.args[0]); + return $.when({action: action}); + } + return this._super(route, args); + }, + }); + clientAction.appendTo(target); + + clientActions.push(clientAction); + }; + + // init - mock coming from kiosk + createGreetingMessage ($target, 1); + assert.strictEqual(clientActions.length, 1, 'Number of clientAction must = 1.'); + + core.bus.trigger('barcode_scanned', 1); + /* + As action is given when instantiate GreetingMessage, we simulate that we come from the KioskMode + So rescanning the same barcode won't lead to another RPC. + */ + assert.strictEqual(clientActions.length, 1, 'Number of clientActions must = 1.'); + assert.strictEqual(rpcCount, 0, 'RPC call should not have been done.'); + + core.bus.trigger('barcode_scanned', 2); + assert.strictEqual(clientActions.length, 2, 'Number of clientActions must = 2.'); + assert.strictEqual(rpcCount, 1, 'RPC call should have been done only once.'); + core.bus.trigger('barcode_scanned', 2); + assert.strictEqual(clientActions.length, 2, 'Number of clientActions must = 2.'); + assert.strictEqual(rpcCount, 1, 'RPC call should have been done only once.'); + + core.bus.trigger('barcode_scanned', 1); + assert.strictEqual(clientActions.length, 3, 'Number of clientActions must = 3.'); + core.bus.trigger('barcode_scanned', 1); + assert.strictEqual(clientActions.length, 3, 'Number of clientActions must = 3.'); + assert.strictEqual(rpcCount, 2, 'RPC call should have been done only twice.'); + + _.each(clientActions, function(clientAction) { + clientAction.destroy(); + }); + }); + }); }); diff --git a/addons/mrp/report/mrp_report_bom_structure.py b/addons/mrp/report/mrp_report_bom_structure.py index 5734ce2a6b4..f7e8a0ea6a5 100644 --- a/addons/mrp/report/mrp_report_bom_structure.py +++ b/addons/mrp/report/mrp_report_bom_structure.py @@ -119,16 +119,17 @@ class ReportBomStructure(models.AbstractModel): continue price = line.product_id.uom_id._compute_price(line.product_id.standard_price, line.product_uom_id) * line_quantity if line.child_bom_id: - factor = float_round(line.product_uom_id._compute_quantity(line_quantity, line.child_bom_id.product_uom_id) / line.child_bom_id.product_qty, precision_rounding=1, rounding_method='UP') + factor = line.product_uom_id._compute_quantity(line_quantity, line.child_bom_id.product_uom_id) / line.child_bom_id.product_qty sub_total = self._get_price(line.child_bom_id, factor) else: sub_total = price + sub_total = self.env.user.company_id.currency_id.round(sub_total) components.append({ 'prod_id': line.product_id.id, 'prod_name': line.product_id.display_name, 'prod_qty': line_quantity, 'prod_uom': line.product_uom_id.name, - 'prod_cost': price, + 'prod_cost': self.env.user.company_id.currency_id.round(price), 'parent_id': bom.id, 'line_id': line.id, 'level': level or 0, @@ -151,14 +152,21 @@ class ReportBomStructure(models.AbstractModel): 'operation': operation, 'name': operation.name + ' - ' + operation.workcenter_id.name, 'duration_expected': duration_expected, - 'total': float_round(total, precision_rounding=self.env.user.company_id.currency_id.rounding), + 'total': self.env.user.company_id.currency_id.round(total), }) return operations def _get_price(self, bom, factor): price = 0 if bom.routing_id: - operations = self._get_operation_line(bom.routing_id, factor, 0) + # routing are defined on a BoM and don't have a concept of quantity. + # It means that the operation time are defined for the quantity on + # the BoM (the user produces a batch of products). E.g the user + # product a batch of 10 units with a 5 minutes operation, the time + # will be the 5 for a quantity between 1-10, then doubled for + # 11-20,... + operation_cycle = float_round(factor, precision_rounding=1, rounding_method='UP') + operations = self._get_operation_line(bom.routing_id, operation_cycle, 0) price += sum([op['total'] for op in operations]) for line in bom.bom_line_ids: @@ -168,7 +176,8 @@ class ReportBomStructure(models.AbstractModel): price += sub_price else: prod_qty = line.product_qty * factor - price += (line.product_id.uom_id._compute_price(line.product_id.standard_price, line.product_uom_id) * prod_qty) + not_rounded_price = line.product_id.uom_id._compute_price(line.product_id.standard_price, line.product_uom_id) * prod_qty + price += self.env.user.company_id.currency_id.round(not_rounded_price) return price def _get_pdf_line(self, bom_id, product_id=False, qty=1, child_bom_ids=[], unfolded=False): diff --git a/addons/mrp/tests/test_bom.py b/addons/mrp/tests/test_bom.py index 71421216411..8dcfb0cfb13 100644 --- a/addons/mrp/tests/test_bom.py +++ b/addons/mrp/tests/test_bom.py @@ -3,6 +3,8 @@ from odoo import exceptions from odoo.addons.mrp.tests.common import TestMrpCommon +from odoo.tests import Form +from odoo.tools import float_compare, float_round class TestBoM(TestMrpCommon): @@ -158,3 +160,190 @@ class TestBoM(TestMrpCommon): }) with self.assertRaises(exceptions.UserError): test_bom_3.explode(self.product_9, 1) + + def test_bom_report(self): + """ Simulate a crumble receipt with mrp and open the bom structure + report and check that data insde are correct. + """ + uom_kg = self.env.ref('uom.product_uom_kgm') + uom_litre = self.env.ref('uom.product_uom_litre') + crumble = self.env['product.product'].create({ + 'name': 'Crumble', + 'type': 'product', + 'uom_id': uom_kg.id, + 'uom_po_id': uom_kg.id, + }) + butter = self.env['product.product'].create({ + 'name': 'Butter', + 'type': 'product', + 'uom_id': uom_kg.id, + 'uom_po_id': uom_kg.id, + 'standard_price': 7.01 + }) + biscuit = self.env['product.product'].create({ + 'name': 'Biscuit', + 'type': 'product', + 'uom_id': uom_kg.id, + 'uom_po_id': uom_kg.id, + 'standard_price': 1.5 + }) + bom_form_crumble = Form(self.env['mrp.bom']) + bom_form_crumble.product_tmpl_id = crumble.product_tmpl_id + bom_form_crumble.product_qty = 11 + bom_form_crumble.product_uom_id = uom_kg + bom_crumble = bom_form_crumble.save() + + with Form(bom_crumble) as bom: + with bom.bom_line_ids.new() as line: + line.product_id = butter + line.product_uom_id = uom_kg + line.product_qty = 5 + with bom.bom_line_ids.new() as line: + line.product_id = biscuit + line.product_uom_id = uom_kg + line.product_qty = 6 + + workcenter = self.env['mrp.workcenter'].create({ + 'costs_hour': 10, + 'name': 'Deserts Table' + }) + + routing_form = Form(self.env['mrp.routing']) + routing_form.name = "Crumble process" + routing_crumble = routing_form.save() + + with Form(routing_crumble) as routing: + with routing.operation_ids.new() as operation: + operation.workcenter_id = workcenter + operation.name = 'Prepare biscuits' + operation.time_cycle_manual = 5 + with routing.operation_ids.new() as operation: + operation.workcenter_id = workcenter + operation.name = 'Prepare butter' + operation.time_cycle_manual = 3 + with routing.operation_ids.new() as operation: + operation.workcenter_id = workcenter + operation.name = 'Mix manually' + operation.time_cycle_manual = 5 + + bom_crumble.routing_id = routing_crumble.id + + # TEST BOM STRUCTURE VALUE WITH BOM QUANTITY + report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=11, searchVariant=False) + # 5 min 'Prepare biscuits' + 3 min 'Prepare butter' + 5 min 'Mix manually' = 13 minutes + self.assertEqual(report_values['lines']['operations_time'], 13.0, 'Operation time should be the same for 1 unit or for the batch') + # Operation cost is the sum of operation line. + operation_cost = float_round(5 / 60 * 10, precision_digits=2) * 2 + float_round(3 / 60 * 10, precision_digits=2) + self.assertEqual(float_compare(report_values['lines']['operations_cost'], operation_cost, precision_digits=2), 0, '13 minute for 10$/hours -> 2.16') + + for component_line in report_values['lines']['components']: + # standard price * bom line quantity * current quantity / bom finished product quantity + if component_line['prod_id'] == butter.id: + # 5 kg of butter at 7.01$ for 11kg of crumble -> 35.05$ + self.assertEqual(float_compare(component_line['total'], (7.01 * 5), precision_digits=2), 0) + if component_line['prod_id'] == biscuit.id: + # 6 kg of biscuits at 1.50$ for 11kg of crumble -> 9$ + self.assertEqual(float_compare(component_line['total'], (1.5 * 6), precision_digits=2), 0) + # total price = 35.05 + 9 + operation_cost(0.83 + 0.83 + 0.5 = 2.16) = 46,21 + self.assertEqual(float_compare(report_values['lines']['total'], 46.21, precision_digits=2), 0, 'Product Bom Price is not correct') + self.assertEqual(float_compare(report_values['lines']['total'] / 11.0, 4.20, precision_digits=2), 0, 'Product Unit Bom Price is not correct') + + # TEST BOM STRUCTURE VALUE BY UNIT + report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=1, searchVariant=False) + # 5 min 'Prepare biscuits' + 3 min 'Prepare butter' + 5 min 'Mix manually' = 13 minutes + self.assertEqual(report_values['lines']['operations_time'], 13.0, 'Operation time should be the same for 1 unit or for the batch') + # Operation cost is the sum of operation line. + operation_cost = float_round(5 / 60 * 10, precision_digits=2) * 2 + float_round(3 / 60 * 10, precision_digits=2) + self.assertEqual(float_compare(report_values['lines']['operations_cost'], operation_cost, precision_digits=2), 0, '13 minute for 10$/hours -> 2.16') + + for component_line in report_values['lines']['components']: + # standard price * bom line quantity * current quantity / bom finished product quantity + if component_line['prod_id'] == butter.id: + # 5 kg of butter at 7.01$ for 11kg of crumble -> / 11 for price per unit (3.19) + self.assertEqual(float_compare(component_line['total'], (7.01 * 5) * (1 / 11), precision_digits=2), 0) + if component_line['prod_id'] == biscuit.id: + # 6 kg of biscuits at 1.50$ for 11kg of crumble -> / 11 for price per unit (0.82) + self.assertEqual(float_compare(component_line['total'], (1.5 * 6) * (1 / 11), precision_digits=2), 0) + # total price = 3.19 + 0.82 + operation_cost(0.83 + 0.83 + 0.5 = 2.16) = 6,17 + self.assertEqual(float_compare(report_values['lines']['total'], 6.17, precision_digits=2), 0, 'Product Unit Bom Price is not correct') + + # TEST OPERATION COST WHEN PRODUCED QTY > BOM QUANTITY + report_values_12 = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=12, searchVariant=False) + report_values_22 = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=22, searchVariant=False) + operation_cost = float_round(10 / 60 * 10, precision_digits=2) * 2 + float_round(6 / 60 * 10, precision_digits=2) + # Both needs 2 operation cycle + self.assertEqual(report_values_12['lines']['operations_cost'], report_values_22['lines']['operations_cost']) + self.assertEqual(report_values_22['lines']['operations_cost'], operation_cost) + report_values_23 = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=23, searchVariant=False) + operation_cost = float_round(15 / 60 * 10, precision_digits=2) * 2 + float_round(9 / 60 * 10, precision_digits=2) + self.assertEqual(report_values_23['lines']['operations_cost'], operation_cost) + + # Create a more complex BoM with a sub product + cheese_cake = self.env['product.product'].create({ + 'name': 'Cheese Cake 300g', + 'type': 'product', + }) + cream = self.env['product.product'].create({ + 'name': 'cream', + 'type': 'product', + 'uom_id': uom_litre.id, + 'uom_po_id': uom_litre.id, + 'standard_price': 5.17, + }) + bom_form_cheese_cake = Form(self.env['mrp.bom']) + bom_form_cheese_cake.product_tmpl_id = cheese_cake.product_tmpl_id + bom_form_cheese_cake.product_qty = 60 + bom_form_cheese_cake.product_uom_id = self.uom_unit + bom_cheese_cake = bom_form_cheese_cake.save() + + with Form(bom_cheese_cake) as bom: + with bom.bom_line_ids.new() as line: + line.product_id = cream + line.product_uom_id = uom_litre + line.product_qty = 3 + with bom.bom_line_ids.new() as line: + line.product_id = crumble + line.product_uom_id = uom_kg + line.product_qty = 5.4 + + workcenter_2 = self.env['mrp.workcenter'].create({ + 'name': 'cake mounting', + 'costs_hour': 20, + 'time_start': 10, + 'time_stop': 15 + }) + + routing_form = Form(self.env['mrp.routing']) + routing_form.name = "Cheese cake process" + routing_cheese = routing_form.save() + + with Form(routing_cheese) as routing: + with routing.operation_ids.new() as operation: + operation.workcenter_id = workcenter + operation.name = 'Mix cheese and crumble' + operation.time_cycle_manual = 10 + with routing.operation_ids.new() as operation: + operation.workcenter_id = workcenter_2 + operation.name = 'Cake mounting' + operation.time_cycle_manual = 5 + + bom_cheese_cake.routing_id = routing_cheese.id + + # TEST CHEESE BOM STRUCTURE VALUE WITH BOM QUANTITY + report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_cheese_cake.id, searchQty=60, searchVariant=False) + self.assertEqual(report_values['lines']['operations_time'], 40.0, 'Operation time should be the same for 1 unit or for the batch') + # Operation cost is the sum of operation line. + operation_cost = float_round(10 / 60 * 10, precision_digits=2) + float_round(30 / 60 * 20, precision_digits=2) + self.assertEqual(float_compare(report_values['lines']['operations_cost'], operation_cost, precision_digits=2), 0) + + for component_line in report_values['lines']['components']: + # standard price * bom line quantity * current quantity / bom finished product quantity + if component_line['prod_id'] == cream.id: + # 3 liter of cream at 5.17$ for 60 unit of cheese cake -> 15.51$ + self.assertEqual(float_compare(component_line['total'], (3 * 5.17), precision_digits=2), 0) + if component_line['prod_id'] == crumble.id: + # 5.4 kg of crumble at the cost of a batch. + crumble_cost = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=5.4, searchVariant=False)['lines']['total'] + self.assertEqual(float_compare(component_line['total'], crumble_cost, precision_digits=2), 0) + # total price = 15.51 + crumble_cost + operation_cost(10 + 1.67 = 11.67) = 27.18 + crumble_cost + self.assertEqual(float_compare(report_values['lines']['total'], 27.18 + crumble_cost, precision_digits=2), 0, 'Product Bom Price is not correct') diff --git a/addons/product/models/product_attribute.py b/addons/product/models/product_attribute.py index b7883a6c82f..465c3722fdf 100644 --- a/addons/product/models/product_attribute.py +++ b/addons/product/models/product_attribute.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo.osv import expression from odoo import api, fields, models, _ from odoo.addons import decimal_precision as dp from odoo.exceptions import UserError, ValidationError @@ -108,7 +109,8 @@ class ProductAttributeLine(models.Model): # search on a m2o and one on a m2m, probably this will quickly become # difficult to compute - check if performance optimization is required if name and operator in ('=', 'ilike', '=ilike', 'like', '=like'): - args = expression.AND([['|', ('attribute_id', operator, name), ('value_ids', operator, name)], args]) - attribute_ids = self._search(args, limit=limit, access_rights_uid=name_get_uid) + args = args or [] + domain = ['|', ('attribute_id', operator, name), ('value_ids', operator, name)] + attribute_ids = self._search(expression.AND([domain, args]), limit=limit, access_rights_uid=name_get_uid) return self.browse(attribute_ids).name_get() return super(ProductAttributeLine, self)._name_search(name=name, args=args, operator=operator, limit=limit, name_get_uid=name_get_uid) diff --git a/addons/purchase_requisition/models/purchase_requisition.py b/addons/purchase_requisition/models/purchase_requisition.py index af375ba011b..ce0cb5e2d2a 100644 --- a/addons/purchase_requisition/models/purchase_requisition.py +++ b/addons/purchase_requisition/models/purchase_requisition.py @@ -310,7 +310,7 @@ class PurchaseOrder(models.Model): self.partner_id = partner.id self.fiscal_position_id = fpos.id - self.payment_term_id = payment_term.id, + self.payment_term_id = payment_term.id self.company_id = requisition.company_id.id self.currency_id = requisition.currency_id.id if not self.origin or requisition.name not in self.origin.split(', '): diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index 731238150e9..43881a9a55b 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -359,6 +359,9 @@ class WebsiteSale(http.Controller): revive: Revival method when abandoned cart. Can be 'merge' or 'squash' """ order = request.website.sale_get_order() + if order and order.state != 'draft': + request.session['sale_order_id'] = None + order = request.website.sale_get_order() values = {} if access_token: abandoned_order = request.env['sale.order'].sudo().search([('access_token', '=', access_token)], limit=1) @@ -402,7 +405,7 @@ class WebsiteSale(http.Controller): @http.route(['/shop/cart/update'], type='http', auth="public", methods=['POST'], website=True, csrf=False) def cart_update(self, product_id, add_qty=1, set_qty=0, **kw): - sale_order = request.website.sale_get_order(force_create=1) + sale_order = request.website.sale_get_order(force_create=True) if sale_order.state != 'draft': request.session['sale_order_id'] = None sale_order = request.website.sale_get_order(force_create=True) diff --git a/addons/website_sale_delivery/controllers/main.py b/addons/website_sale_delivery/controllers/main.py index a7b7d41998b..5eae72e2bbb 100644 --- a/addons/website_sale_delivery/controllers/main.py +++ b/addons/website_sale_delivery/controllers/main.py @@ -58,9 +58,14 @@ class WebsiteSaleDelivery(WebsiteSale): def _update_website_sale_delivery(self, **post): order = request.website.sale_get_order() carrier_id = int(post['carrier_id']) - currency = order.currency_id if order: order._check_carrier_quotation(force_carrier_id=carrier_id) + return self._update_website_sale_delivery_return(order, **post) + + def _update_website_sale_delivery_return(self, order, **post): + carrier_id = int(post['carrier_id']) + currency = order.currency_id + if order: return {'status': order.delivery_rating_success, 'error_message': order.delivery_message, 'carrier_id': carrier_id, diff --git a/addons/website_sale_options/controllers/main.py b/addons/website_sale_options/controllers/main.py index c4d5f761258..d55b29c2e4e 100644 --- a/addons/website_sale_options/controllers/main.py +++ b/addons/website_sale_options/controllers/main.py @@ -19,7 +19,10 @@ class WebsiteSaleOptions(WebsiteSale): if lang: request.website = request.website.with_context(lang=lang) - order = request.website.sale_get_order(force_create=1) + order = request.website.sale_get_order(force_create=True) + if order.state != 'draft': + request.session['sale_order_id'] = None + order = request.website.sale_get_order(force_create=True) product = request.env['product.product'].browse(int(product_id)) option_ids = product.optional_product_ids.mapped('product_variant_ids').ids diff --git a/odoo/service/server.py b/odoo/service/server.py index c39d0310061..9b974e30998 100644 --- a/odoo/service/server.py +++ b/odoo/service/server.py @@ -59,6 +59,14 @@ def memory_info(process): pmem = (getattr(process, 'memory_info', None) or process.get_memory_info)() return (pmem.rss, pmem.vms) +def empty_pipe(fd): + try: + while os.read(fd, 1): + pass + except OSError as e: + if e.errno not in [errno.EAGAIN]: + raise + #---------------------------------------------------------- # Werkzeug WSGI servers patched #---------------------------------------------------------- @@ -562,13 +570,7 @@ class PreforkServer(CommonServer): for fd in ready[0]: if fd in fds: fds[fd].watchdog_time = time.time() - try: - # empty pipe - while os.read(fd, 1): - pass - except OSError as e: - if e.errno not in [errno.EAGAIN]: - raise + empty_pipe(fd) except select.error as e: if e.args[0] not in [errno.EINTR]: raise @@ -659,6 +661,7 @@ class Worker(object): self.watchdog_time = time.time() self.watchdog_pipe = multi.pipe_new() self.eintr_pipe = multi.pipe_new() + self.wakeup_fd_r, self.wakeup_fd_w = self.eintr_pipe # Can be set to None if no watchdog is desired. self.watchdog_timeout = multi.timeout self.ppid = os.getpid() @@ -682,8 +685,9 @@ class Worker(object): def sleep(self): try: - wakeup_fd = self.eintr_pipe[0] - select.select([self.multi.socket, wakeup_fd], [], [], self.multi.beat) + select.select([self.multi.socket, self.wakeup_fd_r], [], [], self.multi.beat) + # clear wakeup pipe if we were interrupted + empty_pipe(self.wakeup_fd_r) except select.error as e: if e.args[0] not in [errno.EINTR]: raise @@ -737,7 +741,7 @@ class Worker(object): signal.signal(signal.SIGINT, self.signal_handler) signal.signal(signal.SIGTERM, signal.SIG_DFL) signal.signal(signal.SIGCHLD, signal.SIG_DFL) - signal.set_wakeup_fd(self.eintr_pipe[1]) + signal.set_wakeup_fd(self.wakeup_fd_w) def stop(self): pass @@ -809,8 +813,9 @@ class WorkerCron(Worker): # simulate interruptible sleep with select(wakeup_fd, timeout) try: - wakeup_fd = self.eintr_pipe[0] - select.select([wakeup_fd], [], [], interval) + select.select([self.wakeup_fd_r], [], [], interval) + # clear wakeup pipe if we were interrupted + empty_pipe(self.wakeup_fd_r) except select.error as e: if e.args[0] != errno.EINTR: raise