← Back to team overview

openerp-community-reviewer team mailing list archive

lp:~camptocamp/openerp-humanitarian-ngo/ngo-addons-add_agreement_sourcing-nbi into lp:openerp-humanitarian-ngo

 

Nicolas Bessi - Camptocamp has proposed merging lp:~camptocamp/openerp-humanitarian-ngo/ngo-addons-add_agreement_sourcing-nbi into lp:openerp-humanitarian-ngo.

Requested reviews:
  OpenERP for Humanitarian Core Editors (humanitarian-core-editors)

For more details, see:
https://code.launchpad.net/~camptocamp/openerp-humanitarian-ngo/ngo-addons-add_agreement_sourcing-nbi/+merge/196676

Add sourcing using framework agreement
-- 
https://code.launchpad.net/~camptocamp/openerp-humanitarian-ngo/ngo-addons-add_agreement_sourcing-nbi/+merge/196676
Your team OpenERP for Humanitarian Core Editors is requested to review the proposed merge of lp:~camptocamp/openerp-humanitarian-ngo/ngo-addons-add_agreement_sourcing-nbi into lp:openerp-humanitarian-ngo.
=== added directory 'framework_agreement_requisition'
=== added file 'framework_agreement_requisition/__init__.py'
--- framework_agreement_requisition/__init__.py	1970-01-01 00:00:00 +0000
+++ framework_agreement_requisition/__init__.py	2013-12-05 10:14:17 +0000
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    Author: Nicolas Bessi
+#    Copyright 2013 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 model

=== added file 'framework_agreement_requisition/__openerp__.py'
--- framework_agreement_requisition/__openerp__.py	1970-01-01 00:00:00 +0000
+++ framework_agreement_requisition/__openerp__.py	2013-12-05 10:14:17 +0000
@@ -0,0 +1,53 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    Author: Nicolas Bessi
+#    Copyright 2013 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': 'Framework Agreement Negociation',
+ 'version': '0.1',
+ 'author': 'Camptocamp',
+ 'maintainer': 'Camptocamp',
+ 'category': 'NGO',
+ 'complexity': 'normal',
+ 'depends': ['purchase_requisition',
+             'purchase_requisition_extended',
+             'framework_agreement'],
+ 'description': """
+Negociate framework agreement using tender process
+==================================================
+
+Module add state "Agreement selected" on tender and PO.
+It also adds a button on tender "Agreement selected"
+That will close flow of tender and related PO accordingly.
+
+
+It also add an agreement check box on tender to allows
+agreement negociation using tender process.
+
+""",
+ 'website': 'http://www.camptocamp.com',
+ 'data': ['requisition_workflow.xml',
+          'purchase_workflow.xml',
+          'view/purchase_requisition_view.xml'],
+ 'demo': [],
+ 'test': ['test/agreement_requisition.yml'],
+ 'installable': True,
+ 'auto_install': False,
+ 'license': 'AGPL-3',
+ 'application': False,
+ }

=== added directory 'framework_agreement_requisition/model'
=== added file 'framework_agreement_requisition/model/__init__.py'
--- framework_agreement_requisition/model/__init__.py	1970-01-01 00:00:00 +0000
+++ framework_agreement_requisition/model/__init__.py	2013-12-05 10:14:17 +0000
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    Author: Nicolas Bessi
+#    Copyright 2013 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_requisition
+from . import purchase

=== added file 'framework_agreement_requisition/model/purchase.py'
--- framework_agreement_requisition/model/purchase.py	1970-01-01 00:00:00 +0000
+++ framework_agreement_requisition/model/purchase.py	2013-12-05 10:14:17 +0000
@@ -0,0 +1,86 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    Author: Nicolas Bessi
+#    Copyright 2013 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 import netsvc
+from openerp.osv import orm
+
+SELECTED_STATE = ('agreement_selected', 'Agreement selected')
+AGR_SELECT = 'agreement_selected'
+
+
+class purchase_order(orm.Model):
+    """Add workflow behavior"""
+
+    _inherit = "purchase.order"
+
+    def __init__(self, pool, cr):
+        """Add a new state value using PO class property"""
+        if SELECTED_STATE not in super(purchase_order, self).STATE_SELECTION:
+            super(purchase_order, self).STATE_SELECTION.append(SELECTED_STATE)
+        return super(purchase_order, self).__init__(pool, cr)
+
+    def select_agreement(self, cr, uid, agr_id, context=None):
+        """Pass PO in state 'Agreement selected'"""
+        if isinstance(agr_id, (list, tuple)):
+            assert len(agr_id) == 1
+            agr_id = agr_id[0]
+            wf_service = netsvc.LocalService("workflow")
+        return wf_service.trg_validate(uid, 'purchase.order',
+                                       agr_id, 'select_agreement', cr)
+
+    def po_tender_agreement_selected(self, cr, uid, ids, context=None):
+        """Workflow function that write state 'Agreement selected'"""
+        return self.write(cr, uid, ids, {'state': AGR_SELECT},
+                          context=context)
+
+
+class purchase_order_line(orm.Model):
+    """Add make_agreement function"""
+
+    _inherit = "purchase.order.line"
+
+    def _agreement_data(self, cr, uid, po_line, origin, context=None):
+        """Get agreement values from PO line
+
+        :param po_line: Po line records
+
+        :returns: agreement dict to be used by orm.Model.create
+        """
+        vals = {}
+        vals['supplier_id'] = po_line.order_id.partner_id.id
+        vals['product_id'] = po_line.product_id.id
+        vals['quantity'] = po_line.product_qty
+        vals['origin'] = origin if origin else False
+        return vals
+
+    def make_agreement(self, cr, uid, line_id, origin, context=None):
+        """ generate a draft framework agreement
+
+        :returns: a record of LTA
+
+        """
+        agr_model = self.pool['framework.agreement']
+        if isinstance(line_id, (list, tuple)):
+            assert len(line_id) == 1
+            line_id = line_id[0]
+        current = self.browse(cr, uid, line_id, context=context)
+        vals = self._agreement_data(cr, uid, current, origin, context=context)
+        agr_id = agr_model.create(cr, uid, vals, context=context)
+        return agr_model.browse(cr, uid, agr_id, context=context)

=== added file 'framework_agreement_requisition/model/purchase_requisition.py'
--- framework_agreement_requisition/model/purchase_requisition.py	1970-01-01 00:00:00 +0000
+++ framework_agreement_requisition/model/purchase_requisition.py	2013-12-05 10:14:17 +0000
@@ -0,0 +1,93 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    Author: Nicolas Bessi
+#    Copyright 2013 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 itertools import chain
+from openerp import netsvc
+from openerp.osv import orm, fields
+from openerp.tools.translate import _
+from .purchase import AGR_SELECT as PO_AGR_SELECT
+
+SELECTED_STATE = ('agreement_selected', 'Agreement selected')
+AGR_SELECT = 'agreement_selected'
+
+
+class purchase_requisition(orm.Model):
+    """Add support to negociate LTA using tender process"""
+
+    def __init__(self, pool, cr):
+        """Nasty hack to add fields to select fields
+
+        We do this in order not to compromising other state added
+        by other addons that are not in inheritance chain...
+
+        """
+        sel = super(purchase_requisition, self)._columns['state']
+        if SELECTED_STATE not in sel.selection:
+            sel.selection.append(SELECTED_STATE)
+        return super(purchase_requisition, self).__init__(pool, cr)
+
+    _inherit = "purchase.requisition"
+    _columns = {
+        'framework_agreement_tender': fields.boolean('Negociate Agreement'),
+    }
+
+    def tender_agreement_selected(self, cr, uid, ids, context=None):
+        """Workflow function that write state 'Agreement selected'"""
+        return self.write(cr, uid, ids, {'state': AGR_SELECT},
+                          context=context)
+
+    def select_agreement(self, cr, uid, agr_id, context=None):
+        """Pass tender to state 'Agreement selected'"""
+        if isinstance(agr_id, (list, tuple)):
+            assert len(agr_id) == 1
+            agr_id = agr_id[0]
+        wf_service = netsvc.LocalService("workflow")
+        return wf_service.trg_validate(uid, 'purchase.requisition',
+                                       agr_id, 'select_agreement', cr)
+
+    def agreement_selected(self, cr, uid, ids, context=None):
+        """Tells tender that an agreement has been selected"""
+        if isinstance(ids, (int, long)):
+            ids = [ids]
+        for req in self.browse(cr, uid, ids, context=context):
+            if not req.framework_agreement_tender:
+                raise orm.except_orm(_('Invalid tender'),
+                                     _('Request is not of type agreement'))
+            self.select_agreement(cr, uid, req.id, context=context)
+            req.refresh()
+            if req.state != AGR_SELECT:
+                raise RuntimeError('requisiton %s does not pass to state'
+                                   ' agreement_selected' %
+                                   req.name)
+            rfqs = chain.from_iterable(req_line.purchase_line_ids
+                                       for req_line in req.line_ids)
+            rfqs = [rfq for rfq in rfqs if rfq.state == 'confirmed']
+            if not rfqs:
+                raise orm.except_orm(_('No confirmed RFQ related to tender'),
+                                     _('Please choose at least one'))
+            for rfq in rfqs:
+                rfq.make_agreement(req.name)
+                p_order = rfq.order_id
+                p_order.select_agreement()
+                p_order.refresh()
+                if p_order.state != PO_AGR_SELECT:
+                    raise RuntimeError('Purchase order %s does not pass to %' %
+                                       (p_order.name, PO_AGR_SELECT))
+        return True

