← Back to team overview

openerp-community-reviewer team mailing list archive

[Merge] lp:~camptocamp/purchase-wkfl/7.0-add_framework_agreement-nbi into lp:purchase-wkfl

 

Nicolas Bessi - Camptocamp has proposed merging lp:~camptocamp/purchase-wkfl/7.0-add_framework_agreement-nbi into lp:purchase-wkfl.

Requested reviews:
  Purchase Core Editors (purchase-core-editors)

For more details, see:
https://code.launchpad.net/~camptocamp/purchase-wkfl/7.0-add_framework_agreement-nbi/+merge/196681

Add framework agreement
-- 
https://code.launchpad.net/~camptocamp/purchase-wkfl/7.0-add_framework_agreement-nbi/+merge/196681
Your team Purchase Core Editors is requested to review the proposed merge of lp:~camptocamp/purchase-wkfl/7.0-add_framework_agreement-nbi into lp:purchase-wkfl.
=== added directory 'framework_agreement'
=== added file 'framework_agreement/__init__.py'
--- framework_agreement/__init__.py	1970-01-01 00:00:00 +0000
+++ framework_agreement/__init__.py	2013-12-04 14:42:16 +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 model
+from . import utils

=== added file 'framework_agreement/__openerp__.py'
--- framework_agreement/__openerp__.py	1970-01-01 00:00:00 +0000
+++ framework_agreement/__openerp__.py	2013-12-04 14:42:16 +0000
@@ -0,0 +1,67 @@
+# -*- 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': 'Simple Framework Agreement',
+ 'version': '0.1',
+ 'author': 'Camptocamp',
+ 'maintainer': 'Camptocamp',
+ 'category': 'Purchase Management',
+ 'complexity': 'normal',
+ 'depends': ['stock', 'procurement', 'purchase'],
+ 'description': """
+Long Term Agreement (or Framework Agreement) on price.
+======================================================
+
+Agreements are defined by a product, a date range , a supplier, a price, a lead time
+and agreed quantity.
+
+Agreements are set on a product view or using a menu in the product configuration.
+
+There can be only one agreement for the same supplier and product at the same time, even
+if we may have different prices depending on lead time/qty.
+
+There is an option on company to restrict one agreement per product at same time.
+
+If an agreement is running its price will be automatically used in PO.
+A warning will be raised in case of exhaustion of override of agreement price.
+
+**Technical aspect**
+
+The module provide an observalbe mixin to enable generic on_change management on various model
+related to agreements.
+
+The framework agreement is by default related to purchase order but the addon
+provides a library to integrate it with any other model easily
+""",
+ 'website': 'http://www.camptocamp.com',
+ 'data': ['data.xml',
+          'view/product_view.xml',
+          'view/framework_agreement_view.xml',
+          'view/purchase_view.xml',
+          'view/company_view.xml',
+          'security/multicompany.xml',
+          'security/ir.model.access.csv'],
+ 'demo': [],
+ 'test': [],
+ 'installable': True,
+ 'auto_install': False,
+ 'license': 'AGPL-3',
+ 'application': False,
+ }

=== added file 'framework_agreement/data.xml'
--- framework_agreement/data.xml	1970-01-01 00:00:00 +0000
+++ framework_agreement/data.xml	2013-12-04 14:42:16 +0000
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+  <data>
+    <record id="framework_agreement_sequence_type" model="ir.sequence.type">
+      <field name="name">Framework Agreement</field>
+      <field name="code">framework.agreement</field>
+    </record>
+    <record id="seq_mrp_repair" model="ir.sequence">
+      <field name="name">framework.agreement</field>
+      <field name="code">framework.agreement</field>
+      <field name="prefix">LTA</field>
+    </record>
+
+    <record id="framework_agreement_currency_type" model="res.currency.rate.type">
+      <field name="name">Framework Agreement</field>
+    </record>
+
+  </data>
+</openerp>

=== added directory 'framework_agreement/i18n'
=== added directory 'framework_agreement/model'
=== added file 'framework_agreement/model/__init__.py'
--- framework_agreement/model/__init__.py	1970-01-01 00:00:00 +0000
+++ framework_agreement/model/__init__.py	2013-12-04 14:42:16 +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 pricelist
+from . import product
+from . import framework_agreement
+from . import purchase
+from . import company

=== added file 'framework_agreement/model/company.py'
--- framework_agreement/model/company.py	1970-01-01 00:00:00 +0000
+++ framework_agreement/model/company.py	2013-12-04 14:42:16 +0000
@@ -0,0 +1,32 @@
+# -*- 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
+
+
+class res_Company(orm.Model):
+    """Add a field on company"""
+
+    _inherit = "res.company"
+    _columns = {'one_agreement_per_product': fields.boolean('One agreement per product',
+                                                            help='If checked you can have only'
+                                                                 ' one framework agreement '
+                                                                 ' per product at the same time')}
+    # TODO add check on activation deactivation of check box

