openerp-community-reviewer team mailing list archive
-
openerp-community-reviewer team
-
Mailing list archive
-
Message #01951
lp:~camptocamp/openerp-humanitarian-ngo/ngo-addons-add_agreement_sourcing-nbi into lp:openerp-humanitarian-ngo
Nicolas Bessi - Camptocamp has proposed merging lp:~camptocamp/openerp-humanitarian-ngo/ngo-addons-add_agreement_sourcing-nbi into lp:openerp-humanitarian-ngo.
Requested reviews:
OpenERP for Humanitarian Core Editors (humanitarian-core-editors)
For more details, see:
https://code.launchpad.net/~camptocamp/openerp-humanitarian-ngo/ngo-addons-add_agreement_sourcing-nbi/+merge/196676
Add sourcing using framework agreement
--
https://code.launchpad.net/~camptocamp/openerp-humanitarian-ngo/ngo-addons-add_agreement_sourcing-nbi/+merge/196676
Your team OpenERP for Humanitarian Core Editors is requested to review the proposed merge of lp:~camptocamp/openerp-humanitarian-ngo/ngo-addons-add_agreement_sourcing-nbi into lp:openerp-humanitarian-ngo.
=== added directory 'framework_agreement_requisition'
=== added file 'framework_agreement_requisition/__init__.py'
--- framework_agreement_requisition/__init__.py 1970-01-01 00:00:00 +0000
+++ framework_agreement_requisition/__init__.py 2013-12-05 10:14:17 +0000
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Nicolas Bessi
+# Copyright 2013 Camptocamp SA
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+from . import model
=== added file 'framework_agreement_requisition/__openerp__.py'
--- framework_agreement_requisition/__openerp__.py 1970-01-01 00:00:00 +0000
+++ framework_agreement_requisition/__openerp__.py 2013-12-05 10:14:17 +0000
@@ -0,0 +1,53 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Nicolas Bessi
+# Copyright 2013 Camptocamp SA
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+{'name': 'Framework Agreement Negociation',
+ 'version': '0.1',
+ 'author': 'Camptocamp',
+ 'maintainer': 'Camptocamp',
+ 'category': 'NGO',
+ 'complexity': 'normal',
+ 'depends': ['purchase_requisition',
+ 'purchase_requisition_extended',
+ 'framework_agreement'],
+ 'description': """
+Negociate framework agreement using tender process
+==================================================
+
+Module add state "Agreement selected" on tender and PO.
+It also adds a button on tender "Agreement selected"
+That will close flow of tender and related PO accordingly.
+
+
+It also add an agreement check box on tender to allows
+agreement negociation using tender process.
+
+""",
+ 'website': 'http://www.camptocamp.com',
+ 'data': ['requisition_workflow.xml',
+ 'purchase_workflow.xml',
+ 'view/purchase_requisition_view.xml'],
+ 'demo': [],
+ 'test': ['test/agreement_requisition.yml'],
+ 'installable': True,
+ 'auto_install': False,
+ 'license': 'AGPL-3',
+ 'application': False,
+ }
=== added directory 'framework_agreement_requisition/model'
=== added file 'framework_agreement_requisition/model/__init__.py'
--- framework_agreement_requisition/model/__init__.py 1970-01-01 00:00:00 +0000
+++ framework_agreement_requisition/model/__init__.py 2013-12-05 10:14:17 +0000
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Nicolas Bessi
+# Copyright 2013 Camptocamp SA
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+from . import purchase_requisition
+from . import purchase
=== added file 'framework_agreement_requisition/model/purchase.py'
--- framework_agreement_requisition/model/purchase.py 1970-01-01 00:00:00 +0000
+++ framework_agreement_requisition/model/purchase.py 2013-12-05 10:14:17 +0000
@@ -0,0 +1,86 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Nicolas Bessi
+# Copyright 2013 Camptocamp SA
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+from openerp import netsvc
+from openerp.osv import orm
+
+SELECTED_STATE = ('agreement_selected', 'Agreement selected')
+AGR_SELECT = 'agreement_selected'
+
+
+class purchase_order(orm.Model):
+ """Add workflow behavior"""
+
+ _inherit = "purchase.order"
+
+ def __init__(self, pool, cr):
+ """Add a new state value using PO class property"""
+ if SELECTED_STATE not in super(purchase_order, self).STATE_SELECTION:
+ super(purchase_order, self).STATE_SELECTION.append(SELECTED_STATE)
+ return super(purchase_order, self).__init__(pool, cr)
+
+ def select_agreement(self, cr, uid, agr_id, context=None):
+ """Pass PO in state 'Agreement selected'"""
+ if isinstance(agr_id, (list, tuple)):
+ assert len(agr_id) == 1
+ agr_id = agr_id[0]
+ wf_service = netsvc.LocalService("workflow")
+ return wf_service.trg_validate(uid, 'purchase.order',
+ agr_id, 'select_agreement', cr)
+
+ def po_tender_agreement_selected(self, cr, uid, ids, context=None):
+ """Workflow function that write state 'Agreement selected'"""
+ return self.write(cr, uid, ids, {'state': AGR_SELECT},
+ context=context)
+
+
+class purchase_order_line(orm.Model):
+ """Add make_agreement function"""
+
+ _inherit = "purchase.order.line"
+
+ def _agreement_data(self, cr, uid, po_line, origin, context=None):
+ """Get agreement values from PO line
+
+ :param po_line: Po line records
+
+ :returns: agreement dict to be used by orm.Model.create
+ """
+ vals = {}
+ vals['supplier_id'] = po_line.order_id.partner_id.id
+ vals['product_id'] = po_line.product_id.id
+ vals['quantity'] = po_line.product_qty
+ vals['origin'] = origin if origin else False
+ return vals
+
+ def make_agreement(self, cr, uid, line_id, origin, context=None):
+ """ generate a draft framework agreement
+
+ :returns: a record of LTA
+
+ """
+ agr_model = self.pool['framework.agreement']
+ if isinstance(line_id, (list, tuple)):
+ assert len(line_id) == 1
+ line_id = line_id[0]
+ current = self.browse(cr, uid, line_id, context=context)
+ vals = self._agreement_data(cr, uid, current, origin, context=context)
+ agr_id = agr_model.create(cr, uid, vals, context=context)
+ return agr_model.browse(cr, uid, agr_id, context=context)
=== added file 'framework_agreement_requisition/model/purchase_requisition.py'
--- framework_agreement_requisition/model/purchase_requisition.py 1970-01-01 00:00:00 +0000
+++ framework_agreement_requisition/model/purchase_requisition.py 2013-12-05 10:14:17 +0000
@@ -0,0 +1,93 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Nicolas Bessi
+# Copyright 2013 Camptocamp SA
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+from itertools import chain
+from openerp import netsvc
+from openerp.osv import orm, fields
+from openerp.tools.translate import _
+from .purchase import AGR_SELECT as PO_AGR_SELECT
+
+SELECTED_STATE = ('agreement_selected', 'Agreement selected')
+AGR_SELECT = 'agreement_selected'
+
+
+class purchase_requisition(orm.Model):
+ """Add support to negociate LTA using tender process"""
+
+ def __init__(self, pool, cr):
+ """Nasty hack to add fields to select fields
+
+ We do this in order not to compromising other state added
+ by other addons that are not in inheritance chain...
+
+ """
+ sel = super(purchase_requisition, self)._columns['state']
+ if SELECTED_STATE not in sel.selection:
+ sel.selection.append(SELECTED_STATE)
+ return super(purchase_requisition, self).__init__(pool, cr)
+
+ _inherit = "purchase.requisition"
+ _columns = {
+ 'framework_agreement_tender': fields.boolean('Negociate Agreement'),
+ }
+
+ def tender_agreement_selected(self, cr, uid, ids, context=None):
+ """Workflow function that write state 'Agreement selected'"""
+ return self.write(cr, uid, ids, {'state': AGR_SELECT},
+ context=context)
+
+ def select_agreement(self, cr, uid, agr_id, context=None):
+ """Pass tender to state 'Agreement selected'"""
+ if isinstance(agr_id, (list, tuple)):
+ assert len(agr_id) == 1
+ agr_id = agr_id[0]
+ wf_service = netsvc.LocalService("workflow")
+ return wf_service.trg_validate(uid, 'purchase.requisition',
+ agr_id, 'select_agreement', cr)
+
+ def agreement_selected(self, cr, uid, ids, context=None):
+ """Tells tender that an agreement has been selected"""
+ if isinstance(ids, (int, long)):
+ ids = [ids]
+ for req in self.browse(cr, uid, ids, context=context):
+ if not req.framework_agreement_tender:
+ raise orm.except_orm(_('Invalid tender'),
+ _('Request is not of type agreement'))
+ self.select_agreement(cr, uid, req.id, context=context)
+ req.refresh()
+ if req.state != AGR_SELECT:
+ raise RuntimeError('requisiton %s does not pass to state'
+ ' agreement_selected' %
+ req.name)
+ rfqs = chain.from_iterable(req_line.purchase_line_ids
+ for req_line in req.line_ids)
+ rfqs = [rfq for rfq in rfqs if rfq.state == 'confirmed']
+ if not rfqs:
+ raise orm.except_orm(_('No confirmed RFQ related to tender'),
+ _('Please choose at least one'))
+ for rfq in rfqs:
+ rfq.make_agreement(req.name)
+ p_order = rfq.order_id
+ p_order.select_agreement()
+ p_order.refresh()
+ if p_order.state != PO_AGR_SELECT:
+ raise RuntimeError('Purchase order %s does not pass to %' %
+ (p_order.name, PO_AGR_SELECT))
+ return True
=== added file 'framework_agreement_requisition/purchase_workflow.xml'
--- framework_agreement_requisition/purchase_workflow.xml 1970-01-01 00:00:00 +0000
+++ framework_agreement_requisition/purchase_workflow.xml 2013-12-05 10:14:17 +0000
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+ <data>
+ <record id="act_po_agreement_selected" model="workflow.activity">
+ <field name="wkf_id" ref="purchase.purchase_order"/>
+ <field name="name">Agreement selected</field>
+ <field name="kind">function</field>
+ <field name="action">po_tender_agreement_selected()</field>
+ <field name="flow_stop">True</field>
+ </record>
+ <record id="trans_po_agreement_selected" model="workflow.transition">
+ <field name="act_from" ref="purchase.act_bid"/>
+ <field name="act_to" ref="act_po_agreement_selected"/>
+ <field name="signal">select_agreement</field>
+ </record>
+ </data>
+</openerp>
=== added file 'framework_agreement_requisition/requisition_workflow.xml'
--- framework_agreement_requisition/requisition_workflow.xml 1970-01-01 00:00:00 +0000
+++ framework_agreement_requisition/requisition_workflow.xml 2013-12-05 10:14:17 +0000
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+ <data>
+ <record id="act_agreement_selected" model="workflow.activity">
+ <field name="wkf_id" ref="purchase_requisition.purchase_requisition_workflow"/>
+ <field name="name">Agreement selected</field>
+ <field name="kind">function</field>
+ <field name="action">tender_agreement_selected()</field>
+ <field name="flow_stop">True</field>
+ </record>
+
+ <record id="trans_agreement_selected" model="workflow.transition">
+ <field name="act_from" ref="purchase_requisition_extended.act_closed"/>
+ <field name="act_to" ref="act_agreement_selected"/>
+ <field name="signal">select_agreement</field>
+ </record>
+ </data>
+</openerp>
=== added directory 'framework_agreement_requisition/test'
=== added file 'framework_agreement_requisition/test/agreement_requisition.yml'
--- framework_agreement_requisition/test/agreement_requisition.yml 1970-01-01 00:00:00 +0000
+++ framework_agreement_requisition/test/agreement_requisition.yml 2013-12-05 10:14:17 +0000
@@ -0,0 +1,98 @@
+-
+ Standard flow of a Call for agreement Bids in mode open
+-
+ Create Call for Bids
+-
+ !record {model: purchase.requisition, id: purchase_requisition_agreement}:
+ date_start: '2013-08-02 00:00:00'
+ date_end: '2013-08-30 00:00:00'
+ bid_tendering_mode: 'open'
+ schedule_date: '2013-09-30'
+ req_validity: '2013-09-10'
+ framework_agreement_tender: True
+ line_ids:
+ - product_id: product.product_product_15
+ product_qty: 2500.0
+-
+ Confirm Call
+-
+ !python {model: purchase.requisition}: |
+ import netsvc
+ wf_service = netsvc.LocalService("workflow")
+ wf_service.trg_validate(uid, 'purchase.requisition', ref("purchase_requisition_agreement"), 'sent_suppliers', cr)
+-
+ Create RFQ1. I run the 'Request a quotation' wizard. I fill the supplier.
+-
+ !record {model: purchase.requisition.partner, id: purchase_requisition_agreement_partner1_create}:
+ partner_id: base.res_partner_2
+-
+ Create RFQ1. I confirm the wizard.
+-
+ !python {model: purchase.requisition.partner}: |
+ self.create_order(cr, uid, [ref("purchase_requisition_agreement_partner1_create")],{
+ 'active_model': 'purchase.requisition',
+ 'active_id': ref("purchase_requisition_agreement"),
+ 'active_ids': [ref("purchase_requisition_agreement")],
+ })
+-
+ I encode the bid. I set a 300 price on the line.
+-
+ !python {model: purchase.requisition}: |
+ purchase_req = self.browse(cr, uid, ref("purchase_requisition_agreement"))
+ assert len(purchase_req.purchase_ids) == 1, "There must be 1 RFQs linked to this Call for bids"
+ price = 300
+ for rfq in purchase_req.purchase_ids:
+ for line in rfq.order_line:
+ self.pool.get('purchase.order.line').write(cr, uid, [line.id], {'price_unit': price})
+
+-
+ I send the RFQ. For this, I print the RFQ.
+-
+ !python {model: purchase.requisition}: |
+ purchase_req = self.browse(cr, uid, ref("purchase_requisition_agreement"))
+ for rfq in purchase_req.purchase_ids:
+ self.pool.get('purchase.order').print_quotation(cr, uid, [rfq.id])
+-
+ I run the 'Bid encoded' wizard of bid1. I fill the date.
+-
+ !record {model: purchase.action_modal_datetime, id: purchase_requisition_agreement_bid1_bidencoded}:
+ datetime: '2013-08-13 00:00:00'
+-
+ I launch wizard action.
+-
+ !python {model: purchase.action_modal_datetime}: |
+ purchase_req = self.pool['purchase.requisition'].browse(cr, uid, ref("purchase_requisition_agreement"))
+ po_id = purchase_req.purchase_ids[0].id
+ self.action(cr, uid, [ref('purchase_requisition_agreement_bid1_bidencoded')],
+ {'action': 'bid_received_ok',
+ 'active_id': po_id,
+ 'active_ids': [po_id],
+ 'active_model': 'purchase.order',
+ 'default_datetime': '2013-08-13 00:00:00',
+ 'uid': 1})
+-
+ I close the Call for bids and move to bids selection
+-
+ !python {model: purchase.requisition}: |
+ import netsvc
+ wf_service = netsvc.LocalService("workflow")
+ wf_service.trg_validate(uid, 'purchase.requisition', ref("purchase_requisition_agreement"), 'open_bid', cr)
+
+-
+ In the bids selection, I confirm line 1 of bid 1
+-
+ !python {model: purchase.requisition}: |
+ purchase_req = self.browse(cr, uid, ref("purchase_requisition_agreement"))
+ self.pool.get('purchase.order.line').action_confirm(cr, uid, [purchase_req.purchase_ids[0].order_line[0].id])
+-
+ I close the call for bids
+-
+ !python {model: purchase.requisition}: |
+ self.close_callforbids(cr, uid, [ref("purchase_requisition_agreement")])
+-
+ I mark the tender as agreement selected
+-
+ !python {model: purchase.requisition}: |
+ self.agreement_selected(cr, uid, ref("purchase_requisition_agreement"))
+ purchase_req = self.browse(cr, uid, ref("purchase_requisition_agreement"))
+ assert purchase_req.state == 'agreement_selected'
=== added directory 'framework_agreement_requisition/view'
=== added file 'framework_agreement_requisition/view/purchase_requisition_view.xml'
--- framework_agreement_requisition/view/purchase_requisition_view.xml 1970-01-01 00:00:00 +0000
+++ framework_agreement_requisition/view/purchase_requisition_view.xml 2013-12-05 10:14:17 +0000
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+ <data noupdate="0">
+ <record model="ir.ui.view" id="view_purchase_requisition_form_agreement">
+ <field name="name">purchase.requisition.form.inherit.aggrement.button</field>
+ <field name="model">purchase.requisition</field>
+ <field name="inherit_id" ref="purchase_requisition.view_purchase_requisition_form"/>
+ <field name="arch" type="xml">
+ <field name="multiple_rfq_per_supplier"
+ position="after">
+ <field name="framework_agreement_tender"/>
+ </field>
+ <button name="cancel_requisition" position="after">
+ <button name="agreement_selected"
+ type="object"
+ class="po_buttons oe_form_buttons"
+ attrs="{'invisible': ['|', ('framework_agreement_tender', '=', False), ('state', '!=', 'closed')]}"
+ string="Framework agreement selected"/>
+ </button>
+ </field>
+ </record>
+
+ <record model="ir.ui.view" id="view_purchase_requisition_filter">
+ <field name="name">purchase.requisition.form.inherit.agreement.filter</field>
+ <field name="model">purchase.requisition</field>
+ <field name="inherit_id" ref="purchase_requisition.view_purchase_requisition_filter"/>
+ <field name="arch" type="xml">
+ <filter name="draft"
+ position="after">
+ <filter icon="terp-document-new"
+ name="framework_agreement"
+ string="Framework Agreement?"
+ domain="[(framework_agreement_tender,'=',True)]"/>
+ </filter>
+ </field>
+ </record>
+
+ </data>
+</openerp>
=== added directory 'framework_agreement_sourcing'
=== added file 'framework_agreement_sourcing/__init__.py'
--- framework_agreement_sourcing/__init__.py 1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/__init__.py 2013-12-05 10:14:17 +0000
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Nicolas Bessi
+# Copyright 2013 Camptocamp SA
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+from . import model
=== added file 'framework_agreement_sourcing/__openerp__.py'
--- framework_agreement_sourcing/__openerp__.py 1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/__openerp__.py 2013-12-05 10:14:17 +0000
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Nicolas Bessi
+# Copyright 2013 Camptocamp SA
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+{'name': 'Framework agreement integration in sourcing',
+ 'version': '0.1',
+ 'author': 'Camptocamp',
+ 'maintainer': 'Camptocamp',
+ 'category': 'NGO',
+ 'complexity': 'normal',
+ 'depends': ['framework_agreement', 'logistic_requisition'],
+ 'description': """Add the possibility to have a framework agreement as a source.
+
+it will do auto completion and warning and create PO from source line""",
+ 'website': 'http://www.camptocamp.com',
+ 'data': ['view/requisition_view.xml'],
+ 'demo': [],
+ 'test': [],
+ 'installable': True,
+ 'auto_install': False,
+ 'license': 'AGPL-3',
+ 'application': False,
+ }
=== added directory 'framework_agreement_sourcing/i18n'
=== added directory 'framework_agreement_sourcing/model'
=== added file 'framework_agreement_sourcing/model/__init__.py'
--- framework_agreement_sourcing/model/__init__.py 1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/model/__init__.py 2013-12-05 10:14:17 +0000
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Nicolas Bessi
+# Copyright 2013 Camptocamp SA
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+from . import logistic_requisition
+from . import logistic_requisition_source
+from . import purchase
+from . import adapter_util
+from . import sale_order
+from . import logistic_requisition_cost_estimate
=== added file 'framework_agreement_sourcing/model/adapter_util.py'
--- framework_agreement_sourcing/model/adapter_util.py 1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/model/adapter_util.py 2013-12-05 10:14:17 +0000
@@ -0,0 +1,116 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Nicolas Bessi
+# Copyright 2013 Camptocamp SA
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+"""Provides basic mechanism to unify the way records are transformed into
+other records
+
+"""
+
+from openerp.osv import orm
+
+
+class BrowseAdapterSourceMixin(object):
+ """Mixin class used by Model that are transformation sources"""
+
+ def _company(self, cr, uid, context):
+ """Return company id
+
+ :returns: company id
+
+ """
+ return self.pool['res.company']._company_default_get(cr, uid, 'purchase.order',
+ context=context)
+
+ def _direct_map(self, line, mapping, context=None):
+ """Take a dict of left key right key and make direct mapping
+ into the model
+
+ :returns: data dict ready to be used
+ """
+ data = {}
+ for po_key, source_key in mapping.iteritems():
+ value = line[source_key]
+ if isinstance(value, orm.browse_record):
+ value = value.id
+ elif isinstance(value, orm.browse_null):
+ value = False
+ elif isinstance(value, orm.browse_record_list):
+ raise NotImplementedError('List are not supported in direct map')
+
+ data[po_key] = value
+ return data
+
+
+class BrowseAdapterMixin(object):
+
+ def _do_checks(self, cr, uid, model, data, context=None):
+ """Perform validation check of adapted data.
+
+ All missing or incorrect values are return at once.
+
+ :returns: array of exceptions
+
+ """
+ required_keys = set(k for k, v in model._columns.iteritems()
+ if v.required and not getattr(v, '_fnct', False))
+ empty_required = set(x for x in data
+ if x in required_keys and not data[x])
+ missing_required = required_keys - set(data.keys())
+ missing_required.update(empty_required)
+ if missing_required:
+ return[ValueError('Following value are missing or False'
+ ' while adapting %s: %s' %
+ (model._name, ", ".join(missing_required)))]
+ return []
+
+ def _validate_adapted_data(self, cr, uid, model, data, context=None):
+ """Perform validation check of adapted data.
+
+ All missing or incorrect values are return at once.
+
+ :returns: validated data or raise Value error
+
+ """
+ errors = self._do_checks(cr, uid, model, data, context=context)
+ if errors:
+ raise ValueError('Data are invalid for following reason %s' %
+ ("\n".join(repr(e) for e in errors)))
+ return data
+
+ def _adapt_origin(self, cr, uid, model, origin,
+ map_fun, post_fun=None, context=None, **kwargs):
+ """Do transformation of source data to dest data using transforms function.
+
+ :param origin: source record
+ :param map_fun: transform function
+ :param post_fun: post transformation hook function
+
+ :returns: transformed data
+
+ """
+ if not callable(map_fun):
+ raise ValueError('Mapping function is not callable')
+ if post_fun and not callable(post_fun):
+ raise ValueError('Post hook function is not callable')
+ data = map_fun(cr, uid, origin, context=context, **kwargs)
+ # we complete with default
+ missing = set(model._columns.keys()) - set(data.keys())
+ data.update(model.default_get(cr, uid, missing, context=context))
+ return data
=== added file 'framework_agreement_sourcing/model/logistic_requisition.py'
--- framework_agreement_sourcing/model/logistic_requisition.py 1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/model/logistic_requisition.py 2013-12-05 10:14:17 +0000
@@ -0,0 +1,243 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Nicolas Bessi
+# Copyright 2013 Camptocamp SA
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+from collections import namedtuple
+from openerp.tools.translate import _
+from openerp.osv import orm
+from .adapter_util import BrowseAdapterSourceMixin
+from .logistic_requisition_source import AGR_PROC
+
+
+class logistic_requisition_line(orm.Model, BrowseAdapterSourceMixin):
+ """Override to enable generation of source line"""
+
+ _inherit = "logistic.requisition.line"
+
+ def _map_agr_requisiton_to_source(self, cr, uid, line, context=None,
+ qty=0, agreement=None, **kwargs):
+ """Prepare data dict for source line using agreement as source
+
+ :params line: browse record of origin requistion.line
+ :params agreement: browse record of origin agreement
+ :params qty: quantity to be set on source line
+
+ :returns: dict to be used by Model.create
+
+ """
+ res = {}
+ direct_map = {
+ 'proposed_product_id': 'product_id',
+ 'requisition_line_id': 'id',
+ 'proposed_uom_id': 'requested_uom_id'}
+
+ if not agreement:
+ raise ValueError("Missing agreement")
+ if not agreement.product_id.id == line.product_id.id:
+ raise ValueError("Product mismatch for agreement and requisition line")
+ # currency = self._get_source_currency(cr, uid, line, context=context)
+ res['unit_cost'] = 0.0
+ res['proposed_qty'] = qty
+ res['framework_agreement_id'] = agreement.id
+ res['procurement_method'] = AGR_PROC
+ res.update(self._direct_map(line, direct_map))
+ return res
+
+ def _map_requisition_to_source(self, cr, uid, line, context=None,
+ qty=0, **kwargs):
+ """Prepare data dict to generate source line using requisition as source
+
+ :params line: browse record of origin requistion.line
+ :params qty: quantity to be set on source line
+
+ :returns: dict to be used by Model.create
+
+ """
+ res = {}
+ direct_map = {'proposed_product_id': 'product_id',
+ 'requisition_line_id': 'id',
+ 'proposed_uom_id': 'requested_uom_id'}
+ res['unit_cost'] = 0.0
+ res['proposed_qty'] = qty
+ res['framework_agreement_id'] = False
+ res['procurement_method'] = 'procurement'
+ res.update(self._direct_map(line, direct_map))
+ return res
+
+ def _generate_lines_from_agreements(self, cr, uid, container, line,
+ agreements, qty, currency=None, context=None):
+ """Generate 1/n source line(s) for one requisition line.
+
+ This is done using available agreements.
+ We first look for cheapeast agreement.
+ Then if no more quantity are available and there is still remaining needs
+ we look for next cheapest agreement or return remaining qty
+
+ :param container: list of agreements browse
+ :param qty: quantity to be sourced
+ :param line: origin requisition line
+
+ :returns: remaining quantity to source
+
+ """
+ agreements = agreements if agreements is not None else []
+ if currency:
+ agreements = [x for x in agreements if x.has_currency(currency)]
+ if not agreements:
+ return qty
+ agreements.sort(key=lambda x: x.get_price(qty, currency=currency))
+ current_agr = agreements.pop(0)
+ avail = current_agr.available_quantity
+ if not avail:
+ return qty
+ avail_sold = avail - qty
+ to_consume = qty if avail_sold >= 0 else avail
+
+ source_id = self.make_source_line(cr, uid, line, force_qty=to_consume,
+ agreement=current_agr, context=context)
+ container.append(source_id)
+ difference = qty - to_consume
+ if difference:
+ return self._generate_lines_from_agreements(cr, uid, container, line,
+ agreements, difference, context=context)
+ else:
+ return 0
+
+ def _source_lines_for_agreements(self, cr, uid, line, agreements, currency=None, context=None):
+ """Generate 1/n source line(s) for one requisition line
+
+ This is done using available agreements.
+ We first look for cheapeast agreement.
+ Then if no more quantity are available and there is still remaining needs
+ we look for next cheapest agreement or we create a tender source line
+
+ :param line: requisition line browse record
+ :returns: (generated line ids, remaining qty not covered by agreement)
+
+ """
+ Sourced = namedtuple('Sourced', ['generated', 'remaining'])
+ qty = line.requested_qty
+ generated = []
+ remaining_qty = self._generate_lines_from_agreements(cr, uid, generated,
+ line, agreements, qty,
+ currency=currency, context=context)
+ return Sourced(generated, remaining_qty)
+
+ def make_source_line(self, cr, uid, line, force_qty=None, agreement=None, context=None):
+ """Generate a source line for a tender from a requisition line
+
+ :param line: browse record of origin logistic.request
+ :param force_qty: if set this quantity will be used instead
+ of requested quantity
+ :returns: id of generated source line
+
+ """
+ qty = force_qty if force_qty else line.requested_qty
+ src_obj = self.pool['logistic.requisition.source']
+ if agreement:
+ return src_obj._make_source_line_from_origin(cr, uid, line,
+ self._map_agr_requisiton_to_source,
+ context=context, qty=qty,
+ agreement=agreement)
+ else:
+ return src_obj._make_source_line_from_origin(cr, uid, line,
+ self._map_requisition_to_source,
+ context=context, qty=qty)
+
+ def _get_source_currency(self, cr, uid, line, context=None):
+ agr_obj = self.pool['framework.agreement']
+ comp_obj = self.pool['res.company']
+ currency = line.requisition_id.get_pricelist().currency_id
+ company_id = agr_obj._company_get(cr, uid, context=context)
+ comp_currency = comp_obj.browse(cr, uid, company_id, context=context).currency_id
+ if currency == comp_currency:
+ return None
+ return currency
+
+ def _generate_source_line(self, cr, uid, line, context=None):
+ """Generate one or n source line(s) per requisition line.
+
+ Depending on the available resources. If there is framework agreement(s)
+ running we generate one or n source line using agreements otherwise we generate one
+ source line using tender process
+
+ :param line: browse record of origin logistic.request
+
+ :returns: list of generated source line ids
+
+ """
+ if line.source_ids:
+ return None
+ agr_obj = self.pool['framework.agreement']
+ date = line.requisition_id.date
+ currency = self._get_source_currency(cr, uid, line, context=context)
+ product_id = line.product_id.id
+ agreements = agr_obj.get_all_product_agreements(cr, uid, product_id, date,
+ context=context)
+ generated_lines = []
+ if agreements:
+ line_ids, missing_qty = self._source_lines_for_agreements(cr, uid, line,
+ agreements, currency=currency)
+ generated_lines.extend(line_ids)
+ if missing_qty:
+ generated_lines.append(self.make_source_line(cr, uid, line,
+ force_qty=missing_qty))
+ else:
+ generated_lines.append(self.make_source_line(cr, uid, line))
+
+ return generated_lines
+
+ def _do_confirm(self, cr, uid, ids, context=None):
+ """Override to generate source lines from requision line.
+
+ Please refer to _generate_source_line documentation
+
+ """
+ # TODO refactor
+ # this should probably be in logistic_requisition module
+ # providing a mechanism to allow each type of sourcing method
+ # to generate source line
+ res = super(logistic_requisition_line, self)._do_confirm(cr, uid, ids,
+ context=context)
+ for line_br in self.browse(cr, uid, ids, context=context):
+ self._generate_source_line(cr, uid, line_br, context=context)
+ return res
+
+
+class logistic_requisition(orm.Model):
+ """Add get pricelist function"""
+
+ _inherit = "logistic.requisition"
+
+ def get_pricelist(self, cr, uid, requisition_id, context=None):
+ """Retrive pricelist id to use in sourcing by agreement process
+
+ :returns: pricelist record
+
+ """
+ if isinstance(requisition_id, (list, tuple)):
+ assert len(requisition_id) == 1
+ requisition_id = requisition_id[0]
+ requisiton = self.browse(cr, uid, requisition_id, context=context)
+ plist = requisiton.partner_id.property_product_pricelist
+ if not plist:
+ raise orm.except_orm(_('No price list on customer'),
+ _('Please set sale price list on %s partner') %
+ requisiton.partner_id.name)
+ return plist
=== added file 'framework_agreement_sourcing/model/logistic_requisition_cost_estimate.py'
--- framework_agreement_sourcing/model/logistic_requisition_cost_estimate.py 1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/model/logistic_requisition_cost_estimate.py 2013-12-05 10:14:17 +0000
@@ -0,0 +1,69 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Nicolas Bessi
+# Copyright 2013 Camptocamp SA
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+from openerp.osv import orm
+from .logistic_requisition_source import AGR_PROC
+
+
+class logistic_requisition_cost_estimate(orm.Model):
+ """Add update of agreement price"""
+
+ _inherit = "logistic.requisition.cost.estimate"
+
+ def _update_agreement_source(self, cr, uid, source, context=None):
+ """Update price of source line using related confirmed PO"""
+ if source.procurement_method == AGR_PROC:
+ self._link_po_lines_to_source(cr, uid, source, context=context)
+ price = source.get_agreement_price_from_po()
+ source.write({'unit_cost': price})
+ source.refresh()
+
+ def _link_po_lines_to_source(self, cr, uid, source, context=None):
+ po_l_obj = self.pool['purchase.order.line']
+ agr = source.framework_agreement_id
+ line_ids = po_l_obj.search(
+ cr, uid,
+ [('order_id.framework_agreement_id', '=', agr.id),
+ ('lr_source_line_id', '=', source.id),
+ ('order_id.partner_id', '=', agr.supplier_id.id)],
+ context=context)
+ lines = po_l_obj.browse(cr, uid, line_ids, context=context)
+ po_ids = ([line.order_id.id for line in lines])
+
+ all_line_ids = po_l_obj.search(
+ cr, uid,
+ [('order_id', 'in', po_ids)],
+ context=context)
+
+ po_l_obj.write(cr, uid, all_line_ids,
+ {'lr_source_line_id': source.id},
+ context=context)
+ source.refresh()
+
+ def _prepare_cost_estimate_line(self, cr, uid, sourcing, context=None):
+ """Override in order to update agreement source line
+
+ We update the price of source line that will be used in cost estimate
+
+ """
+ self._update_agreement_source(cr, uid, sourcing, context=context)
+ return super(logistic_requisition_cost_estimate,
+ self)._prepare_cost_estimate_line(cr, uid, sourcing,
+ context=context)
=== added file 'framework_agreement_sourcing/model/logistic_requisition_source.py'
--- framework_agreement_sourcing/model/logistic_requisition_source.py 1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/model/logistic_requisition_source.py 2013-12-05 10:14:17 +0000
@@ -0,0 +1,363 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Nicolas Bessi
+# Copyright 2013 Camptocamp SA
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+from openerp.osv import orm, fields
+from openerp.tools.translate import _
+from openerp.addons.framework_agreement.model.framework_agreement import\
+ FrameworkAgreementObservable
+from openerp.addons.framework_agreement.utils import id_boilerplate
+
+from .adapter_util import BrowseAdapterMixin, BrowseAdapterSourceMixin
+
+AGR_PROC = 'fw_agreement'
+
+
+class logistic_requisition_source(orm.Model, BrowseAdapterMixin,
+ BrowseAdapterSourceMixin, FrameworkAgreementObservable):
+ """Adds support of framework agreement to source line"""
+
+ _inherit = "logistic.requisition.source"
+
+ _columns = {'framework_agreement_id': fields.many2one('framework.agreement',
+ 'Agreement'),
+
+ 'purchase_pricelist_id': fields.many2one('product.pricelist',
+ 'Purchase (PO) Pricelist',
+ help="This pricelist will be used"
+ " when generating PO"),
+ 'pricelist_id': fields.related('requisition_line_id', 'requisition_id',
+ 'partner_id',
+ 'property_product_pricelist',
+ relation='product.pricelist',
+ type='many2one',
+ string='Price list',
+ readonly=True),
+
+ 'supplier_id': fields.related('framework_agreement_id', 'supplier_id',
+ type='many2one', relation='res.partner',
+ string='Agreement Supplier')}
+
+ def _get_procur_method_hook(self, cr, uid, context=None):
+ """Adds framework agreement as a procurement method in selection field"""
+ res = super(logistic_requisition_source, self)._get_procur_method_hook(cr, uid,
+ context=context)
+ res.append((AGR_PROC, 'Framework agreement'))
+ return res
+
+ def _get_purchase_line_id(self, cr, uid, ids, field_name, arg, context=None):
+ """For each source line, get the related purchase order line
+
+ For more detail please refer to function fields documentation
+
+ """
+ po_line_model = self.pool['purchase.order.line']
+ res = super(logistic_requisition_source, self)._get_purchase_line_id(cr, uid, ids,
+ field_name,
+ arg,
+ context=context)
+ for line in self.browse(cr, uid, ids, context=context):
+ if line.procurement_method == AGR_PROC:
+ po_l_ids = po_line_model.search(cr, uid,
+ [('lr_source_line_id', '=', line.id),
+ ('state', '!=', 'cancel')],
+ context=context)
+ if po_l_ids:
+ if len(po_l_ids) > 1:
+ raise orm.except_orm(_('Many Purchase order lines found for %s') % line.name,
+ _('Please cancel uneeded one'))
+ res[line.id] = po_l_ids[0]
+ else:
+ res[line.id] = False
+ return res
+
+ #------------------ adapting source line to po -----------------------------
+
+ def _map_source_to_po(self, cr, uid, line, context=None, **kwargs):
+ """Map source line to dict to be used by PO create defaults are optional
+
+ :returns: data dict to be used by adapter
+
+ """
+ supplier = line.framework_agreement_id.supplier_id
+ add = line.requisition_id.consignee_shipping_id
+ term = supplier.property_supplier_payment_term
+ term = term.id if term else False
+ position = supplier.property_account_position
+ position = position.id if position else False
+ requisition = line.requisition_id
+ data = {}
+ data['framework_agreement_id'] = line.framework_agreement_id.id
+ data['partner_id'] = supplier.id
+ data['company_id'] = self._company(cr, uid, context)
+ data['pricelist_id'] = line.purchase_pricelist_id.id
+ data['dest_address_id'] = add.id
+ data['location_id'] = add.property_stock_customer.id
+ data['payment_term_id'] = term
+ data['fiscal_position'] = position
+ data['origin'] = requisition.name
+ data['date_order'] = requisition.date
+ data['name'] = requisition.name
+ data['consignee_id'] = requisition.consignee_id.id
+ data['incoterm_id'] = requisition.incoterm_id.id
+ data['incoterm_address'] = requisition.incoterm_address
+ data['type'] = 'purchase'
+ return data
+
+ def _map_source_to_po_line(self, cr, uid, line, context=None, **kwargs):
+ """Map source line to dict to be used by PO line create
+ Map source line to dict to be used by PO create
+ defaults are optional
+
+ :returns: data dict to be used by adapter
+
+ """
+ acc_pos_obj = self.pool['account.fiscal.position']
+ supplier = line.framework_agreement_id.supplier_id
+ taxes_ids = line.proposed_product_id.supplier_taxes_id
+ taxes = acc_pos_obj.map_tax(cr, uid, supplier.property_account_position,
+ taxes_ids)
+ currency = line.purchase_pricelist_id.currency_id
+ price = line.framework_agreement_id.get_price(line.proposed_qty, currency=currency)
+ data = {}
+ direct_map = {'product_qty': 'proposed_qty',
+ 'product_id': 'proposed_product_id',
+ 'product_uom': 'proposed_uom_id',
+ 'lr_source_line_id': 'id'}
+
+ data.update(self._direct_map(line, direct_map))
+ data['price_unit'] = price
+ data['name'] = line.proposed_product_id.name
+ data['date_planned'] = line.requisition_id.date_delivery
+ data['taxes_id'] = [(6, 0, taxes)]
+ return data
+
+ def _make_po_from_source_line(self, cr, uid, source_line, context=None):
+ """adapt a source line to purchase order
+
+ :returns: generated PO id
+
+ """
+
+ po_obj = self.pool['purchase.order']
+ pid = po_obj._make_purchase_order_from_origin(cr, uid, source_line,
+ self._map_source_to_po,
+ self._map_source_to_po_line,
+ context=context)
+ return pid
+
+ def make_purchase_order(self, cr, uid, ids, context=None):
+ """ adapt each source line to purchase order
+
+ :returns: generated PO ids
+
+ """
+ po_ids = []
+ for source_line in self.browse(cr, uid, ids, context=context):
+ po_id = self._make_po_from_source_line(cr, uid, source_line, context=None)
+ po_ids.append(po_id)
+ return po_ids
+
+ def action_create_agreement_po_requisition(self, cr, uid, ids, context=None):
+ """ Implement buttons that create PO from selected source lines"""
+ # We force empty context
+ act_obj = self.pool.get('ir.actions.act_window')
+ po_ids = self.make_purchase_order(cr, uid, ids, context=context)
+ res = act_obj.for_xml_id(cr, uid,
+ 'purchase', 'purchase_rfq', context=context)
+ res.update({'domain': [('id', 'in', po_ids)],
+ 'res_id': False,
+ 'context': '{}',
+ })
+ return res
+
+ def _is_sourced_fw_agreement(self, cr, uid, source, context=None):
+ """Predicate that tells if source line of type agreement are sourced
+
+ :retuns: boolean True if sourced
+
+ """
+ po_line_obj = self.pool['purchase.order.line']
+ sources_ids = po_line_obj.search(cr, uid, [('lr_source_line_id', '=', source.id)],
+ context=context)
+ # predicate
+ return bool(sources_ids)
+
+ def get_agreement_price_from_po(self, cr, uid, source_id, context=None):
+ """Get price from PO.
+
+ The price is retreived on the po line generated by sourced line.
+
+ :returns: price in float
+ """
+ if isinstance(source_id, (list, tuple)):
+ assert len(source_id) == 1
+ source_id = source_id[0]
+ po_l_obj = self.pool['purchase.order.line']
+ currency_obj = self.pool['res.currency']
+ current = self.browse(cr, uid, source_id, context=context)
+ agreement = current.framework_agreement_id
+
+ if not agreement:
+ raise ValueError('No framework agreement on source line %s' %
+ current.name)
+ line_ids = po_l_obj.search(cr, uid,
+ [('order_id.framework_agreement_id', '=', agreement.id),
+ ('lr_source_line_id', '=', current.id),
+ ('order_id.partner_id', '=', agreement.supplier_id.id)],
+ context=context)
+ price = 0.0
+ lines = po_l_obj.browse(cr, uid, line_ids, context=context)
+ if lines:
+ price = sum(x.price_subtotal for x in lines) # To avoid rounding problems
+ from_curr = lines[0].order_id.pricelist_id.currency_id.id
+ to_curr = current.pricelist_id.currency_id.id
+ price = currency_obj.compute(cr, uid, from_curr, to_curr, price, False)
+ return price
+
+ #---------------------- provide adapter middleware -------------------------
+
+ def _make_source_line_from_origin(self, cr, uid, origin, map_fun,
+ post_fun=None, context=None, **kwargs):
+ model = self.pool['logistic.requisition.source']
+ data = self._adapt_origin(cr, uid, model, origin, map_fun,
+ post_fun=post_fun, context=context, **kwargs)
+ self._validate_adapted_data(cr, uid, model, data, context=context)
+ s_id = self.create(cr, uid, data, context=context)
+ if callable(post_fun):
+ post_fun(cr, uid, s_id, origin, context=context, **kwargs)
+ return s_id
+
+ #---------------OpenERP tedious onchange management ------------------------
+
+ def _get_date(self, cr, uid, requision_line_id, context=None):
+ """helper to retrive date to be used by framework agreement
+ when in source line context
+
+ :param source_id: requisition.line.source id that should
+ provide date
+
+ :returns: date/datetime string
+
+ """
+ req_obj = self.pool['logistic.requisition.line']
+ current = req_obj.browse(cr, uid, requision_line_id, context=context)
+ now = fields.datetime.now()
+ return current.requisition_id.date or now
+
+ @id_boilerplate
+ def onchange_sourcing_method(self, cr, uid, source_id, method, req_line_id, proposed_product_id,
+ pricelist_id, proposed_qty=0, context=None):
+ """
+ Called when source method is set on a source line.
+
+ If sourcing method is framework agreement
+ it will set price, agreement and supplier if possible
+ and raise quantity warning.
+
+ """
+ res = {'value': {'framework_agreement_id': False}}
+ if (method != AGR_PROC or not proposed_product_id or not pricelist_id):
+ return res
+ currency = self._currency_get(cr, uid, pricelist_id, context=context)
+ agreement_obj = self.pool['framework.agreement']
+ date = self._get_date(cr, uid, req_line_id, context=context)
+ agreement, enough_qty = agreement_obj.get_cheapest_agreement_for_qty(cr, uid,
+ proposed_product_id,
+ date,
+ proposed_qty,
+ currency=currency,
+ context=context)
+ if not agreement:
+ return res
+ price = agreement.get_price(proposed_qty, currency=currency)
+ res['value'] = {'framework_agreement_id': agreement.id,
+ 'unit_cost': price,
+ 'total_cost': price * proposed_qty,
+ 'supplier_id': agreement.supplier_id.id}
+ if not enough_qty:
+ msg = _("You have ask for a quantity of %s \n"
+ " but there is only %s available"
+ " for current agreement") % (proposed_qty, agreement.available_quantity)
+ res['warning'] = msg
+ return res
+
+ @id_boilerplate
+ def onchange_pricelist(self, cr, uid, source_id, method, req_line_id,
+ proposed_product_id, proposed_qty,
+ pricelist_id, context=None):
+ """Call when pricelist is set on a source line.
+
+ If sourcing method is framework agreement
+ it will set price, agreement and supplier if possible
+ and raise quantity warning.
+
+ """
+ res = {}
+ if (method != AGR_PROC or not proposed_product_id or not pricelist_id):
+ return res
+
+ return self.onchange_sourcing_method(cr, uid, source_id, method, req_line_id,
+ proposed_product_id, pricelist_id,
+ proposed_qty=proposed_qty,
+ context=context)
+
+ @id_boilerplate
+ def onchange_quantity(self, cr, uid, source_id, method, req_line_id, qty,
+ proposed_product_id, pricelist_id, context=None):
+ """Raise a warning if agreed qty is not sufficient"""
+ if (method != AGR_PROC or not proposed_product_id):
+ return {}
+ currency = self._currency_get(cr, uid, pricelist_id, context=context)
+ date = self._get_date(cr, uid, req_line_id, context=context)
+ return self.onchange_quantity_obs(cr, uid, source_id, qty, date,
+ proposed_product_id,
+ currency=currency,
+ price_field='dummy',
+ context=context)
+
+ @id_boilerplate
+ def onchange_product_id(self, cr, uid, source_id, method, req_line_id,
+ proposed_product_id, proposed_qty,
+ pricelist_id, context=None):
+ """Call when product is set on a source line.
+
+ If sourcing method is framework agreement
+ it will set price, agreement and supplier if possible
+ and raise quantity warning.
+
+ """
+ if (method != AGR_PROC or not proposed_product_id):
+ return {}
+
+ return self.onchange_sourcing_method(cr, uid, source_id, method, req_line_id,
+ proposed_product_id, pricelist_id,
+ proposed_qty=proposed_qty,
+ context=context)
+
+ @id_boilerplate
+ def onchange_agreement(self, cr, uid, source_id, agreement_id, req_line_id, qty,
+ proposed_product_id, pricelist_id, context=None):
+ if not proposed_product_id or not pricelist_id or not agreement_id:
+ return {}
+ currency = self._currency_get(cr, uid, pricelist_id, context=context)
+ date = self._get_date(cr, uid, req_line_id, context=context)
+ return self.onchange_agreement_obs(cr, uid, source_id, agreement_id, qty,
+ date, proposed_product_id,
+ currency=currency, price_field='dummy')
=== added file 'framework_agreement_sourcing/model/purchase.py'
--- framework_agreement_sourcing/model/purchase.py 1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/model/purchase.py 2013-12-05 10:14:17 +0000
@@ -0,0 +1,91 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Nicolas Bessi
+# Copyright 2013 Camptocamp SA
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+from openerp.osv import orm
+from . adapter_util import BrowseAdapterMixin
+
+
+class purchase_order(orm.Model, BrowseAdapterMixin):
+ """Add function to create PO from source line.
+ It maybe goes against YAGNI principle.
+ The idea would be to propose a small design
+ to be ported back into purchase_requisition_extended module
+ or an other base modules.
+
+ Then we should extend it to propose an API
+ to generate PO from various sources
+ """
+
+ _inherit = "purchase.order"
+
+ #------ PO adapter middleware maybe to put in aside class but not easy in OpenERP context ----
+ def _make_purchase_order_from_origin(self, cr, uid, origin, map_fun, map_line_fun,
+ post_fun=None, post_line_fun=None, context=None):
+ """Create a PO browse record from any other record
+
+ :returns: created record ids
+
+ """
+ po_id = self._adapt_origin_to_po(cr, uid, origin, map_fun,
+ post_fun=post_fun, context=context)
+ self._adapt_origin_to_po_line(cr, uid, po_id, origin, map_line_fun,
+ post_fun=post_line_fun,
+ context=context)
+ return po_id
+
+ def _adapt_origin_to_po(self, cr, uid, origin, map_fun,
+ post_fun=None, context=None):
+ """PO adapter function
+
+ :returns: created PO id
+
+ """
+ model = self.pool['purchase.order']
+ data = self._adapt_origin(cr, uid, model, origin, map_fun,
+ post_fun=post_fun, context=context)
+ self._validate_adapted_data(cr, uid, model, data, context=context)
+ po_id = self.create(cr, uid, data, context=context)
+ if callable(post_fun):
+ post_fun(cr, uid, po_id, origin, context=context)
+ return po_id
+
+ def _adapt_origin_to_po_line(self, cr, uid, po_id, origin, map_fun,
+ post_fun=None, context=None):
+ """PO line adapter
+
+ :returns: created PO line id
+
+ """
+ model = self.pool['purchase.order.line']
+ data = self._adapt_origin(cr, uid, model, origin, map_fun,
+ post_fun=post_fun, context=context)
+ data['order_id'] = po_id
+ self._validate_adapted_data(cr, uid, model, data, context=context)
+ l_id = model.create(cr, uid, data, context=context)
+ if callable(post_fun):
+ post_fun(cr, uid, l_id, origin, context=context)
+ return l_id
+
+ def action_confirm(self, cr, uid, ids, context=None):
+ super(purchase_order_line, self).action_confirm(cr, uid, ids, context=context)
+ for element in self.browse(cr, uid, ids, context=context):
+ if not element.quantity_bid and not element.framework_agreement_id:
+ self.write(cr, uid, ids, {'quantity_bid': element.product_qty}, context=context)
+ return True
=== added file 'framework_agreement_sourcing/model/sale_order.py'
--- framework_agreement_sourcing/model/sale_order.py 1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/model/sale_order.py 2013-12-05 10:14:17 +0000
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Nicolas Bessi
+# Copyright 2013 Camptocamp SA
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+from openerp import netsvc
+from openerp.osv import orm
+
+
+class sale_order_line(orm.Model):
+ """Pass agreement PO into state confirmed when SO is confirmed"""
+
+ _inherit = "sale.order.line"
+
+ def button_confirm(self, cr, uid, ids, context=None):
+ """Override confirmation of request of cotation to support LTA
+
+ Related PO generated by agreement source line will be pased to state confirm.
+
+ """
+ result = super(sale_order_line, self).button_confirm(cr, uid, ids, context=context)
+ po_line_model = self.pool['purchase.order.line']
+ lines = self.browse(cr, uid, ids, context=context)
+ source_ids = [x.logistic_requisition_source_id.id for x in lines
+ if x.logistic_requisition_source_id]
+ po_line_ids = po_line_model.search(cr, uid,
+ [('lr_source_line_id', 'in', source_ids)],
+ context=context)
+ po_lines = po_line_model.read(cr, uid, po_line_ids, ['order_id'], load='_classic_write')
+ po_ids = set(x['order_id'] for x in po_lines)
+ wf_service = netsvc.LocalService("workflow")
+ for po_id in po_ids:
+ wf_service.trg_validate(uid, 'purchase.order', po_id, 'draft_po', cr)
+ wf_service.trg_validate(uid, 'purchase.order', po_id, 'purchase_confirm', cr)
+ return result
=== added directory 'framework_agreement_sourcing/security'
=== added directory 'framework_agreement_sourcing/tests'
=== added file 'framework_agreement_sourcing/tests/__init__.py'
--- framework_agreement_sourcing/tests/__init__.py 1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/tests/__init__.py 2013-12-05 10:14:17 +0000
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Nicolas Bessi
+# Copyright 2013 Camptocamp SA
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+from . import common
+from . import test_logistic_order_line_to_source_line
+from . import test_agreement_souce_line_to_po
+checks = [test_logistic_order_line_to_source_line,
+ test_agreement_souce_line_to_po]
=== added file 'framework_agreement_sourcing/tests/common.py'
--- framework_agreement_sourcing/tests/common.py 1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/tests/common.py 2013-12-05 10:14:17 +0000
@@ -0,0 +1,143 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Nicolas Bessi
+# Copyright 2013 Camptocamp SA
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+from datetime import timedelta
+from openerp.tools import DEFAULT_SERVER_DATE_FORMAT
+import openerp.tests.common as test_common
+from openerp.addons.logistic_requisition.tests import logistic_requisition
+from openerp.addons.framework_agreement.tests.common import BaseAgreementTestMixin
+
+
+class CommonSourcingSetUp(test_common.TransactionCase, BaseAgreementTestMixin):
+
+ def setUp(self):
+ """
+ Setup a standard configuration for test
+ """
+ super(CommonSourcingSetUp, self).setUp()
+ self.commonsetUp()
+ self.requisition_model = self.registry('logistic.requisition')
+ self.requisition_line_model = self.registry('logistic.requisition.line')
+ self.source_line_model = self.registry('logistic.requisition.source')
+ self.make_common_agreements()
+ self.make_common_requisition()
+
+ def make_common_requisition(self):
+ """Create a standard logistic requisition"""
+ start_date = self.now + timedelta(days=12)
+ start_date = start_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
+ req = {
+ 'partner_id': self.ref('base.res_partner_1'),
+ 'consignee_id': self.ref('base.res_partner_3'),
+ 'date_delivery': start_date,
+ 'date': start_date,
+ 'user_id': self.uid,
+ 'budget_holder_id': self.uid,
+ 'finance_officer_id': self.uid,
+ }
+ agr_line = {
+ 'product_id': self.product_id,
+ 'requested_qty': 100,
+ 'requested_uom_id': self.ref('product.product_uom_unit'),
+ 'date_delivery': self.now.strftime(DEFAULT_SERVER_DATE_FORMAT),
+ 'budget_tot_price': 100000000,
+ }
+ other_line = {
+ 'product_id': self.ref('product.product_product_7'),
+ 'requested_qty': 10,
+ 'requested_uom_id': self.ref('product.product_uom_unit'),
+ 'date_delivery': self.now.strftime(DEFAULT_SERVER_DATE_FORMAT),
+ 'budget_tot_price': 100000000,
+ }
+
+ requisition_id = logistic_requisition.create(self, req)
+ logistic_requisition.add_line(self, requisition_id,
+ agr_line)
+ logistic_requisition.add_line(self, requisition_id,
+ other_line)
+ self.requisition = self.requisition_model.browse(self.cr, self.uid, requisition_id)
+
+ def make_common_agreements(self):
+ """Create two default agreements.
+
+ We have two agreement for same product but using
+ different suppliers
+
+ One supplier has a better price for lower qty the other
+ has better price for higher qty
+
+ We also create one requisition with one line of agreement product
+ And one line of other product
+
+ """
+
+ cr, uid = self.cr, self.uid
+ start_date = self.now + timedelta(days=10)
+ start_date = start_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
+ end_date = self.now + timedelta(days=20)
+ end_date = end_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
+ # Agreement 1
+ agr_id = self.agreement_model.create(cr, uid,
+ {'supplier_id': self.supplier_id,
+ 'product_id': self.product_id,
+ 'start_date': start_date,
+ 'end_date': end_date,
+ 'draft': False,
+ 'delay': 5,
+ 'quantity': 2000})
+
+ pl_id = self.agreement_pl_model.create(cr, uid,
+ {'framework_agreement_id': agr_id,
+ 'currency_id': self.ref('base.EUR')})
+ self.agreement_line_model.create(cr, uid,
+ {'framework_agreement_pricelist_id': pl_id,
+ 'quantity': 0,
+ 'price': 77.0})
+
+ self.agreement_line_model.create(cr, uid,
+ {'framework_agreement_pricelist_id': pl_id,
+ 'quantity': 1000,
+ 'price': 30.0})
+
+ self.cheap_on_high_agreement = self.agreement_model.browse(cr, uid, agr_id)
+
+ # Agreement 2
+ agr_id = self.agreement_model.create(cr, uid,
+ {'supplier_id': self.ref('base.res_partner_3'),
+ 'product_id': self.product_id,
+ 'start_date': start_date,
+ 'end_date': end_date,
+ 'draft': False,
+ 'delay': 5,
+ 'quantity': 1200})
+
+ pl_id = self.agreement_pl_model.create(cr, uid,
+ {'framework_agreement_id': agr_id,
+ 'currency_id': self.ref('base.EUR')})
+
+ self.agreement_line_model.create(cr, uid,
+ {'framework_agreement_pricelist_id': pl_id,
+ 'quantity': 0,
+ 'price': 50.0})
+ self.agreement_line_model.create(cr, uid,
+ {'framework_agreement_pricelist_id': pl_id,
+ 'quantity': 1000,
+ 'price': 45.0})
+ self.cheap_on_low_agreement = self.agreement_model.browse(cr, uid, agr_id)
=== added file 'framework_agreement_sourcing/tests/test_agreement_souce_line_to_po.py'
--- framework_agreement_sourcing/tests/test_agreement_souce_line_to_po.py 1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/tests/test_agreement_souce_line_to_po.py 2013-12-05 10:14:17 +0000
@@ -0,0 +1,73 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Nicolas Bessi
+# Copyright 2013 Camptocamp SA
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+from .common import CommonSourcingSetUp
+
+
+class TestSourceToPo(CommonSourcingSetUp):
+
+ def setUp(self):
+ # we generate a source line
+ super(TestSourceToPo, self).setUp()
+ cr, uid = self.cr, self.uid
+ lines = self.requisition.line_ids
+ agr_line = None
+ for line in lines:
+ if line.product_id == self.cheap_on_low_agreement.product_id:
+ agr_line = line
+ break
+ self.assertTrue(agr_line)
+ agr_line.write({'requested_qty': 400})
+ agr_line.refresh()
+ source_ids = self.requisition_line_model._generate_source_line(cr, uid, agr_line)
+ self.assertTrue(len(source_ids) == 1)
+ self.source_line = self.source_line_model.browse(cr, uid, source_ids[0])
+
+ def test_01_transform_source_to_agreement(self):
+ """Test transformation of an agreement source line into PO"""
+ cr, uid = self.cr, self.uid
+ self.assertTrue(self.source_line)
+ plist = self.source_line.framework_agreement_id.supplier_id.property_product_pricelist_purchase
+ self.source_line.write({'purchase_pricelist_id': plist.id})
+ self.source_line.refresh()
+ po_id = self.source_line_model._make_po_from_source_line(cr, uid,
+ self.source_line)
+ self.assertTrue(po_id)
+ supplier = self.source_line.framework_agreement_id.supplier_id
+ add = self.source_line.requisition_id.consignee_shipping_id
+ consignee = self.source_line.requisition_id.consignee_id
+ po = self.registry('purchase.order').browse(cr, uid, po_id)
+ date_order = self.source_line.requisition_id.date
+ date_delivery = self.source_line.requisition_id.date_delivery
+ self.assertEqual(po.partner_id, supplier)
+ self.assertEqual(po.pricelist_id, supplier.property_product_pricelist_purchase)
+ self.assertEqual(po.date_order, date_order)
+ self.assertEqual(po.dest_address_id, add)
+ self.assertEqual(po.consignee_id, consignee)
+
+ self.assertEqual(len(po.order_line), 1)
+ po_line = po.order_line[0]
+ self.assertEqual(po_line.product_qty, self.source_line.proposed_qty)
+ self.assertEqual(po_line.product_id, self.source_line.proposed_product_id)
+ self.assertEqual(po_line.product_qty, self.source_line.proposed_qty)
+ self.assertEqual(po_line.product_uom, self.source_line.proposed_uom_id)
+ self.assertEqual(po_line.price_unit, 50.0)
+ self.assertEqual(po_line.lr_source_line_id, self.source_line)
+ self.assertEqual(po_line.date_planned, date_delivery)
=== added file 'framework_agreement_sourcing/tests/test_logistic_order_line_to_source_line.py'
--- framework_agreement_sourcing/tests/test_logistic_order_line_to_source_line.py 1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/tests/test_logistic_order_line_to_source_line.py 2013-12-05 10:14:17 +0000
@@ -0,0 +1,135 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Nicolas Bessi
+# Copyright 2013 Camptocamp SA
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+from ..model.logistic_requisition_source import AGR_PROC
+from .common import CommonSourcingSetUp
+
+
+class TestTransformation(CommonSourcingSetUp):
+
+ def test_01_enough_qty_on_first_agr(self):
+ """Test that we can source a line with one agreement and low qty"""
+ cr, uid = self.cr, self.uid
+ lines = self.requisition.line_ids
+ agr_line = None
+ for line in lines:
+ if line.product_id == self.cheap_on_low_agreement.product_id:
+ agr_line = line
+ break
+ self.assertTrue(agr_line)
+ agr_line.write({'requested_qty': 400})
+ agr_line.refresh()
+ to_validate_ids = self.requisition_line_model._generate_source_line(cr, uid, agr_line)
+ self.assertTrue(len(to_validate_ids) == 1)
+ to_validate = self.source_line_model.browse(cr, uid, to_validate_ids[0])
+ self.assertEqual(to_validate.procurement_method, AGR_PROC)
+ self.assertEqual(to_validate.unit_cost, 0.0)
+ self.assertEqual(to_validate.proposed_qty, 400)
+
+ def test_02_enough_qty_on_high_agr(self):
+ """Test that we can source a line correctly on both agreement"""
+ cr, uid = self.cr, self.uid
+ lines = self.requisition.line_ids
+ agr_line = None
+ for line in lines:
+ if line.product_id == self.cheap_on_high_agreement.product_id:
+ agr_line = line
+ break
+ self.assertTrue(agr_line)
+ agr_line.write({'requested_qty': 1500})
+ agr_line.refresh()
+ to_validate_ids = self.requisition_line_model._generate_source_line(cr, uid, agr_line)
+ self.assertTrue(len(to_validate_ids) == 1)
+ to_validate = self.source_line_model.browse(cr, uid, to_validate_ids[0])
+ self.assertEqual(to_validate.procurement_method, AGR_PROC)
+ self.assertEqual(to_validate.unit_cost, 0.0)
+ self.assertEqual(to_validate.proposed_qty, 1500)
+
+ def test_03_not_enough_qty_on_high_agreement(self):
+ """Test that we can source a line with one agreement and high qty"""
+ cr, uid = self.cr, self.uid
+ lines = self.requisition.line_ids
+ agr_line = None
+ for line in lines:
+ if line.product_id == self.cheap_on_high_agreement.product_id:
+ agr_line = line
+ break
+ self.assertTrue(agr_line)
+ agr_line.write({'requested_qty': 2400})
+ agr_line.refresh()
+ to_validate_ids = self.requisition_line_model._generate_source_line(cr, uid, agr_line)
+ self.assertTrue(len(to_validate_ids) == 2)
+ # We validate generated line
+ to_validates = self.source_line_model.browse(cr, uid, to_validate_ids)
+ # high_line
+ # idiom taken from Python cookbook
+ high_line = next((x for x in to_validates
+ if x.framework_agreement_id == self.cheap_on_high_agreement), None)
+ self.assertTrue(high_line, msg="High agreement was not used")
+ self.assertEqual(high_line.procurement_method, AGR_PROC)
+ self.assertEqual(high_line.proposed_qty, 2000)
+ self.assertEqual(high_line.unit_cost, 0.0)
+
+ # low_line
+ low_line = next((x for x in to_validates
+ if x.framework_agreement_id == self.cheap_on_low_agreement), None)
+ self.assertTrue(low_line, msg="Low agreement was not used")
+ self.assertEqual(low_line.procurement_method, AGR_PROC)
+ self.assertEqual(low_line.proposed_qty, 400)
+ self.assertEqual(low_line.unit_cost, 0.0)
+
+ def test_03_not_enough_qty_on_all_agreemenst(self):
+ """Test that we """
+ cr, uid = self.cr, self.uid
+ lines = self.requisition.line_ids
+ agr_line = None
+ for line in lines:
+ if line.product_id == self.cheap_on_high_agreement.product_id:
+ agr_line = line
+ break
+ self.assertTrue(agr_line)
+ agr_line.write({'requested_qty': 5000})
+ agr_line.refresh()
+ to_validate_ids = self.requisition_line_model._generate_source_line(cr, uid, agr_line)
+ self.assertTrue(len(to_validate_ids) == 3)
+ # We validate generated line
+ to_validates = self.source_line_model.browse(cr, uid, to_validate_ids)
+ # high_line
+ # idiom taken from Python cookbook
+ high_line = next((x for x in to_validates
+ if x.framework_agreement_id == self.cheap_on_high_agreement), None)
+ self.assertTrue(high_line, msg="High agreement was not used")
+ self.assertEqual(high_line.procurement_method, AGR_PROC)
+ self.assertEqual(high_line.proposed_qty, 2000)
+ self.assertEqual(high_line.unit_cost, 0.0)
+
+ # low_line
+ low_line = next((x for x in to_validates
+ if x.framework_agreement_id == self.cheap_on_low_agreement), None)
+ self.assertTrue(low_line, msg="Low agreement was not used")
+ self.assertEqual(low_line.procurement_method, AGR_PROC)
+ self.assertEqual(low_line.proposed_qty, 1200)
+ self.assertEqual(low_line.unit_cost, 0.0)
+
+ # Tender line
+ tender_line = next((x for x in to_validates
+ if not x.framework_agreement_id), None)
+ self.assertTrue(tender_line, msg="Tender line was not generated")
+ self.assertNotEqual(tender_line.procurement_method, AGR_PROC)
=== added file 'framework_agreement_sourcing/touch'
=== added directory 'framework_agreement_sourcing/view'
=== added file 'framework_agreement_sourcing/view/requisition_view.xml'
--- framework_agreement_sourcing/view/requisition_view.xml 1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/view/requisition_view.xml 2013-12-05 10:14:17 +0000
@@ -0,0 +1,147 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+ <data>
+ <record id="add_supplier_and_agreement_on_source_line" model="ir.ui.view">
+ <field name="name">add supplier and agrement on source line</field>
+ <field name="model">logistic.requisition.source</field>
+ <field name="inherit_id" ref="logistic_requisition.view_logistic_requisition_source_form"/>
+ <field name="arch" type="xml">
+ <field name="unit_cost"
+ position="before">
+ <field name="framework_agreement_id"
+ domain="[('draft', '=', False)]"
+ attrs="{'required': [('procurement_method', '=', 'fw_agreement')],
+ 'invisible': [('procurement_method', '!=', 'fw_agreement')]}"
+ on_change="onchange_agreement(framework_agreement_id, requisition_line_id, proposed_qty, proposed_product_id, purchase_pricelist_id, context)"/>/>
+ <field name="pricelist_id"
+ invisible="1"/>
+ <field name="purchase_pricelist_id"
+ attrs="{'required': [('procurement_method', '=', 'fw_agreement')],
+ 'invisible': [('procurement_method', '!=', 'fw_agreement')]}"
+ on_change="onchange_pricelist(framework_agreement_id, requisition_line_id, proposed_qty, proposed_product_id, purchase_pricelist_id, context)"
+ domain="[('type', '=', 'purchase')]"/>
+
+ <field name="supplier_id"
+ invisible="1"/>
+ </field>
+ <field name="proposed_uom_id"
+ position="attributes">
+ <attribute name="attrs">{'invisible': [('procurement_method', '=', 'fw_agreement')]}</attribute>
+ </field>
+ <field name="unit_cost"
+ position="attributes">
+ <attribute name="attrs">{'invisible': [('procurement_method', '=', 'fw_agreement')]}</attribute>
+ </field>
+ <field name="total_cost"
+ position="attributes">
+ <attribute name="attrs">{'invisible': [('procurement_method', '=', 'fw_agreement')]}</attribute>
+ </field>
+ <field name="price_is"
+ position="attributes">
+ <attribute name="attrs">{'invisible': [('procurement_method', '=', 'fw_agreement')]}</attribute>
+ </field>
+ </field>
+ </record>
+
+ <record id="add_hide_button_create_bids" model="ir.ui.view">
+ <field name="name">hidde button</field>
+ <field name="model">logistic.requisition.source</field>
+ <field name="inherit_id" ref="logistic_requisition.view_logistic_requisition_source_form"/>
+ <field name="priority" eval="10"/>
+ <field name="arch" type="xml">
+ <button position="replace">
+ </button>
+ </field>
+ </record>
+
+
+ <record id="add_create_po_button" model="ir.ui.view">
+ <field name="name">add create po button</field>
+ <field name="model">logistic.requisition.source</field>
+ <field name="inherit_id" ref="logistic_requisition.view_logistic_requisition_source_form"/>
+ <field name="arch" type="xml">
+ <sheet position="before">
+ <header>
+ <button name="action_create_agreement_po_requisition"
+ context="{}"
+ string="Create Draft PO"
+ type="object"
+ attrs="{'invisible': [('procurement_method', '!=', 'fw_agreement')]}"/>
+ <button name="action_create_po_requisition"
+ string="Call for Bids"
+ type="object"
+ attrs="{'invisible': ['|', ('po_requisition_id', '!=', False), ('procurement_method', '!=', 'procurement')]}"
+ />
+ </header>
+ </sheet>
+ </field>
+ </record>
+
+ <record id="addd_agreement_source_line_onchange" model="ir.ui.view">
+ <field name="name">addd agreement source line onchange</field>
+ <field name="model">logistic.requisition.source</field>
+ <field name="inherit_id" ref="logistic_requisition.view_logistic_requisition_source_form"/>
+ <field name="arch" type="xml">
+ <data>
+ <field name="procurement_method"
+ position="attributes">
+ <attribute name="on_change">onchange_sourcing_method(procurement_method, requisition_line_id, proposed_product_id, purchase_pricelist_id, proposed_qty)</attribute>
+ </field>
+
+ <field name="proposed_qty"
+ position="attributes">
+ <attribute name="on_change">onchange_quantity(procurement_method, requisition_line_id, proposed_qty, proposed_product_id, purchase_pricelist_id)</attribute>
+ </field>
+ <field name="proposed_product_id"
+ position="attributes">
+ <attribute name="on_change">onchange_product_id(procurement_method, requisition_line_id, proposed_product_id, proposed_qty, purchase_pricelist_id)</attribute>
+ </field>
+
+ </data>
+ </field>
+ </record>
+
+
+ <!-- <record id="add_supplier_and_agreement_on_source_line_embeeded" model="ir.ui.view"> -->
+ <!-- <field name="name">add supplier and agrement on source line</field> -->
+ <!-- <field name="model">logistic.requisition.line</field> -->
+ <!-- <field name="inherit_id" ref="logistic_requisition.view_logistic_requisition_line_form"/> -->
+ <!-- <field name="arch" type="xml"> -->
+ <!-- <field name="proposed_qty" position="after"> -->
+ <!-- <div> -->
+ <!-- <field name="framework_agreement_id_dummy" readonly="1"/> -->
+ <!-- <field name="framework_agreement_id" invisible="1"/> -->
+ <!-- <field name="supplier_id" invisible="1"/> -->
+ <!-- </div> -->
+ <!-- </field> -->
+ <!-- </field> -->
+ <!-- </record> -->
+
+ <!-- <record id="addd_agreement_source_line_onchange_embeeded" model="ir.ui.view"> -->
+ <!-- <field name="name">addd agreement source line onchange</field> -->
+ <!-- <field name="model">logistic.requisition.line</field> -->
+ <!-- <field name="inherit_id" ref="logistic_requisition.view_logistic_requisition_line_form"/> -->
+ <!-- <field name="arch" type="xml"> -->
+ <!-- <data> -->
+ <!-- <field name="procurement_method" -->
+ <!-- onchange="onchange_sourcing_method(procurement_method, proposed_product_id, proposed_qty)"> -->
+ <!-- </field> -->
+
+ <!-- <field name="unit_cost" -->
+ <!-- onchange="onchange_price(procurement_method, unit_cost, proposed_product_id, supplier_id, proposed_qty)"> -->
+ <!-- </field> -->
+
+ <!-- <field name="proposed_product_id" -->
+ <!-- onchange="onchange_product_id(procurement_method, unit_cost, proposed_product_id, proposed_qty)"> -->
+ <!-- </field> -->
+ <!-- <field name="proposed_qty" -->
+ <!-- onchange="onchange_product_id(procurement_method, proposed_qty, supplier_id, proposed_product_id)"> -->
+ <!-- </field> -->
+
+ <!-- </data> -->
+ <!-- </field> -->
+ <!-- </record> -->
+
+
+ </data>
+</openerp>
=== modified file 'logistic_requisition/view/logistic_requisition.xml'
--- logistic_requisition/view/logistic_requisition.xml 2013-10-31 09:03:35 +0000
+++ logistic_requisition/view/logistic_requisition.xml 2013-12-05 10:14:17 +0000
@@ -555,7 +555,7 @@
</group>
<group>
<group string="Purchase Requisition"
- attrs="{'invisible': [('procurement_method', 'not in', ['procurement', 'fw_agreement'])]}"
+ attrs="{'invisible': [('procurement_method', '!=', 'procurement')]}"
colspan="4">
<label for="po_requisition_id"/>
<div>
@@ -571,8 +571,8 @@
colspan="4"
attrs="{'invisible': [('procurement_method', '!=', 'wh_dispatch')]}">
<field name="dispatch_location_id"
- on_change="onchange_dispatch_location_id(dispatch_location_id)"
- attrs="{'required': [('procurement_method', '=', 'wh_dispatch')]}"
+ on_change="onchange_dispatch_location_id(dispatch_location_id)"
+ attrs="{'required': [('procurement_method', '=', 'wh_dispatch')]}"
domain="[('usage', '!=', 'view')]"/>
<field name="stock_owner" />
</group>
@@ -624,7 +624,7 @@
<field name="res_model">logistic.requisition.source</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
- <field name="context">{"search_default_groupby_procurement_method" : True}</field>
+ <field name="context">{}</field>
<field name="search_view_id" ref="view_logistic_requisition_source_filter"/>
<field name="help"></field>
</record>
Follow ups