=== added file 'framework_agreement_requisition/purchase_workflow.xml'
--- framework_agreement_requisition/purchase_workflow.xml	1970-01-01 00:00:00 +0000
+++ framework_agreement_requisition/purchase_workflow.xml	2013-12-05 10:14:17 +0000
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+  <data>
+    <record id="act_po_agreement_selected" model="workflow.activity">
+      <field name="wkf_id" ref="purchase.purchase_order"/>
+      <field name="name">Agreement selected</field>
+      <field name="kind">function</field>
+      <field name="action">po_tender_agreement_selected()</field>
+      <field name="flow_stop">True</field>
+    </record>
+    <record id="trans_po_agreement_selected" model="workflow.transition">
+      <field name="act_from" ref="purchase.act_bid"/>
+      <field name="act_to" ref="act_po_agreement_selected"/>
+      <field name="signal">select_agreement</field>
+    </record>
+  </data>
+</openerp>

=== added file 'framework_agreement_requisition/requisition_workflow.xml'
--- framework_agreement_requisition/requisition_workflow.xml	1970-01-01 00:00:00 +0000
+++ framework_agreement_requisition/requisition_workflow.xml	2013-12-05 10:14:17 +0000
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+  <data>
+    <record id="act_agreement_selected" model="workflow.activity">
+      <field name="wkf_id" ref="purchase_requisition.purchase_requisition_workflow"/>
+      <field name="name">Agreement selected</field>
+      <field name="kind">function</field>
+      <field name="action">tender_agreement_selected()</field>
+      <field name="flow_stop">True</field>
+    </record>
+
+    <record id="trans_agreement_selected" model="workflow.transition">
+      <field name="act_from" ref="purchase_requisition_extended.act_closed"/>
+      <field name="act_to" ref="act_agreement_selected"/>
+      <field name="signal">select_agreement</field>
+    </record>
+  </data>
+</openerp>

=== added directory 'framework_agreement_requisition/test'
=== added file 'framework_agreement_requisition/test/agreement_requisition.yml'
--- framework_agreement_requisition/test/agreement_requisition.yml	1970-01-01 00:00:00 +0000
+++ framework_agreement_requisition/test/agreement_requisition.yml	2013-12-05 10:14:17 +0000
@@ -0,0 +1,98 @@
+-
+  Standard flow of a Call for agreement Bids in mode open
+-
+  Create Call for Bids
+-
+  !record {model: purchase.requisition, id: purchase_requisition_agreement}:
+    date_start: '2013-08-02 00:00:00'
+    date_end: '2013-08-30 00:00:00'
+    bid_tendering_mode: 'open'
+    schedule_date: '2013-09-30'
+    req_validity: '2013-09-10'
+    framework_agreement_tender: True
+    line_ids:
+      - product_id: product.product_product_15
+        product_qty: 2500.0
+-
+  Confirm Call
+-
+  !python {model: purchase.requisition}: |
+    import netsvc
+    wf_service = netsvc.LocalService("workflow")
+    wf_service.trg_validate(uid, 'purchase.requisition', ref("purchase_requisition_agreement"), 'sent_suppliers', cr)
+-
+  Create RFQ1. I run the 'Request a quotation' wizard. I fill the supplier.
+-
+  !record {model: purchase.requisition.partner, id: purchase_requisition_agreement_partner1_create}:
+    partner_id: base.res_partner_2
+-
+  Create RFQ1. I confirm the wizard.
+-
+  !python {model: purchase.requisition.partner}: |
+    self.create_order(cr, uid, [ref("purchase_requisition_agreement_partner1_create")],{
+        'active_model': 'purchase.requisition',
+        'active_id': ref("purchase_requisition_agreement"),
+        'active_ids': [ref("purchase_requisition_agreement")],
+        })
+-
+  I encode the bid. I set a 300 price on the line.
+-
+  !python {model: purchase.requisition}: |
+    purchase_req = self.browse(cr, uid, ref("purchase_requisition_agreement"))
+    assert len(purchase_req.purchase_ids) == 1, "There must be 1 RFQs linked to this Call for bids"
+    price = 300
+    for rfq in purchase_req.purchase_ids:
+        for line in rfq.order_line:
+            self.pool.get('purchase.order.line').write(cr, uid, [line.id], {'price_unit': price})
+
+-
+  I send the RFQ. For this, I print the RFQ.
+-
+  !python {model: purchase.requisition}: |
+    purchase_req = self.browse(cr, uid, ref("purchase_requisition_agreement"))
+    for rfq in purchase_req.purchase_ids:
+        self.pool.get('purchase.order').print_quotation(cr, uid, [rfq.id])
+-
+  I run the 'Bid encoded' wizard of bid1. I fill the date.
+-
+  !record {model: purchase.action_modal_datetime, id: purchase_requisition_agreement_bid1_bidencoded}:
+    datetime: '2013-08-13 00:00:00'
+-
+ I launch wizard action.
+-
+  !python {model: purchase.action_modal_datetime}: |
+    purchase_req = self.pool['purchase.requisition'].browse(cr, uid, ref("purchase_requisition_agreement"))
+    po_id = purchase_req.purchase_ids[0].id
+    self.action(cr, uid, [ref('purchase_requisition_agreement_bid1_bidencoded')],
+                {'action': 'bid_received_ok',
+                'active_id': po_id,
+                'active_ids': [po_id],
+                'active_model': 'purchase.order',
+                'default_datetime': '2013-08-13 00:00:00',
+                'uid': 1})
+-
+  I close the Call for bids and move to bids selection
+-
+  !python {model: purchase.requisition}: |
+    import netsvc
+    wf_service = netsvc.LocalService("workflow")
+    wf_service.trg_validate(uid, 'purchase.requisition', ref("purchase_requisition_agreement"), 'open_bid', cr)
+
+-
+  In the bids selection, I confirm line 1 of bid 1
+-
+  !python {model: purchase.requisition}: |
+    purchase_req = self.browse(cr, uid, ref("purchase_requisition_agreement"))
+    self.pool.get('purchase.order.line').action_confirm(cr, uid, [purchase_req.purchase_ids[0].order_line[0].id])
+-
+  I close the call for bids
+-
+  !python {model: purchase.requisition}: |
+    self.close_callforbids(cr, uid, [ref("purchase_requisition_agreement")])
+-
+  I mark the tender as agreement selected
+-
+  !python {model: purchase.requisition}: |
+    self.agreement_selected(cr, uid, ref("purchase_requisition_agreement"))
+    purchase_req = self.browse(cr, uid, ref("purchase_requisition_agreement"))
+    assert purchase_req.state == 'agreement_selected'

=== added directory 'framework_agreement_requisition/view'
=== added file 'framework_agreement_requisition/view/purchase_requisition_view.xml'
--- framework_agreement_requisition/view/purchase_requisition_view.xml	1970-01-01 00:00:00 +0000
+++ framework_agreement_requisition/view/purchase_requisition_view.xml	2013-12-05 10:14:17 +0000
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+  <data noupdate="0">
+    <record model="ir.ui.view" id="view_purchase_requisition_form_agreement">
+      <field name="name">purchase.requisition.form.inherit.aggrement.button</field>
+      <field name="model">purchase.requisition</field>
+      <field name="inherit_id" ref="purchase_requisition.view_purchase_requisition_form"/>
+      <field name="arch" type="xml">
+        <field name="multiple_rfq_per_supplier"
+               position="after">
+          <field name="framework_agreement_tender"/>
+        </field>
+        <button name="cancel_requisition" position="after">
+          <button name="agreement_selected"
+                  type="object"
+                  class="po_buttons oe_form_buttons"
+                  attrs="{'invisible': ['|', ('framework_agreement_tender', '=', False), ('state', '!=', 'closed')]}"
+                  string="Framework agreement selected"/>
+        </button>
+      </field>
+    </record>
+
+    <record model="ir.ui.view" id="view_purchase_requisition_filter">
+      <field name="name">purchase.requisition.form.inherit.agreement.filter</field>
+      <field name="model">purchase.requisition</field>
+      <field name="inherit_id" ref="purchase_requisition.view_purchase_requisition_filter"/>
+      <field name="arch" type="xml">
+        <filter name="draft"
+                position="after">
+                  <filter icon="terp-document-new"
+                          name="framework_agreement"
+                          string="Framework Agreement?"
+                          domain="[(framework_agreement_tender,'=',True)]"/>
+        </filter>
+      </field>
+    </record>
+
+  </data>
+</openerp>

=== added directory 'framework_agreement_sourcing'
=== added file 'framework_agreement_sourcing/__init__.py'
--- framework_agreement_sourcing/__init__.py	1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/__init__.py	2013-12-05 10:14:17 +0000
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    Author: Nicolas Bessi
+#    Copyright 2013 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 model

=== added file 'framework_agreement_sourcing/__openerp__.py'
--- framework_agreement_sourcing/__openerp__.py	1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/__openerp__.py	2013-12-05 10:14:17 +0000
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    Author: Nicolas Bessi
+#    Copyright 2013 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': 'Framework agreement integration in sourcing',
+ 'version': '0.1',
+ 'author': 'Camptocamp',
+ 'maintainer': 'Camptocamp',
+ 'category': 'NGO',
+ 'complexity': 'normal',
+ 'depends': ['framework_agreement', 'logistic_requisition'],
+ 'description': """Add the possibility to have a framework agreement as a source.
+
+it will do auto completion and warning and create PO from source line""",
+ 'website': 'http://www.camptocamp.com',
+ 'data': ['view/requisition_view.xml'],
+ 'demo': [],
+ 'test': [],
+ 'installable': True,
+ 'auto_install': False,
+ 'license': 'AGPL-3',
+ 'application': False,
+ }