=== added file 'framework_agreement/model/framework_agreement.py'
--- framework_agreement/model/framework_agreement.py	1970-01-01 00:00:00 +0000
+++ framework_agreement/model/framework_agreement.py	2013-12-04 14:42:16 +0000
@@ -0,0 +1,712 @@
+# -*- 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 operator import attrgetter
+from collections import namedtuple
+from datetime import datetime
+from openerp.osv import orm, fields
+from openerp.osv.orm import except_orm
+from openerp.tools import DEFAULT_SERVER_DATE_FORMAT
+from openerp.tools.translate import _
+import openerp.addons.decimal_precision as dp
+
+AGR_PO_STATE = ('confirmed', 'approved',
+                'done', 'except_picking', 'except_invoice')
+
+
+class framework_agreement(orm.Model):
+    """Long term agreement on product price with a supplier"""
+
+    _name = 'framework.agreement'
+    _description = 'Agreement on price'
+
+    def _check_running_date(self, cr, agreement, context=None):
+        """ Returns agreement state based on date.
+
+        Available qty is ignored in this method
+
+        :param agreement: an agreement record
+
+        :returns: a string - "running" if now is between,
+                           - "future" if agreement is in future,
+                           - "closed" if agreement is outdated
+
+        """
+        now, start, end = self._get_dates(agreement, context=context)
+        if start > now:
+            return 'future'
+        elif end < now:
+            return 'closed'
+        elif start <= now <= end:
+            return 'running'
+        else:
+            raise ValueError('Agreement start/end dates are incorrect')
+
+    def _get_dates(self, agreement, context=None):
+        """Return current time, start date and end date of agreement
+
+        Boiler plate as OpenERP returns string instead of date/time objects...
+
+        :param agreement: agreement record
+
+        :returns: (now, start, end)
+
+        """
+        AGDates = namedtuple('AGDates', ['now', 'start', 'end'])
+        now = datetime.strptime(fields.date.today(),
+                                DEFAULT_SERVER_DATE_FORMAT)
+        start = datetime.strptime(agreement.start_date,
+                                  DEFAULT_SERVER_DATE_FORMAT)
+        end = datetime.strptime(agreement.end_date,
+                                DEFAULT_SERVER_DATE_FORMAT)
+        return AGDates(now, start, end)
+
+    def date_valid(self, cr, uid, agreement_id, date, context=None):
+        """Predicate that checks that date is in agreement
+
+        :param date: date to validate
+
+        :returns: True if date is valid
+
+        """
+
+        if isinstance(agreement_id, (list, tuple)):
+            assert len(agreement_id) == 1
+            agreement_id = agreement_id[0]
+        current = self.browse(cr, uid, agreement_id, context=context)
+        now, start, end = self._get_dates(current, context=context)
+        pdate = datetime.strptime(date,
+                                  DEFAULT_SERVER_DATE_FORMAT)
+        return start <= pdate <= end
+
+    def _get_self(self, cr, uid, ids, context=None):
+        """ Store field function to get current ids
+
+        :returns: list of current ids
+
+        """
+        return ids
+
+    def _compute_state(self, cr, uid, ids, field_name, arg, context=None):
+        """ Compute current state of agreement based on date and consumption
+
+        Please refer to function field documentation for more details.
+
+        """
+        res = {}
+        for agreement in self.browse(cr, uid, ids, context=context):
+            if (agreement.draft or not agreement.start_date or
+                    not agreement.end_date):
+                res[agreement.id] = 'draft'
+                continue
+            dates_state = self._check_running_date(cr, agreement,
+                                                   context=context)
+            if dates_state == 'running':
+                if agreement.available_quantity <= 0:
+                    res[agreement.id] = 'consumed'
+                else:
+                    res[agreement.id] = 'running'
+            else:
+                res[agreement.id] = dates_state
+        return res
+
+    def _search_state(self, cr, uid, obj, name, args, context=None):
+        """Implement search on state function field.
+
+        Only support "and" mode.
+        supported opperators are =, in, not in, <>.
+        For more information please refer to fnct_search OpenERP documentation.
+
+        """
+        if not args:
+            return []
+        ids = self.search(cr, uid, [], context=context)
+        # this can be problematic in term of performace but the
+        # state field can be changed by values and time evolution
+        # In a business point of view there should be around 30 yearly LTA
+
+        found_ids = []
+        res = self.read(cr, uid, ids, ['state'], context=context)
+        for field, operator, value in args:
+            assert field == name
+            if operator == '=':
+                found_ids += [frm['id'] for frm in res if frm['state'] in value]
+            elif operator == 'in' and isinstance(value, list):
+                found_ids += [frm['id'] for frm in res if frm['state'] in value]
+            elif operator in ("!=", "<>"):
+                found_ids += [frm['id'] for frm in res if frm['state'] != value]
+            elif operator == 'not in'and isinstance(value, list):
+                found_ids += [frm['id'] for frm in res if frm['state'] not in value]
+            else:
+                raise NotImplementedError('Search operator %s not implemented'
+                                          ' for value %s'
+                                          % (operator, value))
+        to_return = set(found_ids)
+        return [('id', 'in', [x['id'] for x in to_return])]
+
+    def _compute_available_qty(self, cr, uid, ids, field_name, arg,
+                               context=None):
+        """Compute available qty of current agreements.
+
+        Consumption is based on confirmed po lines.
+        Please refer to function field documentation for more details.
+
+        """
+        company_id = self._company_get(cr, uid, context=None)
+        res = {}
+        for agreement in self.browse(cr, uid, ids, context=context):
+            sql = """SELECT SUM(po_line.product_qty) FROM purchase_order_line AS po_line
+            LEFT JOIN purchase_order AS po ON po_line.order_id = po.id
+            WHERE po_line.framework_agreement_id = %s
+            AND po.partner_id = %s
+            AND po.state IN %s
+            AND po.company_id = %s"""
+            cr.execute(sql, (agreement.id,
+                             agreement.supplier_id.id,
+                             AGR_PO_STATE,
+                             company_id))
+            amount = cr.fetchone()[0]
+            if amount is None:
+                amount = 0
+            res[agreement.id] = agreement.quantity - amount
+        return res
+
+    def _get_available_qty(self, cr, uid, ids, field_name, arg, context=None):
+        """Compute available qty of current agreements.
+
+        Consumption is based on confirmed po lines.
+        Please refer to function field documentation for more details.
+
+        """
+        return self._compute_available_qty(cr, uid, ids, field_name, arg,
+                                           context=context)
+
+    def _get_state(self, cr, uid, ids, field_name, arg, context=None):
+        """ Compute current state of agreement based on date and consumption
+
+        Please refer to function field documentation for more details.
+
+        """
+        return self._compute_state(cr, uid, ids, field_name, arg,
+                                   context=context)
+
+    def open_agreement(self, cr, uid, ids, context=None):
+        """Open agreement
+
+        Agreement goes from state draft to X
+
+        """
+        if isinstance(ids, (int, long)):
+            ids = [ids]
+            import pdb; pdb.set_trace()
+            for agr in self.browse(cr, uid, ids, context=context):
+                mandatory = [agr.start_date,
+                             agr.end_date,
+                             agr.framework_agreement_pricelist_id]
+                if not all(mandatory):
+                    raise orm.except_orm(_('Data are missing'),
+                                         _('Please enter dates'
+                                           ' and price informations'))
+        self.write(cr, uid, ids, {'draft': False}, context=context)
+
+    def _get_po_store(self, cr, uid, ids, context=None):
+        res = set()
+        po_obj = self.pool.get('purchase.order')
+        for row in po_obj.browse(cr, uid, ids, context=context):
+            if row.framework_agreement_id:
+                res.update([row.framework_agreement_id.id])
+        return res
+
+    def _get_po_line_store(self, cr, uid, ids, context=None):
+        # TODO DRY with _get_po_store
+        res = set()
+        pol_obj = self.pool.get('purchase.order.line')
+        for row in pol_obj.browse(cr, uid, ids, context=context):
+            if row.framework_agreement_id:
+                res.update([row.framework_agreement_id.id])
+        return res
+
+    _store_tuple = (lambda self, cr, uid, ids, c={}: ids, ['quantity'], 10)
+    _po_store_tuple = (_get_po_store, ['framework_agreement_id', 'state'], 20)
+    _po_line_store_tuple = (_get_po_line_store, [], 20)
+
+    _columns = {'name': fields.char('Number',
+                                    required=True,
+                                    readonly=True),
+                'supplier_id': fields.many2one('res.partner',
+                                               'Supplier',
+                                               required=True),
+                'product_id': fields.many2one('product.product',
+                                              'Product',
+                                              required=True),
+                'origin': fields.char('Origin'),
+                'start_date': fields.date('Begin of Agreement'),
+                'end_date': fields.date('End of Agreement'),
+                'delay': fields.integer('Lead time in days'),
+                'quantity': fields.integer('Negociated quantity',
+                                           required=True),
+                'framework_agreement_pricelist_ids': fields.one2many('framework.agreement.pricelist',
+                                                                     'framework_agreement_id',
+                                                                     'Price lists'),
+                'available_quantity': fields.function(_get_available_qty,
+                                                      type='integer',
+                                                      string='Available quantity',
+                                                      readonly=True,
+                                                      store={'framework.agreement': _store_tuple,
+                                                             'purchase.order': _po_store_tuple,
+                                                             'purchase.order.line': _po_line_store_tuple}),
+                'state': fields.function(_get_state,
+                                         fnct_search=_search_state,
+                                         string='state',
+                                         type='selection',
+                                         selection=[('draft', 'Draft'),
+                                                    ('future', 'Future'),
+                                                    ('running', 'Running'),
+                                                    ('consumed', 'Consumed'),
+                                                    ('closed', 'Closed')],
+                                         readonly=True),
+                'company_id': fields.many2one('res.company',
+                                              'Company'),
+                'draft': fields.boolean('Is draft'),
+                }
+
+    def _sequence_get(self, cr, uid, context=None):
+        return self.pool['ir.sequence'].get(cr, uid, 'framework.agreement')
+
+    def _company_get(self, cr, uid, context=None):
+        return self.pool['res.company']._company_default_get(cr, uid,
+                                                             'framework.agreement',
+                                                             context=context)
+
+    def _check_overlap(self, cr, uid, ids, context=None):
+        """Constraint to check that no agreements for same product/supplier overlap.
+
+        One agreement per product limit is checked if one_agreement_per_product
+        is set to True on company
+
+        """
+        comp_obj = self.pool['res.company']
+        company_id = self._company_get(cr, uid, context=context)
+        strict = comp_obj.read(cr, uid, company_id,
+                               ['one_agreement_per_product'],
+                               context=context)['one_agreement_per_product']
+        for agreement in self.browse(cr, uid, ids, context=context):
+            # we do not add current id in domain for readability reasons
+            overlap = self.search(cr, uid,
+                                  ['&',
+                                      ('draft', '=', False),
+                                      ('product_id', '=', agreement.product_id.id),
+                                      '|',
+                                         '&',
+                                            ('start_date', '>=', agreement.start_date),
+                                            ('start_date', '<=', agreement.end_date),
+                                         '&',
+                                            ('end_date', '>=', agreement.start_date),
+                                            ('end_date', '<=', agreement.end_date),
+                                   ])
+            # we also look for the one that includes current offer
+            overlap += self.search(cr, uid, [('start_date', '<=', agreement.start_date),
+                                             ('end_date', '>=', agreement.end_date),
+                                             ('id', '!=', agreement.id),
+                                             ('product_id', '=', agreement.product_id.id)])
+            overlap = self.browse(cr, uid,
+                                  [x for x in overlap if x != agreement.id],
+                                  context=context)
+            # we ensure that there is only one agreement at time per product
+            # if strict agreement is set on company
+            if strict and overlap:
+                return False
+            # We ensure that there are not multiple agreements for same supplier at same time
+            if any((x.supplier_id.id == agreement.supplier_id.id) for x in overlap):
+                return False
+        return True
+
+    def check_overlap(self, cr, uid, ids, context=None):
+        """Constraint to check that no agreements for same product/supplier overlap.
+
+        One agreement per product limit is checked if one_agreement_per_product
+        is set to True on company
+
+        """
+        return self._check_overlap(cr, uid, ids, context=context)
+
+    _defaults = {'name': _sequence_get,
+                 'company_id': _company_get,
+                 'draft': True}
+
+    _sql_constraints = [('date_priority',
+                         'check(start_date < end_date)',
+                         'Start/end date inversion')]
+
+    _constraints = [(check_overlap,
+                     "You can not have overlapping dates for same supplier and product",
+                     ('start_date', 'end_date'))]
+
+    def get_all_product_agreements(self, cr, uid, product_id, lookup_dt, qty=None, context=None):
+        """Get the all the active agreement of a given product at a given date
+
+        :param product_id: product id of the product
+        :param lookup_dt: date string of the lookup date
+        :param qty: quantity that should be available if parameter is
+                    passed and qty is insuffisant no agreement would be returned
+
+        :returns: a list of corresponding agreements or None
+
+        """
+        search_args = [('product_id', '=', product_id),
+                       ('start_date', '<=', lookup_dt),
+                       ('end_date', '>=', lookup_dt),
+                       ('draft', '=', False)]
+        if qty:
+            search_args.append(('available_quantity', '>=', qty))
+        agreement_ids = self.search(cr, uid, search_args)
+        if agreement_ids:
+            return self.browse(cr, uid, agreement_ids, context=context)
+        return None
+
+    def get_cheapest_agreement_for_qty(self, cr, uid, product_id, date, qty,
+                                       currency=None, context=None):
+        """Return the cheapest agreement that has enough available qty.
+
+        If not enough quantity fallback on the cheapest agreement available
+        for quantity.
+
+        :param product_id:
+        :param date:
+        :param qty:
+        :param currency: currency record to make price convertion
+
+        returns (cheapest agreement, enough qty)
+
+        """
+        Cheapest = namedtuple('Cheapest', ['cheapest_agreement', 'enough'])
+        agreements = self.get_all_product_agreements(cr, uid, product_id,
+                                                     date, qty, context=context)
+        if not agreements:
+            return Cheapest(None, None)
+        agreements.sort(key=lambda x: x.get_price(qty, currency=currency))
+        enough = True
+        cheapest_agreement = None
+        for agr in agreements:
+            if agr.available_quantity >= qty:
+                cheapest_agreement = agr
+                break
+        if not cheapest_agreement:
+            cheapest_agreement = agreements[0]
+            enough = False
+        return Cheapest(cheapest_agreement, enough)
+
+    def get_product_agreement(self, cr, uid, product_id, supplier_id,
+                              lookup_dt, qty=None, context=None):
+        """Get the matching agreement for a given product/supplier at date
+        :param product_id: product id of the product
+        :param supplier_id: supplier to look for agreement
+        :param lookup_dt: date string of the lookup date
+        :param qty: quantity that should be available if parameter is
+        passed and qty is insuffisant no aggrement would be returned
+
+        :returns: a corresponding agreement or None
+
+        """
+        search_args = [('product_id', '=', product_id),
+                       ('supplier_id', '=', supplier_id),
+                       ('start_date', '<=', lookup_dt),
+                       ('end_date', '>=', lookup_dt),
+                       ('draft', '=', False)]
+        if qty:
+            search_args.append(('available_quantity', '>=', qty))
+        agreement_ids = self.search(cr, uid, search_args)
+        if len(agreement_ids) > 1:
+            raise except_orm(_('Many agreements found for the product with id %s'
+                               ' at date %s') % (product_id, lookup_dt),
+                             _('Please contact your ERP administrator'))
+        if agreement_ids:
+            agreement = self.browse(cr, uid, agreement_ids[0], context=context)
+            return agreement
+        return None
+
+    def has_currency(self, cr, uid, agr_id, currency, context=None):
+        """Predicate that check that agreement has a given currency pricelist
+
+        :returns: boolean (True if a price list in given currency is present)
+
+        """
+        if isinstance(agr_id, (list, tuple)):
+            assert len(agr_id) == 1
+            agr_id = agr_id[0]
+        agreement = self.browse(cr, uid, agr_id, context=context)
+        plists = agreement.framework_agreement_pricelist_ids
+        return any(x for x in plists if x.currency_id == currency)
+
+    def _get_pricelist_lines(self, cr, uid, agreement,
+                             currency, context=None):
+        plists = agreement.framework_agreement_pricelist_ids
+        # we do not use has_agreement for performance reason
+        # Python cookbook idiom
+        plist = next((x for x in plists if x.currency_id == currency), None)
+        if not plist:
+            raise orm.except_orm(_('Missing Agreement price list'),
+                                 _('Please set a price list in currency %s for agreement %s') %
+                                 (currency.name, agreement.name))
+        return plist.framework_agreement_line_ids
+
+    def get_price(self, cr, uid, agreement_id, qty=0,
+                  currency=None, context=None):
+        """Return price negociated for quantity
+
+        :returns: price float
+
+        """
+        if isinstance(agreement_id, list):
+            assert len(agreement_id) == 1
+            agreement_id = agreement_id[0]
+        current = self.browse(cr, uid, agreement_id, context=context)
+        if not currency:
+            comp_obj = self.pool['res.company']
+            comp_id = self._company_get(cr, uid, context=context)
+            currency = comp_obj.browse(cr, uid, comp_id, context=context).currency_id
+        lines = self._get_pricelist_lines(cr, uid, current, currency,
+                                          context=context)
+        lines.sort(key=attrgetter('quantity'), reverse=True)
+        for line in lines:
+            if qty >= line.quantity:
+                return line.price
+        return lines[-1].price
+
+    def _get_currency(self, cr, uid, supplier_id, pricelist_id, context=None):
+        """Helper to retrieve correct currency.
+
+        It will look for currency on supplied pricelist if availwichable
+        else it will look for partner pricelist currency
+
+        :param supplier_id: supplier of agreement
+        :param pricelist_id: primary price list
+
+        :returns: currency browse record
+
+        """
+
+        plist_obj = self.pool['product.pricelist']
+        partner_obj = self.pool['res.partner']
+        if pricelist_id:
+            plist = plist_obj.browse(cr, uid, pricelist_id, context=context)
+            return plist.currency_id
+        partner = partner_obj.browse(cr, uid, supplier_id, context=context)
+        if not partner.property_product_pricelist_purchase:
+            raise orm.except_orm(_('No pricelist found'),
+                                 _('Please set a pricelist on PO or supplier %s') % partner.name)
+        return partner.property_product_pricelist_purchase.currency_id
+
+
+class framework_agreement_pricelist(orm.Model):
+    """Price list container"""
+
+    _name = "framework.agreement.pricelist"
+    _rec_name = 'currency_id'
+    _columns = {'framework_agreement_id': fields.many2one('framework.agreement',
+                                                          'Agreement',
+                                                          required=True),
+                'currency_id': fields.many2one('res.currency',
+                                               'Currency',
+                                               required=True),
+                'framework_agreement_line_ids': fields.one2many('framework.agreement.line',
+                                                                'framework_agreement_pricelist_id',
+                                                                'Price lines',
+                                                                required=True)}
+
+
+class framework_agreement_line(orm.Model):
+    """Price list line of framework agreement
+    that contains price and qty"""
+
+    _name = 'framework.agreement.line'
+    _description = 'Framework agreement line'
+    _rec_name = "quantity"
+    _order = "quantity"
+
+    _columns = {'framework_agreement_pricelist_id': fields.many2one('framework.agreement.pricelist',
+                                                                    'Price list',
+                                                                    required=True),
+                'quantity': fields.integer('Quantity',
+                                           required=True),
+
+                'price': fields.float('Price', 'Negociated price',
+                                      required=True,
+                                      digits_compute=dp.get_precision('Product Price'))}
+
+
+class FrameworkAgreementObservable(object):
+    """Base functions for model that have to be (pseudo) observable
+    by framework agreement using OpenERP on_change mechanism"""
+
+    def _currency_get(self, cr, uid, pricelist_id, context=None):
+        return self.pool['product.pricelist'].browse(cr, uid,
+                                                     pricelist_id,
+                                                     context=context).currency_id
+
+    def onchange_price_obs(self, cr, uid, ids, price, agreement_id,
+                           currency=None, qty=0, context=None):
+        """Raise a warning if a agreed price is changed on observed object"""
+        if context is None:
+            context = {}
+        if not agreement_id or context.get('no_chained'):
+            return {}
+        agr_obj = self.pool['framework.agreement']
+        agreement = agr_obj.browse(cr, uid, agreement_id, context=context)
+        if agreement.get_price(qty, currency=currency) != price:
+            msg = _("You have set the price to %s \n"
+                    " but there is a running agreement"
+                    " with price %s") % (price, agreement.get_price(qty, currency=currency))
+            return {'warning': {'title': _('Agreement Warning!'),
+                                'message': msg}}
+        return {}
+
+    def onchange_quantity_obs(self, cr, uid, ids, qty, date,
+                              product_id, currency=None,
+                              supplier_id=None,
+                              price_field='price', context=None):
+        """Raise a warning if agreed qty is not sufficient when changed on observed object
+
+        :param qty: requested quantity
+        :param currency: currency to get price
+        :param price field: key on which we should return price
+
+        :returns: on change dict
+
+        """
+        res = {'value': {'framework_agreement_id': False}}
+        agreement, status = self._get_agreement_and_qty_status(cr, uid, ids, qty, date,
+                                                               product_id,
+                                                               supplier_id=supplier_id,
+                                                               currency=currency,
+                                                               context=context)
+        if agreement:
+            res['value'] = {price_field: agreement.get_price(qty, currency=currency),
+                            'framework_agreement_id': agreement.id}
+        if status:
+            res['warning'] = {'title': _('Agreement Warning!'),
+                              'message': status}
+        return res
+
+    def _get_agreement_and_qty_status(self, cr, uid, ids, qty, date,
+                                      product_id, supplier_id,
+                                      currency=None, context=None):
+        """Lookup for agreement and return (matching_agreement, status)
+
+        Agreement or status can be None.
+
+        :param qty: requested quantity
+        :param date: date to look for agreement
+        :param supplier_id: supplier id who has signed an agreement
+        :param product_id: product id to look for an agreement
+        :param price field: key on which we should return price
+
+        :returns: (agreement record, status)
+
+        """
+        FoundAgreement = namedtuple('FoundAgreement', ['Agreement', 'message'])
+        agreement_obj = self.pool['framework.agreement']
+        if supplier_id:
+            agreement = agreement_obj.get_product_agreement(cr, uid, product_id,
+                                                            supplier_id, date,
+                                                            context=context)
+        else:
+            agreement, enough = agreement_obj.get_cheapest_agreement_for_qty(cr,
+                                                                             uid,
+                                                                             product_id,
+                                                                             date,
+                                                                             qty,
+                                                                             currency=currency,
+                                                                             context=context)
+        if agreement is None:
+            return FoundAgreement(None, None)
+        msg = None
+        if agreement.available_quantity < qty:
+            msg = _("You have ask for a quantity of %s \n"
+                    " but there is only %s available"
+                    " for current agreement") % (qty, agreement.available_quantity)
+        return FoundAgreement(agreement, msg)
+
+    def onchange_product_id_obs(self, cr, uid, ids, qty, date,
+                                supplier_id, product_id, pricelist_id=None,
+                                currency=None, price_field='price', context=None):
+        """
+        Lookup for agreement corresponding to product or return None.
+
+        It will raise a warning if not enough available qty.
+
+        :param qty: requested quantity
+        :param date: date to look for agreement
+        :param supplier_id: supplier id who has signed an agreement
+        :param pricelist_id: if of prefered pricelist
+        :param product_id: product id to look for an agreement
+        :param price field: key on which we should return price
+
+        :returns: on change dict
+
+        """
+        if context is None:
+            context = {}
+        res = {'value': {'framework_agreement_id': False}}
+        if not supplier_id or not product_id:
+            return res
+        agreement, status = self._get_agreement_and_qty_status(cr, uid, ids, qty, date,
+                                                               product_id,
+                                                               supplier_id=supplier_id,
+                                                               currency=currency,
+                                                               context=context)
+        # agr_obj = self.pool['framework.agreement']
+        # currency = agr_obj._get_currency(cr, uid, supplier_id,
+        #                                  pricelist_id, context=context)
+        if agreement:
+            res['value'] = {price_field: agreement.get_price(qty, currency=currency),
+                            'framework_agreement_id': agreement.id}
+        if status:
+            res['warning'] = {'title': _('Agreement Warning!'),
+                              'message': status}
+        if not agreement:
+            context['no_chained'] = True
+        return res
+
+    def onchange_agreement_obs(self, cr, uid, ids, agreement_id, qty, date, product_id,
+                               supplier_id=None, currency=None, price_field='price',
+                               context=None):
+        res = {}
+        if not agreement_id or not product_id:
+            return res
+        agr_obj = self.pool['framework.agreement']
+        agreement = agr_obj.browse(cr, uid, agreement_id, context=context)
+        if not agreement.date_valid(date, context=context):
+            raise orm.except_orm(_('Invalid date'),
+                                 _('Agreement and purchase date does not match'))
+        if agreement.product_id.id != product_id:
+            raise orm.except_orm(_('User Error'),
+                                 _('Wrong product for choosen agreement'))
+        if supplier_id and agreement.supplier_id.id != supplier_id:
+            raise orm.except_orm(_('User Error'),
+                                 _('Wrong supplier for choosen agreement'))
+        res['value'] = {price_field: agreement.get_price(qty, currency=currency)}
+        if qty and agreement.available_quantity < qty:
+            msg = _("You have ask for a quantity of %s \n"
+                    " but there is only %s available"
+                    " for current agreement") % (qty, agreement.available_quantity)
+            res['warning'] = {'title': _('Agreement Warning!'),
+                              'message': msg}
+        return res

