← Back to team overview

openerp-community-reviewer team mailing list archive

[Merge] lp:~camptocamp/purchase-wkfl/7.0-merge-po-hooks-lep into lp:purchase-wkfl

 

Leonardo Pistone - camptocamp has proposed merging lp:~camptocamp/purchase-wkfl/7.0-merge-po-hooks-lep into lp:purchase-wkfl.

Requested reviews:
  martin (riess82)
  Romain Deheele - Camptocamp (romaindeheele)
  Pedro Manuel Baeza (pedro.baeza)
Related bugs:
  Bug #1217869 in OpenERP Addons: "purchase order merge limits"
  https://bugs.launchpad.net/openobject-addons/+bug/1217869

For more details, see:
https://code.launchpad.net/~camptocamp/purchase-wkfl/7.0-merge-po-hooks-lep/+merge/216745
-- 
https://code.launchpad.net/~camptocamp/purchase-wkfl/7.0-merge-po-hooks-lep/+merge/216745
Your team Purchase Core Editors is subscribed to branch lp:purchase-wkfl.
=== added directory 'purchase_group_hooks'
=== added file 'purchase_group_hooks/__init__.py'
--- purchase_group_hooks/__init__.py	1970-01-01 00:00:00 +0000
+++ purchase_group_hooks/__init__.py	2014-04-29 08:56:20 +0000
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    Author: Leonardo Pistone
+#    Copyright 2014 Camptocamp SA
+#
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU Affero General Public License as
+#    published by the Free Software Foundation, either version 3 of the
+#    License, or (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU Affero General Public License for more details.
+#
+#    You should have received a copy of the GNU Affero General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+from . import purchase_group_hooks  # noqa

=== added file 'purchase_group_hooks/__openerp__.py'
--- purchase_group_hooks/__openerp__.py	1970-01-01 00:00:00 +0000
+++ purchase_group_hooks/__openerp__.py	2014-04-29 08:56:20 +0000
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    Author: Leonardo Pistone
+#    Copyright 2014 Camptocamp SA
+#
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU Affero General Public License as
+#    published by the Free Software Foundation, either version 3 of the
+#    License, or (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU Affero General Public License for more details.
+#
+#    You should have received a copy of the GNU Affero General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+{'name': 'Add hooks to the merge PO feature.',
+ 'version': '0.1',
+ 'author': 'Camptocamp',
+ 'maintainer': 'Camptocamp',
+ 'category': 'Purchase Management',
+ 'complexity': "normal",
+ 'depends': ['purchase'],
+ 'description': """
+ In the core OpenERP purchase module, there is a wizard to merge purchase
+ orders. That feature is convenient, but as soon as a field is added to the
+ purchase order, it does not work anymore and needs to be patched.
+ The original implementation does not provide any hooks for extension, and
+ modules can only reimplement a method completely. This required a lot of copy
+ and paste, and worse, it breaks if two modules attempt to do that.
+
+ Therefore, this module reimplements the feature, with the same basic result
+ in the standard case. Hooks are provided for extra modules that add fields
+ or change the logic.
+ """,
+ 'website': 'http://www.camptocamp.com/',
+ 'data': [],
+ 'installable': True,
+ 'auto_install': False,
+ 'license': 'AGPL-3',
+ 'application': False,
+ 'test': ['test/merge_order.yml'],
+ }