=== added directory 'framework_agreement_sourcing/i18n'
=== added directory 'framework_agreement_sourcing/model'
=== added file 'framework_agreement_sourcing/model/__init__.py'
--- framework_agreement_sourcing/model/__init__.py	1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/model/__init__.py	2013-12-05 10:14:17 +0000
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    Author: Nicolas Bessi
+#    Copyright 2013 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 logistic_requisition
+from . import logistic_requisition_source
+from . import purchase
+from . import adapter_util
+from . import sale_order
+from . import logistic_requisition_cost_estimate

=== added file 'framework_agreement_sourcing/model/adapter_util.py'
--- framework_agreement_sourcing/model/adapter_util.py	1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/model/adapter_util.py	2013-12-05 10:14:17 +0000
@@ -0,0 +1,116 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    Author: Nicolas Bessi
+#    Copyright 2013 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/>.
+#
+##############################################################################
+"""Provides basic mechanism to unify the way records are transformed into
+other records
+
+"""
+
+from openerp.osv import orm
+
+
+class BrowseAdapterSourceMixin(object):
+    """Mixin class used by Model that are transformation sources"""
+
+    def _company(self, cr, uid, context):
+        """Return company id
+
+        :returns: company id
+
+        """
+        return self.pool['res.company']._company_default_get(cr, uid, 'purchase.order',
+                                                             context=context)
+
+    def _direct_map(self, line, mapping, context=None):
+        """Take a dict of left key right key and make direct mapping
+        into the model
+
+        :returns: data dict ready to be used
+        """
+        data = {}
+        for po_key, source_key in mapping.iteritems():
+            value = line[source_key]
+            if isinstance(value, orm.browse_record):
+                value = value.id
+            elif isinstance(value, orm.browse_null):
+                value = False
+            elif isinstance(value, orm.browse_record_list):
+                raise NotImplementedError('List are not supported in direct map')
+
+            data[po_key] = value
+        return data
+
+
+class BrowseAdapterMixin(object):
+
+    def _do_checks(self, cr, uid, model, data, context=None):
+        """Perform validation check of adapted data.
+
+        All missing or incorrect values are return at once.
+
+        :returns: array of exceptions
+
+        """
+        required_keys = set(k for k, v in model._columns.iteritems()
+                                if v.required and not getattr(v, '_fnct', False))
+        empty_required = set(x for x in data
+                                if x in required_keys and not data[x])
+        missing_required = required_keys - set(data.keys())
+        missing_required.update(empty_required)
+        if missing_required:
+            return[ValueError('Following value are missing or False'
+                              ' while adapting %s: %s' %
+                                  (model._name, ", ".join(missing_required)))]
+        return []
+
+    def _validate_adapted_data(self, cr, uid, model, data, context=None):
+        """Perform validation check of adapted data.
+
+        All missing or incorrect values are return at once.
+
+        :returns: validated data or raise Value error
+
+        """
+        errors = self._do_checks(cr, uid, model, data, context=context)
+        if errors:
+            raise ValueError('Data are invalid for following reason %s' %
+                                 ("\n".join(repr(e) for e in errors)))
+        return data
+
+    def _adapt_origin(self, cr, uid, model, origin,
+                      map_fun, post_fun=None, context=None, **kwargs):
+        """Do transformation of source data to dest data using transforms function.
+
+        :param origin: source record
+        :param map_fun: transform function
+        :param post_fun: post transformation hook function
+
+        :returns: transformed data
+
+        """
+        if not callable(map_fun):
+            raise ValueError('Mapping function is not callable')
+        if post_fun and not callable(post_fun):
+            raise ValueError('Post hook function is not callable')
+        data = map_fun(cr, uid, origin, context=context, **kwargs)
+        # we complete with default
+        missing = set(model._columns.keys()) - set(data.keys())
+        data.update(model.default_get(cr, uid, missing, context=context))
+        return data

=== added file 'framework_agreement_sourcing/model/logistic_requisition.py'
--- framework_agreement_sourcing/model/logistic_requisition.py	1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/model/logistic_requisition.py	2013-12-05 10:14:17 +0000
@@ -0,0 +1,243 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    Author: Nicolas Bessi
+#    Copyright 2013 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 collections import namedtuple
+from openerp.tools.translate import _
+from openerp.osv import orm
+from .adapter_util import BrowseAdapterSourceMixin
+from .logistic_requisition_source import AGR_PROC
+
+
+class logistic_requisition_line(orm.Model, BrowseAdapterSourceMixin):
+    """Override to enable generation of source line"""
+
+    _inherit = "logistic.requisition.line"
+
+    def _map_agr_requisiton_to_source(self, cr, uid, line, context=None,
+                                      qty=0, agreement=None, **kwargs):
+        """Prepare data dict for source line using agreement as source
+
+        :params line: browse record of origin requistion.line
+        :params agreement: browse record of origin agreement
+        :params qty: quantity to be set on source line
+
+        :returns: dict to be used by Model.create
+
+        """
+        res = {}
+        direct_map = {
+            'proposed_product_id': 'product_id',
+            'requisition_line_id': 'id',
+            'proposed_uom_id': 'requested_uom_id'}
+
+        if not agreement:
+            raise ValueError("Missing agreement")
+        if not agreement.product_id.id == line.product_id.id:
+            raise ValueError("Product mismatch for agreement and requisition line")
+        # currency = self._get_source_currency(cr, uid, line, context=context)
+        res['unit_cost'] = 0.0
+        res['proposed_qty'] = qty
+        res['framework_agreement_id'] = agreement.id
+        res['procurement_method'] = AGR_PROC
+        res.update(self._direct_map(line, direct_map))
+        return res
+
+    def _map_requisition_to_source(self, cr, uid, line, context=None,
+                                   qty=0, **kwargs):
+        """Prepare data dict to generate source line using requisition as source
+
+        :params line: browse record of origin requistion.line
+        :params qty: quantity to be set on source line
+
+        :returns: dict to be used by Model.create
+
+        """
+        res = {}
+        direct_map = {'proposed_product_id': 'product_id',
+                      'requisition_line_id': 'id',
+                      'proposed_uom_id': 'requested_uom_id'}
+        res['unit_cost'] = 0.0
+        res['proposed_qty'] = qty
+        res['framework_agreement_id'] = False
+        res['procurement_method'] = 'procurement'
+        res.update(self._direct_map(line, direct_map))
+        return res
+
+    def _generate_lines_from_agreements(self, cr, uid, container, line,
+                                        agreements, qty, currency=None, context=None):
+        """Generate 1/n source line(s) for one requisition line.
+
+        This is done using available agreements.
+        We first look for cheapeast agreement.
+        Then if no more quantity are available and there is still remaining needs
+        we look for next cheapest agreement or return remaining qty
+
+        :param container: list of agreements browse
+        :param qty: quantity to be sourced
+        :param line: origin requisition line
+
+        :returns: remaining quantity to source
+
+        """
+        agreements = agreements if agreements is not None else []
+        if currency:
+            agreements = [x for x in agreements if x.has_currency(currency)]
+        if not agreements:
+            return qty
+        agreements.sort(key=lambda x: x.get_price(qty, currency=currency))
+        current_agr = agreements.pop(0)
+        avail = current_agr.available_quantity
+        if not avail:
+            return qty
+        avail_sold = avail - qty
+        to_consume = qty if avail_sold >= 0 else avail
+
+        source_id = self.make_source_line(cr, uid, line, force_qty=to_consume,
+                                          agreement=current_agr, context=context)
+        container.append(source_id)
+        difference = qty - to_consume
+        if difference:
+            return self._generate_lines_from_agreements(cr, uid, container, line,
+                                                        agreements, difference, context=context)
+        else:
+            return 0
+
+    def _source_lines_for_agreements(self, cr, uid, line, agreements, currency=None, context=None):
+        """Generate 1/n source line(s) for one requisition line
+
+        This is done using available agreements.
+        We first look for cheapeast agreement.
+        Then if no more quantity are available and there is still remaining needs
+        we look for next cheapest agreement or we create a tender source line
+
+        :param line: requisition line browse record
+        :returns: (generated line ids, remaining qty not covered by agreement)
+
+        """
+        Sourced = namedtuple('Sourced', ['generated', 'remaining'])
+        qty = line.requested_qty
+        generated = []
+        remaining_qty = self._generate_lines_from_agreements(cr, uid, generated,
+                                                             line, agreements, qty,
+                                                             currency=currency, context=context)
+        return Sourced(generated, remaining_qty)
+
+    def make_source_line(self, cr, uid, line, force_qty=None, agreement=None, context=None):
+        """Generate a source line for a tender from a requisition line
+
+        :param line: browse record of origin logistic.request
+        :param force_qty: if set this quantity will be used instead
+        of requested quantity
+        :returns: id of generated source line
+
+        """
+        qty = force_qty if force_qty else line.requested_qty
+        src_obj = self.pool['logistic.requisition.source']
+        if agreement:
+            return src_obj._make_source_line_from_origin(cr, uid, line,
+                                                         self._map_agr_requisiton_to_source,
+                                                         context=context, qty=qty,
+                                                         agreement=agreement)
+        else:
+            return src_obj._make_source_line_from_origin(cr, uid, line,
+                                                         self._map_requisition_to_source,
+                                                         context=context, qty=qty)
+
+    def _get_source_currency(self, cr, uid, line, context=None):
+        agr_obj = self.pool['framework.agreement']
+        comp_obj = self.pool['res.company']
+        currency = line.requisition_id.get_pricelist().currency_id
+        company_id = agr_obj._company_get(cr, uid, context=context)
+        comp_currency = comp_obj.browse(cr, uid, company_id, context=context).currency_id
+        if currency == comp_currency:
+            return None
+        return currency
+
+    def _generate_source_line(self, cr, uid, line, context=None):
+        """Generate one or n source line(s) per requisition line.
+
+        Depending on the available resources. If there is framework agreement(s)
+        running we generate one or n source line using agreements otherwise we generate one
+        source line using tender process
+
+        :param line: browse record of origin logistic.request
+
+        :returns: list of generated source line ids
+
+        """
+        if line.source_ids:
+            return None
+        agr_obj = self.pool['framework.agreement']
+        date = line.requisition_id.date
+        currency = self._get_source_currency(cr, uid, line, context=context)
+        product_id = line.product_id.id
+        agreements = agr_obj.get_all_product_agreements(cr, uid, product_id, date,
+                                                        context=context)
+        generated_lines = []
+        if agreements:
+            line_ids, missing_qty = self._source_lines_for_agreements(cr, uid, line,
+                                                                      agreements, currency=currency)
+            generated_lines.extend(line_ids)
+            if missing_qty:
+                generated_lines.append(self.make_source_line(cr, uid, line,
+                                                             force_qty=missing_qty))
+        else:
+            generated_lines.append(self.make_source_line(cr, uid, line))
+
+        return generated_lines
+
+    def _do_confirm(self, cr, uid, ids, context=None):
+        """Override to generate source lines from requision line.
+
+        Please refer to _generate_source_line documentation
+
+        """
+        # TODO refactor
+        # this should probably be in logistic_requisition module
+        # providing a mechanism to allow each type of sourcing method
+        # to generate source line
+        res = super(logistic_requisition_line, self)._do_confirm(cr, uid, ids,
+                                                                 context=context)
+        for line_br in self.browse(cr, uid, ids, context=context):
+            self._generate_source_line(cr, uid, line_br, context=context)
+        return res
+
+
+class logistic_requisition(orm.Model):
+    """Add get pricelist function"""
+
+    _inherit = "logistic.requisition"
+
+    def get_pricelist(self, cr, uid, requisition_id, context=None):
+        """Retrive pricelist id to use in sourcing by agreement process
+
+        :returns: pricelist record
+
+        """
+        if isinstance(requisition_id, (list, tuple)):
+            assert len(requisition_id) == 1
+            requisition_id = requisition_id[0]
+        requisiton = self.browse(cr, uid, requisition_id, context=context)
+        plist = requisiton.partner_id.property_product_pricelist
+        if not plist:
+            raise orm.except_orm(_('No price list on customer'),
+                                 _('Please set sale price list on %s partner') %
+                                 requisiton.partner_id.name)
+        return plist