=== added file 'framework_agreement/model/pricelist.py'
--- framework_agreement/model/pricelist.py	1970-01-01 00:00:00 +0000
+++ framework_agreement/model/pricelist.py	2013-12-04 14:42:16 +0000
@@ -0,0 +1,80 @@
+# -*- 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 datetime
+from openerp.tools import DEFAULT_SERVER_DATE_FORMAT
+from openerp.osv import orm, fields
+
+
+class product_pricelist(orm.Model):
+    """Add framework agreement behavior on pricelist"""
+
+    _inherit = "product.pricelist"
+
+    def _plist_is_agreement(self, cr, uid, pricelist_id, context=None):
+        """Check that a price list can be subject to agreement.
+
+        :param pricelist_id: the price list to be validated
+
+        :returns: a boolean (True if agreement is applicable)
+
+        """
+        p_list = self.browse(cr, uid, pricelist_id, context=context)
+        if p_list.type == 'purchase':
+            return True
+        return False
+
+    def price_get(self, cr, uid, ids, prod_id, qty, partner=None, context=None):
+        """Override of price retrival function in order to support framework agreement.
+
+        If it is a supplier price list agrreement will be taken in account
+        and use the price of the agreement if required.
+
+        If there is not enough available qty on agreement, standard price will be used.
+
+        This is mabye a faulty design and we should use on_change override
+
+        """
+        if context is None:
+            context = {}
+        agreement_obj = self.pool['framework.agreement']
+        res = super(product_pricelist, self).price_get(cr, uid, ids, prod_id, qty,
+                                                       partner=partner, context=context)
+        if not partner:
+            return res
+        for pricelist_id in res:
+            if (pricelist_id == 'item_id' or not
+                    self._plist_is_agreement(cr, uid, pricelist_id, context=context)):
+                continue
+            now = datetime.strptime(fields.date.today(),
+                                    DEFAULT_SERVER_DATE_FORMAT)
+            date = context.get('date') or context.get('date_order') or now
+            if context.get('from_agreement_id'):
+                agreement = agreement_obj.browse(cr, uid, context['from_agreement_id'],
+                                                 context=context)
+            else:
+                agreement = agreement_obj.get_product_agreement(cr, uid, prod_id,
+                                                                partner, date,
+                                                                qty=qty, context=context)
+            if agreement is not None:
+                currency = agreement_obj._get_currency(cr, uid, partner, pricelist_id,
+                                                       context=context)
+                res[pricelist_id] = agreement.get_price(qty, currency=currency)
+        return res

