openerp-community-reviewer team mailing list archive
-
openerp-community-reviewer team
-
Mailing list archive
-
Message #01952
[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