=== added file 'framework_agreement_sourcing/model/logistic_requisition_cost_estimate.py'
--- framework_agreement_sourcing/model/logistic_requisition_cost_estimate.py	1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/model/logistic_requisition_cost_estimate.py	2013-12-05 10:14:17 +0000
@@ -0,0 +1,69 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    Author: Nicolas Bessi
+#    Copyright 2013 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 import orm
+from .logistic_requisition_source import AGR_PROC
+
+
+class logistic_requisition_cost_estimate(orm.Model):
+    """Add update of agreement price"""
+
+    _inherit = "logistic.requisition.cost.estimate"
+
+    def _update_agreement_source(self, cr, uid, source, context=None):
+        """Update price of source line using related confirmed PO"""
+        if source.procurement_method == AGR_PROC:
+            self._link_po_lines_to_source(cr, uid, source, context=context)
+            price = source.get_agreement_price_from_po()
+            source.write({'unit_cost': price})
+            source.refresh()
+
+    def _link_po_lines_to_source(self, cr, uid, source, context=None):
+        po_l_obj = self.pool['purchase.order.line']
+        agr = source.framework_agreement_id
+        line_ids = po_l_obj.search(
+            cr, uid,
+            [('order_id.framework_agreement_id', '=', agr.id),
+             ('lr_source_line_id', '=', source.id),
+             ('order_id.partner_id', '=', agr.supplier_id.id)],
+            context=context)
+        lines = po_l_obj.browse(cr, uid, line_ids, context=context)
+        po_ids = ([line.order_id.id for line in lines])
+
+        all_line_ids = po_l_obj.search(
+            cr, uid,
+            [('order_id', 'in', po_ids)],
+            context=context)
+
+        po_l_obj.write(cr, uid, all_line_ids,
+                       {'lr_source_line_id': source.id},
+                       context=context)
+        source.refresh()
+
+    def _prepare_cost_estimate_line(self, cr, uid, sourcing, context=None):
+        """Override in order to update agreement source line
+
+        We update the price of source line that will be used in cost estimate
+
+        """
+        self._update_agreement_source(cr, uid, sourcing, context=context)
+        return super(logistic_requisition_cost_estimate,
+                     self)._prepare_cost_estimate_line(cr, uid, sourcing,
+                                                       context=context)