=== added file 'framework_agreement/model/product.py'
--- framework_agreement/model/product.py	1970-01-01 00:00:00 +0000
+++ framework_agreement/model/product.py	2013-12-04 14:42:16 +0000
@@ -0,0 +1,40 @@
+# -*- 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
+
+
+class product_product(orm.Model):
+    """Add relation to framework agreement"""
+
+    _inherit = "product.product"
+    _columns = {'framework_agreement_ids': fields.one2many('framework.agreement',
+                                                           'product_id',
+                                                           'Framework Agreements (LTA)')
+                }
+
+    def copy(self, cr, uid, id, default=None, context=None):
+        """Override of copy in order not to copy agreements"""
+        if not default:
+            default = {}
+        default['framework_agreement_ids'] = False
+        return super(product_product, self).copy(cr, uid, id,
+                                                 default=default,
+                                                 context=context)

=== added file 'framework_agreement/model/purchase.py'
--- framework_agreement/model/purchase.py	1970-01-01 00:00:00 +0000
+++ framework_agreement/model/purchase.py	2013-12-04 14:42:16 +0000
@@ -0,0 +1,146 @@
+# -*- 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
+
+
+class purchase_order_line(orm.Model, FrameworkAgreementObservable):
+    """Add on change on price to raise a warning if line is subject to
+    an agreement"""
+
+    _inherit = "purchase.order.line"
+
+    _columns = {'framework_agreement_id': fields.related('order_id',
+                                                         'framework_agreement_id',
+                                                         type='many2one',
+                                                         readonly=True,
+                                                         store=True,
+                                                         relation='framework.agreement',
+                                                         string='Agreement')}
+
+    def onchange_price(self, cr, uid, ids, price, agreement_id, qty, pricelist_id,
+                       product_id, context=None):
+        """Raise a warning if a agreed price is changed"""
+        if not product_id or not agreement_id:
+            return {}
+        currency = self._currency_get(cr, uid, pricelist_id, context=context)
+        product = self.pool['product.product'].browse(cr, uid, product_id, context=context)
+        if product.type != 'product':
+            return {}
+        return self.onchange_price_obs(cr, uid, ids, price, agreement_id, currency=currency,
+                                       qty=qty, context=None)
+
+    def onchange_product_id(self, cr, uid, ids, pricelist_id, product_id, qty, uom_id,
+                            partner_id, date_order=False, fiscal_position_id=False,
+                            date_planned=False, name=False, price_unit=False,
+                            context=None, agreement_id=False, **kwargs):
+        """ We override this function to check qty change (I know...)
+
+        The price retrieval is managed by the override of product.pricelist.price_get
+        that is overidden to support agreement.
+        This is mabye a faulty design as it has a low level impact
+
+        """
+        # rock n'roll
+        if context is None:
+            context = {}
+        if agreement_id:
+            context['from_agreement_id'] = agreement_id
+        res = super(purchase_order_line, self).onchange_product_id(
+                cr, uid, ids, pricelist_id, product_id, qty, uom_id,
+                partner_id, date_order=date_order, fiscal_position_id=fiscal_position_id,
+                date_planned=date_planned, name=name, price_unit=price_unit, context=context, **kwargs)
+        if not product_id or not agreement_id:
+            return res
+        product = self.pool['product.product'].browse(cr, uid, product_id, context=context)
+        if product.type == 'product' and agreement_id:
+            agreement = self.pool['framework.agreement'].browse(cr, uid,
+                                                                agreement_id,
+                                                                context=context)
+            if agreement.product_id.id != product_id:
+                return {'warning':  _('Product not in agreement')}
+            currency = self._currency_get(cr, uid, pricelist_id, context=context)
+            res['value']['price_unit'] = agreement.get_price(qty, currency=currency)
+        return res
+
+
+class purchase_order(orm.Model):
+    """Oveeride on change to raise warning"""
+
+    _inherit = "purchase.order"
+
+    _columns = {'framework_agreement_id': fields.many2one('framework.agreement',
+                                                          'Agreement')}
+
+    def onchange_agreement(self, cr, uid, ids, agreement_id, partner_id, date, context=None):
+        res = {}
+        agr_obj = self.pool['framework.agreement']
+        if agreement_id:
+            agreement = agr_obj.browse(cr, uid, agreement_id, context=context)
+            if not agreement.date_valid(date, context=context):
+                raise orm.except_orm(_('Invalid date'),
+                                     _('Agreement and purchase date does not match'))
+            if agreement.supplier_id.id != partner_id:
+                raise orm.except_orm(_('Invalid agreement'),
+                                     _('Agreement and supplier does not match'))
+
+        warning = {'title': _('Agreement Warning!'),
+                   'message': _('If you change the agreement of this order'
+                                ' (and eventually the currency),'
+                                ' existing order lines will not be updated.')}
+        res['warning'] = warning
+        return res
+
+    def onchange_pricelist(self, cr, uid, ids, pricelist_id, context=None):
+        res = super(purchase_order, self).onchange_pricelist(cr, uid, ids, pricelist_id,
+                                                             context=context)
+        if not pricelist_id:
+            return res
+
+
+        warning = {'title': _('Pricelist Warning!'),
+                   'message': _('If you change the pricelist of this order'
+                                ' (and eventually the currency),'
+                                ' prices of existing order lines will not be updated.')}
+        res['warning'] = warning
+        return res
+
+    def _date_valid(self, cr, uid, agreement_id, date, context=None):
+        """predicate that check that date of invoice is in agreement"""
+        agr_model = self.pool['framework.agreement']
+        return agr_model.date_valid(cr, uid, agreement_id, date, context=context)
+
+    def onchange_date(self, cr, uid, ids, agreement_id, date, context=None):
+        """Check that date is in agreement"""
+        if agreement_id and not self._date_valid(cr, uid, agreement_id, date, context=context):
+            raise orm.except_orm(_('Invalid date'),
+                                 _('Agreement and purchase date does not match'))
+        return {}
+
+    # no context in original def...
+    def onchange_partner_id(self, cr, uid, ids, partner_id, agreement_id):
+        """Override to ensure that partner can not be changed if agreement"""
+        res = super(purchase_order, self).onchange_partner_id(cr, uid, ids, partner_id)
+        if agreement_id:
+            raise orm.except_orm(_('You can not change supplier'),
+                                 _('PO is linked to an agreement'))
+        return res

