From 0038bf43f23bbd19dd7156d649dd5aa4447cb6a4 Mon Sep 17 00:00:00 2001 From: Josse Colpaert Date: Mon, 16 Oct 2017 14:48:32 +0200 Subject: [PATCH 01/10] [FIX] mrp: avoid inactive move lots validation as a safety We try to avoid to take the inactive move lots. While they are normally already filtered through the split, we had a case when you update the quantity of the mo, that the split would not separate them. --- addons/mrp/models/stock_move.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py index a87d6623dcc..183bd0dc1c6 100644 --- a/addons/mrp/models/stock_move.py +++ b/addons/mrp/models/stock_move.py @@ -284,7 +284,7 @@ class StockMove(models.Model): quants = quant_obj.quants_get_preferred_domain(move.product_qty, move, domain=main_domain, preferred_domain_list=preferred_domain_list) self.env['stock.quant'].quants_move(quants, move, move.location_dest_id) else: - for movelot in move.move_lot_ids: + for movelot in move.active_move_lot_ids: if float_compare(movelot.quantity_done, 0, precision_rounding=rounding) > 0: if not movelot.lot_id: raise UserError(_('You need to supply a lot/serial number.')) From d62b307597a5d93517461e67481d2eaca667f46b Mon Sep 17 00:00:00 2001 From: Josse Colpaert Date: Wed, 18 Oct 2017 15:23:12 +0200 Subject: [PATCH 02/10] [FIX] mrp: unlink temp stock move lots when updating quantity to done qty You start with an MO with a quantity of 2 with routing and we produce just 1. At least one of the components is tracked by lots. When we update the MO to a quantity of 1 again, the temporary stock.move.lots continued to exist which led to processing a the consume d move of e.g. 1 piece with 2 pieces of the quants processed when posting inventory. A test is added in order to avoid this case opw-767526 and opw-772786 --- addons/mrp/tests/test_workorder_operation.py | 114 ++++++++++++++++++- addons/mrp/wizard/change_production_qty.py | 11 +- 2 files changed, 121 insertions(+), 4 deletions(-) diff --git a/addons/mrp/tests/test_workorder_operation.py b/addons/mrp/tests/test_workorder_operation.py index 14eb7a65b31..eed76496be8 100644 --- a/addons/mrp/tests/test_workorder_operation.py +++ b/addons/mrp/tests/test_workorder_operation.py @@ -80,7 +80,6 @@ class TestWorkOrderProcess(common.TransactionCase): # -------------------------------------------------------------- # Process cutting operation... # --------------------------------------------------------- - finished_lot =self.env['stock.production.lot'].create({'product_id': production_table.product_id.id}) workorders[0].write({'final_lot_id': finished_lot.id}) workorders[0].button_start() @@ -148,6 +147,119 @@ class TestWorkOrderProcess(common.TransactionCase): self.assertEqual(finished_quant.consumed_quant_ids, consume_quants) + def test_00b_workorder_process(self): + """ Testing consume quants and produced quants with workorder """ + dining_table = self.env.ref("mrp.product_product_computer_desk") + product_table_sheet = self.env.ref('mrp.product_product_computer_desk_head') + product_table_leg = self.env.ref('mrp.product_product_computer_desk_leg') + product_bolt = self.env.ref('mrp.product_product_computer_desk_bolt') + + production_table = self.env['mrp.production'].create({ + 'product_id': dining_table.id, + 'product_qty': 2.0, + 'product_uom_id': dining_table.uom_id.id, + 'bom_id': self.ref("mrp.mrp_bom_desk") + }) + + # Set tracking lot on finish and consume products. + dining_table.tracking = 'lot' + product_table_sheet.tracking = 'lot' + product_table_leg.tracking = 'lot' + product_bolt.tracking = "lot" + + # Initial inventory of product sheet, lags and bolt + lot_sheet = self.env['stock.production.lot'].create({'product_id': product_table_sheet.id}) + lot_leg = self.env['stock.production.lot'].create({'product_id': product_table_leg.id}) + lot_bolt = self.env['stock.production.lot'].create({'product_id': product_bolt.id}) + + # Initialize inventory + # -------------------- + inventory = self.env['stock.inventory'].create({ + 'name': 'Inventory Product Table', + 'filter': 'partial', + 'line_ids': [(0, 0, { + 'product_id': product_table_sheet.id, + 'product_uom_id': product_table_sheet.uom_id.id, + 'product_qty': 20, + 'prod_lot_id': lot_sheet.id, + 'location_id': self.source_location_id + }), (0, 0, { + 'product_id': product_table_leg.id, + 'product_uom_id': product_table_leg.uom_id.id, + 'product_qty': 20, + 'prod_lot_id': lot_leg.id, + 'location_id': self.source_location_id + }), (0, 0, { + 'product_id': product_bolt.id, + 'product_uom_id': product_bolt.uom_id.id, + 'product_qty': 20, + 'prod_lot_id': lot_bolt.id, + 'location_id': self.source_location_id + })] + }) + inventory.action_done() + + # Create work order + production_table.button_plan() + # Check Work order created or not + self.assertEqual(len(production_table.workorder_ids), 3) + + # --------------------------------------------------------- + # Process all workorder and check it state. + # ---------------------------------------------------------- + + workorders = production_table.workorder_ids + self.assertEqual(workorders[0].state, 'ready', "First workorder state should be ready.") + self.assertEqual(workorders[1].state, 'pending') + self.assertEqual(workorders[2].state, 'pending') + + # -------------------------------------------------------------- + # Process cutting operation... + # --------------------------------------------------------- + finished_lot = self.env['stock.production.lot'].create({'product_id': production_table.product_id.id}) + workorders[0].write({'final_lot_id': finished_lot.id, 'qty_producing': 1.0}) + workorders[0].button_start() + workorders[0].active_move_lot_ids[0].write({'lot_id': lot_sheet.id, 'quantity_done': 1}) + self.assertEqual(workorders[0].state, 'progress') + workorders[0].record_production() + + move_table_sheet = production_table.move_raw_ids.filtered(lambda x : x.product_id == product_table_sheet) + self.assertEqual(move_table_sheet.quantity_done, 1) + + # -------------------------------------------------------------- + # Process drilling operation ... + # --------------------------------------------------------- + workorders[1].button_start() + workorders[1].qty_producing = 1.0 + workorders[1].active_move_lot_ids[0].write({'lot_id': lot_leg.id, 'quantity_done': 4}) + workorders[1].record_production() + move_leg = production_table.move_raw_ids.filtered(lambda x : x.product_id == product_table_leg) + #self.assertEqual(workorders[1].state, 'done') + self.assertEqual(move_leg.quantity_done, 4) + + # -------------------------------------------------------------- + # Process fitting operation ... + # --------------------------------------------------------- + workorders[2].button_start() + workorders[2].qty_producing = 1.0 + move_lot = workorders[2].active_move_lot_ids[0] + move_lot.write({'lot_id': lot_bolt.id, 'quantity_done': 4}) + move_table_bolt = production_table.move_raw_ids.filtered(lambda x : x.product_id.id == product_bolt.id) + workorders[2].record_production() + self.assertEqual(move_table_bolt.quantity_done, 4) + + # Change the quantity of the production order to 1 + wiz = self.env['change.production.qty'].create({'mo_id': production_table.id , + 'product_qty': 1.0}) + wiz.change_prod_qty() + # --------------------------------------------------------------- + # Check consume quants and produce quants after posting inventory + # --------------------------------------------------------------- + production_table.post_inventory() + self.assertEqual(sum(move_table_sheet.quant_ids.mapped('qty')), 1, "Wrong quantity of consumed product %s" % move_table_sheet.product_id.name) + self.assertEqual(sum(move_leg.quant_ids.mapped('qty')), 4, "Wrong quantity of consumed product %s" % move_leg.product_id.name) + self.assertEqual(sum(move_table_bolt.quant_ids.mapped('qty')), 4, "Wrong quantity of consumed product %s" % move_table_bolt.product_id.name) + def test_01_without_workorder(self): """ Testing consume quants and produced quants without workorder """ unit = self.ref("product.product_uom_unit") diff --git a/addons/mrp/wizard/change_production_qty.py b/addons/mrp/wizard/change_production_qty.py index 463c30c0a7c..e9588516a0c 100644 --- a/addons/mrp/wizard/change_production_qty.py +++ b/addons/mrp/wizard/change_production_qty.py @@ -4,6 +4,7 @@ from odoo import api, fields, models, _ from odoo.addons import decimal_precision as dp from odoo.exceptions import UserError +from odoo.tools import float_is_zero import math class ChangeProductionQty(models.TransientModel): @@ -37,6 +38,7 @@ class ChangeProductionQty(models.TransientModel): @api.multi def change_prod_qty(self): + precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') for wizard in self: production = wizard.mo_id produced = sum(production.move_finished_ids.mapped('quantity_done')) @@ -64,11 +66,14 @@ class ChangeProductionQty(models.TransientModel): wo.duration_expected = (operation.workcenter_id.time_start + operation.workcenter_id.time_stop + cycle_number * operation.time_cycle * 100.0 / operation.workcenter_id.time_efficiency) + quantity = wo.qty_production - wo.qty_produced if production.product_id.tracking == 'serial': - quantity = 1.0 + quantity = 1.0 if float_is_zero(quantity, precision_digits=precision) else 0.0 else: - quantity = wo.qty_production - wo.qty_produced quantity = quantity if (quantity > 0) else 0 + if float_is_zero(quantity, precision_digits=precision): + wo.final_lot_id = False + wo.active_move_lot_ids.unlink() wo.qty_producing = quantity if wo.qty_produced < wo.qty_production and wo.state == 'done': wo.state = 'progress' @@ -81,6 +86,6 @@ class ChangeProductionQty(models.TransientModel): moves_finished = production.move_finished_ids.filtered(lambda move: move.operation_id == operation) #TODO: code does nothing, unless maybe by_products? moves_raw.mapped('move_lot_ids').write({'workorder_id': wo.id}) (moves_finished + moves_raw).write({'workorder_id': wo.id}) - if wo.move_raw_ids.filtered(lambda x: x.product_id.tracking != 'none') and not wo.active_move_lot_ids: + if quantity > 0 and wo.move_raw_ids.filtered(lambda x: x.product_id.tracking != 'none') and not wo.active_move_lot_ids: wo._generate_lot_ids() return {} From f3691b5354087024c46543df5dc8d7752757e97e Mon Sep 17 00:00:00 2001 From: Goffin Simon Date: Thu, 19 Oct 2017 16:08:42 +0200 Subject: [PATCH 03/10] [FIX] website_sale: Pay Now Button is Clickable even without clicking policies and conditions The button "Pay Now" is clickable by default. So each time a new delivery carrier is selected, the button "Pay Now" will be cickable when the delivery carrier is updated. Before the fix, the button "Pay now" was available even the checkbox "Policies and conditions" were not checked because the deleted code was applied after the one checking the policies and conditions. opw:771418 --- .../website_sale_delivery/static/src/js/website_sale_delivery.js | 1 - 1 file changed, 1 deletion(-) diff --git a/addons/website_sale_delivery/static/src/js/website_sale_delivery.js b/addons/website_sale_delivery/static/src/js/website_sale_delivery.js index 0775b7657cc..12710db3b4c 100644 --- a/addons/website_sale_delivery/static/src/js/website_sale_delivery.js +++ b/addons/website_sale_delivery/static/src/js/website_sale_delivery.js @@ -1,7 +1,6 @@ $(document).ready(function () { var $pay_button = $('.oe_sale_acquirer_button button'); - $pay_button.prop('disabled', false); // When choosing an delivery carrier, update the quotation and the acquirers. Disable the 'Pay // Now' button to avoid being redirected to payment acquier if the delivery carrier update is From 6182053c5af12fabf24b7194a3ca74858dbeb68a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Mon, 18 Sep 2017 15:11:19 +0200 Subject: [PATCH 04/10] [FIX] mail: prevent pos tour from sometimes failing Here is what could (sometimes) happen in community: - the point of sale tour is started by the runbot - it loads the '/web' url - this instantiate the mail client action (init) - the tour sees that the POS link is in the dom and clicks on it - the action manager destroys the discuss client action (before its start method is called) - the destroy method from the client action is called, this.$buttons is not set, the destroy method causes a traceback --- addons/mail/static/src/js/client_action.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/addons/mail/static/src/js/client_action.js b/addons/mail/static/src/js/client_action.js index 0e3985c3e1b..bd7638ab9e2 100644 --- a/addons/mail/static/src/js/client_action.js +++ b/addons/mail/static/src/js/client_action.js @@ -667,7 +667,9 @@ var ChatAction = Widget.extend(ControlPanelMixin, { }); }, destroy: function() { - this.$buttons.off().destroy(); + if (this.$buttons) { + this.$buttons.off().destroy(); + } this._super.apply(this, arguments); }, }); From d0e71fccf8306cff9066da7a6dca39ac01c11063 Mon Sep 17 00:00:00 2001 From: Ayman Nedjmeddine Date: Thu, 19 Oct 2017 09:14:23 +0100 Subject: [PATCH 05/10] [CLA] IOAyman's signature Done at #20305 --- doc/cla/individual/IOAyman.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 doc/cla/individual/IOAyman.md diff --git a/doc/cla/individual/IOAyman.md b/doc/cla/individual/IOAyman.md new file mode 100644 index 00000000000..96a37ff862c --- /dev/null +++ b/doc/cla/individual/IOAyman.md @@ -0,0 +1,11 @@ +Algeria, 10-19-2017 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Ayman Nedjmeddine theycallmethedr@gmail.com https://github.com/IOAyman From 5f0b7942d551f441aa41e75ee06f2dd163a9c6f6 Mon Sep 17 00:00:00 2001 From: Ayman Nedjmeddine Date: Thu, 19 Oct 2017 09:01:57 +0100 Subject: [PATCH 06/10] [FIX] base: Add filter names To be able to use xpath on it Closes #20305 --- odoo/addons/base/res/res_partner_view.xml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/odoo/addons/base/res/res_partner_view.xml b/odoo/addons/base/res/res_partner_view.xml index 216063b6145..2ce80912386 100644 --- a/odoo/addons/base/res/res_partner_view.xml +++ b/odoo/addons/base/res/res_partner_view.xml @@ -329,7 +329,8 @@ - + @@ -343,8 +344,8 @@ - - + + From 9e3c723c19e3889e589e9497236f80e09e5e612e Mon Sep 17 00:00:00 2001 From: "Lucas Perais (lpe)" Date: Mon, 23 Oct 2017 13:06:04 +0200 Subject: [PATCH 07/10] [FIX] account: fix rounding of taxes in invoices Set the round_globally method on the company. Have an invoice with several lines and some of them having two (or more) taxes. See the OPW 775501 for exact numbers. It is a practical example case where round(0.003 + 0.004) = 0.007 ~ 0.01 BUT round(0.003) + round(0.004) ~ 0.00 + 0.00 = 0.00 Before this commit there could be a difference between amount_residual and amount_total on invoices. This was because the two amounts are based on something different: AMOUNT RESIDUAL It computed in function of account_move_lines of the invoice. For such an invoice the structure of those lines would be 1 line per tax + 1 line in the receivable account Here the amount tax of each line is rounded before the addition. In summary, the total amount of each tax are rounded and then added to each other AMOUNT TOTAL It is computed in function of the on-the-fly computation of the taxes with two cases: - round per line: obviously each invoice_line gets its taxes rounded before addition, which isn't problematic as regards to the previous paragraph - round globally: Here lied the problem. the rounding of the tax_line_ids was made **after** each tax amount was added to each other, which contradicts the above argument After this commit, we round tax_line_ids before their addition, it is rather logical and matches the expectation one can have reading the tooltip of the rounding method field >> If you select 'Round per Line' : for each tax, the tax amount will first be computed and rounded for each PO/SO/invoice line and then these rounded amounts will be summed, leading to the total amount for that tax. If you select 'Round Globally': for *****each tax*****, the tax amount will be computed for each PO/SO/invoice line, then these amounts will be summed and eventually ***this total tax amount*** will be rounded << OPW 775501 After this commit, it is expected that the behavior of sale.order and purchase.order won't be the same as the invoice in the same conditions. See PR #20349 for an explanation --- addons/account/models/account_invoice.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 06fba95630c..2dc3222456f 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -44,8 +44,9 @@ class AccountInvoice(models.Model): @api.one @api.depends('invoice_line_ids.price_subtotal', 'tax_line_ids.amount', 'currency_id', 'company_id', 'date_invoice', 'type') def _compute_amount(self): + round_curr = self.currency_id.round self.amount_untaxed = sum(line.price_subtotal for line in self.invoice_line_ids) - self.amount_tax = sum(line.amount for line in self.tax_line_ids) + self.amount_tax = sum(round_curr(line.amount) for line in self.tax_line_ids) self.amount_total = self.amount_untaxed + self.amount_tax amount_total_company_signed = self.amount_total amount_untaxed_signed = self.amount_untaxed From 47c0cfc6e790df0f655ac3aa096fb807a6306a46 Mon Sep 17 00:00:00 2001 From: Joren Van Onder Date: Mon, 23 Oct 2017 14:51:17 -0700 Subject: [PATCH 08/10] [FIX] point_of_sale: don't redeclare sign var is function-scope (not block-scope) so this happens to work, but it's confusing. --- addons/point_of_sale/static/src/js/models.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 43c375937ed..caaa1d6acbc 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -1416,7 +1416,7 @@ exports.Orderline = Backbone.Model.extend({ var sign = 1; if(base < 0){ base = -base; - var sign = -1; + sign = -1; } var incl_fixed_amount = 0.0; From 33d20a224164cd83f09018addcf02496f22bced2 Mon Sep 17 00:00:00 2001 From: Joren Van Onder Date: Mon, 23 Oct 2017 14:51:28 -0700 Subject: [PATCH 09/10] [FIX] point_of_sale: allow negative prices again 5afb5ecd04cf49be6bedb655b187b19a65e1e02d rewrote the tax computation in both account and point_of_sale to fix complex tax computations but it no longer allowed negative prices in the point of sale. opw-778013 --- addons/point_of_sale/static/src/js/models.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index caaa1d6acbc..edc1114389a 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -1463,8 +1463,8 @@ exports.Orderline = Backbone.Model.extend({ }); return { taxes: taxes_vals, - total_excluded: round_pr(total_excluded, currency_rounding_bak), - total_included: round_pr(total_included, currency_rounding_bak) + total_excluded: sign * round_pr(total_excluded, currency_rounding_bak), + total_included: sign * round_pr(total_included, currency_rounding_bak) }; }, get_all_prices: function(){ From 7bd8f9a5b2cde69cb68c71998973a1fe38c97e02 Mon Sep 17 00:00:00 2001 From: qsm-odoo Date: Mon, 23 Oct 2017 17:12:57 +0200 Subject: [PATCH 10/10] [FIX] web_editor: replacing selected text in a hierarchy on Firefox When following these steps on Firefox: - Select all text in a bold link (triple click) - Type some text The typed text did not replace the selected text but was instead put at the front of it. --- addons/web_editor/static/src/js/summernote.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/addons/web_editor/static/src/js/summernote.js b/addons/web_editor/static/src/js/summernote.js index 51dd3df97ed..bdd365ff93f 100644 --- a/addons/web_editor/static/src/js/summernote.js +++ b/addons/web_editor/static/src/js/summernote.js @@ -550,6 +550,13 @@ dom.removeBetween = function (sc, so, ec, eo, towrite) { so = 0; eo = 1; } + + var parentNode = sc && sc.parentNode; + if (parentNode && sc.tagName === 'BR') { + sc = parentNode; + ec = parentNode; + } + return { sc: sc, so: so, @@ -1164,9 +1171,9 @@ $.summernote.pluginEvents.visible = function (event, editor, layoutInfo) { if (!r) return; if (!r.isCollapsed()) { - if (dom.isCell(dom.node(r.sc)) || dom.isCell(dom.node(r.ec))) { + if ((dom.isCell(dom.node(r.sc)) || dom.isCell(dom.node(r.ec))) && dom.node(r.sc) !== dom.node(r.ec)) { remove_table_content(r); - r = range.create(r.ec, 0).select(); + r = range.create(r.ec, 0); } else { r = r.deleteContents(true); }