=== added file 'framework_agreement_sourcing/model/logistic_requisition_source.py'
--- framework_agreement_sourcing/model/logistic_requisition_source.py	1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/model/logistic_requisition_source.py	2013-12-05 10:14:17 +0000
@@ -0,0 +1,363 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    Author: Nicolas Bessi
+#    Copyright 2013 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 import orm, fields
+from openerp.tools.translate import _
+from openerp.addons.framework_agreement.model.framework_agreement import\
+    FrameworkAgreementObservable
+from openerp.addons.framework_agreement.utils import id_boilerplate
+
+from .adapter_util import BrowseAdapterMixin, BrowseAdapterSourceMixin
+
+AGR_PROC = 'fw_agreement'
+
+
+class logistic_requisition_source(orm.Model, BrowseAdapterMixin,
+                                  BrowseAdapterSourceMixin, FrameworkAgreementObservable):
+    """Adds support of framework agreement to source line"""
+
+    _inherit = "logistic.requisition.source"
+
+    _columns = {'framework_agreement_id': fields.many2one('framework.agreement',
+                                                          'Agreement'),
+
+                'purchase_pricelist_id': fields.many2one('product.pricelist',
+                                                         'Purchase (PO) Pricelist',
+                                                         help="This pricelist will be used"
+                                                              " when generating PO"),
+                'pricelist_id': fields.related('requisition_line_id', 'requisition_id',
+                                               'partner_id',
+                                               'property_product_pricelist',
+                                               relation='product.pricelist',
+                                               type='many2one',
+                                               string='Price list',
+                                               readonly=True),
+
+                'supplier_id': fields.related('framework_agreement_id', 'supplier_id',
+                                              type='many2one',  relation='res.partner',
+                                              string='Agreement Supplier')}
+
+    def _get_procur_method_hook(self, cr, uid, context=None):
+        """Adds framework agreement as a procurement method in selection field"""
+        res = super(logistic_requisition_source, self)._get_procur_method_hook(cr, uid,
+                                                                               context=context)
+        res.append((AGR_PROC, 'Framework agreement'))
+        return res
+
+    def _get_purchase_line_id(self, cr, uid, ids, field_name, arg, context=None):
+        """For each source line, get the related purchase order line
+
+        For more detail please refer to function fields documentation
+
+        """
+        po_line_model = self.pool['purchase.order.line']
+        res = super(logistic_requisition_source, self)._get_purchase_line_id(cr, uid, ids,
+                                                                             field_name,
+                                                                             arg,
+                                                                             context=context)
+        for line in self.browse(cr, uid, ids, context=context):
+            if line.procurement_method == AGR_PROC:
+                po_l_ids = po_line_model.search(cr, uid,
+                                                [('lr_source_line_id', '=', line.id),
+                                                 ('state', '!=', 'cancel')],
+                                                context=context)
+                if po_l_ids:
+                    if len(po_l_ids) > 1:
+                        raise orm.except_orm(_('Many Purchase order lines found for %s') % line.name,
+                                             _('Please cancel uneeded one'))
+                    res[line.id] = po_l_ids[0]
+                else:
+                    res[line.id] = False
+        return res
+
+    #------------------ adapting source line to po -----------------------------
+
+    def _map_source_to_po(self, cr, uid, line, context=None, **kwargs):
+        """Map source line to dict to be used by PO create defaults are optional
+
+        :returns: data dict to be used by adapter
+
+        """
+        supplier = line.framework_agreement_id.supplier_id
+        add = line.requisition_id.consignee_shipping_id
+        term = supplier.property_supplier_payment_term
+        term = term.id if term else False
+        position = supplier.property_account_position
+        position = position.id if position else False
+        requisition = line.requisition_id
+        data = {}
+        data['framework_agreement_id'] = line.framework_agreement_id.id
+        data['partner_id'] = supplier.id
+        data['company_id'] = self._company(cr, uid, context)
+        data['pricelist_id'] = line.purchase_pricelist_id.id
+        data['dest_address_id'] = add.id
+        data['location_id'] = add.property_stock_customer.id
+        data['payment_term_id'] = term
+        data['fiscal_position'] = position
+        data['origin'] = requisition.name
+        data['date_order'] = requisition.date
+        data['name'] = requisition.name
+        data['consignee_id'] = requisition.consignee_id.id
+        data['incoterm_id'] = requisition.incoterm_id.id
+        data['incoterm_address'] = requisition.incoterm_address
+        data['type'] = 'purchase'
+        return data
+
+    def _map_source_to_po_line(self, cr, uid, line, context=None, **kwargs):
+        """Map source line to dict to be used by PO line create
+        Map source line to dict to be used by PO create
+        defaults are optional
+
+        :returns: data dict to be used by adapter
+
+        """
+        acc_pos_obj = self.pool['account.fiscal.position']
+        supplier = line.framework_agreement_id.supplier_id
+        taxes_ids = line.proposed_product_id.supplier_taxes_id
+        taxes = acc_pos_obj.map_tax(cr, uid, supplier.property_account_position,
+                                    taxes_ids)
+        currency = line.purchase_pricelist_id.currency_id
+        price = line.framework_agreement_id.get_price(line.proposed_qty, currency=currency)
+        data = {}
+        direct_map = {'product_qty': 'proposed_qty',
+                      'product_id': 'proposed_product_id',
+                      'product_uom': 'proposed_uom_id',
+                      'lr_source_line_id': 'id'}
+
+        data.update(self._direct_map(line, direct_map))
+        data['price_unit'] = price
+        data['name'] = line.proposed_product_id.name
+        data['date_planned'] = line.requisition_id.date_delivery
+        data['taxes_id'] = [(6, 0, taxes)]
+        return data
+
+    def _make_po_from_source_line(self, cr, uid, source_line, context=None):
+        """adapt a source line to purchase order
+
+        :returns: generated PO id
+
+        """
+
+        po_obj = self.pool['purchase.order']
+        pid = po_obj._make_purchase_order_from_origin(cr, uid, source_line,
+                                                      self._map_source_to_po,
+                                                      self._map_source_to_po_line,
+                                                      context=context)
+        return pid
+
+    def make_purchase_order(self, cr, uid, ids, context=None):
+        """ adapt each source line to purchase order
+
+        :returns: generated PO ids
+
+        """
+        po_ids = []
+        for source_line in self.browse(cr, uid, ids, context=context):
+            po_id = self._make_po_from_source_line(cr, uid, source_line, context=None)
+            po_ids.append(po_id)
+        return po_ids
+
+    def action_create_agreement_po_requisition(self, cr, uid, ids, context=None):
+        """ Implement buttons that create PO from selected source lines"""
+        # We force empty context
+        act_obj = self.pool.get('ir.actions.act_window')
+        po_ids = self.make_purchase_order(cr, uid, ids, context=context)
+        res = act_obj.for_xml_id(cr, uid,
+                                 'purchase', 'purchase_rfq', context=context)
+        res.update({'domain': [('id', 'in', po_ids)],
+                    'res_id': False,
+                    'context': '{}',
+                    })
+        return res
+
+    def _is_sourced_fw_agreement(self, cr, uid, source, context=None):
+        """Predicate that tells if source line of type agreement are sourced
+
+        :retuns: boolean True if sourced
+
+        """
+        po_line_obj = self.pool['purchase.order.line']
+        sources_ids = po_line_obj.search(cr, uid, [('lr_source_line_id', '=', source.id)],
+                                         context=context)
+        # predicate
+        return bool(sources_ids)
+
+    def get_agreement_price_from_po(self, cr, uid, source_id, context=None):
+        """Get price from PO.
+
+        The price is retreived on the po line generated by sourced line.
+
+        :returns: price in float
+        """
+        if isinstance(source_id, (list, tuple)):
+            assert len(source_id) == 1
+            source_id = source_id[0]
+        po_l_obj = self.pool['purchase.order.line']
+        currency_obj = self.pool['res.currency']
+        current = self.browse(cr, uid, source_id, context=context)
+        agreement = current.framework_agreement_id
+
+        if not agreement:
+            raise ValueError('No framework agreement on source line %s' %
+                             current.name)
+        line_ids = po_l_obj.search(cr, uid,
+                                   [('order_id.framework_agreement_id', '=', agreement.id),
+                                    ('lr_source_line_id', '=', current.id),
+                                    ('order_id.partner_id', '=', agreement.supplier_id.id)],
+                                   context=context)
+        price = 0.0
+        lines = po_l_obj.browse(cr, uid, line_ids, context=context)
+        if lines:
+            price = sum(x.price_subtotal for x in lines) # To avoid rounding problems
+            from_curr = lines[0].order_id.pricelist_id.currency_id.id
+            to_curr = current.pricelist_id.currency_id.id
+            price = currency_obj.compute(cr, uid, from_curr, to_curr, price, False)
+        return price
+
+    #---------------------- provide adapter middleware -------------------------
+
+    def _make_source_line_from_origin(self, cr, uid, origin, map_fun,
+                                      post_fun=None, context=None, **kwargs):
+        model = self.pool['logistic.requisition.source']
+        data = self._adapt_origin(cr, uid, model, origin, map_fun,
+                                  post_fun=post_fun, context=context, **kwargs)
+        self._validate_adapted_data(cr, uid, model, data, context=context)
+        s_id = self.create(cr, uid, data, context=context)
+        if callable(post_fun):
+            post_fun(cr, uid, s_id, origin, context=context, **kwargs)
+        return s_id
+
+    #---------------OpenERP tedious onchange management ------------------------
+
+    def _get_date(self, cr, uid, requision_line_id, context=None):
+        """helper to retrive date to be used by framework agreement
+        when in source line context
+
+        :param source_id: requisition.line.source id that should
+            provide date
+
+        :returns: date/datetime string
+
+        """
+        req_obj = self.pool['logistic.requisition.line']
+        current = req_obj.browse(cr, uid, requision_line_id, context=context)
+        now = fields.datetime.now()
+        return current.requisition_id.date or now
+
+    @id_boilerplate
+    def onchange_sourcing_method(self, cr, uid, source_id, method, req_line_id, proposed_product_id,
+                                 pricelist_id, proposed_qty=0, context=None):
+        """
+        Called when source method is set on a source line.
+
+        If sourcing method is framework agreement
+        it will set price, agreement and supplier if possible
+        and raise quantity warning.
+
+        """
+        res = {'value': {'framework_agreement_id': False}}
+        if (method != AGR_PROC or not proposed_product_id or not pricelist_id):
+            return res
+        currency = self._currency_get(cr, uid, pricelist_id, context=context)
+        agreement_obj = self.pool['framework.agreement']
+        date = self._get_date(cr, uid, req_line_id, context=context)
+        agreement, enough_qty = agreement_obj.get_cheapest_agreement_for_qty(cr, uid,
+                                                                             proposed_product_id,
+                                                                             date,
+                                                                             proposed_qty,
+                                                                             currency=currency,
+                                                                             context=context)
+        if not agreement:
+            return res
+        price = agreement.get_price(proposed_qty, currency=currency)
+        res['value'] = {'framework_agreement_id': agreement.id,
+                        'unit_cost': price,
+                        'total_cost': price * proposed_qty,
+                        'supplier_id': agreement.supplier_id.id}
+        if not enough_qty:
+            msg = _("You have ask for a quantity of %s \n"
+                    " but there is only %s available"
+                    " for current agreement") % (proposed_qty, agreement.available_quantity)
+            res['warning'] = msg
+        return res
+
+    @id_boilerplate
+    def onchange_pricelist(self, cr, uid, source_id, method, req_line_id,
+                           proposed_product_id, proposed_qty,
+                           pricelist_id, context=None):
+        """Call when pricelist is set on a source line.
+
+        If sourcing method is framework agreement
+        it will set price, agreement and supplier if possible
+        and raise quantity warning.
+
+        """
+        res = {}
+        if (method != AGR_PROC or not proposed_product_id or not pricelist_id):
+            return res
+
+        return self.onchange_sourcing_method(cr, uid, source_id, method, req_line_id,
+                                             proposed_product_id, pricelist_id,
+                                             proposed_qty=proposed_qty,
+                                             context=context)
+
+    @id_boilerplate
+    def onchange_quantity(self, cr, uid, source_id, method, req_line_id, qty,
+                          proposed_product_id, pricelist_id, context=None):
+        """Raise a warning if agreed qty is not sufficient"""
+        if (method != AGR_PROC or not proposed_product_id):
+            return {}
+        currency = self._currency_get(cr, uid, pricelist_id, context=context)
+        date = self._get_date(cr, uid, req_line_id, context=context)
+        return self.onchange_quantity_obs(cr, uid, source_id, qty, date,
+                                          proposed_product_id,
+                                          currency=currency,
+                                          price_field='dummy',
+                                          context=context)
+
+    @id_boilerplate
+    def onchange_product_id(self, cr, uid, source_id, method, req_line_id,
+                            proposed_product_id, proposed_qty,
+                            pricelist_id, context=None):
+        """Call when product is set on a source line.
+
+        If sourcing method is framework agreement
+        it will set price, agreement and supplier if possible
+        and raise quantity warning.
+
+        """
+        if (method != AGR_PROC or not proposed_product_id):
+            return {}
+
+        return self.onchange_sourcing_method(cr, uid, source_id, method, req_line_id,
+                                             proposed_product_id, pricelist_id,
+                                             proposed_qty=proposed_qty,
+                                             context=context)
+
+    @id_boilerplate
+    def onchange_agreement(self, cr, uid, source_id, agreement_id, req_line_id, qty,
+                           proposed_product_id, pricelist_id, context=None):
+        if not proposed_product_id or not pricelist_id or not agreement_id:
+            return {}
+        currency = self._currency_get(cr, uid, pricelist_id, context=context)
+        date = self._get_date(cr, uid, req_line_id, context=context)
+        return self.onchange_agreement_obs(cr, uid, source_id, agreement_id, qty,
+                                           date, proposed_product_id,
+                                           currency=currency, price_field='dummy')