=== added file 'purchase_group_hooks/purchase_group_hooks.py'
--- purchase_group_hooks/purchase_group_hooks.py	1970-01-01 00:00:00 +0000
+++ purchase_group_hooks/purchase_group_hooks.py	2014-04-29 08:56:20 +0000
@@ -0,0 +1,236 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    Author: Leonardo Pistone
+#    Copyright 2014 Camptocamp SA
+#
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU Affero General Public License as
+#    published by the Free Software Foundation, either version 3 of the
+#    License, or (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU Affero General Public License for more details.
+#
+#    You should have received a copy of the GNU Affero General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+from openerp.osv.orm import Model
+from openerp import netsvc
+from openerp.osv.orm import browse_record, browse_null
+
+
+class PurchaseOrder(Model):
+    _inherit = 'purchase.order'
+
+    def _key_fields_for_grouping(self):
+        """Return a list of fields used to identify orders that can be merged.
+
+        Orders that have this fields equal can be merged.
+
+        This function can be extended by other modules to modify the list.
+        """
+        return ('partner_id', 'location_id', 'pricelist_id')
+
+    def _key_fields_for_grouping_lines(self):
+        """Return a list of fields used to identify order lines that can be
+        merged.
+
+        Lines that have this fields equal can be merged.
+
+        This function can be extended by other modules to modify the list.
+        """
+        return ('name', 'date_planned', 'taxes_id', 'price_unit', 'product_id',
+                'move_dest_id', 'account_analytic_id')
+
+    def _make_key_for_grouping(self, order, fields):
+        """From an order, return a tuple to be used as a key.
+
+        If two orders have the same key, they can be merged.
+        """
+        key_list = []
+        for field in fields:
+            field_value = getattr(order, field)
+
+            if isinstance(field_value, browse_record):
+                field_value = field_value.id
+            elif isinstance(field_value, browse_null):
+                field_value = False
+            elif isinstance(field_value, list):
+                field_value = ((6, 0, tuple([v.id for v in field_value])),)
+            key_list.append((field, field_value))
+        key_list.sort()
+        return tuple(key_list)
+
+    def _can_merge(self, order):
+        """Can the order be considered for merging with others?
+
+        This method can be surcharged in other modules.
+        """
+        return order.state == 'draft'
+
+    def _initial_merged_order_data(self, order):
+        """Build the initial values of a merged order."""
+        return {
+            'origin': order.origin,
+            'date_order': order.date_order,
+            'partner_id': order.partner_id.id,
+            'dest_address_id': order.dest_address_id.id,
+            'warehouse_id': order.warehouse_id.id,
+            'location_id': order.location_id.id,
+            'pricelist_id': order.pricelist_id.id,
+            'state': 'draft',
+            'order_line': {},
+            'notes': '%s' % (order.notes or '',),
+            'fiscal_position': (
+                order.fiscal_position and order.fiscal_position.id or False
+            ),
+        }
+
+    def _update_merged_order_data(self, merged_data, order):
+        if order.date_order < merged_data['date_order']:
+            merged_data['date_order'] = order.date_order
+        if order.notes:
+            merged_data['notes'] = (
+                (merged_data['notes'] or '') + ('\n%s' % (order.notes,))
+            )
+        if order.origin:
+            if (
+                order.origin not in merged_data['origin']
+                and merged_data['origin'] not in order.origin
+            ):
+                merged_data['origin'] = (
+                    (merged_data['origin'] or '') + ' ' + order.origin
+                )
+        return merged_data
+
+    def _group_orders(self, input_orders):
+        """Return a dictionary where each element is in the form:
+
+        tuple_key: (dict_of_new_order_data, list_of_old_order_ids)
+
+        """
+        key_fields = self._key_fields_for_grouping()
+        grouped_orders = {}
+
+        if len(input_orders) < 2:
+            return {}
+
+        for input_order in input_orders:
+            key = self._make_key_for_grouping(input_order, key_fields)
+            if key in grouped_orders:
+                grouped_orders[key] = (
+                    self._update_merged_order_data(
+                        grouped_orders[key][0],
+                        input_order
+                    ),
+                    grouped_orders[key][1] + [input_order.id]
+                )
+            else:
+                grouped_orders[key] = (
+                    self._initial_merged_order_data(input_order),
+                    [input_order.id]
+                )
+            grouped_order_data = grouped_orders[key][0]
+
+            for input_line in input_order.order_line:
+                line_key = self._make_key_for_grouping(
+                    input_line,
+                    self._key_fields_for_grouping_lines()
+                )
+                o_line = grouped_order_data['order_line'].setdefault(
+                    line_key, {}
+                )
+                if o_line:
+                    # merge the line with an existing line
+                    o_line['product_qty'] += (
+                        input_line.product_qty
+                        * input_line.product_uom.factor
+                        / o_line['uom_factor']
+                    )
+                else:
+                    # append a new "standalone" line
+                    for field in ('product_qty', 'product_uom'):
+                        field_val = getattr(input_line, field)
+                        if isinstance(field_val, browse_record):
+                            field_val = field_val.id
+                        o_line[field] = field_val
+                    o_line['uom_factor'] = (
+                        input_line.product_uom
+                        and input_line.product_uom.factor
+                        or 1.0)
+
+        return self._cleanup_merged_line_data(grouped_orders)
+
+    def _cleanup_merged_line_data(self, grouped_orders):
+        """Remove keys from merged lines, and merges of 1 order."""
+        result = {}
+        for order_key, (order_data, old_ids) in grouped_orders.iteritems():
+            if len(old_ids) > 1:
+                for key, value in order_data['order_line'].iteritems():
+                    del value['uom_factor']
+                    value.update(dict(key))
+                order_data['order_line'] = [
+                    (0, 0, value)
+                    for value in order_data['order_line'].itervalues()
+                ]
+                result[order_key] = (order_data, old_ids)
+        return result
+
+    def _create_new_orders(self, cr, uid, grouped_orders, context=None):
+        """Create the new merged orders in the database.
+
+        Return a dictionary that puts the created order ids in relation to the
+        original ones, in the form
+
+        new_order_id: [old_order_1_id, old_order_2_id]
+
+        """
+        new_old_rel = {}
+        for key in grouped_orders:
+            new_order_data, old_order_ids = grouped_orders[key]
+            new_id = self.create(cr, uid, new_order_data, context=context)
+            new_old_rel[new_id] = old_order_ids
+        return new_old_rel
+
+    def _fix_workflow(self, cr, uid, new_old_rel):
+        """Fix the workflow of the old and new orders.
+
+        Specifically, cancel the old ones and assign workflows to the new ones.
+
+        """
+        wf_service = netsvc.LocalService("workflow")
+        for new_order_id in new_old_rel:
+            old_order_ids = new_old_rel[new_order_id]
+            for old_id in old_order_ids:
+                wf_service.trg_redirect(uid, 'purchase.order', old_id,
+                                        new_order_id, cr)
+                wf_service.trg_validate(uid, 'purchase.order', old_id,
+                                        'purchase_cancel', cr)
+
+    def do_merge(self, cr, uid, input_order_ids, context=None):
+        """Merge Purchase Orders.
+
+        This method replaces the original one in the purchase module because
+        it did not provide any hooks for customization.
+
+        Receive a list of order ids, and return a dictionary where each
+        element is in the form:
+
+        new_order_id: [old_order_1_id, old_order_2_id]
+
+        New orders are created, and old orders are deleted.
+
+        """
+        input_orders = self.browse(cr, uid, input_order_ids, context=context)
+        mergeable_orders = filter(self._can_merge, input_orders)
+        grouped_orders = self._group_orders(mergeable_orders)
+
+        new_old_rel = self._create_new_orders(cr, uid, grouped_orders,
+                                              context=context)
+        self._fix_workflow(cr, uid, new_old_rel)
+        return new_old_rel