=== added directory 'framework_agreement/security'
=== added file 'framework_agreement/security/ir.model.access.csv'
--- framework_agreement/security/ir.model.access.csv	1970-01-01 00:00:00 +0000
+++ framework_agreement/security/ir.model.access.csv	2013-12-04 14:42:16 +0000
@@ -0,0 +1,3 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_agreement,framework.agreement.user,model_framework_agreement,purchase.group_purchase_user,1,0,0,0
+access_agreement,framework.agreement.user,model_framework_agreement,purchase.group_purchase_manager,1,1,1,1
\ No newline at end of file

=== added file 'framework_agreement/security/multicompany.xml'
--- framework_agreement/security/multicompany.xml	1970-01-01 00:00:00 +0000
+++ framework_agreement/security/multicompany.xml	2013-12-04 14:42:16 +0000
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+  <data noupdate="1">
+    <record model="ir.rule" id="framework_agreement_mc_rule">
+      <field name="name">Framework Agreement company rule</field>
+      <field name="model_id" ref="model_framework_agreement"/>
+      <field name="global" eval="True"/>
+      <field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field>
+    </record>
+  </data>
+</openerp>

=== added directory 'framework_agreement/tests'
=== added file 'framework_agreement/tests/__init__.py'
--- framework_agreement/tests/__init__.py	1970-01-01 00:00:00 +0000
+++ framework_agreement/tests/__init__.py	2013-12-04 14:42:16 +0000
@@ -0,0 +1,30 @@
+# -*- 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_framework_agreement_dates_and_constraints
+from . import test_framework_agreement_consumed_qty
+from . import test_framework_agreement_on_change
+from . import test_framework_agreement_price_list
+
+checks = [test_framework_agreement_dates_and_constraints,
+          test_framework_agreement_consumed_qty,
+          test_framework_agreement_on_change,
+          test_framework_agreement_price_list]

=== added file 'framework_agreement/tests/common.py'
--- framework_agreement/tests/common.py	1970-01-01 00:00:00 +0000
+++ framework_agreement/tests/common.py	2013-12-04 14:42:16 +0000
@@ -0,0 +1,97 @@
+# -*- 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 datetime, timedelta
+from openerp.osv import fields
+from openerp.tools import DEFAULT_SERVER_DATE_FORMAT
+
+
+class BaseAgreementTestMixin(object):
+    """Class that contain common behavior for all agreement related unit test classes.
+
+    We use Mixin because we want to have those behaviors on the various
+    unit test subclasses provided by OpenERP in test common.
+
+    """
+
+    def commonsetUp(self):
+        cr, uid = self.cr, self.uid
+        self.agreement_model = self.registry('framework.agreement')
+        self.agreement_pl_model = self.registry('framework.agreement.pricelist')
+        self.agreement_line_model = self.registry('framework.agreement.line')
+        self.now = datetime.strptime(fields.date.today(),
+                                     DEFAULT_SERVER_DATE_FORMAT)
+        self.product_id = self.registry('product.product').create(cr, uid,
+                                                                  {'name': 'test_1',
+                                                                   'type': 'product',
+                                                                   'list_price': 10.00})
+        self.supplier_id = self.registry('res.partner').create(cr, uid, {'name': 'toto',
+                                                                         'supplier': 'True'})
+
+    def _map_agreement_to_po(self, agreement, delta_days):
+        """Map agreement to dict to be used by PO create"""
+        supplier = agreement.supplier_id
+        add = self.browse_ref('base.res_partner_3')
+        term = supplier.property_supplier_payment_term
+        term = term.id if term else False
+        start_date = datetime.strptime(agreement.start_date, DEFAULT_SERVER_DATE_FORMAT)
+        date = start_date + timedelta(days=delta_days)
+        data = {}
+        data['partner_id'] = supplier.id
+        data['pricelist_id'] = supplier.property_product_pricelist_purchase.id
+        data['dest_address_id'] = add.id
+        data['location_id'] = add.property_stock_customer.id
+        data['payment_term_id'] = term
+        data['origin'] = agreement.name
+        data['date_order'] = date.strftime(DEFAULT_SERVER_DATE_FORMAT)
+        data['name'] = agreement.name
+        data['framework_agreement_id'] = agreement.id
+        return data
+
+    def _map_agreement_to_po_line(self, agreement, qty, order_id):
+        """Map agreement to dict to be used by PO line create"""
+        data = {}
+        supplier = agreement.supplier_id
+        data['product_qty'] = qty
+        data['product_id'] = agreement.product_id.id
+        data['product_uom'] = agreement.product_id.uom_id.id
+        currency = supplier.property_product_pricelist_purchase.currency_id
+        data['price_unit'] = agreement.get_price(qty, currency=currency)
+        data['name'] = agreement.product_id.name
+        data['order_id'] = order_id
+        data['date_planned'] = self.now
+        return data
+
+    def make_po_from_agreement(self, agreement, qty=0, delta_days=1):
+        """Create a purchase order from an agreement
+
+        :param agreement: origin agreement browse record
+        :param qty: qty to be used on po line
+        :delta days: set date of po to agreement start date + delta
+
+        :returns: purchase order browse record
+
+        """
+        cr, uid = self.cr, self.uid
+        po_model = self.registry('purchase.order')
+        po_line_model = self.registry('purchase.order.line')
+        po_id = po_model.create(cr, uid, self._map_agreement_to_po(agreement, delta_days))
+        po_line_model.create(cr, uid, self._map_agreement_to_po_line(agreement, qty, po_id))
+        return po_model.browse(cr, uid, po_id)