=== added file 'framework_agreement_sourcing/model/purchase.py'
--- framework_agreement_sourcing/model/purchase.py	1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/model/purchase.py	2013-12-05 10:14:17 +0000
@@ -0,0 +1,91 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    Author: Nicolas Bessi
+#    Copyright 2013 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 import orm
+from . adapter_util import BrowseAdapterMixin
+
+
+class purchase_order(orm.Model, BrowseAdapterMixin):
+    """Add function to create PO from source line.
+    It maybe goes against YAGNI principle.
+    The idea would be to propose a small design
+    to be ported back into purchase_requisition_extended module
+    or an other base modules.
+
+    Then we should extend it to propose an API
+    to generate PO from various sources
+    """
+
+    _inherit = "purchase.order"
+
+    #------ PO adapter middleware maybe to put in aside class but not easy in OpenERP context ----
+    def _make_purchase_order_from_origin(self, cr, uid, origin, map_fun, map_line_fun,
+                                         post_fun=None, post_line_fun=None, context=None):
+        """Create a PO browse record from any other record
+
+        :returns: created record ids
+
+        """
+        po_id = self._adapt_origin_to_po(cr, uid, origin, map_fun,
+                                         post_fun=post_fun, context=context)
+        self._adapt_origin_to_po_line(cr, uid, po_id, origin, map_line_fun,
+                                      post_fun=post_line_fun,
+                                      context=context)
+        return po_id
+
+    def _adapt_origin_to_po(self, cr, uid, origin, map_fun,
+                            post_fun=None, context=None):
+        """PO adapter function
+
+        :returns: created PO id
+
+        """
+        model = self.pool['purchase.order']
+        data = self._adapt_origin(cr, uid, model, origin, map_fun,
+                                  post_fun=post_fun, context=context)
+        self._validate_adapted_data(cr, uid, model, data, context=context)
+        po_id = self.create(cr, uid, data, context=context)
+        if callable(post_fun):
+            post_fun(cr, uid, po_id, origin, context=context)
+        return po_id
+
+    def _adapt_origin_to_po_line(self, cr, uid, po_id, origin, map_fun,
+                                 post_fun=None, context=None):
+        """PO line adapter
+
+        :returns: created PO line id
+
+        """
+        model = self.pool['purchase.order.line']
+        data = self._adapt_origin(cr, uid, model, origin, map_fun,
+                                  post_fun=post_fun, context=context)
+        data['order_id'] = po_id
+        self._validate_adapted_data(cr, uid, model, data, context=context)
+        l_id = model.create(cr, uid, data, context=context)
+        if callable(post_fun):
+            post_fun(cr, uid, l_id, origin, context=context)
+        return l_id
+
+    def action_confirm(self, cr, uid, ids, context=None):
+        super(purchase_order_line, self).action_confirm(cr, uid, ids, context=context)
+        for element in self.browse(cr, uid, ids, context=context):
+            if not element.quantity_bid and not element.framework_agreement_id:
+                self.write(cr, uid, ids, {'quantity_bid': element.product_qty}, context=context)
+        return True

=== added file 'framework_agreement_sourcing/model/sale_order.py'
--- framework_agreement_sourcing/model/sale_order.py	1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/model/sale_order.py	2013-12-05 10:14:17 +0000
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    Author: Nicolas Bessi
+#    Copyright 2013 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 import netsvc
+from openerp.osv import orm
+
+
+class sale_order_line(orm.Model):
+    """Pass agreement PO into state confirmed when SO is confirmed"""
+
+    _inherit = "sale.order.line"
+
+    def button_confirm(self, cr, uid, ids, context=None):
+        """Override confirmation of request of cotation to support LTA
+
+        Related PO generated by agreement source line will be pased to state confirm.
+
+        """
+        result = super(sale_order_line, self).button_confirm(cr, uid, ids, context=context)
+        po_line_model = self.pool['purchase.order.line']
+        lines = self.browse(cr, uid, ids, context=context)
+        source_ids = [x.logistic_requisition_source_id.id for x in lines
+                           if x.logistic_requisition_source_id]
+        po_line_ids = po_line_model.search(cr, uid,
+                                           [('lr_source_line_id', 'in', source_ids)],
+                                           context=context)
+        po_lines = po_line_model.read(cr, uid, po_line_ids, ['order_id'], load='_classic_write')
+        po_ids = set(x['order_id'] for x in po_lines)
+        wf_service = netsvc.LocalService("workflow")
+        for po_id in po_ids:
+            wf_service.trg_validate(uid, 'purchase.order', po_id, 'draft_po', cr)
+            wf_service.trg_validate(uid, 'purchase.order', po_id, 'purchase_confirm', cr)
+        return result

=== added directory 'framework_agreement_sourcing/security'
=== added directory 'framework_agreement_sourcing/tests'
=== added file 'framework_agreement_sourcing/tests/__init__.py'
--- framework_agreement_sourcing/tests/__init__.py	1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/tests/__init__.py	2013-12-05 10:14:17 +0000
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    Author: Nicolas Bessi
+#    Copyright 2013 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 common
+from . import test_logistic_order_line_to_source_line
+from . import test_agreement_souce_line_to_po
+checks = [test_logistic_order_line_to_source_line,
+          test_agreement_souce_line_to_po]

=== added file 'framework_agreement_sourcing/tests/common.py'
--- framework_agreement_sourcing/tests/common.py	1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/tests/common.py	2013-12-05 10:14:17 +0000
@@ -0,0 +1,143 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    Author: Nicolas Bessi
+#    Copyright 2013 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 datetime import timedelta
+from openerp.tools import DEFAULT_SERVER_DATE_FORMAT
+import openerp.tests.common as test_common
+from openerp.addons.logistic_requisition.tests import logistic_requisition
+from openerp.addons.framework_agreement.tests.common import BaseAgreementTestMixin
+
+
+class CommonSourcingSetUp(test_common.TransactionCase, BaseAgreementTestMixin):
+
+    def setUp(self):
+        """
+        Setup a standard configuration for test
+        """
+        super(CommonSourcingSetUp, self).setUp()
+        self.commonsetUp()
+        self.requisition_model = self.registry('logistic.requisition')
+        self.requisition_line_model = self.registry('logistic.requisition.line')
+        self.source_line_model = self.registry('logistic.requisition.source')
+        self.make_common_agreements()
+        self.make_common_requisition()
+
+    def make_common_requisition(self):
+        """Create a standard logistic requisition"""
+        start_date = self.now + timedelta(days=12)
+        start_date = start_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
+        req = {
+            'partner_id': self.ref('base.res_partner_1'),
+            'consignee_id': self.ref('base.res_partner_3'),
+            'date_delivery': start_date,
+            'date': start_date,
+            'user_id': self.uid,
+            'budget_holder_id': self.uid,
+            'finance_officer_id': self.uid,
+        }
+        agr_line = {
+            'product_id': self.product_id,
+            'requested_qty': 100,
+            'requested_uom_id': self.ref('product.product_uom_unit'),
+            'date_delivery': self.now.strftime(DEFAULT_SERVER_DATE_FORMAT),
+            'budget_tot_price': 100000000,
+        }
+        other_line = {
+            'product_id': self.ref('product.product_product_7'),
+            'requested_qty': 10,
+            'requested_uom_id': self.ref('product.product_uom_unit'),
+            'date_delivery': self.now.strftime(DEFAULT_SERVER_DATE_FORMAT),
+            'budget_tot_price': 100000000,
+        }
+
+        requisition_id = logistic_requisition.create(self, req)
+        logistic_requisition.add_line(self, requisition_id,
+                                      agr_line)
+        logistic_requisition.add_line(self, requisition_id,
+                                      other_line)
+        self.requisition = self.requisition_model.browse(self.cr, self.uid, requisition_id)
+
+    def make_common_agreements(self):
+        """Create two default agreements.
+
+        We have two agreement for same product but using
+        different suppliers
+
+        One supplier has a better price for lower qty the other
+        has better price for higher qty
+
+        We also create one requisition with one line of agreement product
+        And one line of other product
+
+        """
+
+        cr, uid = self.cr, self.uid
+        start_date = self.now + timedelta(days=10)
+        start_date = start_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
+        end_date = self.now + timedelta(days=20)
+        end_date = end_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
+        # Agreement 1
+        agr_id = self.agreement_model.create(cr, uid,
+                                             {'supplier_id': self.supplier_id,
+                                              'product_id': self.product_id,
+                                              'start_date': start_date,
+                                              'end_date': end_date,
+                                              'draft': False,
+                                              'delay': 5,
+                                              'quantity': 2000})
+
+        pl_id = self.agreement_pl_model.create(cr, uid,
+                                               {'framework_agreement_id': agr_id,
+                                                'currency_id': self.ref('base.EUR')})
+        self.agreement_line_model.create(cr, uid,
+                                         {'framework_agreement_pricelist_id': pl_id,
+                                          'quantity': 0,
+                                          'price': 77.0})
+
+        self.agreement_line_model.create(cr, uid,
+                                         {'framework_agreement_pricelist_id': pl_id,
+                                          'quantity': 1000,
+                                          'price': 30.0})
+
+        self.cheap_on_high_agreement = self.agreement_model.browse(cr, uid, agr_id)
+
+        # Agreement 2
+        agr_id = self.agreement_model.create(cr, uid,
+                                             {'supplier_id': self.ref('base.res_partner_3'),
+                                              'product_id': self.product_id,
+                                              'start_date': start_date,
+                                              'end_date': end_date,
+                                              'draft': False,
+                                              'delay': 5,
+                                              'quantity': 1200})
+
+        pl_id = self.agreement_pl_model.create(cr, uid,
+                                               {'framework_agreement_id': agr_id,
+                                                'currency_id': self.ref('base.EUR')})
+
+        self.agreement_line_model.create(cr, uid,
+                                         {'framework_agreement_pricelist_id': pl_id,
+                                          'quantity': 0,
+                                          'price': 50.0})
+        self.agreement_line_model.create(cr, uid,
+                                         {'framework_agreement_pricelist_id': pl_id,
+                                          'quantity': 1000,
+                                          'price': 45.0})
+        self.cheap_on_low_agreement = self.agreement_model.browse(cr, uid, agr_id)

