From edcbf067d65910c58f08e1e64f9aec2decd4d5aa Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Tue, 10 Mar 2015 19:16:07 +0100 Subject: [PATCH 01/36] [FIX] workflow.workitem: deleting a subflow should never cascade to workitems It's always dangerous because (cascade-)deleting workitems has a great chance of killing the workflow instances they belong to, putting the records in the workflow limbo permanently. (They will appear stuck as they don't have enough workitems anymore) In addition, in some rare cases a subflow activity is converted into a regular activity during an update, and nullifying the `subflow_id` column is all there is to fixing the corresponding workitems. It will simply take them out of their subflow, and back into the main flow. This is the case for the modification of the purchase.order workflow in Odoo 8 (saas-5), for the picking subflow. --- openerp/addons/base/ir/workflow/workflow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/addons/base/ir/workflow/workflow.py b/openerp/addons/base/ir/workflow/workflow.py index cfef52a7b88..dc1fc9afb79 100644 --- a/openerp/addons/base/ir/workflow/workflow.py +++ b/openerp/addons/base/ir/workflow/workflow.py @@ -157,7 +157,7 @@ class wkf_workitem(osv.osv): _columns = { 'act_id': fields.many2one('workflow.activity', 'Activity', required=True, ondelete="cascade", select=True), 'wkf_id': fields.related('act_id','wkf_id', type='many2one', relation='workflow', string='Workflow'), - 'subflow_id': fields.many2one('workflow.instance', 'Subflow', ondelete="cascade", select=True), + 'subflow_id': fields.many2one('workflow.instance', 'Subflow', ondelete="set null", select=True), 'inst_id': fields.many2one('workflow.instance', 'Instance', required=True, ondelete="cascade", select=True), 'state': fields.char('Status', size=64, select=True), } From 2275af4b775c548804dd784d3034cb46dc8c4e14 Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Tue, 10 Mar 2015 13:36:13 +0100 Subject: [PATCH 02/36] [FIX] base: ir sequence number is now reset to the requested value when calling the write method opw: 626974 --- openerp/addons/base/ir/ir_sequence.py | 2 +- openerp/addons/base/tests/__init__.py | 2 ++ openerp/addons/base/tests/test_ir_sequence.py | 35 +++++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 openerp/addons/base/tests/test_ir_sequence.py diff --git a/openerp/addons/base/ir/ir_sequence.py b/openerp/addons/base/ir/ir_sequence.py index 0f505814c0c..9e9e0696cbe 100644 --- a/openerp/addons/base/ir/ir_sequence.py +++ b/openerp/addons/base/ir/ir_sequence.py @@ -198,7 +198,7 @@ class ir_sequence(openerp.osv.osv.osv): if new_implementation in ('standard', None): # Implementation has NOT changed. # Only change sequence if really requested. - if row['number_next'] != n: + if values.get('number_next'): self._alter_sequence(cr, row['id'], i, n) else: # Just in case only increment changed diff --git a/openerp/addons/base/tests/__init__.py b/openerp/addons/base/tests/__init__.py index 3c6029c49ea..d1438de25f7 100644 --- a/openerp/addons/base/tests/__init__.py +++ b/openerp/addons/base/tests/__init__.py @@ -6,6 +6,7 @@ import test_ir_values import test_menu import test_search import test_views +import test_ir_sequence checks = [ test_base, @@ -16,4 +17,5 @@ checks = [ test_menu, test_search, test_views, + test_ir_sequence, ] diff --git a/openerp/addons/base/tests/test_ir_sequence.py b/openerp/addons/base/tests/test_ir_sequence.py new file mode 100644 index 00000000000..85ae21bb8cc --- /dev/null +++ b/openerp/addons/base/tests/test_ir_sequence.py @@ -0,0 +1,35 @@ +import unittest2 + +import openerp.tests.common as common + + +class Test_ir_sequence(common.TransactionCase): + + def test_00(self): + registry, cr, uid = self.registry, self.cr, self.uid + + # test if read statement return the good number_next value (from postgreSQL sequence and not ir_sequence value) + sequence = registry('ir.sequence') + # first creation of sequence (normal) + values = {'number_next': 1, + 'company_id': 1, + 'padding': 4, + 'number_increment': 1, + 'implementation': 'standard', + 'name': 'test-sequence-00'} + seq_id = sequence.create(cr, uid, values) + # Call get next 4 times + sequence.next_by_id(cr, uid, seq_id) + sequence.next_by_id(cr, uid, seq_id) + sequence.next_by_id(cr, uid, seq_id) + read_sequence = sequence.next_by_id(cr, uid, seq_id) + # Read the value of the current sequence + assert read_sequence == "0004", 'The actual sequence value must be 4. reading : %s' % read_sequence + # reset sequence to 1 by write method calling + sequence.write(cr, uid, [seq_id], {'number_next': 1}) + # Read the value of the current sequence + read_sequence = sequence.next_by_id(cr, uid, seq_id) + assert read_sequence == "0001", 'The actual sequence value must be 1. reading : %s' % read_sequence + +if __name__ == "__main__": + unittest2.main() From 170674f994f728ccae4a2566e70a97c4bfc0485e Mon Sep 17 00:00:00 2001 From: Jos De Graeve Date: Thu, 12 Mar 2015 15:08:10 +0100 Subject: [PATCH 03/36] Fix nl_BE language name --- openerp/tools/misc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/tools/misc.py b/openerp/tools/misc.py index 9fba582ad30..5110527d3f2 100644 --- a/openerp/tools/misc.py +++ b/openerp/tools/misc.py @@ -536,7 +536,7 @@ ALL_LANGUAGES = { 'mn_MN': u'Mongolian / монгол', 'nb_NO': u'Norwegian Bokmål / Norsk bokmål', 'nl_NL': u'Dutch / Nederlands', - 'nl_BE': u'Flemish (BE) / Vlaams (BE)', + 'nl_BE': u'Dutch (BE) / Nederlands (BE)', 'oc_FR': u'Occitan (FR, post 1500) / Occitan', 'pl_PL': u'Polish / Język polski', 'pt_BR': u'Portuguese (BR) / Português (BR)', From 3923f4051dcfcf91ae3a89b9330bd2899c312f47 Mon Sep 17 00:00:00 2001 From: Thomas Groutars Date: Thu, 12 Mar 2015 12:01:34 +0100 Subject: [PATCH 04/36] [FIX] models: propagate context in action returned by get_formview_action When opening a record from a many2one, the context is not propagated to fields_view_get. This is a problem if you set "form_view_ref" in the context for example. opw-629628 --- openerp/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openerp/models.py b/openerp/models.py index 2e13eca08a0..9a712602d66 100644 --- a/openerp/models.py +++ b/openerp/models.py @@ -1606,6 +1606,7 @@ class BaseModel(object): 'views': [(view_id, 'form')], 'target': 'current', 'res_id': id, + 'context': context, } def get_access_action(self, cr, uid, id, context=None): From 8eb62ca282974a0428d34a85070f8cc1cd1e3564 Mon Sep 17 00:00:00 2001 From: Denis Ledoux Date: Thu, 12 Mar 2015 17:54:30 +0100 Subject: [PATCH 05/36] [FIX] purchase: invoice control on purchase order lines menu. Only display purchase orders having as invoice control method 'on purchase order lines'. opw-630016 --- addons/purchase/purchase_view.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/purchase/purchase_view.xml b/addons/purchase/purchase_view.xml index 994c83e4fbe..bab77116176 100644 --- a/addons/purchase/purchase_view.xml +++ b/addons/purchase/purchase_view.xml @@ -533,7 +533,7 @@ On Purchase Order Lines ir.actions.act_window purchase.order.line - [('order_id.invoice_method','<>','picking'), ('state','in',('confirmed','done')), ('invoiced', '=', False)] + [('order_id.invoice_method','=','manual'), ('state','in',('confirmed','done')), ('invoiced', '=', False)] form tree,form From 06909462b13dc1fcee45bb32beea2851a56de821 Mon Sep 17 00:00:00 2001 From: Josse Colpaert Date: Thu, 12 Mar 2015 10:46:19 +0100 Subject: [PATCH 06/36] [IMP] mrp: Make it possible to define a rule from stock > production to use the correct procure method It is very cumbersome to define routes which have 2 step in on production for certain products with procurement rules (e.g. Stock > Raw Materials Location > Production), when the products are MTS elsewhere. That is why we add the possibility, which might be elaborated in the future, to define rules e.g. from Stock > Production, which you can put on the product / product categories. These rules will only define the procure_method for now. --- addons/mrp/mrp.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/addons/mrp/mrp.py b/addons/mrp/mrp.py index 3ef5ac3000e..f5bf9dd2add 100644 --- a/addons/mrp/mrp.py +++ b/addons/mrp/mrp.py @@ -1076,14 +1076,27 @@ class mrp_production(osv.osv): #is 1 element long, so we can take the first. return stock_move.action_confirm(cr, uid, [move_id], context=context)[0] - def _get_raw_material_procure_method(self, cr, uid, product, context=None): - '''This method returns the procure_method to use when creating the stock move for the production raw materials''' + def _get_raw_material_procure_method(self, cr, uid, product, location_id=False, location_dest_id=False, context=None): + '''This method returns the procure_method to use when creating the stock move for the production raw materials + Besides the standard configuration of looking if the product or product category has the MTO route, + you can also define a rule e.g. from Stock to Production (which might be used in the future like the sale orders) + ''' warehouse_obj = self.pool['stock.warehouse'] + routes = product.route_ids + product.categ_id.total_route_ids + + if location_id and location_dest_id: + pull_obj = self.pool['procurement.rule'] + pulls = pull_obj.search(cr, uid, [('route_id', 'in', [x.id for x in routes]), + ('location_id', '=', location_dest_id), + ('location_src_id', '=', location_id)], limit=1, context=context) + if pulls: + return pull_obj.browse(cr, uid, pulls[0], context=context).procure_method + try: mto_route = warehouse_obj._get_mto_route(cr, uid, context=context) except: return "make_to_stock" - routes = product.route_ids + product.categ_id.total_route_ids + if mto_route in [x.id for x in routes]: return "make_to_order" return "make_to_stock" @@ -1115,7 +1128,8 @@ class mrp_production(osv.osv): move = stock_move.copy(cr, uid, move_id, default = { 'location_id': source_location_id, 'location_dest_id': dest_location_id, - 'procure_method': self._get_raw_material_procure_method(cr, uid, product, context=context), + 'procure_method': self._get_raw_material_procure_method(cr, uid, product, location_id=source_location_id, + location_dest_id=dest_location_id, context=context), 'raw_material_production_id': False, 'move_dest_id': move_id, 'picking_type_id': types and types[0] or False, @@ -1148,7 +1162,8 @@ class mrp_production(osv.osv): 'location_id': source_location_id, 'location_dest_id': destination_location_id, 'company_id': production.company_id.id, - 'procure_method': prev_move and 'make_to_stock' or self._get_raw_material_procure_method(cr, uid, product, context=context), #Make_to_stock avoids creating procurement + 'procure_method': prev_move and 'make_to_stock' or self._get_raw_material_procure_method(cr, uid, product, location_id=source_location_id, + location_dest_id=destination_location_id, context=context), #Make_to_stock avoids creating procurement 'raw_material_production_id': production.id, #this saves us a browse in create() 'price_unit': product.standard_price, From 91e5456fafc16b90359086a58e795dda4089031c Mon Sep 17 00:00:00 2001 From: "Ronald Portier (Therp BV)" Date: Thu, 12 Mar 2015 20:10:46 +0100 Subject: [PATCH 07/36] [CLA] Therp BV Closes #5588 --- doc/cla/corporate/therp.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 doc/cla/corporate/therp.md diff --git a/doc/cla/corporate/therp.md b/doc/cla/corporate/therp.md new file mode 100644 index 00000000000..428ecb2b411 --- /dev/null +++ b/doc/cla/corporate/therp.md @@ -0,0 +1,21 @@ +Netherlands, 2015-03-09 + +Therp BV agrees to the terms of the Odoo Corporate +Contributor License Agreement v1.0. + +We declare that we are authorized and able to make this agreement and sign +this declaration. + +Signed, + +* Ronald Portier rportier@therp.nl https://github.com/nl66278 +* Stefan Rijnhart stefan@therp.nl https://github.com/stefanrijnhart + +List of contributors: + +* Giovanni Capalbo giovanni@therp.nl https://github.com/gfcapalbo +* Holger Brunn hbrunn@therp.nl https://github.com/hbrunn +* Lara Freeke lfreeke@therp.nl https://github.com/lfreeke +* Ronald Portier rportier@therp.nl https://github.com/nl66278 +* Stefan Rijnhart stefan@therp.nl https://github.com/stefanrijnhart + From da8f1f7a57b073b371034590ab99f52986dba57e Mon Sep 17 00:00:00 2001 From: Lorenzo Battistini Date: Wed, 18 Feb 2015 11:48:36 +0100 Subject: [PATCH 08/36] [CLA] Lorenzo Battistini Closes #5709 --- doc/cla/individual/eLBati.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 doc/cla/individual/eLBati.md diff --git a/doc/cla/individual/eLBati.md b/doc/cla/individual/eLBati.md new file mode 100644 index 00000000000..50104556d80 --- /dev/null +++ b/doc/cla/individual/eLBati.md @@ -0,0 +1,11 @@ +Italy, 2015-02-18 + +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, + +Lorenzo Battistini lorenzo.battistini@agilebg.com https://github.com/eLBati From 7e0ff24ccac15b0074c6a31cbf65411fd39c56a6 Mon Sep 17 00:00:00 2001 From: Thomas Groutars Date: Thu, 12 Mar 2015 18:29:00 +0100 Subject: [PATCH 09/36] [FIX] base: delete cascade value mapping on server action deletion When deleting a server action, its fields_lines were not deleted. It causes an issue for example in this case: Create a new custom field on any model Create a server action with Action To Do: Write on a Record Base model: model of the custom field Add a line in the value mapping, using the custom field you just created Delete the server action Delete the custom field => Throws an error because required field col1 is not defined on the ir_server_object_lines object --- openerp/addons/base/ir/ir_actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/addons/base/ir/ir_actions.py b/openerp/addons/base/ir/ir_actions.py index c8abbf4f226..7dcdb5ce279 100644 --- a/openerp/addons/base/ir/ir_actions.py +++ b/openerp/addons/base/ir/ir_actions.py @@ -1035,7 +1035,7 @@ class ir_server_object_lines(osv.osv): _sequence = 'ir_actions_id_seq' _columns = { - 'server_id': fields.many2one('ir.actions.server', 'Related Server Action'), + 'server_id': fields.many2one('ir.actions.server', 'Related Server Action', ondelete='cascade'), 'col1': fields.many2one('ir.model.fields', 'Field', required=True), 'value': fields.text('Value', required=True, help="Expression containing a value specification. \n" "When Formula type is selected, this field may be a Python expression " From deb1db281b404be2d892f1597aef4faf12d61b33 Mon Sep 17 00:00:00 2001 From: Commandant Custo Date: Sun, 8 Mar 2015 18:46:41 +0100 Subject: [PATCH 10/36] [FIX] account: Missing label for partner VAT in invoice report Missing label for partner VAT field in invoice report --- addons/account/views/report_invoice.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/account/views/report_invoice.xml b/addons/account/views/report_invoice.xml index 7aaff437a8e..27f2f909397 100644 --- a/addons/account/views/report_invoice.xml +++ b/addons/account/views/report_invoice.xml @@ -8,7 +8,7 @@
- + VAT:
From 57593e5898e572ccba743c9e898de32c132f19d4 Mon Sep 17 00:00:00 2001 From: Josse Colpaert Date: Wed, 11 Mar 2015 18:13:52 +0100 Subject: [PATCH 11/36] [IMP] stock: picking report should show only what is reserved when partially available Only when the picking is partially available, the system will only show the amount of products available. That way, when working with paper, the warehouse operator will only get what is available. This is the case where we did not launch the bar code interface or the transfer wizard. --- addons/stock/views/report_stockpicking.xml | 27 ++++++++++++++-------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/addons/stock/views/report_stockpicking.xml b/addons/stock/views/report_stockpicking.xml index 7fd4b9d08f9..6348394f570 100644 --- a/addons/stock/views/report_stockpicking.xml +++ b/addons/stock/views/report_stockpicking.xml @@ -83,15 +83,24 @@ - - - - - - - - - + + + + + + + + + + + + + + + + + + From 09abac7608cc60a56de8e59184575f40629215d5 Mon Sep 17 00:00:00 2001 From: Josse Colpaert Date: Thu, 26 Feb 2015 11:02:12 +0100 Subject: [PATCH 12/36] [FIX] Stock: Make sure inventory works with packs and lot = False The inventory should work with packs. If a pack is not indicated in the inventory line, it means we correct the quantity for the product that is not in a pack. Also, when the lot is False, it should not behave as normal stock moves. It should correct for the quantity that has no lot. We try to reconcile the negative quants in the pack also. --- addons/stock/stock.py | 61 ++++++++++----------------- addons/stock/tests/test_stock_flow.py | 52 +++++++++++++++++++++++ 2 files changed, 74 insertions(+), 39 deletions(-) diff --git a/addons/stock/stock.py b/addons/stock/stock.py index 85af75c9cd0..4bd984b6558 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -2644,14 +2644,6 @@ class stock_inventory(osv.osv): self.pool.get('stock.inventory.line').write(cr, uid, line_ids, {'product_qty': 0}) return True - def _inventory_line_hook(self, cr, uid, inventory_line, move_vals): - """ Creates a stock move from an inventory line - @param inventory_line: - @param move_vals: - @return: - """ - return self.pool.get('stock.move').create(cr, uid, move_vals) - def action_done(self, cr, uid, ids, context=None): """ Finish the inventory @return: True @@ -2670,35 +2662,7 @@ class stock_inventory(osv.osv): #as they will be moved to inventory loss, and other quants will be created to the encoded quant location. This is a normal behavior #as quants cannot be reuse from inventory location (users can still manually move the products before/after the inventory if they want). move_obj = self.pool.get('stock.move') - move_obj.action_done(cr, uid, [x.id for x in inv.move_ids], context=context) - - def _create_stock_move(self, cr, uid, inventory, todo_line, context=None): - stock_move_obj = self.pool.get('stock.move') - product_obj = self.pool.get('product.product') - inventory_location_id = product_obj.browse(cr, uid, todo_line['product_id'], context=context).property_stock_inventory.id - vals = { - 'name': _('INV:') + (inventory.name or ''), - 'product_id': todo_line['product_id'], - 'product_uom': todo_line['product_uom_id'], - 'date': inventory.date, - 'company_id': inventory.company_id.id, - 'inventory_id': inventory.id, - 'state': 'assigned', - 'restrict_lot_id': todo_line.get('prod_lot_id'), - 'restrict_partner_id': todo_line.get('partner_id'), - } - - if todo_line['product_qty'] < 0: - #found more than expected - vals['location_id'] = inventory_location_id - vals['location_dest_id'] = todo_line['location_id'] - vals['product_uom_qty'] = -todo_line['product_qty'] - else: - #found less than expected - vals['location_id'] = todo_line['location_id'] - vals['location_dest_id'] = inventory_location_id - vals['product_uom_qty'] = todo_line['product_qty'] - return stock_move_obj.create(cr, uid, vals, context=context) + move_obj.action_done(cr, uid, [x.id for x in inv.move_ids if x.state != 'done'], context=context) def action_check(self, cr, uid, ids, context=None): """ Checks the inventory and computes the stock move to do @@ -2712,13 +2676,14 @@ class stock_inventory(osv.osv): stock_move_obj.unlink(cr, uid, move_ids, context=context) for line in inventory.line_ids: #compare the checked quantities on inventory lines to the theorical one - inventory_line_obj._resolve_inventory_line(cr, uid, line, context=context) + stock_move = inventory_line_obj._resolve_inventory_line(cr, uid, line, context=context) def action_cancel_draft(self, cr, uid, ids, context=None): """ Cancels the stock move and change inventory state to draft. @return: True """ for inv in self.browse(cr, uid, ids, context=context): + self.write(cr, uid, [inv.id], {'line_ids': [(5,)]}, context=context) self.pool.get('stock.move').action_cancel(cr, uid, [x.id for x in inv.move_ids], context=context) self.write(cr, uid, [inv.id], {'state': 'draft'}, context=context) return True @@ -2869,6 +2834,7 @@ class stock_inventory_line(osv.osv): def _resolve_inventory_line(self, cr, uid, inventory_line, context=None): stock_move_obj = self.pool.get('stock.move') + quant_obj = self.pool.get('stock.quant') diff = inventory_line.theoretical_qty - inventory_line.product_qty if not diff: return @@ -2895,7 +2861,24 @@ class stock_inventory_line(osv.osv): vals['location_id'] = inventory_line.location_id.id vals['location_dest_id'] = inventory_location_id vals['product_uom_qty'] = diff - return stock_move_obj.create(cr, uid, vals, context=context) + move_id = stock_move_obj.create(cr, uid, vals, context=context) + move = stock_move_obj.browse(cr, uid, move_id, context=context) + if diff > 0: + domain = [('qty', '>', 0.0), ('package_id', '=', inventory_line.package_id.id), ('lot_id', '=', inventory_line.prod_lot_id.id)] + preferred_domain_list = [[('reservation_id', '=', False)], [('reservation_id.inventory_id', '!=', inventory_line.inventory_id.id)]] + quants = quant_obj.quants_get_prefered_domain(cr, uid, move.location_id, move.product_id, move.product_qty, domain=domain, prefered_domain_list=preferred_domain_list, restrict_partner_id=move.restrict_partner_id.id, context=context) + quant_obj.quants_reserve(cr, uid, quants, move, context=context) + elif inventory_line.package_id: + stock_move_obj.action_done(cr, uid, move_id, context=context) + quants = [x.id for x in move.quant_ids] + quant_obj.write(cr, uid, quants, {'package_id': inventory_line.package_id.id}, context=context) + res = quant_obj.search(cr, uid, [('qty', '<', 0.0), ('product_id', '=', move.product_id.id), + ('location_id', '=', move.location_dest_id.id), ('package_id', '!=', False)], limit=1, context=context) + if res: + for quant in move.quant_ids: + if quant.location_id.id == move.location_dest_id.id: #To avoid we take a quant that was reconcile already + quant_obj._quant_reconcile_negative(cr, uid, quant, move, context=context) + return move_id # Should be left out in next version def restrict_change(self, cr, uid, ids, theoretical_qty, context=None): diff --git a/addons/stock/tests/test_stock_flow.py b/addons/stock/tests/test_stock_flow.py index 32c9e5e3eb0..bd3302b5be9 100644 --- a/addons/stock/tests/test_stock_flow.py +++ b/addons/stock/tests/test_stock_flow.py @@ -1095,3 +1095,55 @@ class TestStockFlow(TestStockCommon): total_qty = [quant.qty for quant in quants] self.assertEqual(sum(total_qty), 4000, 'Expecting 4000 kg , got %.4f on location stock!' % (sum(total_qty))) self.assertEqual(productKG.qty_available, 4000, 'Expecting 4000 kg , got %.4f of quantity available!' % (productKG.qty_available)) + + + #-------------------------------------------------------- + # TEST PARTIAL INVENTORY WITH PACKS and LOTS + #--------------------------------------------------------- + + packproduct = self.ProductObj.create({'name': 'Pack Product', 'uom_id': self.uom_unit.id, 'uom_po_id': self.uom_unit.id}) + lotproduct = self.ProductObj.create({'name': 'Lot Product', 'uom_id': self.uom_unit.id, 'uom_po_id': self.uom_unit.id}) + inventory = self.InvObj.create({'name': 'Test Partial and Pack', + 'filter': 'partial', + 'location_id': self.stock_location}) + inventory.prepare_inventory() + pack_obj = self.env['stock.quant.package'] + lot_obj = self.env['stock.production.lot'] + pack1 = pack_obj.create({'name': 'PACK00TEST1'}) + pack2 = pack_obj.create({'name': 'PACK00TEST2'}) + lot1 = lot_obj.create({'name': 'Lot001', 'product_id': lotproduct.id}) + move = self.MoveObj.search([('product_id', '=', productKG.id), ('inventory_id', '=', inventory.id)], limit=1) + self.assertEqual(len(move), 0, "Partial filter should not create a lines upon prepare") + + line_vals = [] + line_vals += [{'location_id': self.stock_location, 'product_id': packproduct.id, 'product_qty': 10, 'product_uom_id': packproduct.uom_id.id}] + line_vals += [{'location_id': self.stock_location, 'product_id': packproduct.id, 'product_qty': 20, 'product_uom_id': packproduct.uom_id.id, 'package_id': pack1.id}] + line_vals += [{'location_id': self.stock_location, 'product_id': lotproduct.id, 'product_qty': 30, 'product_uom_id': lotproduct.uom_id.id, 'prod_lot_id': lot1.id}] + line_vals += [{'location_id': self.stock_location, 'product_id': lotproduct.id, 'product_qty': 25, 'product_uom_id': lotproduct.uom_id.id, 'prod_lot_id': False}] + inventory.write({'line_ids': [(0, 0, x) for x in line_vals]}) + inventory.action_done() + self.assertEqual(packproduct.qty_available, 30, "Wrong qty available for packproduct") + self.assertEqual(lotproduct.qty_available, 55, "Wrong qty available for lotproduct") + quants = self.StockQuantObj.search([('product_id', '=', packproduct.id), ('location_id', '=', self.stock_location), ('package_id', '=', pack1.id)]) + total_qty = sum([quant.qty for quant in quants]) + self.assertEqual(total_qty, 20, 'Expecting 20 units on package 1 of packproduct, but we got %.4f on location stock!' % (total_qty)) + + #Create an inventory that will put the lots without lot to 0 and check that taking without pack will not take it from the pack + inventory2 = self.InvObj.create({'name': 'Test Partial Lot and Pack2', + 'filter': 'partial', + 'location_id': self.stock_location}) + inventory2.prepare_inventory() + line_vals = [] + line_vals += [{'location_id': self.stock_location, 'product_id': packproduct.id, 'product_qty': 20, 'product_uom_id': packproduct.uom_id.id}] + line_vals += [{'location_id': self.stock_location, 'product_id': lotproduct.id, 'product_qty': 0, 'product_uom_id': lotproduct.uom_id.id, 'prod_lot_id': False}] + line_vals += [{'location_id': self.stock_location, 'product_id': lotproduct.id, 'product_qty': 10, 'product_uom_id': lotproduct.uom_id.id, 'prod_lot_id': lot1.id}] + inventory2.write({'line_ids': [(0, 0, x) for x in line_vals]}) + inventory2.action_done() + self.assertEqual(packproduct.qty_available, 40, "Wrong qty available for packproduct") + self.assertEqual(lotproduct.qty_available, 10, "Wrong qty available for lotproduct") + quants = self.StockQuantObj.search([('product_id', '=', lotproduct.id), ('location_id', '=', self.stock_location), ('lot_id', '=', lot1.id)]) + total_qty = sum([quant.qty for quant in quants]) + self.assertEqual(total_qty, 10, 'Expecting 0 units lot of lotproduct, but we got %.4f on location stock!' % (total_qty)) + quants = self.StockQuantObj.search([('product_id', '=', lotproduct.id), ('location_id', '=', self.stock_location), ('lot_id', '=', False)]) + total_qty = sum([quant.qty for quant in quants]) + self.assertEqual(total_qty, 0, 'Expecting 0 units lot of lotproduct, but we got %.4f on location stock!' % (total_qty)) \ No newline at end of file From 20312e28808f3c0abf2df75e850788dcfe54f577 Mon Sep 17 00:00:00 2001 From: Josse Colpaert Date: Fri, 13 Mar 2015 10:47:40 +0100 Subject: [PATCH 13/36] [IMP] Deleting PO lines should work the same way as Cancelling PO Also the test is adapted. When a product as treated as MTO, it should not without notice change to an MTS. --- addons/purchase/purchase.py | 6 +++--- addons/purchase/test/process/run_scheduler.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/addons/purchase/purchase.py b/addons/purchase/purchase.py index 6190c52b3b7..382e8596997 100644 --- a/addons/purchase/purchase.py +++ b/addons/purchase/purchase.py @@ -1026,9 +1026,9 @@ class purchase_order_line(osv.osv): if line.state not in ['draft', 'cancel']: raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a purchase order line which is in state \'%s\'.') %(line.state,)) procurement_obj = self.pool.get('procurement.order') - procurement_ids_to_cancel = procurement_obj.search(cr, uid, [('purchase_line_id', 'in', ids)], context=context) - if procurement_ids_to_cancel: - self.pool['procurement.order'].cancel(cr, uid, procurement_ids_to_cancel) + procurement_ids_to_except = procurement_obj.search(cr, uid, [('purchase_line_id', 'in', ids)], context=context) + if procurement_ids_to_except: + self.pool['procurement.order'].write(cr, uid, procurement_ids_to_except, {'state': 'exception'}, context=context) return super(purchase_order_line, self).unlink(cr, uid, ids, context=context) def onchange_product_uom(self, cr, uid, ids, pricelist_id, product_id, qty, uom_id, diff --git a/addons/purchase/test/process/run_scheduler.yml b/addons/purchase/test/process/run_scheduler.yml index 01aa497311d..83c5c6a099b 100644 --- a/addons/purchase/test/process/run_scheduler.yml +++ b/addons/purchase/test/process/run_scheduler.yml @@ -39,4 +39,4 @@ procurement = self.browse(cr, uid, ref('procurement_order_testcase0')) procurement.purchase_line_id.unlink() procurement.refresh() - assert procurement.state == 'cancel', 'Procurement should be cancelled' + assert procurement.state == 'exception', 'Procurement should be in exception' From 4530faae8ba8ecb80ae2765129302fd6c9718442 Mon Sep 17 00:00:00 2001 From: Goffin Simon Date: Fri, 13 Mar 2015 10:35:48 +0100 Subject: [PATCH 14/36] [FIX] account_payment: compute amount to pay compute the amount_to_pay with function field amount_residual on account.move.line opw:628903, 628428 --- addons/account_payment/account_move_line.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/addons/account_payment/account_move_line.py b/addons/account_payment/account_move_line.py index e8d67e6dd89..9068d8b729c 100644 --- a/addons/account_payment/account_move_line.py +++ b/addons/account_payment/account_move_line.py @@ -29,16 +29,14 @@ class account_move_line(osv.osv): def amount_to_pay(self, cr, uid, ids, name, arg=None, context=None): """ Return the amount still to pay regarding all the payemnt orders (excepting cancelled orders)""" + res = {} if not ids: - return {} - res = dict.fromkeys(ids, 0.0) - cr.execute("""SELECT ml.id, - (SELECT sum(coalesce(residual, 0)) - FROM account_invoice - WHERE move_id = ml.move_id) - FROM account_move_line ml - WHERE id IN %s""", (tuple(ids),)) - res.update(cr.fetchall()) + return res + for ml in self.browse(cr, uid, ids, context=context): + if ml.amount_currency: + res[ml.id] = ml.amount_residual_currency + else: + res[ml.id] = ml.amount_residual return res def _to_pay_search(self, cr, uid, obj, name, args, context=None): From adde3e9569fc7229ee2743fa426e8686005a59f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Ru=CC=88bner?= Date: Thu, 5 Mar 2015 20:18:53 +0100 Subject: [PATCH 15/36] [CLA] bloopark systems GmbH Closes #5617 --- doc/cla/corporate/bloopark.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 doc/cla/corporate/bloopark.md diff --git a/doc/cla/corporate/bloopark.md b/doc/cla/corporate/bloopark.md new file mode 100644 index 00000000000..2664bbe3006 --- /dev/null +++ b/doc/cla/corporate/bloopark.md @@ -0,0 +1,20 @@ +Germany 2015-02-25 + +bloopark systems GmbH & Co. KG agrees to the terms of the Odoo Corporate +Contributor License Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Jens Holze jholze@bloopark.de https://github.com/jholze + +List of contributors: + +Jens Holze jholze@bloopark.de https://github.com/jholze +Christoph Giesel cgiesel@bloopark.de https://github.com/christophlsa +Benjamin Bachmann bBachmann@bloopark.de https://github.com/benniphx +Robert Rübner rruebner@bloopark.de https://github.com/rruebner +Florian Fischer ffischer@bloopark.de https://github.com/florianfischer +Mercerdes Scenna mscenna@bloopark.de https://github.com/mscenna From 17c1556208d895bd227ce13761794014b34f6e0b Mon Sep 17 00:00:00 2001 From: Matt Choplin Date: Fri, 13 Mar 2015 14:52:05 +0000 Subject: [PATCH 16/36] [CLA] Matt Choplin Closes #5753 --- doc/cla/individual/matthieuchoplin.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 doc/cla/individual/matthieuchoplin.md diff --git a/doc/cla/individual/matthieuchoplin.md b/doc/cla/individual/matthieuchoplin.md new file mode 100644 index 00000000000..4533005bfd0 --- /dev/null +++ b/doc/cla/individual/matthieuchoplin.md @@ -0,0 +1,11 @@ +United Kingdom, 13.03.2015 + +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, + +Matthieu Choplin choplin.mat@gmail.com https://github.com/matthieuchoplin From 2606e37b18c92f02331c05620f5c0edc7b9f14ad Mon Sep 17 00:00:00 2001 From: Goffin Simon Date: Mon, 16 Mar 2015 08:55:04 +0100 Subject: [PATCH 17/36] [FIX] account_asset: currency problem with depreciation in asset management The amount used to create an account asset depreciation line must be in the currency of the asset. opw: 628663 --- addons/account_asset/account_asset.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/addons/account_asset/account_asset.py b/addons/account_asset/account_asset.py index 9cf4c14fff2..59a3bdc825b 100644 --- a/addons/account_asset/account_asset.py +++ b/addons/account_asset/account_asset.py @@ -172,10 +172,6 @@ class account_asset_asset(osv.osv): for x in range(len(posted_depreciation_line_ids), undone_dotation_number): i = x + 1 amount = self._compute_board_amount(cr, uid, asset, i, residual_amount, amount_to_depr, undone_dotation_number, posted_depreciation_line_ids, total_days, depreciation_date, context=context) - company_currency = asset.company_id.currency_id.id - current_currency = asset.currency_id.id - # compute amount into company currency - amount = currency_obj.compute(cr, uid, current_currency, company_currency, amount, context=context) residual_amount -= amount vals = { 'amount': amount, From 373b56051101b8d4b87579a741ce552142d82410 Mon Sep 17 00:00:00 2001 From: David Monjoie Date: Mon, 16 Mar 2015 10:33:03 +0100 Subject: [PATCH 18/36] [FIX] account: added check if country has a code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It is possible, if the user created a new country by himself, to have a country without a code, we then need to check that. Fixes opw 629891, where the customer wrote "Belgie" trying to find "België", didn't find it and created a new "Belgie" country without code. --- addons/account/account_pre_install.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/account/account_pre_install.yml b/addons/account/account_pre_install.yml index e295989e698..798c0d954a5 100644 --- a/addons/account/account_pre_install.yml +++ b/addons/account/account_pre_install.yml @@ -7,7 +7,7 @@ wiz = wizards.browse(cr, uid, ref('account.account_configuration_installer_todo')) part = self.pool.get('res.partner').browse(cr, uid, ref('base.main_partner')) # if we know the country and the wizard has not yet been executed, we do it - if (part.country_id.id) and (wiz.state=='open'): + if (part.country_id.id) and (part.country_id.code) and (wiz.state=='open'): mod = 'l10n_'+part.country_id.code.lower() ids = modules.search(cr, uid, [ ('name','=',mod) ], context=context) if ids: From 5733ee865e9448bae6886785f05e769456b0d16e Mon Sep 17 00:00:00 2001 From: Goffin Simon Date: Fri, 13 Mar 2015 17:40:42 +0100 Subject: [PATCH 19/36] [FIX] website: no interpretation for "\2060" on mac with google chrome On mac with google chrome, the character "\2060" has no interpretation and a small square is displayed instead. opw: 630276 --- addons/website/static/src/css/website.css | 2 +- addons/website/static/src/css/website.sass | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/website/static/src/css/website.css b/addons/website/static/src/css/website.css index 1c09080fbf9..bdeafdc26e8 100644 --- a/addons/website/static/src/css/website.css +++ b/addons/website/static/src/css/website.css @@ -104,7 +104,7 @@ header a.navbar-brand img { } #wrapwrap p:empty:after { - content: "\2060"; + content: "\202f"; } /* ----- Snippets Styles ----- */ diff --git a/addons/website/static/src/css/website.sass b/addons/website/static/src/css/website.sass index b7d5bac7fcd..be6c045bb1a 100644 --- a/addons/website/static/src/css/website.sass +++ b/addons/website/static/src/css/website.sass @@ -72,7 +72,7 @@ header // the beginning of paragraphs so they don't "disappear" after saving when // used solely for spacing. #wrapwrap p:empty:after - content: '\2060' + content: '\202f' /* ----- Snippets Styles ----- */ From 7ac4639a24171e78bc3e8fe8d1fd95dd3de9c530 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Mon, 16 Mar 2015 15:31:43 +0100 Subject: [PATCH 20/36] [FIX] fields: fix determination of `field.selection` for custom fields In the case of custom fields, `field.selection` is set directly instead of being set in overridden fields. --- openerp/fields.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/openerp/fields.py b/openerp/fields.py index da357094285..da0ba4dc374 100644 --- a/openerp/fields.py +++ b/openerp/fields.py @@ -1267,20 +1267,18 @@ class Selection(Field): def set_class_name(self, cls, name): super(Selection, self).set_class_name(cls, name) # determine selection (applying 'selection_add' extensions) - selection = None for field in resolve_all_mro(cls, name, reverse=True): if isinstance(field, type(self)): # We cannot use field.selection or field.selection_add here # because those attributes are overridden by `set_class_name`. if 'selection' in field._attrs: - selection = field._attrs['selection'] + self.selection = field._attrs['selection'] if 'selection_add' in field._attrs: # use an OrderedDict to update existing values selection_add = field._attrs['selection_add'] - selection = OrderedDict(selection + selection_add).items() + self.selection = OrderedDict(self.selection + selection_add).items() else: - selection = None - self.selection = selection + self.selection = None def _description_selection(self, env): """ return the selection list (pairs (value, label)); labels are From 01c983919c97ee4cd5a1800d117f5f6457733b92 Mon Sep 17 00:00:00 2001 From: Goffin Simon Date: Sun, 15 Mar 2015 16:52:07 +0100 Subject: [PATCH 21/36] [FIX] website: to keep the spaces around the translations We keep the original spaces around the translations to avoid missing spaces between words. --- addons/website/static/src/js/website.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/addons/website/static/src/js/website.js b/addons/website/static/src/js/website.js index d2f7a8b309f..e464a4e7697 100644 --- a/addons/website/static/src/js/website.js +++ b/addons/website/static/src/js/website.js @@ -322,7 +322,9 @@ function treat_node(node){ if(node.nodeType === 3) { if(node.nodeValue.match(/\S/)){ - node.nodeValue = openerp._t($.trim(node.nodeValue)); + var text_value = $.trim(node.nodeValue); + var spaces = node.nodeValue.split(text_value); + node.nodeValue = spaces[0] + openerp._t(text_value) + spaces[1]; } } else if(node.nodeType === 1 && node.hasChildNodes()) { From b6a528fbf092ef62746772f6d8324917ad38b4f4 Mon Sep 17 00:00:00 2001 From: Jeremy Kersten Date: Sun, 15 Mar 2015 12:17:08 +0100 Subject: [PATCH 22/36] [IMP] website: improve list of template for html editor when we edit a report Since commit 0167acb, the template used in translate_doc are added to the list of template when we are editing a report with the html editor. It's nice, but we don't have all the dependicies from this one. Now we recompute the list of template, to have all the related templates: the external_layer* and all templates which overload this report. --- addons/website_report/controllers/main.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/addons/website_report/controllers/main.py b/addons/website_report/controllers/main.py index 11b4257b6c6..33f10076db2 100644 --- a/addons/website_report/controllers/main.py +++ b/addons/website_report/controllers/main.py @@ -28,6 +28,7 @@ class Website(Website): @route() def customize_template_get(self, xml_id, full=False): res = super(Website, self).customize_template_get(xml_id, full=full) - if full and request.session.get('report_view_ids'): - res += request.session['report_view_ids'] + if full: + for r in request.session.get('report_view_ids', []): + res += super(Website, self).customize_template_get(r.get('xml_id'), full=full) return res From e2ccc5709b1b4909eb857d77ebb2e21c9a5c7fb9 Mon Sep 17 00:00:00 2001 From: Goffin Simon Date: Mon, 16 Mar 2015 09:49:41 +0100 Subject: [PATCH 23/36] [FIX] account_asset: wrong currency to compute the residual amount of an asset The result of the residual amount of an asset is : asset.purchase_value - amount - asset.salvage_value where purchase_value and asset.salvage_value are in the currency of the asset. Amount is the difference between debit and credit of an account move line expressed in the currency of the company. This is why the amount must be convert in the currency of the asset. --- addons/account_asset/account_asset.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/addons/account_asset/account_asset.py b/addons/account_asset/account_asset.py index 59a3bdc825b..ffa0b75dc8c 100644 --- a/addons/account_asset/account_asset.py +++ b/addons/account_asset/account_asset.py @@ -212,7 +212,10 @@ class account_asset_asset(osv.osv): l.asset_id IN %s GROUP BY l.asset_id """, (tuple(ids),)) res=dict(cr.fetchall()) for asset in self.browse(cr, uid, ids, context): - res[asset.id] = asset.purchase_value - res.get(asset.id, 0.0) - asset.salvage_value + company_currency = asset.company_id.currency_id.id + current_currency = asset.currency_id.id + amount = self.pool['res.currency'].compute(cr, uid, company_currency, current_currency, res.get(asset.id, 0.0), context=context) + res[asset.id] = asset.purchase_value - amount - asset.salvage_value for id in ids: res.setdefault(id, 0.0) return res From d117bad18cd26d4dc575b703cffba49d728d60df Mon Sep 17 00:00:00 2001 From: Goffin Simon Date: Tue, 3 Feb 2015 17:08:45 +0100 Subject: [PATCH 24/36] [FIX] point_of_sale: invalid tax amount on grouped entries If "Group Journal Items" option is checked, the generated accounting entries may get an invalid tax amout. This will happen when several entries for the same product do not use the same tax amout (e.g. discount). Avoid grouping products with different taxes under the same line by creating one line per tax_code_id. opw 626695 --- addons/point_of_sale/point_of_sale.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/addons/point_of_sale/point_of_sale.py b/addons/point_of_sale/point_of_sale.py index 0d4f0275401..304f7d71061 100644 --- a/addons/point_of_sale/point_of_sale.py +++ b/addons/point_of_sale/point_of_sale.py @@ -1108,11 +1108,16 @@ class pos_order(osv.osv): if not grouped_data[key]: grouped_data[key].append(values) else: - current_value = grouped_data[key][0] - current_value['quantity'] = current_value.get('quantity', 0.0) + values.get('quantity', 0.0) - current_value['credit'] = current_value.get('credit', 0.0) + values.get('credit', 0.0) - current_value['debit'] = current_value.get('debit', 0.0) + values.get('debit', 0.0) - current_value['tax_amount'] = current_value.get('tax_amount', 0.0) + values.get('tax_amount', 0.0) + for line in grouped_data[key]: + if line.get('tax_code_id') == values.get('tax_code_id'): + current_value = line + current_value['quantity'] = current_value.get('quantity', 0.0) + values.get('quantity', 0.0) + current_value['credit'] = current_value.get('credit', 0.0) + values.get('credit', 0.0) + current_value['debit'] = current_value.get('debit', 0.0) + values.get('debit', 0.0) + current_value['tax_amount'] = current_value.get('tax_amount', 0.0) + values.get('tax_amount', 0.0) + break + else: + grouped_data[key].append(values) else: grouped_data[key].append(values) From e182da22f4a962655e92704a826d5019d61c4e40 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Tue, 17 Mar 2015 11:40:19 +0100 Subject: [PATCH 25/36] [FIX] base/ir_model: upon custom field creation, always clear manual fields in registry The invalidation in registry was done only if the field's model was already in registry. This situation is not the case when you create a custom model with a custom field. --- openerp/addons/base/ir/ir_model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openerp/addons/base/ir/ir_model.py b/openerp/addons/base/ir/ir_model.py index e00d80fe9a4..282d76d7ef0 100644 --- a/openerp/addons/base/ir/ir_model.py +++ b/openerp/addons/base/ir/ir_model.py @@ -383,13 +383,13 @@ class ir_model_fields(osv.osv): if vals.get('relation',False) and not self.pool['ir.model'].search(cr, user, [('model','=',vals['relation'])]): raise except_orm(_('Error'), _("Model %s does not exist!") % vals['relation']) + self.pool.clear_manual_fields() + if vals['model'] in self.pool: model = self.pool[vals['model']] if vals['model'].startswith('x_') and vals['name'] == 'x_name': model._rec_name = 'x_name' - self.pool.clear_manual_fields() - # re-initialize model in registry model.__init__(self.pool, cr) self.pool.setup_models(cr, partial=(not self.pool.ready)) From 64116198111ad17da565e240b602374a3e239a8d Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Tue, 17 Mar 2015 15:45:00 +0100 Subject: [PATCH 26/36] [FIX] account_payment: typo in 9562bf8398d7e45835c0d2fab0b19cc91de4ab2c --- addons/account_payment/account_move_line.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/account_payment/account_move_line.py b/addons/account_payment/account_move_line.py index f909ad6c0a0..b22a8aba03c 100644 --- a/addons/account_payment/account_move_line.py +++ b/addons/account_payment/account_move_line.py @@ -27,7 +27,7 @@ class account_move_line(osv.osv): _inherit = "account.move.line" def _amount_residual(self, cr, uid, ids, field_names, args, context=None): - return self._amount_residual(self, cr, uid, ids, field_names, args, context=context) + return self._amount_residual(cr, uid, ids, field_names, args, context=context) def _to_pay_search(self, cr, uid, obj, name, args, context=None): if not args: From 3239d5fb74db65e6c209c4c609fc99cec9435b03 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Tue, 17 Mar 2015 15:50:11 +0100 Subject: [PATCH 27/36] [FIX] account_payment: typo in 9562bf8398d7e45835c0d2fab0b19cc91de4ab2c --- addons/account_payment/account_move_line.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/addons/account_payment/account_move_line.py b/addons/account_payment/account_move_line.py index b22a8aba03c..d0ef119640c 100644 --- a/addons/account_payment/account_move_line.py +++ b/addons/account_payment/account_move_line.py @@ -26,8 +26,10 @@ from operator import itemgetter class account_move_line(osv.osv): _inherit = "account.move.line" + # delegate to parent, used for local fields.function redefinition def _amount_residual(self, cr, uid, ids, field_names, args, context=None): - return self._amount_residual(cr, uid, ids, field_names, args, context=context) + return super(account_move_line, self)._amount_residual( + cr, uid, ids, field_names, args, context=context) def _to_pay_search(self, cr, uid, obj, name, args, context=None): if not args: From 3d05ddab50c83fdcc9d638800f8d4c164c66c089 Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Fri, 13 Mar 2015 14:07:03 +0100 Subject: [PATCH 28/36] [FIX] sale_stock: Cancel all procurements linked to a sale order During the creation of a Sale Order containing a product for which 'Make To Order' is activated in the Procurements, two procurements are created: - one for 'Partner Locations/Customers' location - one for 'WH/Stock' location If the Sale Order if cancelled, the user must first cancel the picking (delivery order). This will trigger the cancellation of the procurement with the 'Partner Locations/Customers' location. The new behavior is to cancel the procurements linked to the Sale Order, which will trigger the cancellation of the picking. opw: 630170 issue: https://github.com/odoo/odoo/issues/3805 --- addons/sale/sale.py | 2 ++ addons/sale_stock/sale_stock.py | 15 --------------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/addons/sale/sale.py b/addons/sale/sale.py index ff968e1ab67..f182ab293f9 100644 --- a/addons/sale/sale.py +++ b/addons/sale/sale.py @@ -581,6 +581,7 @@ class sale_order(osv.osv): context = {} sale_order_line_obj = self.pool.get('sale.order.line') account_invoice_obj = self.pool.get('account.invoice') + procurement_obj = self.pool.get('procurement.order') for sale in self.browse(cr, uid, ids, context=context): for inv in sale.invoice_ids: if inv.state not in ('draft', 'cancel'): @@ -588,6 +589,7 @@ class sale_order(osv.osv): _('Cannot cancel this sales order!'), _('First cancel all invoices attached to this sales order.')) inv.signal_workflow('invoice_cancel') + procurement_obj.cancel(cr, uid, sum([l.procurement_ids.ids for l in sale.order_line],[])) sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line], {'state': 'cancel'}) self.write(cr, uid, ids, {'state': 'cancel'}) diff --git a/addons/sale_stock/sale_stock.py b/addons/sale_stock/sale_stock.py index 3db5c35ca5b..35a75674ed9 100644 --- a/addons/sale_stock/sale_stock.py +++ b/addons/sale_stock/sale_stock.py @@ -147,21 +147,6 @@ class sale_order(osv.osv): move_obj.write(cr, uid, [x.id for x in picking.move_lines], {'invoice_state': 'invoiced'}, context=context) return res - def action_cancel(self, cr, uid, ids, context=None): - if context is None: - context = {} - sale_order_line_obj = self.pool.get('sale.order.line') - proc_obj = self.pool.get('procurement.order') - stock_obj = self.pool.get('stock.picking') - for sale in self.browse(cr, uid, ids, context=context): - for pick in sale.picking_ids: - if pick.state not in ('draft', 'cancel'): - raise osv.except_osv( - _('Cannot cancel sales order!'), - _('You must first cancel all delivery order(s) attached to this sales order.')) - stock_obj.signal_workflow(cr, uid, [p.id for p in sale.picking_ids], 'button_cancel') - return super(sale_order, self).action_cancel(cr, uid, ids, context=context) - def action_wait(self, cr, uid, ids, context=None): res = super(sale_order, self).action_wait(cr, uid, ids, context=context) for o in self.browse(cr, uid, ids): From 50cdacba903bf57cad31d2edfba9c8cb6bc6783d Mon Sep 17 00:00:00 2001 From: Josse Colpaert Date: Mon, 16 Mar 2015 20:54:59 +0100 Subject: [PATCH 29/36] [FIX] stock: search qty available correction when searching zero quantities When =, >=, <= 0 it cannot use the read_group of quants because the results will not be in the read_group Fixes #5701 --- addons/stock/product.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/stock/product.py b/addons/stock/product.py index dd2d5272cfe..5e0dca5adf8 100644 --- a/addons/stock/product.py +++ b/addons/stock/product.py @@ -185,7 +185,7 @@ class product_product(osv.osv): operator = '==' ids = [] - if name == 'qty_available': + if name == 'qty_available' and (value != 0.0 or operator not in ('==', '>=', '<=')): res.append(('id', 'in', self._search_qty_available(cr, uid, operator, value, context))) else: product_ids = self.search(cr, uid, [], context=context) From fffd52a16ba7d3d9c23033178114fb89d55f7ac0 Mon Sep 17 00:00:00 2001 From: Josse Colpaert Date: Mon, 16 Mar 2015 09:50:29 +0100 Subject: [PATCH 30/36] [FIX] stock: Inventory theoretical qty should only consider the location itself, not its children. Same for reservations. When the theoretical_qty of the stock is calculated, we should only consider the location itself, not its children. That way you can have correct inventory for e.g. iPads that are both in Stock and Stock/Shelf 1 --- addons/stock/stock.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/addons/stock/stock.py b/addons/stock/stock.py index 4bd984b6558..6b4a5b39442 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -2803,7 +2803,7 @@ class stock_inventory_line(osv.osv): def _get_quants(self, cr, uid, line, context=None): quant_obj = self.pool["stock.quant"] - dom = [('company_id', '=', line.company_id.id), ('location_id', 'child_of', line.location_id.id), ('lot_id', '=', line.prod_lot_id.id), + dom = [('company_id', '=', line.company_id.id), ('location_id', '=', line.location_id.id), ('lot_id', '=', line.prod_lot_id.id), ('product_id','=', line.product_id.id), ('owner_id', '=', line.partner_id.id), ('package_id', '=', line.package_id.id)] quants = quant_obj.search(cr, uid, dom, context=context) return quants @@ -2823,7 +2823,7 @@ class stock_inventory_line(osv.osv): product = self.pool.get('product.product').browse(cr, uid, product_id, context=context) if not company_id: company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id - dom = [('company_id', '=', company_id), ('location_id', 'child_of', location_id), ('lot_id', '=', prod_lot_id), + dom = [('company_id', '=', company_id), ('location_id', '=', location_id), ('lot_id', '=', prod_lot_id), ('product_id','=', product_id), ('owner_id', '=', partner_id), ('package_id', '=', package_id)] quants = quant_obj.search(cr, uid, dom, context=context) th_qty = sum([x.qty for x in quant_obj.browse(cr, uid, quants, context=context)]) @@ -2864,7 +2864,7 @@ class stock_inventory_line(osv.osv): move_id = stock_move_obj.create(cr, uid, vals, context=context) move = stock_move_obj.browse(cr, uid, move_id, context=context) if diff > 0: - domain = [('qty', '>', 0.0), ('package_id', '=', inventory_line.package_id.id), ('lot_id', '=', inventory_line.prod_lot_id.id)] + domain = [('qty', '>', 0.0), ('package_id', '=', inventory_line.package_id.id), ('lot_id', '=', inventory_line.prod_lot_id.id), ('location_id', '=', inventory_line.location_id.id)] preferred_domain_list = [[('reservation_id', '=', False)], [('reservation_id.inventory_id', '!=', inventory_line.inventory_id.id)]] quants = quant_obj.quants_get_prefered_domain(cr, uid, move.location_id, move.product_id, move.product_qty, domain=domain, prefered_domain_list=preferred_domain_list, restrict_partner_id=move.restrict_partner_id.id, context=context) quant_obj.quants_reserve(cr, uid, quants, move, context=context) From 082a6ce0abaf772bfff22643402b250bdd271f5d Mon Sep 17 00:00:00 2001 From: Jeremy Kersten Date: Tue, 17 Mar 2015 18:53:26 +0100 Subject: [PATCH 31/36] [FIX] point_of_sale: don't overwritte title from pos template --- addons/point_of_sale/controllers/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/addons/point_of_sale/controllers/main.py b/addons/point_of_sale/controllers/main.py index 857ba9c06ba..10feb068b92 100644 --- a/addons/point_of_sale/controllers/main.py +++ b/addons/point_of_sale/controllers/main.py @@ -32,6 +32,7 @@ class PosController(http.Controller): modules = simplejson.dumps(module_boot(request.db)) init = """ var wc = new s.web.WebClient(); + wc._title_changed = function() {} wc.show_application = function(){ wc.action_manager.do_action("pos.ui"); }; From 98c1a82e681e9152bc68ee76b89e7cf029a7371c Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Tue, 17 Mar 2015 19:29:54 +0100 Subject: [PATCH 32/36] [FIX] mail, share: correct module update. a6eac0f08ee9100d1a44a4068d4f580b1c7c4b2c introduced a new `res.groups` belonging to the "Sharing" category. As this group has the flag `shared`, it is ignore when generating the group's user view (and its virtual fields). When updating `mail` module, the `share` module (which depend on mail) is not loaded yet. This mean this new group is taken into account when getting the `res.users` virtual fields. As this group belong to the same category as the `share.group_share_user` group but dont have an inheritance link between them (`implied_ids`), they are generated as `boolean` instead of a single entry `selection` field. This create a missmatch between the fields and the previously generated view, which forbid inheritance application. Force view regeneration, to avoid it. Also change the group's category as this group is purely technical and must be hidden anyway. --- addons/mail/res_users_view.xml | 12 ++++++++++++ addons/share/security/share_security.xml | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/addons/mail/res_users_view.xml b/addons/mail/res_users_view.xml index eef2924134f..7bbe369c4d7 100644 --- a/addons/mail/res_users_view.xml +++ b/addons/mail/res_users_view.xml @@ -16,6 +16,18 @@
+ + + res.users.form.mail diff --git a/addons/share/security/share_security.xml b/addons/share/security/share_security.xml index b65875608b7..1d77d222800 100644 --- a/addons/share/security/share_security.xml +++ b/addons/share/security/share_security.xml @@ -19,7 +19,7 @@ Members of this groups have access to the sharing wizard, which allows them to i Shared Group This group is a template group for shared users, giving basic access needs, the menus for instance. - + 1 From 6aa28a89bb31389cbdef8b447475d0ecc2dd122d Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Wed, 18 Mar 2015 09:43:45 +0100 Subject: [PATCH 33/36] [FIX] models: compute many2many field on existing records when field is introduced by an update --- openerp/models.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openerp/models.py b/openerp/models.py index 9a712602d66..73782dea625 100644 --- a/openerp/models.py +++ b/openerp/models.py @@ -2474,7 +2474,9 @@ class BaseModel(object): self._o2m_raise_on_missing_reference(cr, f) elif isinstance(f, fields.many2many): - self._m2m_raise_or_create_relation(cr, f) + res = self._m2m_raise_or_create_relation(cr, f) + if res and self._fields[k].depends: + stored_fields.append(self._fields[k]) else: res = column_data.get(k) @@ -2769,6 +2771,9 @@ class BaseModel(object): raise except_orm('Programming Error', "There is no reference field '%s' found for '%s'" % (f._fields_id, f._obj,)) def _m2m_raise_or_create_relation(self, cr, f): + """ Create the table for the relation if necessary. + Return ``True`` if the relation had to be created. + """ m2m_tbl, col1, col2 = f._sql_names(self) # do not create relations for custom fields as they do not belong to a module # they will be automatically removed when dropping the corresponding ir.model.field @@ -2795,6 +2800,7 @@ class BaseModel(object): cr.execute("COMMENT ON TABLE \"%s\" IS 'RELATION BETWEEN %s AND %s'" % (m2m_tbl, self._table, ref)) cr.commit() _schema.debug("Create table '%s': m2m relation between '%s' and '%s'", m2m_tbl, self._table, ref) + return True def _add_sql_constraints(self, cr): From 489a96c2572bbe2cc04c0658ed2b862aca31fc0f Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Wed, 18 Mar 2015 11:22:39 +0100 Subject: [PATCH 34/36] [FIX] product.template: name_search order + mismatch on IDs Improve commit bf31ab6718a8b9c8afdd29120e5056533b0a2ade: - do not use the product.product name_search() when there is no search term (it's only useful with a search term - do not use the product.product name_search() when the extra domain (args) has a criterion on IDs - these will be *template* ids, not *variant* ids, so the results would be wrong - Since templates and variants use a different natural `_order`, perform an extra name_search() on templates after gathering the ids to return, to apply the proper sort ordering. Increases the penalty by doing 2 name_search() calls. This issue could be seen e.g. in the breadcrumbs where the display_name variable is displayed for templates, and indirectly relies on name_search() due to an old hack in call_kw in order to implement the "future_display_name". --- addons/product/product.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/addons/product/product.py b/addons/product/product.py index 1d02642de94..0a765623cb8 100644 --- a/addons/product/product.py +++ b/addons/product/product.py @@ -800,6 +800,12 @@ class product_template(osv.osv): return super(product_template, self).name_get(cr, user, ids, context) def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100): + # Only use the product.product heuristics if there is a search term and the domain + # does not specify a match on `product.template` IDs. + if not name or any(term[0] == 'id' for term in (args or [])): + return super(product_template, self).name_search( + cr, user, name=name, args=args, operator=operator, context=context, limit=limit) + product_product = self.pool['product.product'] results = product_product.name_search( cr, user, name, args, operator=operator, context=context, limit=limit) @@ -807,8 +813,11 @@ class product_template(osv.osv): template_ids = [p.product_tmpl_id.id for p in product_product.browse( cr, user, product_ids, context=context)] - uniq_ids = OrderedDict.fromkeys(template_ids).keys() - return self.name_get(cr, user, uniq_ids, context=context) + + # re-apply product.template order + name_get + return super(product_template, self).name_search( + cr, user, '', args=[('id', 'in', template_ids)], + operator=operator, context=context, limit=limit) class product_product(osv.osv): _name = "product.product" From e1daaff8e0711f4fc39a8360ace01f8529a4754e Mon Sep 17 00:00:00 2001 From: Nicolas Lempereur Date: Tue, 10 Mar 2015 11:13:45 +0100 Subject: [PATCH 35/36] [FIX] kanban: propagate context of many2many field Propagate the context in kanban view when displaying a many2many field. fixes #5623, closes #5681 opw-629698 --- addons/web_kanban/static/src/js/kanban.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/addons/web_kanban/static/src/js/kanban.js b/addons/web_kanban/static/src/js/kanban.js index 23fa4923277..bdc0d539d25 100644 --- a/addons/web_kanban/static/src/js/kanban.js +++ b/addons/web_kanban/static/src/js/kanban.js @@ -28,6 +28,7 @@ instance.web_kanban.KanbanView = instance.web.View.extend({ this.group_by_field = {}; this.grouped_by_m2o = false; this.many2manys = []; + this.m2m_context = {}; this.state = { groups : {}, records : {} @@ -115,6 +116,10 @@ instance.web_kanban.KanbanView = instance.web.View.extend({ this.aggregates[node.attrs.name] = node.attrs[this.group_operators[j]]; break; } + }; + var ftype = node.attrs.widget || this.fields_view.fields[node.attrs.name].type; + if(ftype == "many2many" && "context" in node.attrs) { + this.m2m_context[node.attrs.name] = node.attrs.context; } }, transform_qweb_template: function(node) { @@ -490,7 +495,7 @@ instance.web_kanban.KanbanView = instance.web.View.extend({ var field = record.record[name]; var $el = record.$('.oe_form_field.oe_tags[name=' + name + ']').empty(); if (!relations[field.relation]) { - relations[field.relation] = { ids: [], elements: {}}; + relations[field.relation] = { ids: [], elements: {}, context: self.m2m_context[name]}; } var rel = relations[field.relation]; field.raw_value.forEach(function(id) { @@ -504,7 +509,7 @@ instance.web_kanban.KanbanView = instance.web.View.extend({ }); }); _.each(relations, function(rel, rel_name) { - var dataset = new instance.web.DataSetSearch(self, rel_name, self.dataset.get_context()); + var dataset = new instance.web.DataSetSearch(self, rel_name, self.dataset.get_context(rel.context)); dataset.name_get(_.uniq(rel.ids)).done(function(result) { result.forEach(function(nameget) { $(rel.elements[nameget[0]]).append('' + _.str.escapeHTML(nameget[1]) + ''); From 7dbd49598543bbda49b908118873786a94055351 Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Wed, 18 Mar 2015 13:10:07 +0100 Subject: [PATCH 36/36] [IMP] website: server actions allow trailing paths --- addons/website/controllers/main.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py index 3304d36eb9c..0fd0bd30ebf 100644 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@ -403,7 +403,10 @@ class Website(openerp.addons.web.controllers.main.Home): #------------------------------------------------------ # Server actions #------------------------------------------------------ - @http.route('/website/action/', type='http', auth="public", website=True) + @http.route([ + '/website/action/', + '/website/action//', + ], type='http', auth="public", website=True) def actions_server(self, path_or_xml_id_or_id, **post): cr, uid, context = request.cr, request.uid, request.context res, action_id, action = None, None, None