=== added file 'framework_agreement/tests/test_framework_agreement_consumed_qty.py'
--- framework_agreement/tests/test_framework_agreement_consumed_qty.py	1970-01-01 00:00:00 +0000
+++ framework_agreement/tests/test_framework_agreement_consumed_qty.py	2013-12-04 14:42:16 +0000
@@ -0,0 +1,74 @@
+# -*- 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 import netsvc
+from openerp.tools import DEFAULT_SERVER_DATE_FORMAT
+import openerp.tests.common as test_common
+from .common import BaseAgreementTestMixin
+from ..model.framework_agreement import AGR_PO_STATE
+
+
+class TestAvailabeQty(test_common.TransactionCase, BaseAgreementTestMixin):
+    """Test the function fields available_quantity"""
+
+    def setUp(self):
+        """ Create a default agreement"""
+        super(TestAvailabeQty, self).setUp()
+        self.commonsetUp()
+        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)
+
+        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,
+                                              'price': 77,
+                                              'delay': 5,
+                                              'quantity': 200})
+        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 = self.agreement_model.browse(cr, uid, agr_id)
+        self.agreement.open_agreement()
+
+    def test_00_noting_consumed(self):
+        """Test non consumption"""
+        self.assertEqual(self.agreement.available_quantity, 200)
+
+    def test_01_150_consumed(self):
+        """ test consumption of 150 units"""
+        cr, uid = self.cr, self.uid
+        po = self.make_po_from_agreement(self.agreement, qty=150, delta_days=5)
+        wf_service = netsvc.LocalService("workflow")
+        wf_service.trg_validate(uid, 'purchase.order', po.id, 'purchase_confirm', cr)
+        po.refresh()
+        self.assertIn(po.state, AGR_PO_STATE)
+        self.agreement.refresh()
+        self.assertEqual(self.agreement.available_quantity, 50)

=== added file 'framework_agreement/tests/test_framework_agreement_dates_and_constraints.py'
--- framework_agreement/tests/test_framework_agreement_dates_and_constraints.py	1970-01-01 00:00:00 +0000
+++ framework_agreement/tests/test_framework_agreement_dates_and_constraints.py	2013-12-04 14:42:16 +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 datetime import timedelta
+from openerp.tools import DEFAULT_SERVER_DATE_FORMAT
+import openerp.tests.common as test_common
+from .common import BaseAgreementTestMixin
+
+
+class TestAgreementState(test_common.TransactionCase, BaseAgreementTestMixin):
+
+    def setUp(self):
+        super(TestAgreementState, self).setUp()
+        self.commonsetUp()
+
+    def test_00_future(self):
+        """Test state of a future agreement"""
+        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)
+
+        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,
+                                              'delay': 5,
+                                              'quantity': 20})
+
+        agreement = self.agreement_model.browse(cr, uid, agr_id)
+        agreement.open_agreement()
+        self.assertEqual(agreement.state, 'future')
+
+    def test_01_past(self):
+        """Test state of a past agreement"""
+        cr, uid = self.cr, self.uid
+        start_date = self.now - timedelta(days=20)
+        start_date = start_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
+        end_date = self.now - timedelta(days=10)
+        end_date = end_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
+
+        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,
+                                              'delay': 5,
+                                              'quantity': 20})
+        agreement = self.agreement_model.browse(cr, uid, agr_id)
+        agreement.open_agreement()
+        self.assertEqual(agreement.state, 'closed')
+
+    def test_02_running(self):
+        """Test state of a running agreement"""
+        cr, uid = self.cr, self.uid
+        start_date = self.now - timedelta(days=2)
+        start_date = start_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
+        end_date = self.now + timedelta(days=2)
+        end_date = end_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
+
+        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,
+                                              'delay': 5,
+                                              'quantity': 20})
+        agreement = self.agreement_model.browse(cr, uid, agr_id)
+        agreement.open_agreement()
+        self.assertEqual(agreement.state, 'running')
+
+    def test_03_date_orderconstraint(self):
+        """Test that date order is checked"""
+        cr, uid = self.cr, self.uid
+        start_date = self.now - timedelta(days=40)
+        start_date = start_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
+        end_date = self.now + timedelta(days=30)
+        end_date = end_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
+        with self.assertRaises(Exception) as constraint:
+            self.agreement_model.create(cr, uid,
+                                        {'supplier_id': self.supplier_id,
+                                         'product_id': self.product_id,
+                                         'start_date': end_date,
+                                         'end_date': start_date,
+                                         'draft': False,
+                                         'delay': 5,
+                                         'quantity': 20})
+
+    def test_04_test_overlapp(self):
+        """Test overlapping agreement for same supplier constraint"""
+        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=10)
+        end_date = end_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
+        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': 20})
+        start_date = self.now - timedelta(days=2)
+        start_date = start_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
+        end_date = self.now + timedelta(days=2)
+        end_date = end_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
+        with self.assertRaises(Exception) as constraint:
+            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': 20})