=== added file 'framework_agreement_sourcing/tests/test_agreement_souce_line_to_po.py'
--- framework_agreement_sourcing/tests/test_agreement_souce_line_to_po.py	1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/tests/test_agreement_souce_line_to_po.py	2013-12-05 10:14:17 +0000
@@ -0,0 +1,73 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    Author: Nicolas Bessi
+#    Copyright 2013 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 .common import CommonSourcingSetUp
+
+
+class TestSourceToPo(CommonSourcingSetUp):
+
+    def setUp(self):
+        # we generate a source line
+        super(TestSourceToPo, self).setUp()
+        cr, uid = self.cr, self.uid
+        lines = self.requisition.line_ids
+        agr_line = None
+        for line in lines:
+            if line.product_id == self.cheap_on_low_agreement.product_id:
+                agr_line = line
+                break
+        self.assertTrue(agr_line)
+        agr_line.write({'requested_qty': 400})
+        agr_line.refresh()
+        source_ids = self.requisition_line_model._generate_source_line(cr, uid, agr_line)
+        self.assertTrue(len(source_ids) == 1)
+        self.source_line = self.source_line_model.browse(cr, uid, source_ids[0])
+
+    def test_01_transform_source_to_agreement(self):
+        """Test transformation of an agreement source line into PO"""
+        cr, uid = self.cr, self.uid
+        self.assertTrue(self.source_line)
+        plist = self.source_line.framework_agreement_id.supplier_id.property_product_pricelist_purchase
+        self.source_line.write({'purchase_pricelist_id': plist.id})
+        self.source_line.refresh()
+        po_id = self.source_line_model._make_po_from_source_line(cr, uid,
+                                                                 self.source_line)
+        self.assertTrue(po_id)
+        supplier = self.source_line.framework_agreement_id.supplier_id
+        add = self.source_line.requisition_id.consignee_shipping_id
+        consignee = self.source_line.requisition_id.consignee_id
+        po = self.registry('purchase.order').browse(cr, uid, po_id)
+        date_order = self.source_line.requisition_id.date
+        date_delivery = self.source_line.requisition_id.date_delivery
+        self.assertEqual(po.partner_id, supplier)
+        self.assertEqual(po.pricelist_id, supplier.property_product_pricelist_purchase)
+        self.assertEqual(po.date_order, date_order)
+        self.assertEqual(po.dest_address_id, add)
+        self.assertEqual(po.consignee_id, consignee)
+
+        self.assertEqual(len(po.order_line), 1)
+        po_line = po.order_line[0]
+        self.assertEqual(po_line.product_qty, self.source_line.proposed_qty)
+        self.assertEqual(po_line.product_id, self.source_line.proposed_product_id)
+        self.assertEqual(po_line.product_qty, self.source_line.proposed_qty)
+        self.assertEqual(po_line.product_uom, self.source_line.proposed_uom_id)
+        self.assertEqual(po_line.price_unit, 50.0)
+        self.assertEqual(po_line.lr_source_line_id, self.source_line)
+        self.assertEqual(po_line.date_planned, date_delivery)

=== added file 'framework_agreement_sourcing/tests/test_logistic_order_line_to_source_line.py'
--- framework_agreement_sourcing/tests/test_logistic_order_line_to_source_line.py	1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/tests/test_logistic_order_line_to_source_line.py	2013-12-05 10:14:17 +0000
@@ -0,0 +1,135 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    Author: Nicolas Bessi
+#    Copyright 2013 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 ..model.logistic_requisition_source import AGR_PROC
+from .common import CommonSourcingSetUp
+
+
+class TestTransformation(CommonSourcingSetUp):
+
+    def test_01_enough_qty_on_first_agr(self):
+        """Test that we can source a line with one agreement and low qty"""
+        cr, uid = self.cr, self.uid
+        lines = self.requisition.line_ids
+        agr_line = None
+        for line in lines:
+            if line.product_id == self.cheap_on_low_agreement.product_id:
+                agr_line = line
+                break
+        self.assertTrue(agr_line)
+        agr_line.write({'requested_qty': 400})
+        agr_line.refresh()
+        to_validate_ids = self.requisition_line_model._generate_source_line(cr, uid, agr_line)
+        self.assertTrue(len(to_validate_ids) == 1)
+        to_validate = self.source_line_model.browse(cr, uid, to_validate_ids[0])
+        self.assertEqual(to_validate.procurement_method, AGR_PROC)
+        self.assertEqual(to_validate.unit_cost, 0.0)
+        self.assertEqual(to_validate.proposed_qty, 400)
+
+    def test_02_enough_qty_on_high_agr(self):
+        """Test that we can source a line correctly on both agreement"""
+        cr, uid = self.cr, self.uid
+        lines = self.requisition.line_ids
+        agr_line = None
+        for line in lines:
+            if line.product_id == self.cheap_on_high_agreement.product_id:
+                agr_line = line
+                break
+        self.assertTrue(agr_line)
+        agr_line.write({'requested_qty': 1500})
+        agr_line.refresh()
+        to_validate_ids = self.requisition_line_model._generate_source_line(cr, uid, agr_line)
+        self.assertTrue(len(to_validate_ids) == 1)
+        to_validate = self.source_line_model.browse(cr, uid, to_validate_ids[0])
+        self.assertEqual(to_validate.procurement_method, AGR_PROC)
+        self.assertEqual(to_validate.unit_cost, 0.0)
+        self.assertEqual(to_validate.proposed_qty, 1500)
+
+    def test_03_not_enough_qty_on_high_agreement(self):
+        """Test that we can source a line with one agreement and high qty"""
+        cr, uid = self.cr, self.uid
+        lines = self.requisition.line_ids
+        agr_line = None
+        for line in lines:
+            if line.product_id == self.cheap_on_high_agreement.product_id:
+                agr_line = line
+                break
+        self.assertTrue(agr_line)
+        agr_line.write({'requested_qty': 2400})
+        agr_line.refresh()
+        to_validate_ids = self.requisition_line_model._generate_source_line(cr, uid, agr_line)
+        self.assertTrue(len(to_validate_ids) == 2)
+        # We validate generated line
+        to_validates = self.source_line_model.browse(cr, uid, to_validate_ids)
+        # high_line
+        # idiom taken from Python cookbook
+        high_line = next((x for x in to_validates
+                              if x.framework_agreement_id == self.cheap_on_high_agreement), None)
+        self.assertTrue(high_line, msg="High agreement was not used")
+        self.assertEqual(high_line.procurement_method, AGR_PROC)
+        self.assertEqual(high_line.proposed_qty, 2000)
+        self.assertEqual(high_line.unit_cost, 0.0)
+
+        # low_line
+        low_line = next((x for x in to_validates
+                              if x.framework_agreement_id == self.cheap_on_low_agreement), None)
+        self.assertTrue(low_line, msg="Low agreement was not used")
+        self.assertEqual(low_line.procurement_method, AGR_PROC)
+        self.assertEqual(low_line.proposed_qty, 400)
+        self.assertEqual(low_line.unit_cost, 0.0)
+
+    def test_03_not_enough_qty_on_all_agreemenst(self):
+        """Test that we """
+        cr, uid = self.cr, self.uid
+        lines = self.requisition.line_ids
+        agr_line = None
+        for line in lines:
+            if line.product_id == self.cheap_on_high_agreement.product_id:
+                agr_line = line
+                break
+        self.assertTrue(agr_line)
+        agr_line.write({'requested_qty': 5000})
+        agr_line.refresh()
+        to_validate_ids = self.requisition_line_model._generate_source_line(cr, uid, agr_line)
+        self.assertTrue(len(to_validate_ids) == 3)
+        # We validate generated line
+        to_validates = self.source_line_model.browse(cr, uid, to_validate_ids)
+        # high_line
+        # idiom taken from Python cookbook
+        high_line = next((x for x in to_validates
+                              if x.framework_agreement_id == self.cheap_on_high_agreement), None)
+        self.assertTrue(high_line, msg="High agreement was not used")
+        self.assertEqual(high_line.procurement_method, AGR_PROC)
+        self.assertEqual(high_line.proposed_qty, 2000)
+        self.assertEqual(high_line.unit_cost, 0.0)
+
+        # low_line
+        low_line = next((x for x in to_validates
+                              if x.framework_agreement_id == self.cheap_on_low_agreement), None)
+        self.assertTrue(low_line, msg="Low agreement was not used")
+        self.assertEqual(low_line.procurement_method, AGR_PROC)
+        self.assertEqual(low_line.proposed_qty, 1200)
+        self.assertEqual(low_line.unit_cost, 0.0)
+
+        # Tender line
+        tender_line = next((x for x in to_validates
+                                if not x.framework_agreement_id), None)
+        self.assertTrue(tender_line, msg="Tender line was not generated")
+        self.assertNotEqual(tender_line.procurement_method, AGR_PROC)