=== added directory 'purchase_group_hooks/test'
=== added file 'purchase_group_hooks/test/merge_order.yml'
--- purchase_group_hooks/test/merge_order.yml	1970-01-01 00:00:00 +0000
+++ purchase_group_hooks/test/merge_order.yml	2014-04-29 08:56:20 +0000
@@ -0,0 +1,41 @@
+-
+  In order to merge RFQ, I merge two RFQ which has same supplier and check new merged order.
+-
+  !python {model: purchase.order}: |
+    new_id = self.do_merge(cr, uid, [ref('purchase.purchase_order_4'), ref('purchase.purchase_order_7')])
+    order4 = self.browse(cr, uid, ref('purchase.purchase_order_4'))
+    order7 = self.browse(cr, uid, ref('purchase.purchase_order_7'))
+    total_qty = sum([x.product_qty for x in order4.order_line] + [x.product_qty for x in order7.order_line])
+
+    assert order4.state == 'cancel', "Merged order should be canceled"
+    assert order7.state == 'cancel', "Merged order should be canceled"
+
+    def merged_data(lines):
+        product_id =[]
+        product_uom = []
+        res = {}
+        for line in lines:
+          product_id.append(line.product_id.id) 
+          product_uom.append(line.product_uom.id) 
+        res.update({'product_ids': product_id,'product_uom':product_uom})
+        return res
+
+    for order in self.browse(cr, uid, new_id.keys()):
+       total_new_qty = [x.product_qty for x in order.order_line]
+       total_new_qty = sum(total_new_qty)
+       
+       assert total_new_qty == total_qty,"product quantities are not correspond: {} != {}".format(total_new_qty, total_qty)
+       assert order.partner_id == order4.partner_id ,"partner is not correspond"
+       assert order.warehouse_id == order4.warehouse_id or order7.warehouse_id,"Warehouse is not correspond"
+       assert order.state == 'draft',"New created order state should be in draft"
+       assert order.pricelist_id == order4.pricelist_id,"Price list is not correspond"
+       assert order.date_order == order4.date_order ,"Date of order is not correspond"
+       assert order.location_id == order4.location_id ,"Location is not correspond"
+       n_product_data = merged_data(order.order_line)
+       o_product_data= merged_data(order4.order_line)
+       o_pro_data = merged_data(order7.order_line)
+       
+       assert n_product_data == o_product_data or o_pro_data,"product data are not correspond"
+       
+
+