=== added file 'framework_agreement/tests/test_framework_agreement_on_change.py'
--- framework_agreement/tests/test_framework_agreement_on_change.py	1970-01-01 00:00:00 +0000
+++ framework_agreement/tests/test_framework_agreement_on_change.py	2013-12-04 14:42:16 +0000
@@ -0,0 +1,166 @@
+# -*- 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 .common import BaseAgreementTestMixin
+from ..model.framework_agreement import FrameworkAgreementObservable
+
+
+class TestAgreementOnChange(test_common.TransactionCase, BaseAgreementTestMixin):
+    """Test observer on change and purchase order on chnage"""
+
+    def setUp(self):
+        """ Create a default agreement
+        with 3 price line
+        qty 0  price 70
+        qty 200 price 60
+        """
+        super(TestAgreementOnChange, self).setUp()
+        self.commonsetUp()
+        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)
+        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,
+                                              'delay': 5,
+                                              'draft': False,
+                                              'quantity': 1500})
+        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': 70})
+        self.agreement_line_model.create(cr, uid,
+                                         {'framework_agreement_pricelist_id': pl_id,
+                                          'quantity': 200,
+                                          'price': 60})
+        self.agreement = self.agreement_model.browse(cr, uid, agr_id)
+        self.po_line_model = self.registry('purchase.order.line')
+        self.assertTrue(issubclass(type(self.po_line_model),
+                                   FrameworkAgreementObservable))
+
+    def test_00_observe_price_change(self):
+        """Ensure that on change price observer raise correct warning
+
+        Warning must be rose if there is a running price agreement
+
+        """
+        cr, uid = self.cr, self.uid
+        res = self.po_line_model.onchange_price_obs(cr, uid, False, 20.0,
+                                                    self.agreement.id,
+                                                    currency=self.browse_ref('base.EUR'),
+                                                    qty=100)
+        self.assertTrue(res.get('warning'))
+
+    def test_01_onchange_quantity_obs(self):
+        """Ensure that on change quantity will raise warning or return price"""
+        cr, uid = self.cr, self.uid
+        res = self.po_line_model.onchange_quantity_obs(cr, uid, False, 200.0,
+                                                       self.agreement.start_date,
+                                                       self.agreement.product_id.id,
+                                                       supplier_id=self.agreement.supplier_id.id,
+                                                       currency=self.browse_ref('base.EUR'))
+        self.assertFalse(res.get('warning'))
+        self.assertEqual(res.get('value', {}).get('price'), 60)
+        # test there is a warning if agreement has not enought quantity
+        res = self.po_line_model.onchange_quantity_obs(cr, uid, False, 20000.0,
+                                                       self.agreement.start_date,
+                                                       self.agreement.product_id.id,
+                                                       supplier_id=self.agreement.supplier_id.id,
+                                                       currency=self.browse_ref('base.EUR'))
+        self.assertTrue(res.get('warning'))
+
+        res = self.po_line_model.onchange_quantity_obs(cr, uid, False, 20000.0,
+                                                       self.now.strftime(DEFAULT_SERVER_DATE_FORMAT),
+                                                       self.agreement.product_id.id,
+                                                       supplier_id=self.agreement.supplier_id.id,
+                                                       currency=self.browse_ref('base.EUR'))
+        self.assertFalse(res.get('warning'))
+
+    def test_02_onchange_product_obs(self):
+        """Check that change of product has correct behavior"""
+        cr, uid = self.cr, self.uid
+        res = self.po_line_model.onchange_product_id_obs(cr, uid, False, 180.0,
+                                                         self.agreement.start_date,
+                                                         self.agreement.supplier_id.id,
+                                                         self.agreement.product_id.id)
+        self.assertFalse(res.get('warning'))
+        self.assertEqual(res.get('value', {}).get('price'), 70)
+
+        res = self.po_line_model.onchange_product_id_obs(cr, uid, False, 20000.0,
+                                                         self.agreement.start_date,
+                                                         self.agreement.supplier_id.id,
+                                                         self.agreement.product_id.id)
+        self.assertTrue(res.get('warning'))
+        self.assertEqual(res.get('value', {}).get('price'), 60)
+
+        res = self.po_line_model.onchange_product_id_obs(cr, uid, False, 20000.0,
+                                                         self.now.strftime(DEFAULT_SERVER_DATE_FORMAT),
+                                                         self.agreement.supplier_id.id,
+                                                         self.agreement.product_id.id)
+        self.assertFalse(res.get('warning'))
+
+        # we do the test on non agreement product
+
+        res = self.po_line_model.onchange_product_id_obs(cr, uid, False, 20000.0,
+                                                         self.agreement.start_date,
+                                                         self.ref('product.product_product_33'),
+                                                         self.agreement.product_id.id)
+        self.assertEqual(res, {'value': {'framework_agreement_id': False}})
+
+    def test_03_price_observer_bindings(self):
+        """Check that change of price has correct behavior"""
+        cr, uid = self.cr, self.uid
+        plist = self.agreement.supplier_id.property_product_pricelist_purchase
+        res = self.po_line_model.onchange_price(cr, uid, False, 20.0,
+                                                self.agreement.id,
+                                                200,
+                                                plist.id,
+                                                self.agreement.product_id.id)
+        self.assertTrue(res.get('warning'))
+
+    def test_04_product_observer_bindings(self):
+        """Check that change of product has correct behavior"""
+        cr, uid = self.cr, self.uid
+        pl = self.agreement.supplier_id.property_product_pricelist_purchase.id,
+
+        res = self.po_line_model.onchange_product_id(cr, uid, False,
+                                                     pl,
+                                                     self.agreement.product_id.id,
+                                                     200,
+                                                     self.agreement.product_id.uom_id.id,
+                                                     self.agreement.supplier_id.id,
+                                                     date_order=self.agreement.start_date[0:10],
+                                                     fiscal_position_id=False,
+                                                     date_planned=False,
+                                                     name=False,
+                                                     price_unit=False,
+                                                     context={},
+                                                     agreement_id=self.agreement.id)
+        self.assertFalse(res.get('warning'))

=== added file 'framework_agreement/tests/test_framework_agreement_price_list.py'
--- framework_agreement/tests/test_framework_agreement_price_list.py	1970-01-01 00:00:00 +0000
+++ framework_agreement/tests/test_framework_agreement_price_list.py	2013-12-04 14:42:16 +0000
@@ -0,0 +1,73 @@
+from datetime import timedelta
+from openerp.tools import DEFAULT_SERVER_DATE_FORMAT
+from openerp.osv import orm
+import openerp.tests.common as test_common
+from .common import BaseAgreementTestMixin
+
+
+class TestAgreementPriceList(test_common.TransactionCase, BaseAgreementTestMixin):
+    """Test observer on change and purchase order on chnage"""
+
+    def setUp(self):
+        """ Create a default agreement
+        with 3 price line
+        qty 0  price 70
+        qty 200 price 60
+        qty 500 price 50
+        qty 1000 price 45
+        """
+        super(TestAgreementPriceList, self).setUp()
+        self.commonsetUp()
+        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)
+        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,
+                                              'delay': 5,
+                                              'draft': False,
+                                              'quantity': 1500})
+
+        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': 70.0})
+        self.agreement_line_model.create(cr, uid,
+                                         {'framework_agreement_pricelist_id': pl_id,
+                                          'quantity': 200,
+                                          'price': 60.0})
+        self.agreement_line_model.create(cr, uid,
+                                         {'framework_agreement_pricelist_id': pl_id,
+                                          'quantity': 500,
+                                          'price': 50.0})
+        self.agreement_line_model.create(cr, uid,
+                                         {'framework_agreement_pricelist_id': pl_id,
+                                          'quantity': 1000,
+                                          'price': 45.0})
+        self.agreement = self.agreement_model.browse(cr, uid, agr_id)
+
+    def test_00_test_qty(self):
+        """Test if barem retrieval is correct"""
+        self.assertEqual(self.agreement.get_price(0, currency=self.browse_ref('base.EUR')), 70.0)
+        self.assertEqual(self.agreement.get_price(100, currency=self.browse_ref('base.EUR')), 70.0)
+        self.assertEqual(self.agreement.get_price(200, currency=self.browse_ref('base.EUR')), 60.0)
+        self.assertEqual(self.agreement.get_price(210, currency=self.browse_ref('base.EUR')), 60.0)
+        self.assertEqual(self.agreement.get_price(500, currency=self.browse_ref('base.EUR')), 50.0)
+        self.assertEqual(self.agreement.get_price(800, currency=self.browse_ref('base.EUR')), 50.0)
+        self.assertEqual(self.agreement.get_price(999, currency=self.browse_ref('base.EUR')), 50.0)
+        self.assertEqual(self.agreement.get_price(1000, currency=self.browse_ref('base.EUR')), 45.0)
+        self.assertEqual(self.agreement.get_price(10000, currency=self.browse_ref('base.EUR')), 45.0)
+        self.assertEqual(self.agreement.get_price(-10, currency=self.browse_ref('base.EUR')), 70.0)
+
+    def test_01_failed_wrong_currency(self):
+        """Tests that wrong currency raise an exception"""
+        with self.assertRaises(orm.except_orm) as error:
+            self.agreement.get_price(0, currency=self.browse_ref('base.USD'))

=== added file 'framework_agreement/utils.py'
--- framework_agreement/utils.py	1970-01-01 00:00:00 +0000
+++ framework_agreement/utils.py	2013-12-04 14:42:16 +0000
@@ -0,0 +1,29 @@
+# -*- 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/>.
+#
+##############################################################################
+def id_boilerplate(fun):
+    """Ensure that id agrument passed to on change is not a list"""
+    def wrapper(*args, **kwargs):
+        if isinstance(args[3], (list, tuple)):
+            args = list(args)
+            args[3] = args[3][0] if args[3] else False
+            args = tuple(args)
+        return fun(*args, **kwargs)
+    return wrapper

=== added directory 'framework_agreement/view'
=== added file 'framework_agreement/view/company_view.xml'
--- framework_agreement/view/company_view.xml	1970-01-01 00:00:00 +0000
+++ framework_agreement/view/company_view.xml	2013-12-04 14:42:16 +0000
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+  <data>
+    <record id="add agreement setting on company" model="ir.ui.view">
+      <field name="name">add agreement setting on company</field>
+      <field name="model">res.company</field>
+      <field name="priority">19</field>
+      <field name="inherit_id" ref="base.view_company_form"/>
+      <field name="arch" type="xml">
+        <xpath expr="//group[@name='logistics_grp']" position="inside">
+          <field name="one_agreement_per_product"/>
+        </xpath>
+      </field>
+    </record>
+
+  </data>
+</openerp>