=== added file 'framework_agreement_sourcing/touch'
=== added directory 'framework_agreement_sourcing/view'
=== added file 'framework_agreement_sourcing/view/requisition_view.xml'
--- framework_agreement_sourcing/view/requisition_view.xml	1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/view/requisition_view.xml	2013-12-05 10:14:17 +0000
@@ -0,0 +1,147 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+  <data>
+    <record id="add_supplier_and_agreement_on_source_line" model="ir.ui.view">
+      <field name="name">add supplier and agrement on source line</field>
+      <field name="model">logistic.requisition.source</field>
+      <field name="inherit_id" ref="logistic_requisition.view_logistic_requisition_source_form"/>
+      <field name="arch" type="xml">
+        <field name="unit_cost"
+               position="before">
+          <field name="framework_agreement_id"
+                 domain="[('draft', '=', False)]"
+                 attrs="{'required': [('procurement_method', '=', 'fw_agreement')],
+                        'invisible': [('procurement_method', '!=', 'fw_agreement')]}"
+                 on_change="onchange_agreement(framework_agreement_id, requisition_line_id, proposed_qty, proposed_product_id, purchase_pricelist_id, context)"/>/>
+          <field name="pricelist_id"
+                 invisible="1"/>
+          <field name="purchase_pricelist_id"
+                 attrs="{'required': [('procurement_method', '=', 'fw_agreement')],
+                        'invisible': [('procurement_method', '!=', 'fw_agreement')]}"
+                 on_change="onchange_pricelist(framework_agreement_id, requisition_line_id, proposed_qty, proposed_product_id, purchase_pricelist_id, context)"
+                 domain="[('type', '=', 'purchase')]"/>
+
+          <field name="supplier_id"
+                 invisible="1"/>
+        </field>
+        <field name="proposed_uom_id"
+               position="attributes">
+            <attribute name="attrs">{'invisible': [('procurement_method', '=', 'fw_agreement')]}</attribute>
+          </field>
+          <field name="unit_cost"
+               position="attributes">
+            <attribute name="attrs">{'invisible': [('procurement_method', '=', 'fw_agreement')]}</attribute>
+          </field>
+          <field name="total_cost"
+               position="attributes">
+            <attribute name="attrs">{'invisible': [('procurement_method', '=', 'fw_agreement')]}</attribute>
+          </field>
+          <field name="price_is"
+               position="attributes">
+            <attribute name="attrs">{'invisible': [('procurement_method', '=', 'fw_agreement')]}</attribute>
+          </field>
+      </field>
+    </record>
+
+    <record id="add_hide_button_create_bids" model="ir.ui.view">
+      <field name="name">hidde button</field>
+      <field name="model">logistic.requisition.source</field>
+      <field name="inherit_id" ref="logistic_requisition.view_logistic_requisition_source_form"/>
+      <field name="priority" eval="10"/>
+      <field name="arch" type="xml">
+          <button position="replace">
+          </button>
+      </field>
+    </record>
+
+
+    <record id="add_create_po_button" model="ir.ui.view">
+      <field name="name">add create po button</field>
+      <field name="model">logistic.requisition.source</field>
+      <field name="inherit_id" ref="logistic_requisition.view_logistic_requisition_source_form"/>
+      <field name="arch" type="xml">
+        <sheet position="before">
+          <header>
+          <button name="action_create_agreement_po_requisition"
+                  context="{}"
+                  string="Create Draft PO"
+                  type="object"
+                  attrs="{'invisible': [('procurement_method', '!=', 'fw_agreement')]}"/>
+          <button name="action_create_po_requisition"
+                  string="Call for Bids"
+                  type="object"
+                  attrs="{'invisible': ['|', ('po_requisition_id', '!=', False), ('procurement_method', '!=', 'procurement')]}"
+                  />
+          </header>
+        </sheet>
+      </field>
+    </record>
+
+    <record id="addd_agreement_source_line_onchange" model="ir.ui.view">
+      <field name="name">addd agreement source line onchange</field>
+      <field name="model">logistic.requisition.source</field>
+      <field name="inherit_id" ref="logistic_requisition.view_logistic_requisition_source_form"/>
+      <field name="arch" type="xml">
+        <data>
+          <field name="procurement_method"
+                 position="attributes">
+            <attribute name="on_change">onchange_sourcing_method(procurement_method, requisition_line_id, proposed_product_id, purchase_pricelist_id, proposed_qty)</attribute>
+          </field>
+
+          <field name="proposed_qty"
+                 position="attributes">
+            <attribute name="on_change">onchange_quantity(procurement_method, requisition_line_id, proposed_qty, proposed_product_id, purchase_pricelist_id)</attribute>
+          </field>
+          <field name="proposed_product_id"
+                 position="attributes">
+            <attribute name="on_change">onchange_product_id(procurement_method, requisition_line_id, proposed_product_id, proposed_qty, purchase_pricelist_id)</attribute>
+          </field>
+
+        </data>
+      </field>
+    </record>
+
+
+    <!-- <record id="add_supplier_and_agreement_on_source_line_embeeded" model="ir.ui.view"> -->
+    <!--      <field name="name">add supplier and agrement on source line</field> -->
+    <!--      <field name="model">logistic.requisition.line</field> -->
+    <!--      <field name="inherit_id" ref="logistic_requisition.view_logistic_requisition_line_form"/> -->
+    <!--      <field name="arch" type="xml"> -->
+    <!--        <field name="proposed_qty" position="after"> -->
+    <!--          <div> -->
+    <!--            <field name="framework_agreement_id_dummy" readonly="1"/> -->
+    <!--            <field name="framework_agreement_id" invisible="1"/> -->
+    <!--            <field name="supplier_id" invisible="1"/> -->
+    <!--            </div> -->
+    <!--        </field> -->
+    <!--      </field> -->
+    <!--    </record> -->
+
+    <!-- <record id="addd_agreement_source_line_onchange_embeeded" model="ir.ui.view"> -->
+    <!--   <field name="name">addd agreement source line onchange</field> -->
+    <!--   <field name="model">logistic.requisition.line</field> -->
+    <!--   <field name="inherit_id" ref="logistic_requisition.view_logistic_requisition_line_form"/> -->
+    <!--   <field name="arch" type="xml"> -->
+    <!--     <data> -->
+    <!--       <field name="procurement_method" -->
+    <!--              onchange="onchange_sourcing_method(procurement_method, proposed_product_id, proposed_qty)"> -->
+    <!--       </field> -->
+
+    <!--       <field name="unit_cost" -->
+    <!--              onchange="onchange_price(procurement_method, unit_cost, proposed_product_id, supplier_id, proposed_qty)"> -->
+    <!--       </field> -->
+
+    <!--       <field name="proposed_product_id" -->
+    <!--              onchange="onchange_product_id(procurement_method, unit_cost, proposed_product_id, proposed_qty)"> -->
+    <!--       </field> -->
+    <!--       <field name="proposed_qty" -->
+    <!--              onchange="onchange_product_id(procurement_method, proposed_qty, supplier_id, proposed_product_id)"> -->
+    <!--       </field> -->
+
+    <!--     </data> -->
+    <!--   </field> -->
+    <!-- </record> -->
+
+
+  </data>
+</openerp>

=== modified file 'logistic_requisition/view/logistic_requisition.xml'
--- logistic_requisition/view/logistic_requisition.xml	2013-10-31 09:03:35 +0000
+++ logistic_requisition/view/logistic_requisition.xml	2013-12-05 10:14:17 +0000
@@ -555,7 +555,7 @@
                             </group>
                             <group>
                                 <group string="Purchase Requisition"
-                                    attrs="{'invisible': [('procurement_method', 'not in', ['procurement', 'fw_agreement'])]}"
+                                    attrs="{'invisible': [('procurement_method', '!=', 'procurement')]}"
                                     colspan="4">
                                     <label for="po_requisition_id"/>
                                     <div>
@@ -571,8 +571,8 @@
                                     colspan="4"
                                     attrs="{'invisible': [('procurement_method', '!=', 'wh_dispatch')]}">
                                     <field name="dispatch_location_id"
-                                        on_change="onchange_dispatch_location_id(dispatch_location_id)" 
-                                        attrs="{'required': [('procurement_method', '=', 'wh_dispatch')]}" 
+                                        on_change="onchange_dispatch_location_id(dispatch_location_id)"
+                                        attrs="{'required': [('procurement_method', '=', 'wh_dispatch')]}"
                                         domain="[('usage', '!=', 'view')]"/>
                                     <field name="stock_owner" />
                                 </group>
@@ -624,7 +624,7 @@
         <field name="res_model">logistic.requisition.source</field>
         <field name="view_type">form</field>
         <field name="view_mode">tree,form</field>
-        <field name="context">{"search_default_groupby_procurement_method" : True}</field>
+        <field name="context">{}</field>
         <field name="search_view_id" ref="view_logistic_requisition_source_filter"/>
         <field name="help"></field>
     </record>


Follow ups