=== added directory 'purchase_group_hooks/tests'
=== added file 'purchase_group_hooks/tests/__init__.py'
--- purchase_group_hooks/tests/__init__.py	1970-01-01 00:00:00 +0000
+++ purchase_group_hooks/tests/__init__.py	2014-04-29 08:56:20 +0000
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    Author: Leonardo Pistone
+#    Copyright 2014 Camptocamp SA
+#
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU Affero General Public License as
+#    published by the Free Software Foundation, either version 3 of the
+#    License, or (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU Affero General Public License for more details.
+#
+#    You should have received a copy of the GNU Affero General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+from . import test_merge_po
+
+checks = [
+    test_merge_po,
+]

=== added file 'purchase_group_hooks/tests/test_merge_po.py'
--- purchase_group_hooks/tests/test_merge_po.py	1970-01-01 00:00:00 +0000
+++ purchase_group_hooks/tests/test_merge_po.py	2014-04-29 08:56:20 +0000
@@ -0,0 +1,76 @@
+from mock import Mock
+
+from openerp.tests.common import BaseCase
+from openerp.osv.orm import browse_record
+
+
+class TestGroupOrders(BaseCase):
+
+    def setUp(self):
+        super(TestGroupOrders, self).setUp()
+        self.order1 = Mock()
+        self.order2 = Mock()
+
+        self.order1.order_line = self.order2.order_line = []
+        self.order1.origin = self.order2.origin = ''
+        self.order1.notes = self.order2.notes = ''
+
+        # I have to use the registry to get an instance of a model. I cannot
+        # use the class constructor because that is modified to return nothing.
+        self.po = self.registry('purchase.order')
+
+    def test_no_orders(self):
+        """Group an empty list of orders as an empty dictionary."""
+
+        grouped = self.po._group_orders([])
+        self.assertEquals(grouped, {})
+
+    def test_one_order(self):
+        """A single order will not be grouped."""
+        grouped = self.po._group_orders([self.order1])
+        self.assertEquals(grouped, {})
+
+    def test_two_similar_orders(self):
+        """Two orders with the right conditions can be merged.
+
+        We do not care about the order lines here.
+        """
+        self.order1.partner_id = self.order2.partner_id = Mock(
+            spec=browse_record, id=1)
+        self.order1.location_id = self.order2.location_id = Mock(
+            spec=browse_record, id=2)
+        self.order1.pricelist_id = self.order2.pricelist_id = Mock(
+            spec=browse_record, id=3)
+
+        self.order1.id = 51
+        self.order2.id = 52
+
+        grouped = self.po._group_orders([self.order1, self.order2])
+        expected_key = (('location_id', 2), ('partner_id', 1),
+                        ('pricelist_id', 3))
+        self.assertEquals(grouped.keys(), [expected_key])
+        self.assertEquals(grouped[expected_key][1], [51, 52])
+
+    def test_merge_origin_and_notes(self):
+        self.order1.origin = 'ORIGIN1'
+        self.order2.origin = 'ORIGIN2'
+
+        self.order1.notes = 'Notes1'
+        self.order2.notes = 'Notes2'
+
+        self.order1.partner_id = self.order2.partner_id = Mock(
+            spec=browse_record, id=1)
+        self.order1.location_id = self.order2.location_id = Mock(
+            spec=browse_record, id=2)
+        self.order1.pricelist_id = self.order2.pricelist_id = Mock(
+            spec=browse_record, id=3)
+
+        grouped = self.po._group_orders([self.order1, self.order2])
+
+        expected_key = (('location_id', 2), ('partner_id', 1),
+                        ('pricelist_id', 3))
+
+        merged_data = grouped[expected_key][0]
+
+        self.assertEquals(merged_data['origin'], 'ORIGIN1 ORIGIN2')
+        self.assertEquals(merged_data['notes'], 'Notes1\nNotes2')


References