=== added file 'framework_agreement/view/framework_agreement_view.xml'
--- framework_agreement/view/framework_agreement_view.xml	1970-01-01 00:00:00 +0000
+++ framework_agreement/view/framework_agreement_view.xml	2013-12-04 14:42:16 +0000
@@ -0,0 +1,118 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+  <data>
+    <record id="framework_agreement_list_view" model="ir.ui.view">
+      <field name="name">framework agreement list view</field>
+      <field name="model">framework.agreement</field>
+      <field name="arch" type="xml">
+        <tree version="7.0" string="Framework Agreement (LTA)"> <!-- editable="bottom" -->
+          <field name="name" />
+          <field name="company_id" groups="base.group_multi_company" widget="selection"/>
+          <field name="product_id"/>
+          <field name="supplier_id"/>
+          <field name="delay"/>
+          <field name="quantity"/>
+          <field name="available_quantity"/>
+          <field name="start_date"/>
+          <field name="end_date"/>
+          <field name="state"/>
+        </tree>
+      </field>
+    </record>
+
+    <record id="framework_agreement_form_view" model="ir.ui.view">
+      <field name="name">framework agreement form</field>
+      <field name="model">framework.agreement</field>
+      <field name="arch" type="xml">
+        <form version="7.0" string="Framework Agreement">
+          <header>
+          <button name="open_agreement"
+                  context="{}"
+                  string="Open Agreement"
+                  type="object"
+                  attrs="{'invisible': ['|', ('draft', '=', False), ('start_date', '=', False), ('end_date', '=', False), ('framework_agreement_pricelist_ids', '=', False)]}"/>
+            <field name="state"
+                   widget="statusbar"
+                   nolabel="1"
+                   statusbar_visible="draft,future,running,consumed,closed"
+                   statusbar_colors='{"draft":"blue","future": "blue", "closed": "blue", "running": "green", "consumed": "red"}'/>
+          </header>
+          <sheet>
+            <group>
+              <field name="name"/>
+              <field name="origin"/>
+              <field name="draft" invisible="1"/>
+            </group>
+            <group>
+              <group>
+                <field name="company_id" groups="base.group_multi_company" widget="selection"/>
+                <field name="supplier_id"/>
+                <field name="product_id"/>
+              </group>
+              <group>
+                <field name="delay"/>
+                <field name="quantity"/>
+                <field name="available_quantity"/>
+              </group>
+            </group>
+
+            <group string="Dates">
+              <field name="start_date"
+                     required="1"/>
+              <field name="end_date"
+                     required="1"/>
+            </group>
+            <notebook>
+              <page string="Negociated price lists" colspan="4">
+                <field name="framework_agreement_pricelist_ids"
+                       required="1">
+                  <tree type="7.0" string="Price list">
+                    <field name="currency_id"/>
+                  </tree>
+                  <form type="7.0" string="Price list">
+                    <group>
+                      <field name="currency_id"/>
+                    </group>
+                    <newline/>
+                    <notebook>
+                      <page string="Price lines" colspan="4">
+                        <field name="framework_agreement_line_ids"
+                               nolabel="1">
+                          <tree type="7.0"
+                                string="Price line"
+                                editable="top">
+                            <field name="quantity"/>
+                            <field name="price"/>
+                          </tree>
+                        </field>
+                      </page>
+                    </notebook>
+                  </form>
+                </field>
+              </page>
+            </notebook>
+          </sheet>
+        </form>
+      </field>
+    </record>
+
+
+    <record model="ir.actions.act_window" id="action_framework_agreement">
+      <field name="name">Framework Agreement</field>
+      <field name="type">ir.actions.act_window</field>
+      <field name="res_model">framework.agreement</field>
+      <field name="domain"></field>
+      <field name="view_type">form</field>
+      <field name="view_mode">tree,form</field>
+      <field name="view_id" ref="framework_agreement_list_view"/>
+    </record>
+
+
+    <menuitem
+        name="Framework Agreement"
+        parent="purchase.menu_purchase_config_pricelist"
+        action="action_framework_agreement"
+        id="action_framework_agreement_menu"/>
+
+  </data>
+</openerp>

=== added file 'framework_agreement/view/product_view.xml'
--- framework_agreement/view/product_view.xml	1970-01-01 00:00:00 +0000
+++ framework_agreement/view/product_view.xml	2013-12-04 14:42:16 +0000
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+  <data>
+    <record id="agreement in product view" model="ir.ui.view">
+      <field name="name">agreement in product view</field>
+      <field name="model">product.product</field>
+      <field name="inherit_id" ref="product.product_normal_form_view"/>
+      <field name="arch" type="xml">
+        <group string="Purchase" position="after">
+          <group string="Framework agreements (LTA)" colspan="4">
+            <field name="framework_agreement_ids" nolabel="1">
+              <form version="7.0" string="Framework Agreement">
+                <header>
+                  <button name="open_agreement"
+                          context="{}"
+                          string="Open Agreement"
+                          type="object"
+                          attrs="{'invisible': ['|', ('draft', '=', False), ('start_date', '=', False), ('end_date', '=', False), ('framework_agreement_pricelist_ids', '=', False)]}"/>
+                  <field name="state"
+                         widget="statusbar"
+                         nolabel="1"
+                         statusbar_visible="draft,future,running,consumed,closed"
+                         statusbar_colors='{"draft":"blue","future": "blue", "closed": "blue", "running": "green", "consumed": "red"}'/>
+                </header>
+                <sheet>
+                  <group>
+                    <field name="name"/>
+                    <field name="draft" invisible="1"/>
+                    <field name="origin"/>
+                  </group>
+                  <group>
+                    <group>
+                      <field name="company_id" groups="base.group_multi_company" widget="selection"/>
+                      <field name="supplier_id"/>
+                    </group>
+                    <group>
+                      <field name="delay"/>
+                      <field name="quantity"/>
+                      <field name="available_quantity"/>
+                    </group>
+                  </group>
+
+                  <group string="Dates">
+                    <field name="start_date"
+                           required="1"/>
+                    <field name="end_date"
+                           required="1"/>
+                  </group>
+                  <notebook>
+                    <page string="Negociated price lists" colspan="4">
+                      <field name="framework_agreement_pricelist_ids"
+                             attrs="{'required': [('draft', '=', False)]}">
+                        <tree type="7.0" string="Price list">
+                          <field name="currency_id"/>
+                        </tree>
+                        <form type="7.0" string="Price list">
+                          <group>
+                            <field name="currency_id"/>
+                          </group>
+                          <newline/>
+                          <notebook>
+                            <page string="Price lines" colspan="4">
+                              <field name="framework_agreement_line_ids"
+                                     nolabel="1">
+                                <tree type="7.0"
+                                      string="Price line"
+                                      editable="top">
+                                  <field name="quantity"/>
+                                  <field name="price"/>
+                                </tree>
+                              </field>
+                            </page>
+                          </notebook>
+                        </form>
+                      </field>
+                    </page>
+                  </notebook>
+                </sheet>
+              </form>
+            </field>
+          </group>
+        </group>
+      </field>
+    </record>
+  </data>
+</openerp>

=== added file 'framework_agreement/view/purchase_view.xml'
--- framework_agreement/view/purchase_view.xml	1970-01-01 00:00:00 +0000
+++ framework_agreement/view/purchase_view.xml	2013-12-04 14:42:16 +0000
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+  <data>
+    <record id="add_onchange_on_pruchase_order_form" model="ir.ui.view">
+      <field name="name">add onchange on pruchase form</field>
+      <field name="model">purchase.order</field>
+      <field name="inherit_id" ref="purchase.purchase_order_form" />
+      <field name="arch" type="xml">
+        <field name="price_unit" position="attributes">
+          <attribute name="on_change">onchange_price(price_unit, parent.framework_agreement_id, product_qty, parent.pricelist_id, product_id)</attribute>
+        </field>
+        <field name="pricelist_id" position="after">
+          <field name="framework_agreement_id"
+                 domain="[('draft', '=', False)]"
+                 on_change="onchange_agreement(framework_agreement_id, partner_id, date_order)"/>
+        </field>
+        <field name="date_order"
+               position="attributes">
+          <attribute name="on_change">onchange_date(framework_agreement_id, date_order)</attribute>
+        </field>
+        <field name="product_id"
+               position="attributes">
+          <attribute name="on_change">onchange_product_id(parent.pricelist_id,product_id,product_qty,product_uom,parent.partner_id,parent.date_order,parent.fiscal_position,date_planned,name,price_unit,context,parent.framework_agreement_id)</attribute>
+        </field>
+        <field name="product_qty"
+               position="attributes">
+          <attribute name="on_change">onchange_product_id(parent.pricelist_id,product_id,product_qty,product_uom,parent.partner_id,parent.date_order,parent.fiscal_position,date_planned,name,price_unit,context,parent.framework_agreement_id)</attribute>
+        </field>
+      </field>
+    </record>
+
+    <record id="add_onchange_on_pruchase_order_line_form_standalone" model="ir.ui.view">
+      <field name="name">add onchange on pruchase order line form standalone</field>
+      <field name="model">purchase.order.line</field>
+      <field name="inherit_id" ref="purchase.purchase_order_line_form" />
+      <field name="arch" type="xml">
+        <field name="price_unit" position="attributes">
+          <attribute name="on_change">on_change="onchange_price(price_unit, parent.framework_agreement_id, product_qty, parent.pricelist_id)</attribute>
+        </field>
+      </field>
+    </record>
+  </data>
+</openerp>


Follow ups